OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /** | 5 /// This library provides internationalization and localization. This includes |
6 * This library provides internationalization and localization. This includes | 6 /// message formatting and replacement, date and number formatting and parsing, |
7 * message formatting and replacement, date and number formatting and parsing, | 7 /// and utilities for working with Bidirectional text. |
8 * and utilities for working with Bidirectional text. | 8 /// |
9 * | 9 /// This is part of the [intl package] |
10 * This is part of the [intl package] | 10 /// (https://pub.dartlang.org/packages/intl). |
11 * (https://pub.dartlang.org/packages/intl). | 11 /// |
12 * | 12 /// For things that require locale or other data, there are multiple different |
13 * For things that require locale or other data, there are multiple different | 13 /// ways of making that data available, which may require importing different |
14 * ways of making that data available, which may require importing different | 14 /// libraries. See the class comments for more details. |
15 * libraries. See the class comments for more details. | 15 /// |
16 * | 16 /// There is also a simple example application that can be found in the |
17 * There is also a simple example application that can be found in the | 17 /// [example/basic](https://github.com/dart-lang/intl/tree/master/example/basic) |
18 * [example/basic](https://github.com/dart-lang/intl/tree/master/example/basic) | 18 /// directory. |
19 * directory. | |
20 */ | |
21 library intl; | 19 library intl; |
22 | 20 |
23 import 'dart:async'; | 21 import 'dart:async'; |
24 import 'dart:collection'; | 22 import 'dart:collection'; |
25 import 'dart:convert'; | 23 import 'dart:convert'; |
26 import 'dart:math'; | 24 import 'dart:math'; |
27 | 25 |
28 import 'date_symbols.dart'; | 26 import 'date_symbols.dart'; |
29 import 'number_symbols.dart'; | 27 import 'number_symbols.dart'; |
30 import 'number_symbols_data.dart'; | 28 import 'number_symbols_data.dart'; |
31 import 'src/date_format_internal.dart'; | 29 import 'src/date_format_internal.dart'; |
32 import 'src/intl_helpers.dart'; | 30 import 'src/intl_helpers.dart'; |
| 31 import 'package:intl/src/plural_rules.dart' as plural_rules; |
33 | 32 |
34 part 'src/intl/bidi_formatter.dart'; | 33 part 'src/intl/bidi_formatter.dart'; |
35 part 'src/intl/bidi_utils.dart'; | 34 part 'src/intl/bidi_utils.dart'; |
| 35 |
| 36 part 'src/intl/compact_number_format.dart'; |
36 part 'src/intl/date_format.dart'; | 37 part 'src/intl/date_format.dart'; |
37 part 'src/intl/date_format_field.dart'; | 38 part 'src/intl/date_format_field.dart'; |
38 part 'src/intl/date_format_helpers.dart'; | 39 part 'src/intl/date_format_helpers.dart'; |
39 part 'src/intl/number_format.dart'; | 40 part 'src/intl/number_format.dart'; |
40 | 41 |
41 /** | 42 /// The Intl class provides a common entry point for internationalization |
42 * The Intl class provides a common entry point for internationalization | 43 /// related tasks. An Intl instance can be created for a particular locale |
43 * related tasks. An Intl instance can be created for a particular locale | 44 /// and used to create a date format via `anIntl.date()`. Static methods |
44 * and used to create a date format via `anIntl.date()`. Static methods | 45 /// on this class are also used in message formatting. |
45 * on this class are also used in message formatting. | 46 /// |
46 * | 47 /// Examples: |
47 * Examples: | 48 /// today(date) => Intl.message( |
48 * today(date) => Intl.message( | 49 /// "Today's date is $date", |
49 * "Today's date is $date", | 50 /// name: 'today', |
50 * name: 'today', | 51 /// args: [date], |
51 * args: [date], | 52 /// desc: 'Indicate the current date', |
52 * desc: 'Indicate the current date', | 53 /// examples: const {'date' : 'June 8, 2012'}); |
53 * examples: {'date' : 'June 8, 2012'}); | 54 /// print(today(new DateTime.now().toString()); |
54 * print(today(new DateTime.now().toString()); | 55 /// |
55 * | 56 /// howManyPeople(numberOfPeople, place) => Intl.plural( |
56 * howManyPeople(numberOfPeople, place) => Intl.plural( | 57 /// zero: 'I see no one at all', |
57 * zero: 'I see no one at all', | 58 /// one: 'I see one other person', |
58 * one: 'I see one other person', | 59 /// other: 'I see $numberOfPeople other people')} in $place.''', |
59 * other: 'I see $numberOfPeople other people')} in $place.''', | 60 /// name: 'msg', |
60 * name: 'msg', | 61 /// args: [numberOfPeople, place], |
61 * args: [numberOfPeople, place], | 62 /// desc: 'Description of how many people are seen in a place.', |
62 * desc: 'Description of how many people are seen in a place.', | 63 /// examples: const {'numberOfPeople': 3, 'place': 'London'}); |
63 * examples: {'numberOfPeople': 3, 'place': 'London'}); | 64 /// |
64 * | 65 /// Calling `howManyPeople(2, 'Athens');` would |
65 * Calling `howManyPeople(2, 'Athens');` would | 66 /// produce "I see 2 other people in Athens." as output in the default locale. |
66 * produce "I see 2 other people in Athens." as output in the default locale. | 67 /// If run in a different locale it would produce appropriately translated |
67 * If run in a different locale it would produce appropriately translated | 68 /// output. |
68 * output. | 69 /// |
69 * | 70 /// For more detailed information on messages and localizing them see |
70 * For more detailed information on messages and localizing them see | 71 /// the main [package documentation](https://pub.dartlang.org/packages/intl) |
71 * the main [package documentation](https://pub.dartlang.org/packages/intl) | 72 /// |
72 * | 73 /// You can set the default locale. |
73 * You can set the default locale. | 74 /// Intl.defaultLocale = "pt_BR"; |
74 * Intl.defaultLocale = "pt_BR"; | 75 /// |
75 * | 76 /// To temporarily use a locale other than the default, use the `withLocale` |
76 * To temporarily use a locale other than the default, use the `withLocale` | 77 /// function. |
77 * function. | 78 /// var todayString = new DateFormat("pt_BR").format(new DateTime.now()); |
78 * var todayString = new DateFormat("pt_BR").format(new DateTime.now()); | 79 /// print(withLocale("pt_BR", () => today(todayString)); |
79 * print(withLocale("pt_BR", () => today(todayString)); | 80 /// |
80 * | 81 /// See `tests/message_format_test.dart` for more examples. |
81 * See `tests/message_format_test.dart` for more examples. | |
82 */ | |
83 //TODO(efortuna): documentation example involving the offset parameter? | 82 //TODO(efortuna): documentation example involving the offset parameter? |
84 | 83 |
85 class Intl { | 84 class Intl { |
86 /** | 85 /// String indicating the locale code with which the message is to be |
87 * String indicating the locale code with which the message is to be | 86 /// formatted (such as en-CA). |
88 * formatted (such as en-CA). | |
89 */ | |
90 String _locale; | 87 String _locale; |
91 | 88 |
92 /** | 89 /// The default locale. This defaults to being set from systemLocale, but |
93 * The default locale. This defaults to being set from systemLocale, but | 90 /// can also be set explicitly, and will then apply to any new instances where |
94 * can also be set explicitly, and will then apply to any new instances where | 91 /// the locale isn't specified. Note that a locale parameter to |
95 * the locale isn't specified. Note that a locale parameter to | 92 /// [Intl.withLocale] |
96 * [Intl.withLocale] | 93 /// will supercede this value while that operation is active. Using |
97 * will supercede this value while that operation is active. Using | 94 /// [Intl.withLocale] may be preferable if you are using different locales |
98 * [Intl.withLocale] may be preferable if you are using different locales | 95 /// in the same application. |
99 * in the same application. | |
100 */ | |
101 static String get defaultLocale { | 96 static String get defaultLocale { |
102 var zoneLocale = Zone.current[#Intl.locale]; | 97 var zoneLocale = Zone.current[#Intl.locale]; |
103 return zoneLocale == null ? _defaultLocale : zoneLocale; | 98 return zoneLocale == null ? _defaultLocale : zoneLocale; |
104 } | 99 } |
105 static set defaultLocale(String newLocale) => _defaultLocale = newLocale; | 100 |
| 101 static set defaultLocale(String newLocale) { |
| 102 _defaultLocale = newLocale; |
| 103 } |
| 104 |
106 static String _defaultLocale; | 105 static String _defaultLocale; |
107 | 106 |
108 /** | 107 /// The system's locale, as obtained from the window.navigator.language |
109 * The system's locale, as obtained from the window.navigator.language | 108 /// or other operating system mechanism. Note that due to system limitations |
110 * or other operating system mechanism. Note that due to system limitations | 109 /// this is not automatically set, and must be set by importing one of |
111 * this is not automatically set, and must be set by importing one of | 110 /// intl_browser.dart or intl_standalone.dart and calling findSystemLocale(). |
112 * intl_browser.dart or intl_standalone.dart and calling findSystemLocale(). | |
113 */ | |
114 static String systemLocale = 'en_US'; | 111 static String systemLocale = 'en_US'; |
115 | 112 |
116 /** | 113 /// Return a new date format using the specified [pattern]. |
117 * Return a new date format using the specified [pattern]. | 114 /// If [desiredLocale] is not specified, then we default to [locale]. |
118 * If [desiredLocale] is not specified, then we default to [locale]. | |
119 */ | |
120 DateFormat date([String pattern, String desiredLocale]) { | 115 DateFormat date([String pattern, String desiredLocale]) { |
121 var actualLocale = (desiredLocale == null) ? locale : desiredLocale; | 116 var actualLocale = (desiredLocale == null) ? locale : desiredLocale; |
122 return new DateFormat(pattern, actualLocale); | 117 return new DateFormat(pattern, actualLocale); |
123 } | 118 } |
124 | 119 |
125 /** | 120 /// Constructor optionally [aLocale] for specifics of the language |
126 * Constructor optionally [aLocale] for specifics of the language | 121 /// locale to be used, otherwise, we will attempt to infer it (acceptable if |
127 * locale to be used, otherwise, we will attempt to infer it (acceptable if | 122 /// Dart is running on the client, we can infer from the browser/client |
128 * Dart is running on the client, we can infer from the browser/client | 123 /// preferences). |
129 * preferences). | |
130 */ | |
131 Intl([String aLocale]) { | 124 Intl([String aLocale]) { |
132 _locale = aLocale != null ? aLocale : getCurrentLocale(); | 125 _locale = aLocale != null ? aLocale : getCurrentLocale(); |
133 } | 126 } |
134 | 127 |
135 /** | 128 /// Use this for a message that will be translated for different locales. The |
136 * Use this for a message that will be translated for different locales. The | 129 /// expected usage is that this is inside an enclosing function that only |
137 * expected usage is that this is inside an enclosing function that only | 130 /// returns the value of this call and provides a scope for the variables that |
138 * returns the value of this call and provides a scope for the variables that | 131 /// will be substituted in the message. |
139 * will be substituted in the message. | 132 /// |
140 * | 133 /// The [message_str] is the string to be translated, which may be |
141 * The [message_str] is the string to be translated, which may be interpolated | 134 /// interpolated based on one or more variables. The [name] of the message |
142 * based on one or more variables. The [name] of the message must | 135 /// must match the enclosing function name. For methods, it can also be |
143 * match the enclosing function name. For methods, it can also be | 136 /// className_methodName. So for a method hello in class Simple, the name can |
144 * className_methodName. So for a method hello in class Simple, the name | 137 /// be either "hello" or "Simple_hello". The name must also be globally unique |
145 * can be either "hello" or "Simple_hello". The name must also be globally | 138 /// in the program, so the second form can make it easier to distinguish |
146 * unique in the program, so the second form can make it easier to distinguish | 139 /// messages with the same name but in different classes. |
147 * messages with the same name but in different classes. | 140 /// |
148 * The [args] repeats the arguments of the enclosing | 141 /// The [args] repeats the arguments of the enclosing |
149 * function, [desc] provides a description of usage, | 142 /// function, [desc] provides a description of usage, |
150 * [examples] is a Map of exmaples for each interpolated variable. For example | 143 /// [examples] is a Map of examples for each interpolated variable. |
151 * hello(yourName) => Intl.message( | 144 /// For example |
152 * "Hello, $yourName", | 145 /// |
153 * name: "hello", | 146 /// hello(yourName) => Intl.message( |
154 * args: [yourName], | 147 /// "Hello, $yourName", |
155 * desc: "Say hello", | 148 /// name: "hello", |
156 * examples = {"yourName": "Sparky"}. | 149 /// args: [yourName], |
157 * The source code will be processed via the analyzer to extract out the | 150 /// desc: "Say hello", |
158 * message data, so only a subset of valid Dart code is accepted. In | 151 /// examples = const {"yourName": "Sparky"}. |
159 * particular, everything must be literal and cannot refer to variables | 152 /// |
160 * outside the scope of the enclosing function. The [examples] map must | 153 /// The source code will be processed via the analyzer to extract out the |
161 * be a valid const literal map. Similarly, the [desc] argument must | 154 /// message data, so only a subset of valid Dart code is accepted. In |
162 * be a single, simple string. These two arguments will not be used at runtime | 155 /// particular, everything must be literal and cannot refer to variables |
163 * but will be extracted from | 156 /// outside the scope of the enclosing function. The [examples] map must be a |
164 * the source code and used as additional data for translators. For more | 157 /// valid const literal map. Similarly, the [desc] argument must be a single, |
165 * information see the "Messages" section of the main [package documentation] | 158 /// simple string. These two arguments will not be used at runtime but will be |
166 * (https://pub.dartlang.org/packages/intl). | 159 /// extracted from the source code and used as additional data for |
167 * | 160 /// translators. For more information see the "Messages" section of the main |
168 * The [name] and [args] arguments are required, and are used at runtime | 161 /// [package documentation] (https://pub.dartlang.org/packages/intl). |
169 * to look up the localized version and pass the appropriate arguments to it. | 162 /// |
170 * We may in the future modify the code during compilation to make manually | 163 /// The [name] and [args] arguments are required, and are used at runtime |
171 * passing those arguments unnecessary. | 164 /// to look up the localized version and pass the appropriate arguments to it. |
172 */ | 165 /// We may in the future modify the code during compilation to make manually |
173 static String message(String message_str, {String desc: '', | 166 /// passing those arguments unnecessary. |
174 Map<String, String> examples: const {}, String locale, String name, | 167 static String message(String message_str, |
175 List<String> args, String meaning}) { | 168 {String desc: '', |
| 169 Map<String, dynamic> examples: const {}, |
| 170 String locale, |
| 171 String name, |
| 172 List args, |
| 173 String meaning}) => |
| 174 _message(message_str, locale, name, args, meaning); |
| 175 |
| 176 /// Omit the compile-time only parameters so dart2js can see to drop them. |
| 177 static _message(String message_str, String locale, String name, List args, |
| 178 String meaning) { |
176 return messageLookup.lookupMessage( | 179 return messageLookup.lookupMessage( |
177 message_str, desc, examples, locale, name, args, meaning); | 180 message_str, locale, name, args, meaning); |
178 } | 181 } |
179 | 182 |
180 /** | 183 /// Return the locale for this instance. If none was set, the locale will |
181 * Return the locale for this instance. If none was set, the locale will | 184 /// be the default. |
182 * be the default. | |
183 */ | |
184 String get locale => _locale; | 185 String get locale => _locale; |
185 | 186 |
186 /** | 187 /// Given [newLocale] return a locale that we have data for that is similar |
187 * Return true if the locale exists, or if it is null. The null case | 188 /// to it, if possible. |
188 * is interpreted to mean that we use the default locale. | 189 /// |
189 */ | 190 /// If [newLocale] is found directly, return it. If it can't be found, look up |
190 static bool _localeExists(localeName) => DateFormat.localeExists(localeName); | 191 /// based on just the language (e.g. 'en_CA' -> 'en'). Also accepts '-' |
191 | 192 /// as a separator and changes it into '_' for lookup, and changes the |
192 /** | 193 /// country to uppercase. |
193 * Given [newLocale] return a locale that we have data for that is similar | 194 /// |
194 * to it, if possible. | 195 /// There is a special case that if a locale named "fallback" is present |
195 * | 196 /// and has been initialized, this will return that name. This can be useful |
196 * If [newLocale] is found directly, return it. If it can't be found, look up | 197 /// for messages where you don't want to just use the text from the original |
197 * based on just the language (e.g. 'en_CA' -> 'en'). Also accepts '-' | 198 /// source code, but wish to have a universal fallback translation. |
198 * as a separator and changes it into '_' for lookup, and changes the | 199 /// |
199 * country to uppercase. | 200 /// Note that null is interpreted as meaning the default locale, so if |
200 * | 201 /// [newLocale] is null the default locale will be returned. |
201 * There is a special case that if a locale named "fallback" is present | |
202 * and has been initialized, this will return that name. This can be useful | |
203 * for messages where you don't want to just use the text from the original | |
204 * source code, but wish to have a universal fallback translation. | |
205 * | |
206 * Note that null is interpreted as meaning the default locale, so if | |
207 * [newLocale] is null it will be returned. | |
208 */ | |
209 static String verifiedLocale(String newLocale, Function localeExists, | 202 static String verifiedLocale(String newLocale, Function localeExists, |
210 {Function onFailure: _throwLocaleError}) { | 203 {Function onFailure: _throwLocaleError}) { |
211 // TODO(alanknight): Previously we kept a single verified locale on the Intl | 204 // TODO(alanknight): Previously we kept a single verified locale on the Intl |
212 // object, but with different verification for different uses, that's more | 205 // object, but with different verification for different uses, that's more |
213 // difficult. As a result, we call this more often. Consider keeping | 206 // difficult. As a result, we call this more often. Consider keeping |
214 // verified locales for each purpose if it turns out to be a performance | 207 // verified locales for each purpose if it turns out to be a performance |
215 // issue. | 208 // issue. |
216 if (newLocale == null) { | 209 if (newLocale == null) { |
217 return verifiedLocale(getCurrentLocale(), localeExists, | 210 return verifiedLocale(getCurrentLocale(), localeExists, |
218 onFailure: onFailure); | 211 onFailure: onFailure); |
219 } | 212 } |
220 if (localeExists(newLocale)) { | 213 if (localeExists(newLocale)) { |
221 return newLocale; | 214 return newLocale; |
222 } | 215 } |
223 for (var each in | 216 for (var each in [ |
224 [canonicalizedLocale(newLocale), shortLocale(newLocale), "fallback"]) { | 217 canonicalizedLocale(newLocale), |
| 218 shortLocale(newLocale), |
| 219 "fallback" |
| 220 ]) { |
225 if (localeExists(each)) { | 221 if (localeExists(each)) { |
226 return each; | 222 return each; |
227 } | 223 } |
228 } | 224 } |
229 return onFailure(newLocale); | 225 return onFailure(newLocale); |
230 } | 226 } |
231 | 227 |
232 /** | 228 /// The default action if a locale isn't found in verifiedLocale. Throw |
233 * The default action if a locale isn't found in verifiedLocale. Throw | 229 /// an exception indicating the locale isn't correct. |
234 * an exception indicating the locale isn't correct. | |
235 */ | |
236 static String _throwLocaleError(String localeName) { | 230 static String _throwLocaleError(String localeName) { |
237 throw new ArgumentError("Invalid locale '$localeName'"); | 231 throw new ArgumentError("Invalid locale '$localeName'"); |
238 } | 232 } |
239 | 233 |
240 /** Return the short version of a locale name, e.g. 'en_US' => 'en' */ | 234 /// Return the short version of a locale name, e.g. 'en_US' => 'en' |
241 static String shortLocale(String aLocale) { | 235 static String shortLocale(String aLocale) { |
242 if (aLocale.length < 2) return aLocale; | 236 if (aLocale.length < 2) return aLocale; |
243 return aLocale.substring(0, 2).toLowerCase(); | 237 return aLocale.substring(0, 2).toLowerCase(); |
244 } | 238 } |
245 | 239 |
246 /** | 240 /// Return the name [aLocale] turned into xx_YY where it might possibly be |
247 * Return the name [aLocale] turned into xx_YY where it might possibly be | 241 /// in the wrong case or with a hyphen instead of an underscore. If |
248 * in the wrong case or with a hyphen instead of an underscore. If | 242 /// [aLocale] is null, for example, if you tried to get it from IE, |
249 * [aLocale] is null, for example, if you tried to get it from IE, | 243 /// return the current system locale. |
250 * return the current system locale. | |
251 */ | |
252 static String canonicalizedLocale(String aLocale) { | 244 static String canonicalizedLocale(String aLocale) { |
253 // Locales of length < 5 are presumably two-letter forms, or else malformed. | 245 // Locales of length < 5 are presumably two-letter forms, or else malformed. |
254 // We return them unmodified and if correct they will be found. | 246 // We return them unmodified and if correct they will be found. |
255 // Locales longer than 6 might be malformed, but also do occur. Do as | 247 // Locales longer than 6 might be malformed, but also do occur. Do as |
256 // little as possible to them, but make the '-' be an '_' if it's there. | 248 // little as possible to them, but make the '-' be an '_' if it's there. |
257 // We treat C as a special case, and assume it wants en_ISO for formatting. | 249 // We treat C as a special case, and assume it wants en_ISO for formatting. |
258 // TODO(alanknight): en_ISO is probably not quite right for the C/Posix | 250 // TODO(alanknight): en_ISO is probably not quite right for the C/Posix |
259 // locale for formatting. Consider adding C to the formats database. | 251 // locale for formatting. Consider adding C to the formats database. |
260 if (aLocale == null) return getCurrentLocale(); | 252 if (aLocale == null) return getCurrentLocale(); |
261 if (aLocale == "C") return "en_ISO"; | 253 if (aLocale == "C") return "en_ISO"; |
262 if (aLocale.length < 5) return aLocale; | 254 if (aLocale.length < 5) return aLocale; |
263 if (aLocale[2] != '-' && (aLocale[2] != '_')) return aLocale; | 255 if (aLocale[2] != '-' && (aLocale[2] != '_')) return aLocale; |
264 var region = aLocale.substring(3); | 256 var region = aLocale.substring(3); |
265 // If it's longer than three it's something odd, so don't touch it. | 257 // If it's longer than three it's something odd, so don't touch it. |
266 if (region.length <= 3) region = region.toUpperCase(); | 258 if (region.length <= 3) region = region.toUpperCase(); |
267 return '${aLocale[0]}${aLocale[1]}_$region'; | 259 return '${aLocale[0]}${aLocale[1]}_$region'; |
268 } | 260 } |
269 | 261 |
270 /** | 262 /// Format a message differently depending on [howMany]. Normally used |
271 * Format a message differently depending on [howMany]. Normally used | 263 /// as part of an `Intl.message` text that is to be translated. |
272 * as part of an `Intl.message` text that is to be translated. | 264 /// Selects the correct plural form from |
273 * Selects the correct plural form from | 265 /// the provided alternatives. The [other] named argument is mandatory. |
274 * the provided alternatives. The [other] named argument is mandatory. | 266 static String plural(int howMany, |
275 */ | 267 {zero, |
276 static String plural(int howMany, {zero, one, two, few, many, other, | 268 one, |
277 String desc, Map<String, String> examples, String locale, String name, | 269 two, |
278 List<String> args, String meaning}) { | 270 few, |
| 271 many, |
| 272 other, |
| 273 String desc, |
| 274 Map<String, dynamic> examples, |
| 275 String locale, |
| 276 String name, |
| 277 List args, |
| 278 String meaning}) { |
279 // If we are passed a name and arguments, then we are operating as a | 279 // If we are passed a name and arguments, then we are operating as a |
280 // top-level message, so look up our translation by calling Intl.message | 280 // top-level message, so look up our translation by calling Intl.message |
281 // with ourselves as an argument. | 281 // with ourselves as an argument. |
282 if (name != null) { | 282 if (name != null) { |
283 return message(plural(howMany, | 283 return message( |
| 284 plural(howMany, |
284 zero: zero, | 285 zero: zero, |
285 one: one, | 286 one: one, |
286 two: two, | 287 two: two, |
287 few: few, | 288 few: few, |
288 many: many, | 289 many: many, |
289 other: other), | 290 other: other, |
290 name: name, args: args, locale: locale, meaning: meaning); | 291 locale: locale), |
| 292 name: name, |
| 293 args: args, |
| 294 locale: locale, |
| 295 meaning: meaning); |
291 } | 296 } |
292 if (other == null) { | 297 if (other == null) { |
293 throw new ArgumentError("The 'other' named argument must be provided"); | 298 throw new ArgumentError("The 'other' named argument must be provided"); |
294 } | 299 } |
295 // TODO(alanknight): This algorithm needs to be locale-dependent. | 300 if (howMany == null) { |
296 switch (howMany) { | 301 throw new ArgumentError("The howMany argument to plural cannot be null"); |
297 case 0: | 302 } |
298 return (zero == null) ? other : zero; | 303 // If there's an explicit case for the exact number, we use it. This is not |
299 case 1: | 304 // strictly in accord with the CLDR rules, but it seems to be the |
300 return (one == null) ? other : one; | 305 // expectation. At least I see e.g. Russian translations that have a zero |
301 case 2: | 306 // case defined. The rule for that locale will never produce a zero, and |
302 return (two == null) ? ((few == null) ? other : few) : two; | 307 // treats it as other. But it seems reasonable that, even if the language |
| 308 // rules treat zero as other, we might want a special message for zero. |
| 309 if (howMany == 0 && zero != null) return zero; |
| 310 if (howMany == 1 && one != null) return one; |
| 311 if (howMany == 2 && two != null) return two; |
| 312 var pluralRule = _pluralRule(locale, howMany); |
| 313 var pluralCase = pluralRule(); |
| 314 switch (pluralCase) { |
| 315 case plural_rules.PluralCase.ZERO: |
| 316 return zero ?? other; |
| 317 case plural_rules.PluralCase.ONE: |
| 318 return one ?? other; |
| 319 case plural_rules.PluralCase.TWO: |
| 320 return two ?? few ?? other; |
| 321 case plural_rules.PluralCase.FEW: |
| 322 return few ?? other; |
| 323 case plural_rules.PluralCase.MANY: |
| 324 return many ?? other; |
| 325 case plural_rules.PluralCase.OTHER: |
| 326 return other; |
303 default: | 327 default: |
304 if ((howMany == 3 || howMany == 4) && few != null) return few; | 328 throw new ArgumentError.value( |
305 if (howMany > 10 && howMany < 100 && many != null) return many; | 329 howMany, "howMany", "Invalid plural argument"); |
306 return other; | |
307 } | 330 } |
308 throw new ArgumentError("Invalid plural usage for $howMany"); | |
309 } | 331 } |
310 | 332 |
311 /** | 333 static var _cachedPluralRule; |
312 * Format a message differently depending on [targetGender]. Normally used as | 334 static String _cachedPluralLocale; |
313 * part of an Intl.message message that is to be translated. | 335 |
314 */ | 336 static _pluralRule(String locale, int howMany) { |
315 static String gender(String targetGender, {String male, String female, | 337 plural_rules.startRuleEvaluation(howMany); |
316 String other, String desc, Map<String, String> examples, String locale, | 338 var verifiedLocale = Intl.verifiedLocale( |
317 String name, List<String> args, String meaning}) { | 339 locale, plural_rules.localeHasPluralRules, |
| 340 onFailure: (locale) => 'default'); |
| 341 if (_cachedPluralLocale == verifiedLocale) { |
| 342 return _cachedPluralRule; |
| 343 } else { |
| 344 _cachedPluralRule = plural_rules.pluralRules[verifiedLocale]; |
| 345 _cachedPluralLocale = verifiedLocale; |
| 346 return _cachedPluralRule; |
| 347 } |
| 348 } |
| 349 |
| 350 /// Format a message differently depending on [targetGender]. Normally used as |
| 351 /// part of an Intl.message message that is to be translated. |
| 352 static String gender(String targetGender, |
| 353 {String male, |
| 354 String female, |
| 355 String other, |
| 356 String desc, |
| 357 Map<String, dynamic> examples, |
| 358 String locale, |
| 359 String name, |
| 360 List args, |
| 361 String meaning}) { |
318 // If we are passed a name and arguments, then we are operating as a | 362 // If we are passed a name and arguments, then we are operating as a |
319 // top-level message, so look up our translation by calling Intl.message | 363 // top-level message, so look up our translation by calling Intl.message |
320 // with ourselves as an argument. | 364 // with ourselves as an argument. |
321 if (name != null) { | 365 if (name != null) { |
322 return message( | 366 return message( |
323 gender(targetGender, male: male, female: female, other: other), | 367 gender(targetGender, male: male, female: female, other: other), |
324 name: name, args: args, locale: locale, meaning: meaning); | 368 name: name, |
| 369 args: args, |
| 370 locale: locale, |
| 371 meaning: meaning); |
325 } | 372 } |
326 | 373 |
327 if (other == null) { | 374 if (other == null) { |
328 throw new ArgumentError("The 'other' named argument must be specified"); | 375 throw new ArgumentError("The 'other' named argument must be specified"); |
329 } | 376 } |
330 switch (targetGender) { | 377 switch (targetGender) { |
331 case "female": | 378 case "female": |
332 return female == null ? other : female; | 379 return female == null ? other : female; |
333 case "male": | 380 case "male": |
334 return male == null ? other : male; | 381 return male == null ? other : male; |
335 default: | 382 default: |
336 return other; | 383 return other; |
337 } | 384 } |
338 } | 385 } |
339 | 386 |
340 /** | 387 /// Format a message differently depending on [choice]. We look up the value |
341 * Format a message differently depending on [choice]. We look up the value | 388 /// of [choice] in [cases] and return the result, or an empty string if |
342 * of [choice] in [cases] and return the result, or an empty string if | 389 /// it is not found. Normally used as part |
343 * it is not found. Normally used as part | 390 /// of an Intl.message message that is to be translated. |
344 * of an Intl.message message that is to be translated. | 391 static String select(Object choice, Map<String, String> cases, |
345 */ | 392 {String desc, |
346 static String select(String choice, Map<String, String> cases, {String desc, | 393 Map<String, dynamic> examples, |
347 Map<String, String> examples, String locale, String name, | 394 String locale, |
348 List<String> args, String meaning}) { | 395 String name, |
| 396 List args, |
| 397 String meaning}) { |
| 398 // Allow passing non-strings, e.g. enums to a select. |
| 399 choice = "$choice"; |
349 // If we are passed a name and arguments, then we are operating as a | 400 // If we are passed a name and arguments, then we are operating as a |
350 // top-level message, so look up our translation by calling Intl.message | 401 // top-level message, so look up our translation by calling Intl.message |
351 // with ourselves as an argument. | 402 // with ourselves as an argument. |
352 if (name != null) { | 403 if (name != null) { |
353 return message(select(choice, cases), | 404 return message(select(choice, cases), |
354 name: name, args: args, locale: locale); | 405 name: name, args: args, locale: locale); |
355 } | 406 } |
356 var exact = cases[choice]; | 407 var exact = cases[choice]; |
357 if (exact != null) return exact; | 408 if (exact != null) return exact; |
358 var other = cases["other"]; | 409 var other = cases["other"]; |
359 if (other == | 410 if (other == null) |
360 null) throw new ArgumentError("The 'other' case must be specified"); | 411 throw new ArgumentError("The 'other' case must be specified"); |
361 return other; | 412 return other; |
362 } | 413 } |
363 | 414 |
364 /** | 415 /// Run [function] with the default locale set to [locale] and |
365 * Run [function] with the default locale set to [locale] and | 416 /// return the result. |
366 * return the result. | 417 /// |
367 * | 418 /// This is run in a zone, so async operations invoked |
368 * This is run in a zone, so async operations invoked | 419 /// from within [function] will still have the locale set. |
369 * from within [function] will still have the locale set. | 420 /// |
370 * | 421 /// In simple usage [function] might be a single |
371 * In simple usage [function] might be a single | 422 /// `Intl.message()` call or number/date formatting operation. But it can |
372 * `Intl.message()` call or number/date formatting operation. But it can | 423 /// also be an arbitrary function that calls multiple Intl operations. |
373 * also be an arbitrary function that calls multiple Intl operations. | 424 /// |
374 * | 425 /// For example |
375 * For example | 426 /// |
376 * | 427 /// Intl.withLocale("fr", () => new NumberFormat.format(123456)); |
377 * Intl.withLocale("fr", () => new NumberFormat.format(123456)); | 428 /// |
378 * | 429 /// or |
379 * or | 430 /// |
380 * | 431 /// hello(name) => Intl.message( |
381 * hello(name) => Intl.message( | 432 /// "Hello $name.", |
382 * "Hello $name.", | 433 /// name: 'hello', |
383 * name: 'hello', | 434 /// args: [name], |
384 * args: [name], | 435 /// desc: 'Say Hello'); |
385 * desc: 'Say Hello'); | 436 /// Intl.withLocale("zh", new Timer(new Duration(milliseconds:10), |
386 * Intl.withLocale("zh", new Timer(new Duration(milliseconds:10), | 437 /// () => print(hello("World"))); |
387 * () => print(hello("World"))); | |
388 */ | |
389 static withLocale(String locale, function()) { | 438 static withLocale(String locale, function()) { |
390 var canonical = Intl.canonicalizedLocale(locale); | 439 var canonical = Intl.canonicalizedLocale(locale); |
391 return runZoned(function, zoneValues: {#Intl.locale: canonical}); | 440 return runZoned(function, zoneValues: {#Intl.locale: canonical}); |
392 } | 441 } |
393 | 442 |
394 /** | 443 /// Accessor for the current locale. This should always == the default locale, |
395 * Accessor for the current locale. This should always == the default locale, | 444 /// unless for some reason this gets called inside a message that resets the |
396 * unless for some reason this gets called inside a message that resets the | 445 /// locale. |
397 * locale. | |
398 */ | |
399 static String getCurrentLocale() { | 446 static String getCurrentLocale() { |
400 if (defaultLocale == null) defaultLocale = systemLocale; | 447 if (defaultLocale == null) defaultLocale = systemLocale; |
401 return defaultLocale; | 448 return defaultLocale; |
402 } | 449 } |
403 | 450 |
404 toString() => "Intl($locale)"; | 451 toString() => "Intl($locale)"; |
405 } | 452 } |
| 453 |
| 454 /// Convert a string to beginning of sentence case, in a way appropriate to the |
| 455 /// locale. |
| 456 /// |
| 457 /// Currently this just converts the first letter to uppercase, which works for |
| 458 /// many locales, and we have the option to extend this to handle more cases |
| 459 /// without changing the API for clients. It also hard-codes the case of |
| 460 /// dotted i in Turkish and Azeri. |
| 461 String toBeginningOfSentenceCase(String input, [String locale]) { |
| 462 if (input == null || input.isEmpty) return input; |
| 463 return "${_upperCaseLetter(input[0], locale)}${input.substring(1)}"; |
| 464 } |
| 465 |
| 466 /// Convert the input single-letter string to upper case. A trivial |
| 467 /// hard-coded implementation that only handles simple upper case |
| 468 /// and the dotted i in Turkish/Azeri. |
| 469 /// |
| 470 /// Private to the implementation of [toBeginningOfSentenceCase]. |
| 471 // TODO(alanknight): Consider hard-coding other important cases. |
| 472 // See http://www.unicode.org/Public/UNIDATA/SpecialCasing.txt |
| 473 // TODO(alanknight): Alternatively, consider toLocaleUpperCase in browsers. |
| 474 // See also https://github.com/dart-lang/sdk/issues/6706 |
| 475 String _upperCaseLetter(String input, String locale) { |
| 476 // Hard-code the important edge case of i->İ |
| 477 if (locale != null) { |
| 478 if (input == "i" && locale.startsWith("tr") || locale.startsWith("az")) { |
| 479 return "\u0130"; |
| 480 } |
| 481 } |
| 482 return input.toUpperCase(); |
| 483 } |
OLD | NEW |