| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// This library provides internationalization and localization. This includes |
| /// message formatting and replacement, date and number formatting and parsing, |
| /// and utilities for working with Bidirectional text. |
| /// |
| /// For things that require locale or other data, there are multiple different |
| /// ways of making that data available, which may require importing different |
| /// libraries. See the class comments for more details. |
| library intl; |
| |
| import 'dart:async'; |
| |
| import 'src/global_state.dart' as global_state; |
| import 'src/intl/date_format.dart' show DateFormat; |
| import 'src/intl_helpers.dart' as helpers; |
| import 'src/plural_rules.dart' as plural_rules; |
| |
| export 'src/intl/bidi.dart' show Bidi; |
| export 'src/intl/bidi_formatter.dart' show BidiFormatter; |
| export 'src/intl/date_format.dart' show DateFormat; |
| export 'src/intl/micro_money.dart' show MicroMoney; |
| export 'src/intl/number_format.dart' show NumberFormat; |
| export 'src/intl/text_direction.dart' show TextDirection; |
| |
| /// The Intl class provides a common entry point for internationalization |
| /// related tasks. An Intl instance can be created for a particular locale |
| /// and used to create a date format via `anIntl.date()`. Static methods |
| /// on this class are also used in message formatting. |
| /// |
| /// Examples: |
| /// |
| /// ```dart |
| /// String today(DateTime date) => Intl.message( |
| /// "Today's date is $date", |
| /// name: 'today', |
| /// args: [date], |
| /// desc: 'Indicate the current date', |
| /// examples: const {'date': 'June 8, 2012'}, |
| /// ); |
| /// print(today(DateTime.now().toString()); |
| /// |
| /// String howManyPeople(int numberOfPeople, String place) => Intl.plural( |
| /// numberOfPeople, |
| /// zero: 'I see no one at all in $place.', |
| /// one: 'I see $numberOfPeople other person in $place.', |
| /// other: 'I see $numberOfPeople other people in $place.', |
| /// name: 'howManyPeople', |
| /// args: [numberOfPeople, place], |
| /// desc: 'Description of how many people are seen in a place.', |
| /// examples: const {'numberOfPeople': 3, 'place': 'London'}, |
| /// ); |
| /// ``` |
| /// |
| /// Calling `howManyPeople(2, 'Athens');` would |
| /// produce "I see 2 other people in Athens." as output in the default locale. |
| /// If run in a different locale it would produce appropriately translated |
| /// output. |
| /// |
| /// You can set the default locale. |
| /// |
| /// ```dart |
| /// Intl.defaultLocale = 'pt_BR'; |
| /// ``` |
| |
| class Intl { |
| /// String indicating the locale code with which the message is to be |
| /// formatted (such as en-CA). |
| final String _locale; |
| |
| /// The default locale. This defaults to being set from systemLocale, but |
| /// can also be set explicitly, and will then apply to any new instances where |
| /// the locale isn't specified. Note that a locale parameter to |
| /// [Intl.withLocale] |
| /// will supercede this value while that operation is active. Using |
| /// [Intl.withLocale] may be preferable if you are using different locales |
| /// in the same application. |
| static String? get defaultLocale => global_state.defaultLocale; |
| |
| static set defaultLocale(String? newLocale) => |
| global_state.defaultLocale = newLocale; |
| |
| /// The system's locale, as obtained from the window.navigator.language |
| /// or other operating system mechanism. Note that due to system limitations |
| /// this is not automatically set, and must be set by importing one of |
| /// intl_browser.dart or intl_standalone.dart and calling findSystemLocale(). |
| static String get systemLocale => global_state.systemLocale; |
| static set systemLocale(String locale) => global_state.systemLocale = locale; |
| |
| /// Return a new date format using the specified [pattern]. |
| /// If [desiredLocale] is not specified, then we default to [locale]. |
| DateFormat date([String? pattern, String? desiredLocale]) { |
| var actualLocale = (desiredLocale == null) ? locale : desiredLocale; |
| return DateFormat(pattern, actualLocale); |
| } |
| |
| /// Constructor optionally [aLocale] for specifics of the language |
| /// locale to be used, otherwise, we will attempt to infer it (acceptable if |
| /// Dart is running on the client, we can infer from the browser/client |
| /// preferences). |
| Intl([String? aLocale]) : _locale = aLocale ?? getCurrentLocale(); |
| |
| /// Use this for a message that will be translated for different locales. The |
| /// expected usage is that this is inside an enclosing function that only |
| /// returns the value of this call and provides a scope for the variables that |
| /// will be substituted in the message. |
| /// |
| /// The [messageText] is the string to be translated, which may be |
| /// interpolated based on one or more variables. |
| /// |
| /// The [args] is a list containing the arguments of the enclosing function. |
| /// If there are no arguments, [args] can be omitted. |
| /// |
| /// The [name] is required only for messages that have [args], and optional |
| /// for messages without [args]. It is used at runtime to look up the message |
| /// and pass the appropriate arguments to it. If provided, [name] must be |
| /// globally unique in the program. It must match the enclosing function name, |
| /// or if the function is a method of a class, [name] can also be of the form |
| /// <className>_<methodName>, to make it easier to distinguish messages with |
| /// the same name but in different classes. |
| /// |
| /// The [desc] provides a description of the message usage. |
| /// |
| /// The [examples] is a const Map of examples for each interpolated variable. |
| /// For example |
| /// |
| /// ```dart |
| /// String hello(String yourName) => Intl.message( |
| /// 'Hello, $yourName', |
| /// name: 'hello', |
| /// args: [yourName], |
| /// desc: 'Say hello', |
| /// examples: const {'yourName': 'Sparky'}, |
| /// ); |
| /// ``` |
| /// |
| /// The source code will be processed via the analyzer to extract out the |
| /// message data, so only a subset of valid Dart code is accepted. In |
| /// particular, everything must be literal and cannot refer to variables |
| /// outside the scope of the enclosing function. The [examples] map must be a |
| /// valid const literal map. Similarly, the [desc] argument must be a single, |
| /// simple string and [skip] a boolean literal. These three arguments will not |
| /// be used at runtime but will be extracted from the source code and used as |
| /// additional data for translators. For more information see the "Messages" |
| /// section of the main |
| /// [package documentation] (https://pub.dev/packages/intl). |
| /// |
| /// The [skip] arg will still validate the message, but will be filtered from |
| /// the extracted message output. This can be useful to set up placeholder |
| /// messages during development whose text aren't finalized yet without having |
| /// the placeholder automatically translated. |
| @pragma('dart2js:tryInline') |
| @pragma('vm:prefer-inline') |
| // We want to try to inline these messages, but not inline the internal |
| // messages, so it will eliminate the descriptions and other information |
| // not needed at runtime. |
| static String message(String messageText, |
| {String? desc = '', |
| Map<String, Object>? examples, |
| String? locale, |
| String? name, |
| List<Object>? args, |
| String? meaning, |
| bool? skip}) => |
| _message(messageText, locale, name, args, meaning); |
| |
| /// Omit the compile-time only parameters so dart2js can see to drop them. |
| @pragma('dart2js:noInline') |
| static String _message(String? messageText, String? locale, String? name, |
| List<Object>? args, String? meaning) { |
| return _lookupMessage(messageText, locale, name, args, meaning)!; |
| } |
| |
| static String? _lookupMessage(String? messageText, String? locale, |
| String? name, List<Object>? args, String? meaning) { |
| return helpers.messageLookup |
| .lookupMessage(messageText, locale, name, args, meaning); |
| } |
| |
| /// Return the locale for this instance. If none was set, the locale will |
| /// be the default. |
| String get locale => _locale; |
| |
| /// Given [newLocale] return a locale that we have data for that is similar |
| /// to it, if possible. |
| /// |
| /// If [newLocale] is found directly, return it. If it can't be found, look up |
| /// based on just the language (e.g. 'en_CA' -> 'en'). Also accepts '-' |
| /// as a separator and changes it into '_' for lookup, and changes the |
| /// country to uppercase. |
| /// |
| /// There is a special case that if a locale named "fallback" is present |
| /// and has been initialized, this will return that name. This can be useful |
| /// for messages where you don't want to just use the text from the original |
| /// source code, but wish to have a universal fallback translation. |
| /// |
| /// Note that null is interpreted as meaning the default locale, so if |
| /// [newLocale] is null the default locale will be returned. |
| /// |
| /// Can return `null` only if verification fails and `onFailure` returns |
| /// null. Otherwise, throws instead. |
| static String? verifiedLocale( |
| String? newLocale, bool Function(String) localeExists, |
| {String? Function(String)? onFailure}) => |
| helpers.verifiedLocale(newLocale, localeExists, onFailure); |
| |
| /// Return the short version of a locale name, e.g. 'en_US' => 'en' |
| static String shortLocale(String aLocale) => helpers.shortLocale(aLocale); |
| |
| /// Return the name [aLocale] turned into xx_YY where it might possibly be |
| /// in the wrong case or with a hyphen instead of an underscore. If |
| /// [aLocale] is null, for example, if you tried to get it from IE, |
| /// return the current system locale. |
| static String canonicalizedLocale(String? aLocale) => |
| helpers.canonicalizedLocale(aLocale); |
| |
| /// Formats a message differently depending on [howMany]. |
| /// |
| /// Selects the correct plural form from the provided alternatives. |
| /// The [other] named argument is mandatory. |
| /// The [precision] is the number of fractional digits that would be rendered |
| /// when [howMany] is formatted. In some cases just knowing the numeric value |
| /// of [howMany] itself is not enough, for example "1 mile" vs "1.00 miles" |
| /// |
| /// For an explanation of plurals and the [zero], [one], [two], [few], [many] |
| /// categories see http://cldr.unicode.org/index/cldr-spec/plural-rules |
| @pragma('dart2js:tryInline') |
| @pragma('vm:prefer-inline') |
| static String plural(num howMany, |
| {String? zero, |
| String? one, |
| String? two, |
| String? few, |
| String? many, |
| required String other, |
| String? desc, |
| Map<String, Object>? examples, |
| String? locale, |
| int? precision, |
| String? name, |
| List<Object>? args, |
| String? meaning, |
| bool? skip}) { |
| // Call our internal method, dropping examples and desc because they're not |
| // used at runtime and we want them to be optimized away. |
| return _plural(howMany, |
| zero: zero, |
| one: one, |
| two: two, |
| few: few, |
| many: many, |
| other: other, |
| locale: locale, |
| precision: precision, |
| name: name, |
| args: args, |
| meaning: meaning); |
| } |
| |
| @pragma('dart2js:noInline') |
| static String _plural(num howMany, |
| {String? zero, |
| String? one, |
| String? two, |
| String? few, |
| String? many, |
| required String other, |
| String? locale, |
| int? precision, |
| String? name, |
| List<Object>? args, |
| String? meaning}) { |
| // Look up our translation, but pass in a null message so we don't have to |
| // eagerly evaluate calls that may not be necessary. |
| var translated = _lookupMessage(null, locale, name, args, meaning); |
| |
| /// If there's a translation, return it, otherwise evaluate with our |
| /// original text. |
| return translated ?? |
| pluralLogic(howMany, |
| zero: zero, |
| one: one, |
| two: two, |
| few: few, |
| many: many, |
| other: other, |
| locale: locale, |
| precision: precision); |
| } |
| |
| /// Internal: Implements the logic for plural selection - use [plural] for |
| /// normal messages. |
| static T pluralLogic<T>(num howMany, |
| {T? zero, |
| T? one, |
| T? two, |
| T? few, |
| T? many, |
| required T other, |
| String? locale, |
| int? precision, |
| String? meaning, |
| bool useExplicitNumberCases = true}) { |
| ArgumentError.checkNotNull(other, 'other'); |
| ArgumentError.checkNotNull(howMany, 'howMany'); |
| // If we haven't specified precision and we have a float that is an integer |
| // value, turn it into an integer. This gives us the behavior that 1.0 and 1 |
| // produce the same output, e.g. 1 dollar. |
| var truncated = howMany.truncate(); |
| if (precision == null && truncated == howMany) { |
| howMany = truncated; |
| } |
| |
| // This is for backward compatibility. |
| // We interpret the presence of [precision] parameter as an "opt-in" to |
| // the new behavior, since [precision] did not exist before. |
| // For an English example: if the precision is 2 then the formatted string |
| // would not map to 'one' (for example "1.00 miles") |
| if (useExplicitNumberCases && (precision == null || precision == 0)) { |
| // If there's an explicit case for the exact number, we use it. This is |
| // not strictly in accord with the CLDR rules, but it seems to be the |
| // expectation. At least I see e.g. Russian translations that have a zero |
| // case defined. The rule for that locale will never produce a zero, and |
| // treats it as other. But it seems reasonable that, even if the language |
| // rules treat zero as other, we might want a special message for zero. |
| if (howMany == 0 && zero != null) return zero; |
| if (howMany == 1 && one != null) return one; |
| if (howMany == 2 && two != null) return two; |
| } |
| |
| var pluralRule = _pluralRule(locale, howMany, precision); |
| var pluralCase = pluralRule(); |
| switch (pluralCase) { |
| case plural_rules.PluralCase.ZERO: |
| return zero ?? other; |
| case plural_rules.PluralCase.ONE: |
| return one ?? other; |
| case plural_rules.PluralCase.TWO: |
| return two ?? few ?? other; |
| case plural_rules.PluralCase.FEW: |
| return few ?? other; |
| case plural_rules.PluralCase.MANY: |
| return many ?? other; |
| case plural_rules.PluralCase.OTHER: |
| return other; |
| default: |
| throw ArgumentError.value( |
| howMany, 'howMany', 'Invalid plural argument'); |
| } |
| } |
| |
| static plural_rules.PluralRule? _cachedPluralRule; |
| static String? _cachedPluralLocale; |
| |
| static plural_rules.PluralRule _pluralRule( |
| String? locale, num howMany, int? precision) { |
| plural_rules.startRuleEvaluation(howMany, precision); |
| var verifiedLocale = Intl.verifiedLocale( |
| locale, plural_rules.localeHasPluralRules, |
| onFailure: (locale) => 'default'); |
| if (_cachedPluralLocale == verifiedLocale) { |
| return _cachedPluralRule!; |
| } else { |
| _cachedPluralRule = plural_rules.pluralRules[verifiedLocale]; |
| _cachedPluralLocale = verifiedLocale; |
| return _cachedPluralRule!; |
| } |
| } |
| |
| /// Format a message differently depending on [targetGender]. |
| @pragma('dart2js:tryInline') |
| @pragma('vm:prefer-inline') |
| static String gender(String targetGender, |
| {String? female, |
| String? male, |
| required String other, |
| String? desc, |
| Map<String, Object>? examples, |
| String? locale, |
| String? name, |
| List<Object>? args, |
| String? meaning, |
| bool? skip}) { |
| // Call our internal method, dropping args and desc because they're not used |
| // at runtime and we want them to be optimized away. |
| return _gender(targetGender, |
| male: male, |
| female: female, |
| other: other, |
| locale: locale, |
| name: name, |
| args: args, |
| meaning: meaning); |
| } |
| |
| @pragma('dart2js:noInline') |
| static String _gender(String targetGender, |
| {String? female, |
| String? male, |
| required String other, |
| String? locale, |
| String? name, |
| List<Object>? args, |
| String? meaning}) { |
| // Look up our translation, but pass in a null message so we don't have to |
| // eagerly evaluate calls that may not be necessary. |
| var translated = _lookupMessage(null, locale, name, args, meaning); |
| |
| /// If there's a translation, return it, otherwise evaluate with our |
| /// original text. |
| return translated ?? |
| genderLogic(targetGender, |
| female: female, male: male, other: other, locale: locale); |
| } |
| |
| /// Internal: Implements the logic for gender selection - use [gender] for |
| /// normal messages. |
| static T genderLogic<T>(String targetGender, |
| {T? female, T? male, required T other, String? locale}) { |
| ArgumentError.checkNotNull(other, 'other'); |
| switch (targetGender) { |
| case 'female': |
| return female ?? other; |
| case 'male': |
| return male ?? other; |
| default: |
| return other; |
| } |
| } |
| |
| /// Format a message differently depending on [choice]. |
| /// |
| /// We look up the value |
| /// of [choice] in [cases] and return the result, or an empty string if |
| /// it is not found. Normally used as part |
| /// of an Intl.message message that is to be translated. |
| /// |
| /// It is possible to use a Dart enum as the choice and as the |
| /// key in cases, but note that we will process this by truncating |
| /// toString() of the enum and using just the name part. We will |
| /// do this for any class or strings that are passed, since we |
| /// can't actually identify if something is an enum or not. |
| @pragma('dart2js:tryInline') |
| @pragma('vm:prefer-inline') |
| static String select(Object choice, Map<Object, String> cases, |
| {String? desc, |
| Map<String, Object>? examples, |
| String? locale, |
| String? name, |
| List<Object>? args, |
| String? meaning, |
| bool? skip}) { |
| return _select(choice, cases, |
| locale: locale, name: name, args: args, meaning: meaning); |
| } |
| |
| @pragma('dart2js:noInline') |
| static String _select(Object choice, Map<Object, String> cases, |
| {String? locale, String? name, List<Object>? args, String? meaning}) { |
| if (choice is! String && args != null) { |
| var stringChoice = '$choice'.split('.').last; |
| args = args.map((a) => identical(a, choice) ? stringChoice : a).toList(); |
| } |
| // Look up our translation, but pass in a null message so we don't have to |
| // eagerly evaluate calls that may not be necessary. |
| var translated = _lookupMessage(null, locale, name, args, meaning); |
| |
| /// If there's a translation, return it, otherwise evaluate with our |
| /// original text. |
| return translated ?? selectLogic(choice, cases); |
| } |
| |
| /// Internal: Implements the logic for select - use [select] for |
| /// normal messages. |
| static T selectLogic<T>(Object choice, Map<Object, T> cases) { |
| // This will work if choice is a string, or if it's e.g. an |
| // enum and the map uses the enum values as choices. |
| var exact = cases[choice]; |
| if (exact != null) return exact; |
| // If it didn't match exactly, take the toString and |
| // take the part after the period. We need to do this |
| // because enums print as 'EnumType.enumName' and periods |
| // aren't acceptable in ICU select choices. |
| var stringChoice = '$choice'.split('.').last; |
| var stringMatch = cases[stringChoice]; |
| if (stringMatch != null) return stringMatch; |
| var other = cases['other']; |
| if (other == null) { |
| throw ArgumentError("The 'other' case must be specified"); |
| } |
| return other; |
| } |
| |
| /// Run [function] with the default locale set to [locale] and |
| /// return the result. |
| /// |
| /// This is run in a zone, so async operations invoked |
| /// from within [function] will still have the locale set. |
| /// |
| /// In simple usage [function] might be a single |
| /// `Intl.message()` call or number/date formatting operation. But it can |
| /// also be an arbitrary function that calls multiple Intl operations. |
| /// |
| /// For example |
| /// |
| /// Intl.withLocale('fr', () => NumberFormat.format(123456)); |
| /// |
| /// or |
| /// |
| /// hello(name) => Intl.message( |
| /// 'Hello $name.', |
| /// name: 'hello', |
| /// args: [name], |
| /// desc: 'Say Hello'); |
| /// Intl.withLocale('zh', Timer(Duration(milliseconds:10), |
| /// () => print(hello('World'))); |
| static dynamic withLocale<T>(String? locale, T Function() function) { |
| // TODO(alanknight): Make this return T. This requires work because T might |
| // be Future and the caller could get an unawaited Future. Which is |
| // probably an error in their code, but the change is semi-breaking. |
| var canonical = Intl.canonicalizedLocale(locale); |
| return runZoned(function, zoneValues: {#Intl.locale: canonical}); |
| } |
| |
| /// Accessor for the current locale. This should always == the default locale, |
| /// unless for some reason this gets called inside a message that resets the |
| /// locale. |
| static String getCurrentLocale() { |
| return defaultLocale ??= systemLocale; |
| } |
| |
| @override |
| String toString() => 'Intl($locale)'; |
| } |
| |
| /// Convert a string to beginning of sentence case, in a way appropriate to the |
| /// locale. |
| /// |
| /// Currently this just converts the first letter to uppercase, which works for |
| /// many locales, and we have the option to extend this to handle more cases |
| /// without changing the API for clients. It also hard-codes the case of |
| /// dotted i in Turkish and Azeri. |
| String? toBeginningOfSentenceCase(String? input, [String? locale]) { |
| if (input == null || input.isEmpty) return input; |
| return '${_upperCaseLetter(input[0], locale)}${input.substring(1)}'; |
| } |
| |
| /// Convert the input single-letter string to upper case. A trivial |
| /// hard-coded implementation that only handles simple upper case |
| /// and the dotted i in Turkish/Azeri. |
| /// |
| /// Private to the implementation of [toBeginningOfSentenceCase]. |
| // TODO(alanknight): Consider hard-coding other important cases. |
| // See http://www.unicode.org/Public/UNIDATA/SpecialCasing.txt |
| // TODO(alanknight): Alternatively, consider toLocaleUpperCase in browsers. |
| // See also https://github.com/dart-lang/sdk/issues/6706 |
| String _upperCaseLetter(String input, String? locale) { |
| // Hard-code the important edge case of i->İ |
| if (locale != null) { |
| if (input == 'i' && (locale.startsWith('tr') || locale.startsWith('az'))) { |
| return '\u0130'; |
| } |
| } |
| return input.toUpperCase(); |
| } |