OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * This library provides internationalization and localization. This includes | |
7 * message formatting and replacement, date and number formatting and parsing, | |
8 * and utilities for working with Bidirectional text. | |
9 * | |
10 * This is part of the [intl package] | |
11 * (https://pub.dartlang.org/packages/intl). | |
12 * | |
13 * For things that require locale or other data, there are multiple different | |
14 * ways of making that data available, which may require importing different | |
15 * libraries. See the class comments for more details. | |
16 * | |
17 * There is also a simple example application that can be found in the | |
18 * [example/basic](https://github.com/dart-lang/intl/tree/master/example/basic) | |
19 * directory. | |
20 */ | |
21 library intl; | |
22 | |
23 import 'dart:async'; | |
24 import 'dart:collection'; | |
25 import 'dart:convert'; | |
26 import 'dart:math'; | |
27 | |
28 import 'date_symbols.dart'; | |
29 import 'number_symbols.dart'; | |
30 import 'number_symbols_data.dart'; | |
31 import 'src/date_format_internal.dart'; | |
32 import 'src/intl_helpers.dart'; | |
33 | |
34 part 'src/intl/bidi_formatter.dart'; | |
35 part 'src/intl/bidi_utils.dart'; | |
36 part 'src/intl/date_format.dart'; | |
37 part 'src/intl/date_format_field.dart'; | |
38 part 'src/intl/date_format_helpers.dart'; | |
39 part 'src/intl/number_format.dart'; | |
40 | |
41 /** | |
42 * The Intl class provides a common entry point for internationalization | |
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 | |
45 * on this class are also used in message formatting. | |
46 * | |
47 * Examples: | |
48 * today(date) => Intl.message( | |
49 * "Today's date is $date", | |
50 * name: 'today', | |
51 * args: [date], | |
52 * desc: 'Indicate the current date', | |
53 * examples: {'date' : 'June 8, 2012'}); | |
54 * print(today(new DateTime.now().toString()); | |
55 * | |
56 * howManyPeople(numberOfPeople, place) => Intl.plural( | |
57 * zero: 'I see no one at all', | |
58 * one: 'I see one other person', | |
59 * other: 'I see $numberOfPeople other people')} in $place.''', | |
60 * name: 'msg', | |
61 * args: [numberOfPeople, place], | |
62 * desc: 'Description of how many people are seen in a place.', | |
63 * examples: {'numberOfPeople': 3, 'place': 'London'}); | |
64 * | |
65 * Calling `howManyPeople(2, 'Athens');` would | |
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 | |
68 * output. | |
69 * | |
70 * For more detailed information on messages and localizing them see | |
71 * the main [package documentation](https://pub.dartlang.org/packages/intl) | |
72 * | |
73 * You can set the default locale. | |
74 * Intl.defaultLocale = "pt_BR"; | |
75 * | |
76 * To temporarily use a locale other than the default, use the `withLocale` | |
77 * function. | |
78 * var todayString = new DateFormat("pt_BR").format(new DateTime.now()); | |
79 * print(withLocale("pt_BR", () => today(todayString)); | |
80 * | |
81 * See `tests/message_format_test.dart` for more examples. | |
82 */ | |
83 //TODO(efortuna): documentation example involving the offset parameter? | |
84 | |
85 class Intl { | |
86 /** | |
87 * String indicating the locale code with which the message is to be | |
88 * formatted (such as en-CA). | |
89 */ | |
90 String _locale; | |
91 | |
92 /** | |
93 * The default locale. This defaults to being set from systemLocale, but | |
94 * can also be set explicitly, and will then apply to any new instances where | |
95 * the locale isn't specified. Note that a locale parameter to | |
96 * [Intl.withLocale] | |
97 * will supercede this value while that operation is active. Using | |
98 * [Intl.withLocale] may be preferable if you are using different locales | |
99 * in the same application. | |
100 */ | |
101 static String get defaultLocale { | |
102 var zoneLocale = Zone.current[#Intl.locale]; | |
103 return zoneLocale == null ? _defaultLocale : zoneLocale; | |
104 } | |
105 static set defaultLocale(String newLocale) => _defaultLocale = newLocale; | |
106 static String _defaultLocale; | |
107 | |
108 /** | |
109 * The system's locale, as obtained from the window.navigator.language | |
110 * or other operating system mechanism. Note that due to system limitations | |
111 * this is not automatically set, and must be set by importing one of | |
112 * intl_browser.dart or intl_standalone.dart and calling findSystemLocale(). | |
113 */ | |
114 static String systemLocale = 'en_US'; | |
115 | |
116 /** | |
117 * Return a new date format using the specified [pattern]. | |
118 * If [desiredLocale] is not specified, then we default to [locale]. | |
119 */ | |
120 DateFormat date([String pattern, String desiredLocale]) { | |
121 var actualLocale = (desiredLocale == null) ? locale : desiredLocale; | |
122 return new DateFormat(pattern, actualLocale); | |
123 } | |
124 | |
125 /** | |
126 * Constructor optionally [aLocale] for specifics of the language | |
127 * locale to be used, otherwise, we will attempt to infer it (acceptable if | |
128 * Dart is running on the client, we can infer from the browser/client | |
129 * preferences). | |
130 */ | |
131 Intl([String aLocale]) { | |
132 _locale = aLocale != null ? aLocale : getCurrentLocale(); | |
133 } | |
134 | |
135 /** | |
136 * Use this for a message that will be translated for different locales. The | |
137 * expected usage is that this is inside an enclosing function that only | |
138 * returns the value of this call and provides a scope for the variables that | |
139 * will be substituted in the message. | |
140 * | |
141 * The [message_str] is the string to be translated, which may be interpolated | |
142 * based on one or more variables. The [name] of the message must | |
143 * match the enclosing function name. For methods, it can also be | |
144 * className_methodName. So for a method hello in class Simple, the name | |
145 * can be either "hello" or "Simple_hello". The name must also be globally | |
146 * unique in the program, so the second form can make it easier to distinguish | |
147 * messages with the same name but in different classes. | |
148 * The [args] repeats the arguments of the enclosing | |
149 * function, [desc] provides a description of usage, | |
150 * [examples] is a Map of exmaples for each interpolated variable. For example | |
151 * hello(yourName) => Intl.message( | |
152 * "Hello, $yourName", | |
153 * name: "hello", | |
154 * args: [yourName], | |
155 * desc: "Say hello", | |
156 * examples = {"yourName": "Sparky"}. | |
157 * The source code will be processed via the analyzer to extract out the | |
158 * message data, so only a subset of valid Dart code is accepted. In | |
159 * particular, everything must be literal and cannot refer to variables | |
160 * outside the scope of the enclosing function. The [examples] map must | |
161 * be a valid const literal map. Similarly, the [desc] argument must | |
162 * be a single, simple string. These two arguments will not be used at runtime | |
163 * but will be extracted from | |
164 * the source code and used as additional data for translators. For more | |
165 * information see the "Messages" section of the main [package documentation] | |
166 * (https://pub.dartlang.org/packages/intl). | |
167 * | |
168 * The [name] and [args] arguments are required, and are used at runtime | |
169 * to look up the localized version and pass the appropriate arguments to it. | |
170 * We may in the future modify the code during compilation to make manually | |
171 * passing those arguments unnecessary. | |
172 */ | |
173 static String message(String message_str, {String desc: '', | |
174 Map<String, String> examples: const {}, String locale, String name, | |
175 List<String> args, String meaning}) { | |
176 return messageLookup.lookupMessage( | |
177 message_str, desc, examples, locale, name, args, meaning); | |
178 } | |
179 | |
180 /** | |
181 * Return the locale for this instance. If none was set, the locale will | |
182 * be the default. | |
183 */ | |
184 String get locale => _locale; | |
185 | |
186 /** | |
187 * Return true if the locale exists, or if it is null. The null case | |
188 * is interpreted to mean that we use the default locale. | |
189 */ | |
190 static bool _localeExists(localeName) => DateFormat.localeExists(localeName); | |
191 | |
192 /** | |
193 * Given [newLocale] return a locale that we have data for that is similar | |
194 * to it, if possible. | |
195 * | |
196 * If [newLocale] is found directly, return it. If it can't be found, look up | |
197 * based on just the language (e.g. 'en_CA' -> 'en'). Also accepts '-' | |
198 * as a separator and changes it into '_' for lookup, and changes the | |
199 * country to uppercase. | |
200 * | |
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, | |
210 {Function onFailure: _throwLocaleError}) { | |
211 // TODO(alanknight): Previously we kept a single verified locale on the Intl | |
212 // object, but with different verification for different uses, that's more | |
213 // 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 | |
215 // issue. | |
216 if (newLocale == null) { | |
217 return verifiedLocale(getCurrentLocale(), localeExists, | |
218 onFailure: onFailure); | |
219 } | |
220 if (localeExists(newLocale)) { | |
221 return newLocale; | |
222 } | |
223 for (var each in | |
224 [canonicalizedLocale(newLocale), shortLocale(newLocale), "fallback"]) { | |
225 if (localeExists(each)) { | |
226 return each; | |
227 } | |
228 } | |
229 return onFailure(newLocale); | |
230 } | |
231 | |
232 /** | |
233 * The default action if a locale isn't found in verifiedLocale. Throw | |
234 * an exception indicating the locale isn't correct. | |
235 */ | |
236 static String _throwLocaleError(String localeName) { | |
237 throw new ArgumentError("Invalid locale '$localeName'"); | |
238 } | |
239 | |
240 /** Return the short version of a locale name, e.g. 'en_US' => 'en' */ | |
241 static String shortLocale(String aLocale) { | |
242 if (aLocale.length < 2) return aLocale; | |
243 return aLocale.substring(0, 2).toLowerCase(); | |
244 } | |
245 | |
246 /** | |
247 * Return the name [aLocale] turned into xx_YY where it might possibly be | |
248 * in the wrong case or with a hyphen instead of an underscore. If | |
249 * [aLocale] is null, for example, if you tried to get it from IE, | |
250 * return the current system locale. | |
251 */ | |
252 static String canonicalizedLocale(String aLocale) { | |
253 // Locales of length < 5 are presumably two-letter forms, or else malformed. | |
254 // 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 | |
256 // 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. | |
258 // TODO(alanknight): en_ISO is probably not quite right for the C/Posix | |
259 // locale for formatting. Consider adding C to the formats database. | |
260 if (aLocale == null) return getCurrentLocale(); | |
261 if (aLocale == "C") return "en_ISO"; | |
262 if (aLocale.length < 5) return aLocale; | |
263 if (aLocale[2] != '-' && (aLocale[2] != '_')) return aLocale; | |
264 var region = aLocale.substring(3); | |
265 // If it's longer than three it's something odd, so don't touch it. | |
266 if (region.length <= 3) region = region.toUpperCase(); | |
267 return '${aLocale[0]}${aLocale[1]}_$region'; | |
268 } | |
269 | |
270 /** | |
271 * Format a message differently depending on [howMany]. Normally used | |
272 * as part of an `Intl.message` text that is to be translated. | |
273 * Selects the correct plural form from | |
274 * the provided alternatives. The [other] named argument is mandatory. | |
275 */ | |
276 static String plural(int howMany, {zero, one, two, few, many, other, | |
277 String desc, Map<String, String> examples, String locale, String name, | |
278 List<String> args, String meaning}) { | |
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 | |
281 // with ourselves as an argument. | |
282 if (name != null) { | |
283 return message(plural(howMany, | |
284 zero: zero, | |
285 one: one, | |
286 two: two, | |
287 few: few, | |
288 many: many, | |
289 other: other), | |
290 name: name, args: args, locale: locale, meaning: meaning); | |
291 } | |
292 if (other == null) { | |
293 throw new ArgumentError("The 'other' named argument must be provided"); | |
294 } | |
295 // TODO(alanknight): This algorithm needs to be locale-dependent. | |
296 switch (howMany) { | |
297 case 0: | |
298 return (zero == null) ? other : zero; | |
299 case 1: | |
300 return (one == null) ? other : one; | |
301 case 2: | |
302 return (two == null) ? ((few == null) ? other : few) : two; | |
303 default: | |
304 if ((howMany == 3 || howMany == 4) && few != null) return few; | |
305 if (howMany > 10 && howMany < 100 && many != null) return many; | |
306 return other; | |
307 } | |
308 throw new ArgumentError("Invalid plural usage for $howMany"); | |
309 } | |
310 | |
311 /** | |
312 * Format a message differently depending on [targetGender]. Normally used as | |
313 * part of an Intl.message message that is to be translated. | |
314 */ | |
315 static String gender(String targetGender, {String male, String female, | |
316 String other, String desc, Map<String, String> examples, String locale, | |
317 String name, List<String> args, String meaning}) { | |
318 // 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 | |
320 // with ourselves as an argument. | |
321 if (name != null) { | |
322 return message( | |
323 gender(targetGender, male: male, female: female, other: other), | |
324 name: name, args: args, locale: locale, meaning: meaning); | |
325 } | |
326 | |
327 if (other == null) { | |
328 throw new ArgumentError("The 'other' named argument must be specified"); | |
329 } | |
330 switch (targetGender) { | |
331 case "female": | |
332 return female == null ? other : female; | |
333 case "male": | |
334 return male == null ? other : male; | |
335 default: | |
336 return other; | |
337 } | |
338 } | |
339 | |
340 /** | |
341 * Format a message differently depending on [choice]. We look up the value | |
342 * of [choice] in [cases] and return the result, or an empty string if | |
343 * it is not found. Normally used as part | |
344 * of an Intl.message message that is to be translated. | |
345 */ | |
346 static String select(String choice, Map<String, String> cases, {String desc, | |
347 Map<String, String> examples, String locale, String name, | |
348 List<String> args, String meaning}) { | |
349 // 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 | |
351 // with ourselves as an argument. | |
352 if (name != null) { | |
353 return message(select(choice, cases), | |
354 name: name, args: args, locale: locale); | |
355 } | |
356 var exact = cases[choice]; | |
357 if (exact != null) return exact; | |
358 var other = cases["other"]; | |
359 if (other == | |
360 null) throw new ArgumentError("The 'other' case must be specified"); | |
361 return other; | |
362 } | |
363 | |
364 /** | |
365 * Run [function] with the default locale set to [locale] and | |
366 * return the result. | |
367 * | |
368 * This is run in a zone, so async operations invoked | |
369 * from within [function] will still have the locale set. | |
370 * | |
371 * In simple usage [function] might be a single | |
372 * `Intl.message()` call or number/date formatting operation. But it can | |
373 * also be an arbitrary function that calls multiple Intl operations. | |
374 * | |
375 * For example | |
376 * | |
377 * Intl.withLocale("fr", () => new NumberFormat.format(123456)); | |
378 * | |
379 * or | |
380 * | |
381 * hello(name) => Intl.message( | |
382 * "Hello $name.", | |
383 * name: 'hello', | |
384 * args: [name], | |
385 * desc: 'Say Hello'); | |
386 * Intl.withLocale("zh", new Timer(new Duration(milliseconds:10), | |
387 * () => print(hello("World"))); | |
388 */ | |
389 static withLocale(String locale, function()) { | |
390 var canonical = Intl.canonicalizedLocale(locale); | |
391 return runZoned(function, zoneValues: {#Intl.locale: canonical}); | |
392 } | |
393 | |
394 /** | |
395 * Accessor for the current locale. This should always == the default locale, | |
396 * unless for some reason this gets called inside a message that resets the | |
397 * locale. | |
398 */ | |
399 static String getCurrentLocale() { | |
400 if (defaultLocale == null) defaultLocale = systemLocale; | |
401 return defaultLocale; | |
402 } | |
403 | |
404 toString() => "Intl($locale)"; | |
405 } | |
OLD | NEW |