// 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; 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/number_parser_base.dart' show NumberParserBase; 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 supersede 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 /// `_`, 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? examples, String? locale, String? name, List? 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? args, String? meaning) { return _lookupMessage(messageText, locale, name, args, meaning)!; } static String? _lookupMessage(String? messageText, String? locale, String? name, List? 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? examples, String? locale, int? precision, String? name, List? 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? 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(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; } } 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? examples, String? locale, String? name, List? 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? 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(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 cases, {String? desc, Map? examples, String? locale, String? name, List? 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 cases, {String? locale, String? name, List? 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(Object choice, Map 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(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. T toBeginningOfSentenceCase(T input, [String? locale]) { if (input == null || input.isEmpty) return input; return '${_upperCaseLetter(input[0], locale)}${input.substring(1)}' as T; } /// 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(); }