Implement MessageFormat, equivalent to the Closure goog.i18n.MessageFormat
The implementation is ported directly from Closure.
PiperOrigin-RevId: 279860630
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfa16f5..750d93d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@
will get turned into the correct 'en_US'
* Attempt to compensate for erratic errors in DateTime creation better, and add
tests for the compensation.
+ * Add a MessageFormat class. It can prepares strings for display to users,
+ with optional arguments (variables/placeholders). Common data types will
+ be formatted properly for the given locale. It handles both pluralization
+ and gender. Think of it as "internationalization aware printf."
## 0.16.0
* Fix 'k' formatting (1 to 24 hours) which incorrectly showed 0 to 23.
diff --git a/lib/message_format.dart b/lib/message_format.dart
new file mode 100644
index 0000000..86d4d7b
--- /dev/null
+++ b/lib/message_format.dart
@@ -0,0 +1,826 @@
+// Copyright (c) 2019, 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.
+
+/// `MessageFormat` is a "locale aware printf", with plural / gender support.
+///
+/// `MessageFormat` prepares strings for display to users, with optional
+/// arguments (variables/placeholders). The arguments can occur in any order,
+/// which is necessary for translation into languages with different grammars.
+/// It supports syntax to represent plurals and select options.
+library message_format;
+
+import 'dart:collection';
+import 'intl.dart';
+
+/// **MessageFormat grammar:**
+/// ```
+/// message := messageText (argument messageText)*
+/// argument := simpleArg | pluralArg | selectArg
+///
+/// simpleArg := "#" | "{" argNameOrNumber "}"
+/// pluralArg := "{" argNameOrNumber "," "plural" "," pluralStyle "}"
+/// selectArg := "{" argNameOrNumber "," "select" "," selectStyle "}"
+///
+/// argNameOrNumber := identifier | number
+///
+/// pluralStyle := [offsetValue] (pluralSelector "{" message "}")+
+/// offsetValue := "offset:" number
+/// pluralSelector := explicitValue | pluralKeyword
+/// explicitValue := "=" number // adjacent, no white space in between
+/// pluralKeyword := "zero" | "one" | "two" | "few" | "many" | "other"
+///
+/// selectStyle := (selectSelector "{" message "}")+
+/// selectSelector := keyword
+///
+/// identifier := [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
+/// number := "0" | ("1".."9" ("0".."9")*)
+/// ```
+///
+/// **NOTE:** "#" has special meaning only inside a plural block.
+/// It is "connected" to the argument of the plural, but the value of #
+/// is the value of the plural argument minus the offset.
+///
+/// **Quoting/Escaping:** if syntax characters occur in the text portions,
+/// then they need to be quoted by enclosing the syntax in pairs of ASCII
+/// apostrophes.
+///
+/// A pair of ASCII apostrophes always represents one ASCII apostrophe,
+/// similar to %% in printf representing one %, although this rule still
+/// applies inside quoted text.
+///
+/// ("This '{isn''t}' obvious" → "This {isn't} obvious")
+///
+/// An ASCII apostrophe only starts quoted text if it immediately precedes
+/// a character that requires quoting (that is, "only where needed"), and
+/// works the same in nested messages as on the top level of the pattern.
+///
+/// **Recommendation:** Use the real apostrophe (single quote) character ’
+/// (U+2019) for human-readable text, and use the ASCII apostrophe ' (U+0027)
+/// only in program syntax, like escaping.
+///
+/// This is a subset of the ICU MessageFormat syntax:
+/// http://userguide.icu-project.org/formatparse/messages.
+///
+/// **Message example:**
+/// ```
+/// I see {NUM_PEOPLE, plural, offset:1
+/// =0 {no one at all}
+/// =1 {{WHO}}
+/// one {{WHO} and one other person}
+/// other {{WHO} and # other people}}
+/// in {PLACE}.
+/// ```
+///
+/// Calling `format({'NUM_PEOPLE': 2, 'WHO': 'Mark', 'PLACE': 'Athens'})` would
+/// produce `"I see Mark and one other person in Athens."` as output.
+///
+/// Calling `format({'NUM_PEOPLE': 5, 'WHO': 'Mark', 'PLACE': 'Athens'})` would
+/// produce `"I see Mark and one 4 other people in Athens."` as output.
+/// Notice how the "#" is the value of `NUM_PEOPLE` - 1 (the offset).
+///
+/// Another important thing to notice is the existence of both `"=1"` and
+/// `"one"`. You should think of the plural keywords as names for "buckets of
+/// numbers" which have only a loose connection to the numerical value.
+///
+/// In English there is no difference, but for example in Russian all the
+/// numbers that end with `"1"` but not with `"11"` are mapped to `"one"`
+///
+/// For more information please visit:
+/// http://cldr.unicode.org/index/cldr-spec/plural-rules and
+/// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
+
+// The implementation is based on the
+// [Closure goog.i18n.MessageFormat](https://google.github.io/closure-library/api/goog.i18n.MessageFormat.html)
+// sources at https://github.com/google/closure-library/blob/master/closure/goog/i18n/messageformat.js,
+// so we should try to keep potential fixes in sync.
+//
+// The initial parsing done by [_extractParts] breaks the pattern into top
+// level strings and {...} blocks [_ElementTypeAndVal]
+//
+// The values are all strings, but the ones that contain curly brackets
+// are classified as `blocks` and will be parsed again (recursively)
+//
+// The second round of parsing takes the parts from above and refines them
+// into _BlockTypeAndVal. After this point we have different types of blocks
+// (string, plural, ordinal, select, ...)
+
+class MessageFormat {
+ /// The locale to use for plural, ordinal, decisions,
+ /// number / date / time formatting
+ String _locale;
+
+ /// The pattern we parse and apply positional parameters to.
+ String _pattern;
+
+ /// All encountered literals during parse stage.
+ Queue<String> _initialLiterals;
+
+ /// Working list with all encountered literals during parse and format stages.
+ Queue<String> _literals;
+
+ /// Input pattern gets parsed into objects for faster formatting.
+ Queue<_BlockTypeAndVal> _parsedPattern;
+
+ /// Locale aware number formatter.
+ NumberFormat _numberFormat;
+
+ /// Literal strings, including '', are replaced with \uFDDF_x_ for parsing.
+ ///
+ /// They are recovered during format phase.
+ /// \uFDDF is a Unicode nonprinting character, not expected to be found in the
+ /// typical message.
+ static const String _literalPlaceholder = '\uFDDF_';
+
+ /// Mandatory option in both select and plural form.
+ static const String _other = 'other';
+
+ /// Regular expression for looking for string literals.
+ static final RegExp _regexLiteral = RegExp("'([{}#].*?)'");
+
+ /// Regular expression for looking for '' in the message.
+ static final RegExp _regexDoubleApostrophe = RegExp("''");
+
+ /// Create a MessageFormat for the ICU message string [pattern].
+ /// It does parameter substitutions in a locale-aware way.
+ /// The syntax is similar to the one used by ICU and is described in the
+ /// grammar above.
+ MessageFormat(String pattern, {String locale = 'en'}) {
+ _locale = locale;
+ _pattern = pattern;
+ _numberFormat = NumberFormat.decimalPattern(locale);
+ }
+
+ /// Returns a formatted message, treating '#' as a special placeholder.
+ ///
+ /// It represents the number (plural_variable - offset).
+ ///
+ /// The [namedParameters] either influence the formatting or are used as
+ /// actual data.
+ /// I.e. in call to `fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'})`, the
+ /// map `{'NUM_PEOPLE': 5, 'NAME': 'Angela'}` holds parameters.
+ /// `NUM_PEOPLE` parameter could mean 5 people, which could influence plural
+ /// format, and `NAME` parameter is just a data to be printed out in proper
+ /// position.
+ String format([Map<String, Object> namedParameters]) {
+ return _format(false, namedParameters);
+ }
+
+ /// Returns a formatted message, treating '#' as literal character.
+ ///
+ /// The [namedParameters] either influence the formatting or are used as
+ /// actual data.
+ /// I.e. in call to `fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'})`, the
+ /// map `{'NUM_PEOPLE': 5, 'NAME': 'Angela'}` holds positional parameters.
+ /// `NUM_PEOPLE` parameter could mean 5 people, which could influence plural
+ /// format, and `NAME` parameter is just a data to be printed out in proper
+ /// position.
+ String formatIgnoringPound([Map<String, Object> namedParameters]) {
+ return _format(true, namedParameters);
+ }
+
+ /// Returns a formatted message.
+ ///
+ /// The [namedParameters] either influence the formatting or are used as
+ /// actual data.
+ /// I.e. in call to `fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'})`, the
+ /// map `{'NUM_PEOPLE': 5, 'NAME': 'Angela'}` holds positional parameters.
+ /// `NUM_PEOPLE` parameter could mean 5 people, which could influence plural
+ /// format, and `NAME` parameter is just a data to be printed out in proper
+ /// position.
+ /// If [ignorePound] is true, treat '#' in plural messages as a
+ /// literal character, else treat it as an ICU syntax character, resolving
+ /// to the number (plural_variable - offset).
+ String _format(bool ignorePound, [Map<String, Object> namedParameters]) {
+ _init();
+ if (_parsedPattern == null || _parsedPattern.isEmpty) {
+ return '';
+ }
+ // Clone, we don't want to damage the original
+ _literals = Queue<String>()..addAll(_initialLiterals);
+
+ // Implementation notes: this seems inefficient, we could in theory do the
+ // replace + join in one go.
+ // But would make the code even more unreadable than it is.
+ //
+ // `_formatBlock` replaces "full blocks"
+ // For example replaces this:
+ // `... {count, plural, =1 {one file} few {...} many {...} other {# files} ...`
+ // with
+ // `... one file ...`
+ //
+ // The replace after that (with `message.replaceFirst`) is only replacing
+ // simple parameters (`...{expDate} ... {count}...`)
+ //
+ // So `_formatBlock` is ugly, potentially recursive.
+ // `message.replaceFirst` is very simple, flat.
+ //
+ // I agree that there might be some performance loss.
+ // But in real use the messages don't have that many arguments.
+ // If we think printf, how many arguments are common?
+ // Probably less than 5 or so.
+ var messageParts = Queue<String>();
+ _formatBlock(_parsedPattern, namedParameters, ignorePound, messageParts);
+ var message = messageParts.join('');
+
+ if (!ignorePound) {
+ _checkAndThrow(!message.contains('#'), 'Not all # were replaced.');
+ }
+
+ while (_literals.isNotEmpty) {
+ message = message.replaceFirst(
+ _buildPlaceholder(_literals), _literals.removeLast());
+ }
+
+ return message;
+ }
+
+ /// Takes the parsed tree and the parameters, appending to result.
+ ///
+ /// The [parsedBlocks] parameter holds parsed tree.
+ /// [namedParameters] are parameters that either influence the formatting
+ /// or are used as actual data.
+ /// If [ignorePound] is true, treat '#' in plural messages as a
+ /// literal character, else treat it as an ICU syntax character, resolving
+ /// to the number (plural_variable - offset).
+ /// Each formatting stage appends its product to the [result].
+ /// It can be recursive, as plural / select contain full message patterns.
+ void _formatBlock(
+ Queue<_BlockTypeAndVal> parsedBlocks,
+ Map<String, Object> namedParameters,
+ bool ignorePound,
+ Queue<String> result) {
+ for (var currentPattern in parsedBlocks) {
+ var patternValue = currentPattern._value;
+ var patternType = currentPattern._type;
+
+ _checkAndThrow(patternType is _BlockType,
+ 'The type should be a block type: $patternType');
+ switch (patternType) {
+ case _BlockType.string:
+ result.add(patternValue);
+ break;
+ case _BlockType.simple:
+ _formatSimplePlaceholder(patternValue, namedParameters, result);
+ break;
+ case _BlockType.select:
+ _checkAndThrow(patternValue is Map<String, Object>,
+ 'The value should be a map: $patternValue');
+ Map<String, Object> mapPattern = patternValue;
+ _formatSelectBlock(mapPattern, namedParameters, ignorePound, result);
+ break;
+ case _BlockType.plural:
+ _formatPluralOrdinalBlock(patternValue, namedParameters,
+ _PluralRules.select, ignorePound, result);
+ break;
+ case _BlockType.ordinal:
+ _formatPluralOrdinalBlock(patternValue, namedParameters,
+ _OrdinalRules.select, ignorePound, result);
+ break;
+ default:
+ _checkAndThrow(false, 'Unrecognized block type: $patternType');
+ }
+ }
+ }
+
+ /// Formats a simple placeholder.
+ ///
+ /// [parsedBlocks] is an object containing placeholder info.
+ /// The [namedParameters] that are used as actual data.
+ /// Each formatting stage appends its product to the [result].
+ void _formatSimplePlaceholder(String parsedBlocks,
+ Map<String, Object> namedParameters, Queue<String> result) {
+ var value = namedParameters[parsedBlocks];
+ if (!_isDef(value)) {
+ result.add('Undefined parameter - $parsedBlocks');
+ return;
+ }
+
+ // Don't push the value yet, it may contain any of # { } in it which
+ // will break formatter. Insert a placeholder and replace at the end.
+ String strValue;
+ if (value is int) {
+ strValue = _numberFormat.format(value);
+ } else if (value is String) {
+ strValue = value;
+ } else {
+ strValue = value.toString();
+ }
+ _literals.add(strValue);
+ result.add(_buildPlaceholder(_literals));
+ }
+
+ /// Formats select block. Only one option is selected.
+ ///
+ /// [parsedBlocks] is an object containing select block info.
+ /// [namedParameters] are parameters that either influence the formatting
+ /// or are used as actual data.
+ /// If [ignorePound] is true, treat '#' in plural messages as a
+ /// literal character, else treat it as an ICU syntax character, resolving
+ /// to the number (plural_variable - offset).
+ /// Each formatting stage appends its product to the [result].
+ void _formatSelectBlock(
+ Map<String, Object> parsedBlocks,
+ Map<String, Object> namedParameters,
+ bool ignorePound,
+ Queue<String> result) {
+ var argumentName = parsedBlocks['argumentName'];
+ if (!_isDef(namedParameters[argumentName])) {
+ result.add('Undefined parameter - $argumentName');
+ return;
+ }
+
+ var option = parsedBlocks[namedParameters[argumentName]];
+ if (!_isDef(option)) {
+ option = parsedBlocks[_other];
+ _checkAndThrow(option != null,
+ 'Invalid option or missing other option for select block.');
+ }
+
+ _formatBlock(option, namedParameters, ignorePound, result);
+ }
+
+ /// Formats `plural` / `selectordinal` block, selects an option, replaces `#`
+ ///
+ /// [parsedBlocks] is an object containing plural block info.
+ /// [namedParameters] are parameters that either influence the formatting
+ /// or are used as actual data.
+ /// The [pluralSelector] is a select function from pluralRules or ordinalRules
+ /// which determines which plural/ordinal form to use based on the input
+ /// number's cardinality.
+ /// If [ignorePound] is true, treat '#' in plural messages as a
+ /// literal character, else treat it as an ICU syntax character, resolving
+ /// to the number (plural_variable - offset).
+ /// Each formatting stage appends its product to the [result].
+ void _formatPluralOrdinalBlock(
+ Map<String, Object> parsedBlocks,
+ var namedParameters,
+ Function(num, String) pluralSelector,
+ bool ignorePound,
+ Queue<String> result) {
+ var argumentName = parsedBlocks['argumentName'];
+ var argumentOffset = parsedBlocks['argumentOffset'];
+ var pluralValue = namedParameters[argumentName];
+
+ if (!_isDef(pluralValue)) {
+ result.add('Undefined parameter - $argumentName');
+ return;
+ }
+
+ var numPluralValue =
+ pluralValue is num ? pluralValue : double.tryParse(pluralValue);
+ if (numPluralValue == null) {
+ result.add('Invalid parameter - $argumentName');
+ return;
+ }
+
+ var numArgumentOffset = argumentOffset is num
+ ? argumentOffset
+ : double.tryParse(argumentOffset);
+ if (numArgumentOffset == null) {
+ result.add('Invalid offset - $argumentOffset');
+ return;
+ }
+
+ var diff = numPluralValue - numArgumentOffset;
+
+ // Check if there is an exact match.
+ var option = parsedBlocks[namedParameters[argumentName]];
+ if (!_isDef(option)) {
+ option = parsedBlocks[namedParameters[argumentName].toString()];
+ }
+ if (!_isDef(option)) {
+ var item = pluralSelector(diff.abs(), _locale);
+ _checkAndThrow(item is String, 'Invalid plural key.');
+
+ option = parsedBlocks[item];
+
+ // If option is not provided fall back to "other".
+ if (!_isDef(option)) {
+ option = parsedBlocks[_other];
+ }
+
+ _checkAndThrow(option != null,
+ 'Invalid option or missing other option for plural block.');
+ }
+
+ var pluralResult = Queue<String>();
+ _formatBlock(option, namedParameters, ignorePound, pluralResult);
+ var plural = pluralResult.join('');
+ _checkAndThrow(plural is String, 'Empty block in plural.');
+ if (ignorePound) {
+ result.add(plural);
+ } else {
+ var localeAwareDiff = _numberFormat.format(diff);
+ result.add(plural.replaceAll('#', localeAwareDiff));
+ }
+ }
+
+ /// Set up the MessageFormat.
+ ///
+ /// Parses input pattern into an array, for faster reformatting with
+ /// different input parameters.
+ /// Parsing is locale independent.
+ void _init() {
+ if (_pattern != null) {
+ _initialLiterals = Queue<String>();
+ var pattern = _insertPlaceholders(_pattern);
+
+ _parsedPattern = _parseBlock(pattern);
+ _pattern = null;
+ }
+ }
+
+ /// Replaces string literals with literal placeholders in [pattern].
+ ///
+ /// Literals are string of the form '}...', '{...' and '#...' where ... is
+ /// set of characters not containing '
+ /// Builds a dictionary so we can recover literals during format phase.
+ String _insertPlaceholders(String pattern) {
+ var literals = _initialLiterals;
+ var buildPlaceholder = _buildPlaceholder;
+
+ // First replace '' with single quote placeholder since they can be found
+ // inside other literals.
+ pattern = pattern.replaceAllMapped(_regexDoubleApostrophe, (match) {
+ literals.add("'");
+ return buildPlaceholder(literals);
+ });
+
+ pattern = pattern.replaceAllMapped(_regexLiteral, (match) {
+ // match, text
+ var text = match.group(1);
+ literals.add(text);
+ return buildPlaceholder(literals);
+ });
+
+ return pattern;
+ }
+
+ /// Breaks [pattern] into strings and top level {...} blocks.
+ Queue<_ElementTypeAndVal> _extractParts(String pattern) {
+ var prevPos = 0;
+ var braceStack = Queue<String>();
+ var results = Queue<_ElementTypeAndVal>();
+
+ var braces = RegExp('[{}]');
+
+ Match match;
+ for (match in braces.allMatches(pattern)) {
+ var pos = match.start;
+ if (match[0] == '}') {
+ String brace;
+ try {
+ brace = braceStack.removeLast();
+ } on StateError {
+ _checkAndThrow(brace != '}', 'No matching } for {.');
+ }
+ _checkAndThrow(brace == '{', 'No matching { for }.');
+
+ if (braceStack.isEmpty) {
+ // End of the block.
+ var part = _ElementTypeAndVal(
+ _ElementType.block, pattern.substring(prevPos, pos));
+ results.add(part);
+ prevPos = pos + 1;
+ }
+ } else {
+ if (braceStack.isEmpty) {
+ var substring = pattern.substring(prevPos, pos);
+ if (substring != '') {
+ results.add(_ElementTypeAndVal(_ElementType.string, substring));
+ }
+ prevPos = pos + 1;
+ }
+ braceStack.add('{');
+ }
+ }
+
+ // Take care of the final string, and check if the braceStack is empty.
+ _checkAndThrow(
+ braceStack.isEmpty, 'There are mismatched { or } in the pattern.');
+
+ var substring = pattern.substring(prevPos);
+ if (substring != '') {
+ results.add(_ElementTypeAndVal(_ElementType.string, substring));
+ }
+
+ return results;
+ }
+
+ /// A regular expression to parse the plural block.
+ ///
+ /// It extracts the argument index and offset (if any).
+ static final RegExp _pluralBlockRe =
+ RegExp('^\\s*(\\w+)\\s*,\\s*plural\\s*,(?:\\s*offset:(\\d+))?');
+
+ /// A regular expression to parse the ordinal block.
+ ///
+ /// It extracts the argument index.
+ static final RegExp _ordinalBlockRe =
+ RegExp('^\\s*(\\w+)\\s*,\\s*selectordinal\\s*,');
+
+ /// A regular expression to parse the select block.
+ ///
+ /// It extracts the argument index.
+ static final RegExp _selectBlockRe =
+ RegExp('^\\s*(\\w+)\\s*,\\s*select\\s*,');
+
+ /// Detects the block type of the [pattern].
+ _BlockType _parseBlockType(String pattern) {
+ if (_pluralBlockRe.hasMatch(pattern)) {
+ return _BlockType.plural;
+ }
+
+ if (_ordinalBlockRe.hasMatch(pattern)) {
+ return _BlockType.ordinal;
+ }
+
+ if (_selectBlockRe.hasMatch(pattern)) {
+ return _BlockType.select;
+ }
+
+ if (RegExp('^\\s*\\w+\\s*').hasMatch(pattern)) {
+ return _BlockType.simple;
+ }
+
+ return _BlockType.unknown;
+ }
+
+ /// Parses generic block.
+ ///
+ /// Takes the [pattern], which is the content of the block to parse,
+ /// and returns sub-blocks marked as strings, select, plural, ...
+ Queue<_BlockTypeAndVal> _parseBlock(String pattern) {
+ var result = Queue<_BlockTypeAndVal>();
+ var parts = _extractParts(pattern);
+ for (var thePart in parts) {
+ _BlockTypeAndVal block;
+ if (_ElementType.string == thePart._type) {
+ block = _BlockTypeAndVal(_BlockType.string, thePart._value);
+ } else if (_ElementType.block == thePart._type) {
+ _checkAndThrow(thePart._value is String,
+ 'The value should be a string: ${thePart._value}');
+ var blockType = _parseBlockType(thePart._value);
+
+ switch (blockType) {
+ case _BlockType.select:
+ block = _BlockTypeAndVal(
+ _BlockType.select, _parseSelectBlock(thePart._value));
+ break;
+ case _BlockType.plural:
+ block = _BlockTypeAndVal(
+ _BlockType.plural, _parsePluralBlock(thePart._value));
+ break;
+ case _BlockType.ordinal:
+ block = _BlockTypeAndVal(
+ _BlockType.ordinal, _parseOrdinalBlock(thePart._value));
+ break;
+ case _BlockType.simple:
+ block = _BlockTypeAndVal(_BlockType.simple, thePart._value);
+ break;
+ default:
+ _checkAndThrow(
+ false, 'Unknown block type for pattern: ${thePart._value}');
+ }
+ } else {
+ _checkAndThrow(false, 'Unknown part of the pattern.');
+ }
+ result.add(block);
+ }
+
+ return result;
+ }
+
+ /// Parses a select type of a block and produces an object for it.
+ ///
+ /// The [pattern] is the sub-pattern that needs to be parsed as select,
+ /// and returns an object with select block info.
+ Map<String, Object> _parseSelectBlock(String pattern) {
+ var argumentName = '';
+ var replaceRegex = _selectBlockRe;
+ pattern = pattern.replaceFirstMapped(replaceRegex, (match) {
+ // string, name
+ argumentName = match.group(1);
+ return '';
+ });
+ // The lint complaints about "Omit type annotations for local variables"
+ // But if I make this `var` then it assumes that the value is a
+ // always a string, but it is not.
+ Map<String, Object> result = {'argumentName': argumentName};
+
+ var parts = _extractParts(pattern);
+ // Looking for (key block)+ sequence. One of the keys has to be "other".
+ var pos = 0;
+ while (pos < parts.length) {
+ var thePart = parts.elementAt(pos);
+ _checkAndThrow(thePart._value is String, 'Missing select key element.');
+ var key = thePart._value;
+
+ pos++;
+ _checkAndThrow(
+ pos < parts.length, 'Missing or invalid select value element.');
+ thePart = parts.elementAt(pos);
+
+ Queue<_BlockTypeAndVal> value;
+ if (_ElementType.block == thePart._type) {
+ value = _parseBlock(thePart._value);
+ } else {
+ _checkAndThrow(false, 'Expected block type.');
+ }
+ result[key.replaceAll(RegExp('\\s'), '')] = value;
+ pos++;
+ }
+
+ _checkAndThrow(
+ result.containsKey(_other), 'Missing other key in select statement.');
+ return result;
+ }
+
+ /// Parses a plural type of a block and produces an object for it.
+ ///
+ /// The [pattern] is the sub-pattern that needs to be parsed as plural.
+ /// and returns an bject with plural block info.
+ Map<String, Object> _parsePluralBlock(String pattern) {
+ var argumentName = '';
+ var argumentOffset = 0;
+ var replaceRegex = _pluralBlockRe;
+ pattern = pattern.replaceFirstMapped(replaceRegex, (match) {
+ // string, name, offset
+ argumentName = match.group(1);
+ if (_isDef(match.group(2))) {
+ argumentOffset = int.parse(match.group(2));
+ }
+ return '';
+ });
+
+ var result = {
+ 'argumentName': argumentName,
+ 'argumentOffset': argumentOffset
+ };
+
+ var parts = _extractParts(pattern);
+ // Looking for (key block)+ sequence.
+ var pos = 0;
+ while (pos < parts.length) {
+ var thePart = parts.elementAt(pos);
+ _checkAndThrow(thePart._value is String, 'Missing plural key element.');
+ var key = thePart._value;
+
+ pos++;
+ _checkAndThrow(
+ pos < parts.length, 'Missing or invalid plural value element.');
+ thePart = parts.elementAt(pos);
+
+ Queue<_BlockTypeAndVal> value;
+ if (_ElementType.block == thePart._type) {
+ value = _parseBlock(thePart._value);
+ } else {
+ _checkAndThrow(false, 'Expected block type.');
+ }
+ key = key.replaceFirstMapped(RegExp('\\s*(?:=)?(\\w+)\\s*'), (match) {
+ return match.group(1).toString();
+ });
+ result[key] = value;
+ pos++;
+ }
+
+ _checkAndThrow(
+ result.containsKey(_other), 'Missing other key in plural statement.');
+
+ return result;
+ }
+
+ /// Parses an ordinal type of a block and produces an object for it.
+ ///
+ /// For example the input string:
+ /// `{FOO, selectordinal, one {Message A}other {Message B}}`
+ /// Should result in the output object:
+ /// ```
+ /// {
+ /// argumentName: 'FOO',
+ /// argumentOffest: 0,
+ /// one: [ { type: 4, value: 'Message A' } ],
+ /// other: [ { type: 4, value: 'Message B' } ]
+ /// }
+ /// ```
+ /// The [pattern] is the sub-pattern that needs to be parsed as ordinal,
+ /// and returns an bject with ordinal block info.
+ Map<String, Object> _parseOrdinalBlock(String pattern) {
+ var argumentName = '';
+ var replaceRegex = _ordinalBlockRe;
+ pattern = pattern.replaceFirstMapped(replaceRegex, (match) {
+ // string, name
+ argumentName = match.group(1);
+ return '';
+ });
+
+ var result = {'argumentName': argumentName, 'argumentOffset': 0};
+
+ var parts = _extractParts(pattern);
+ // Looking for (key block)+ sequence.
+ var pos = 0;
+ while (pos < parts.length) {
+ var thePart = parts.elementAt(pos);
+ _checkAndThrow(thePart._value is String, 'Missing ordinal key element.');
+ var key = thePart._value;
+
+ pos++;
+ _checkAndThrow(
+ pos < parts.length, 'Missing or invalid ordinal value element.');
+ thePart = parts.elementAt(pos);
+
+ Queue<_BlockTypeAndVal> value;
+ if (_ElementType.block == thePart._type) {
+ value = _parseBlock(thePart._value);
+ } else {
+ _checkAndThrow(false, 'Expected block type.');
+ }
+ key = key.replaceFirstMapped(RegExp('\\s*(?:=)?(\\w+)\\s*'), (match) {
+ return match.group(1).toString();
+ });
+ result[key] = value;
+ pos++;
+ }
+
+ _checkAndThrow(result.containsKey(_other),
+ 'Missing other key in selectordinal statement.');
+
+ return result;
+ }
+
+ /// Builds a placeholder from the last index of the array.
+ ///
+ /// using all the [literals] encountered during parse.
+ /// It returns a string that looks like this: `"\uFDDF_" + last index + "_"`.
+ String _buildPlaceholder(Queue<String> literals) {
+ _checkAndThrow(literals.isNotEmpty, 'Literal array is empty.');
+
+ var index = (literals.length - 1).toString();
+ return '$_literalPlaceholder${index}_';
+ }
+}
+
+//========== EXTRAS: temporary, to help the move from JS to Dart ==========
+
+// Simple goog.isDef replacement, will probably remove it
+bool _isDef(Object obj) {
+ return obj != null;
+}
+
+// Closure calls assert, which actually ends up with an exception on can catch.
+// In Dart assert is only for debug, so I am using this small wrapper method.
+void _checkAndThrow(bool condition, String message) {
+ if (!condition) {
+ throw AssertionError(message);
+ }
+}
+
+// Dart has no support for ordinals
+// TODO(b/142132665): add ordial rules to intl, then fix this
+class _OrdinalRules {
+ static String select(num n, String locale) {
+ return _PluralRules.select(n, locale);
+ }
+}
+
+// Simple mapping from Intl.pluralLogic to _PluralRules, to change later
+class _PluralRules {
+ static String select(num n, String locale) {
+ return Intl.pluralLogic(n,
+ zero: 'zero',
+ one: 'one',
+ two: 'two',
+ few: 'few',
+ many: 'many',
+ other: 'other',
+ locale: locale);
+ }
+}
+
+// Pairs a value and information about its type.
+class _TypeAndVal<T, V> {
+ final T _type;
+ final V _value;
+
+ _TypeAndVal(var this._type, var this._value);
+
+ @override
+ String toString() {
+ return '{type:$_type, value:$_value}';
+ }
+}
+
+/// Marks a string and block during parsing.
+enum _ElementType { string, block }
+
+class _ElementTypeAndVal extends _TypeAndVal<_ElementType, String> {
+ _ElementTypeAndVal(var _type, var _value) : super(_type, _value);
+}
+
+/// Block type.
+enum _BlockType { plural, ordinal, select, simple, string, unknown }
+
+class _BlockTypeAndVal extends _TypeAndVal<_BlockType, Object> {
+ _BlockTypeAndVal(var _type, var _value) : super(_type, _value);
+}
diff --git a/test/message_format_test.dart b/test/message_format_test.dart
new file mode 100644
index 0000000..ec1ca22
--- /dev/null
+++ b/test/message_format_test.dart
@@ -0,0 +1,444 @@
+// Copyright (c) 2019, 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.
+
+/// Tests for the MessageFormat class.
+///
+/// Currently, these tests are the ones directly ported from Closure.
+
+import 'package:intl/message_format.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('testEmptyPattern', () {
+ var fmt = MessageFormat('');
+ expect(fmt.format({}), '');
+ });
+
+ test('testMissingLeftCurlyBrace', () {
+ expect(() {
+ var fmt = MessageFormat('\'\'{}}');
+ fmt.format({});
+ }, throwsA(predicate((e) {
+ return e is AssertionError && e.message == 'No matching { for }.';
+ })));
+ });
+
+ test('testTooManyLeftCurlyBraces', () {
+ expect(() {
+ var fmt = MessageFormat('{} {');
+ fmt.format({});
+ }, throwsA(predicate((e) {
+ return e is AssertionError &&
+ e.message == 'There are mismatched { or } in the pattern.';
+ })));
+ });
+
+ test('testSimpleReplacement', () {
+ var fmt = MessageFormat('New York in {SEASON} is nice.');
+ expect(fmt.format({'SEASON': 'the Summer'}),
+ 'New York in the Summer is nice.');
+ });
+
+ test('testSimpleSelect', () {
+ var fmt = MessageFormat('{GENDER, select,'
+ 'male {His}'
+ 'female {Her}'
+ 'other {Its}}'
+ ' bicycle is {GENDER, select, male {blue} female {red} other {green}}.');
+
+ expect(fmt.format({'GENDER': 'male'}), 'His bicycle is blue.');
+ expect(fmt.format({'GENDER': 'female'}), 'Her bicycle is red.');
+ expect(fmt.format({'GENDER': 'other'}), 'Its bicycle is green.');
+ expect(fmt.format({'GENDER': 'whatever'}), 'Its bicycle is green.');
+ });
+
+ test('testSimplePlural', () {
+ var fmt = MessageFormat('I see {NUM_PEOPLE, plural, offset:1 '
+ '=0 {no one at all in {PLACE}.} '
+ '=1 {{PERSON} in {PLACE}.} '
+ 'one {{PERSON} and one other person in {PLACE}.} '
+ 'other {{PERSON} and # other people in {PLACE}.}}');
+
+ expect(fmt.format({'NUM_PEOPLE': 0, 'PLACE': 'Belgrade'}),
+ 'I see no one at all in Belgrade.');
+ expect(fmt.format({'NUM_PEOPLE': 1, 'PERSON': 'Markus', 'PLACE': 'Berlin'}),
+ 'I see Markus in Berlin.');
+ expect(fmt.format({'NUM_PEOPLE': 2, 'PERSON': 'Mark', 'PLACE': 'Athens'}),
+ 'I see Mark and one other person in Athens.');
+ expect(
+ fmt.format({'NUM_PEOPLE': 100, 'PERSON': 'Cibu', 'PLACE': 'the cubes'}),
+ 'I see Cibu and 99 other people in the cubes.');
+ });
+
+ test('testSimplePluralNoOffset', () {
+ var fmt = MessageFormat('I see {NUM_PEOPLE, plural, '
+ '=0 {no one at all} '
+ '=1 {{PERSON}} '
+ 'one {{PERSON} and one other person} '
+ 'other {{PERSON} and # other people}} in {PLACE}.');
+
+ expect(fmt.format({'NUM_PEOPLE': 0, 'PLACE': 'Belgrade'}),
+ 'I see no one at all in Belgrade.');
+ expect(fmt.format({'NUM_PEOPLE': 1, 'PERSON': 'Markus', 'PLACE': 'Berlin'}),
+ 'I see Markus in Berlin.');
+ expect(fmt.format({'NUM_PEOPLE': 2, 'PERSON': 'Mark', 'PLACE': 'Athens'}),
+ 'I see Mark and 2 other people in Athens.');
+ expect(
+ fmt.format({'NUM_PEOPLE': 100, 'PERSON': 'Cibu', 'PLACE': 'the cubes'}),
+ 'I see Cibu and 100 other people in the cubes.');
+ });
+
+ test('testSelectNestedInPlural', () {
+ var fmt = MessageFormat('{CIRCLES, plural, '
+ 'one {{GENDER, select, '
+ ' female {{WHO} added you to her circle} '
+ ' other {{WHO} added you to his circle}}} '
+ 'other {{GENDER, select, '
+ ' female {{WHO} added you to her # circles} '
+ ' other {{WHO} added you to his # circles}}}}');
+
+ expect(fmt.format({'GENDER': 'female', 'WHO': 'Jelena', 'CIRCLES': 1}),
+ 'Jelena added you to her circle');
+ expect(fmt.format({'GENDER': 'male', 'WHO': 'Milan', 'CIRCLES': 1234}),
+ 'Milan added you to his 1,234 circles');
+ });
+
+ test('testPluralNestedInSelect', () {
+ // Added offset just for testing purposes. It doesn't make sense
+ // to have it otherwise.
+ var fmt = MessageFormat('{GENDER, select, '
+ 'female {{NUM_GROUPS, plural, '
+ ' one {{WHO} added you to her group} '
+ ' other {{WHO} added you to her # groups}}} '
+ 'other {{NUM_GROUPS, plural, offset:1'
+ ' one {{WHO} added you to his group} '
+ ' other {{WHO} added you to his # groups}}}}');
+
+ expect(fmt.format({'GENDER': 'female', 'WHO': 'Jelena', 'NUM_GROUPS': 1}),
+ 'Jelena added you to her group');
+ expect(fmt.format({'GENDER': 'male', 'WHO': 'Milan', 'NUM_GROUPS': 1234}),
+ 'Milan added you to his 1,233 groups');
+ });
+
+ test('testLiteralOpenCurlyBrace', () {
+ var fmt = MessageFormat(
+ "Anna's house has '{0} and # in the roof' and {NUM_COWS} cows.");
+ expect(fmt.format({'NUM_COWS': '5'}),
+ "Anna's house has {0} and # in the roof and 5 cows.");
+ });
+
+ test('testLiteralClosedCurlyBrace', () {
+ var fmt = MessageFormat(
+ 'Anna\'s house has \'{\'0\'} and # in the roof\' and {NUM_COWS} cows.');
+ expect(fmt.format({'NUM_COWS': '5'}),
+ 'Anna\'s house has {0} and # in the roof and 5 cows.');
+ // Regression for Closure implementation bug: b/34764827
+ expect(fmt.format({'NUM_COWS': '8'}),
+ 'Anna\'s house has {0} and # in the roof and 8 cows.');
+ });
+
+ test('testLiteralPoundSign', () {
+ var fmt = MessageFormat(
+ "Anna's house has '{0}' and '# in the roof' and {NUM_COWS} cows.");
+ expect(fmt.format({'NUM_COWS': '5'}),
+ "Anna's house has {0} and # in the roof and 5 cows.");
+ // Regression for: b/34764827
+ expect(fmt.format({'NUM_COWS': '10'}),
+ 'Anna\'s house has {0} and # in the roof and 10 cows.');
+ });
+
+ test('testNoLiteralsForSingleQuotes', () {
+ var fmt = MessageFormat("Anna's house 'has {NUM_COWS} cows'.");
+ expect(fmt.format({'NUM_COWS': '5'}), "Anna's house 'has 5 cows'.");
+ });
+
+ test('testConsecutiveSingleQuotesAreReplacedWithOneSingleQuote', () {
+ var fmt = MessageFormat("Anna''s house a'{''''b'");
+ expect(fmt.format({}), "Anna's house a{''b");
+ });
+
+ test('testConsecutiveSingleQuotesBeforeSpecialCharDontCreateLiteral', () {
+ var fmt = MessageFormat("a''{NUM_COWS}'b");
+ expect(fmt.format({'NUM_COWS': '5'}), "a'5'b");
+ });
+
+ test('testSerbianSimpleSelect', () {
+ var fmt = MessageFormat(
+ '{GENDER, select, female {Njen} other {Njegov}} bicikl je '
+ '{GENDER, select, female {crven} other {plav}}.',
+ locale: 'sr');
+
+ expect(fmt.format({'GENDER': 'male'}), 'Njegov bicikl je plav.');
+ expect(fmt.format({'GENDER': 'female'}), 'Njen bicikl je crven.');
+ });
+
+ test('testSerbianSimplePlural', () {
+ var fmt = MessageFormat(
+ 'Ja {NUM_PEOPLE, plural, offset:1 '
+ ' =0 {ne vidim nikoga} '
+ ' =1 {vidim {PERSON}} '
+ ' one {vidim {PERSON} i jos # osobu} '
+ ' few {vidim {PERSON} i jos # osobe} '
+ ' many {vidim {PERSON} i jos # osoba} '
+ ' other {vidim {PERSON} i jos # osoba}} '
+ 'u {PLACE}.',
+ locale: 'sr');
+
+ expect(fmt.format({'NUM_PEOPLE': 0, 'PLACE': 'Beogradu'}),
+ 'Ja ne vidim nikoga u Beogradu.');
+ expect(
+ fmt.format({'NUM_PEOPLE': 1, 'PERSON': 'Markusa', 'PLACE': 'Berlinu'}),
+ 'Ja vidim Markusa u Berlinu.');
+ expect(fmt.format({'NUM_PEOPLE': 2, 'PERSON': 'Marka', 'PLACE': 'Atini'}),
+ 'Ja vidim Marka i jos 1 osobu u Atini.');
+ expect(fmt.format({'NUM_PEOPLE': 4, 'PERSON': 'Petra', 'PLACE': 'muzeju'}),
+ 'Ja vidim Petra i jos 3 osobe u muzeju.');
+ expect(
+ fmt.format({'NUM_PEOPLE': 100, 'PERSON': 'Cibua', 'PLACE': 'bazenu'}),
+ 'Ja vidim Cibua i jos 99 osoba u bazenu.');
+ });
+
+ test('testSerbianSimplePluralNoOffset', () {
+ var fmt = MessageFormat(
+ 'Ja {NUM_PEOPLE, plural, '
+ ' =0 {ne vidim nikoga} '
+ ' =1 {vidim {PERSON}} '
+ ' one {vidim {PERSON} i jos # osobu} '
+ ' few {vidim {PERSON} i jos # osobe} '
+ ' many {vidim {PERSON} i jos # osoba} '
+ ' other {vidim {PERSON} i jos # osoba}} '
+ 'u {PLACE}.',
+ locale: 'sr');
+
+ expect(fmt.format({'NUM_PEOPLE': 0, 'PLACE': 'Beogradu'}),
+ 'Ja ne vidim nikoga u Beogradu.');
+ expect(
+ fmt.format({'NUM_PEOPLE': 1, 'PERSON': 'Markusa', 'PLACE': 'Berlinu'}),
+ 'Ja vidim Markusa u Berlinu.');
+ expect(fmt.format({'NUM_PEOPLE': 21, 'PERSON': 'Marka', 'PLACE': 'Atini'}),
+ 'Ja vidim Marka i jos 21 osobu u Atini.');
+ expect(fmt.format({'NUM_PEOPLE': 3, 'PERSON': 'Petra', 'PLACE': 'muzeju'}),
+ 'Ja vidim Petra i jos 3 osobe u muzeju.');
+ expect(
+ fmt.format({'NUM_PEOPLE': 100, 'PERSON': 'Cibua', 'PLACE': 'bazenu'}),
+ 'Ja vidim Cibua i jos 100 osoba u bazenu.');
+ });
+
+ test('testSerbianSelectNestedInPlural', () {
+ var fmt = MessageFormat(
+ '{CIRCLES, plural, '
+ ' one {{GENDER, select, '
+ ' female {{WHO} vas je dodala u njen # kruzok} '
+ ' other {{WHO} vas je dodao u njegov # kruzok}}} '
+ ' few {{GENDER, select, '
+ ' female {{WHO} vas je dodala u njena # kruzoka} '
+ ' other {{WHO} vas je dodao u njegova # kruzoka}}} '
+ ' many {{GENDER, select, '
+ ' female {{WHO} vas je dodala u njenih # kruzoka} '
+ ' other {{WHO} vas je dodao u njegovih # kruzoka}}} '
+ ' other {{GENDER, select, '
+ ' female {{WHO} vas je dodala u njenih # kruzoka} '
+ ' other {{WHO} vas je dodao u njegovih # kruzoka}}}}',
+ locale: 'hr');
+
+ expect(fmt.format({'GENDER': 'female', 'WHO': 'Jelena', 'CIRCLES': 21}),
+ 'Jelena vas je dodala u njen 21 kruzok');
+ expect(fmt.format({'GENDER': 'female', 'WHO': 'Jelena', 'CIRCLES': 3}),
+ 'Jelena vas je dodala u njena 3 kruzoka');
+ expect(fmt.format({'GENDER': 'female', 'WHO': 'Jelena', 'CIRCLES': 5}),
+ 'Jelena vas je dodala u njenih 5 kruzoka');
+ expect(fmt.format({'GENDER': 'male', 'WHO': 'Milan', 'CIRCLES': 1235}),
+ 'Milan vas je dodao u njegovih 1.235 kruzoka');
+ });
+
+ test('testFallbackToOtherOptionInPlurals', () {
+ // Use Arabic plural rules since they have all six cases.
+ // Only locale and numbers matter, the actual language of the message
+ // does not.
+ var fmt =
+ MessageFormat('{NUM_MINUTES, plural, other {# minutes}}', locale: 'ar');
+
+ // These numbers exercise all cases for the arabic plural rules.
+ expect(fmt.format({'NUM_MINUTES': 0}), '0 minutes');
+ expect(fmt.format({'NUM_MINUTES': 1}), '1 minutes');
+ expect(fmt.format({'NUM_MINUTES': 2}), '2 minutes');
+ expect(fmt.format({'NUM_MINUTES': 3}), '3 minutes');
+ expect(fmt.format({'NUM_MINUTES': 11}), '11 minutes');
+ expect(fmt.format({'NUM_MINUTES': 1.5}), '1.5 minutes');
+ });
+
+ test('testPoundShowsNumberMinusOffsetInAllCases', () {
+ var fmt = MessageFormat(
+ '{SOME_NUM, plural, offset:1 =0 {#} =1 {#} =2 {#} one {#} other {#}}');
+
+ expect(fmt.format({'SOME_NUM': '0'}), '-1');
+ expect(fmt.format({'SOME_NUM': '1'}), '0');
+ expect(fmt.format({'SOME_NUM': '2'}), '1');
+ expect(fmt.format({'SOME_NUM': '21'}), '20');
+ });
+
+ test('testSpecialCharactersInParamaterDontChangeFormat', () {
+ var fmt = MessageFormat('{SOME_NUM, plural, other {# {GROUP}}}');
+
+ // Test pound sign.
+ expect(fmt.format({'SOME_NUM': '10', 'GROUP': 'group#1'}), '10 group#1');
+ // Test other special characters in parameters, like { and }.
+ expect(fmt.format({'SOME_NUM': '10', 'GROUP': '} {'}), '10 } {');
+ });
+
+ test('testMissingOrInvalidPluralParameter', () {
+ var fmt = MessageFormat('{SOME_NUM, plural, other {result}}');
+
+ // Key name doesn't match A != SOME_NUM.
+ expect(fmt.format({'A': '10'}), 'Undefined parameter - SOME_NUM');
+
+ // Value is not a number.
+ expect(fmt.format({'SOME_NUM': 'Value'}), 'Invalid parameter - SOME_NUM');
+ });
+
+ test('testMissingSelectParameter', () {
+ var fmt = MessageFormat('{GENDER, select, other {result}}');
+
+ // Key name doesn't match A != GENDER.
+ expect(fmt.format({'A': 'female'}), 'Undefined parameter - GENDER');
+ });
+
+ test('testMissingSimplePlaceholder', () {
+ var fmt = MessageFormat('{result}');
+
+ // Key name doesn't match A != result.
+ expect(fmt.format({'A': 'none'}), 'Undefined parameter - result');
+ });
+
+ test('testPlural', () {
+ var fmt = MessageFormat(
+ '{SOME_NUM, plural,'
+ ' =0 {none}'
+ ' =1 {exactly one}'
+ ' one {# one}'
+ ' few {# few}'
+ ' many {# many}'
+ ' other {# other}'
+ '}',
+ locale: 'ru');
+
+ expect(fmt.format({'SOME_NUM': 0}), 'none');
+ expect(fmt.format({'SOME_NUM': 1}), 'exactly one');
+ expect(fmt.format({'SOME_NUM': 21}), '21 one');
+ expect(fmt.format({'SOME_NUM': 23}), '23 few');
+ expect(fmt.format({'SOME_NUM': 17}), '17 many');
+ expect(fmt.format({'SOME_NUM': 100}), '100 many');
+ expect(fmt.format({'SOME_NUM': 1.4}), '1,4 other');
+ expect(fmt.format({'SOME_NUM': '10.0'}), '10 other');
+ expect(fmt.format({'SOME_NUM': '100.00'}), '100 other');
+ });
+
+ test('testPluralWithIgnorePound', () {
+ var fmt = MessageFormat('{SOME_NUM, plural, other {# {GROUP}}}');
+
+ // Test pound sign.
+ expect(fmt.formatIgnoringPound({'SOME_NUM': '10', 'GROUP': 'group#1'}),
+ '# group#1');
+ // Test other special characters in parameters, like { and }.
+ expect(
+ fmt.formatIgnoringPound({'SOME_NUM': '10', 'GROUP': '} {'}), '# } {');
+ });
+
+ test('testSimplePluralWithIgnorePound', () {
+ var fmt = MessageFormat('I see {NUM_PEOPLE, plural, offset:1 '
+ '=0 {no one at all in {PLACE}.} '
+ '=1 {{PERSON} in {PLACE}.} '
+ 'one {{PERSON} and one other person in {PLACE}.} '
+ 'other {{PERSON} and # other people in {PLACE}.}}');
+
+ expect(
+ fmt.formatIgnoringPound(
+ {'NUM_PEOPLE': 100, 'PERSON': 'Cibu', 'PLACE': 'the cubes'}),
+ 'I see Cibu and # other people in the cubes.');
+ });
+
+ test('testRomanianOffsetWithNegativeValue', () {
+ var fmt = MessageFormat(
+ '{NUM_FLOOR, plural, offset:2 '
+ 'one {One #}'
+ 'few {Few #}'
+ 'other {Other #}}',
+ locale: 'ro');
+
+ // Checking that the decision is done after the offset is substracted
+ expect(fmt.format({'NUM_FLOOR': -1}), 'Few -3');
+ expect(fmt.format({'NUM_FLOOR': 1}), 'One -1');
+ expect(fmt.format({'NUM_FLOOR': -3}), 'Few -5');
+ expect(fmt.format({'NUM_FLOOR': 3}), 'One 1');
+ expect(fmt.format({'NUM_FLOOR': -25}), 'Other -27');
+ expect(fmt.format({'NUM_FLOOR': 25}), 'Other 23');
+ });
+
+ ignoreTest('testSimpleOrdinal', () {
+ // TOFIX. Ordinal not supported in Dart
+ var fmt = MessageFormat('{NUM_FLOOR, selectordinal, '
+ 'one {Take the elevator to the #st floor.}'
+ 'two {Take the elevator to the #nd floor.}'
+ 'few {Take the elevator to the #rd floor.}'
+ 'other {Take the elevator to the #th floor.}}');
+
+ expect(fmt.format({'NUM_FLOOR': 1}), 'Take the elevator to the 1st floor.');
+ expect(fmt.format({'NUM_FLOOR': 2}), 'Take the elevator to the 2nd floor.');
+ expect(fmt.format({'NUM_FLOOR': 3}), 'Take the elevator to the 3rd floor.');
+ expect(fmt.format({'NUM_FLOOR': 4}), 'Take the elevator to the 4th floor.');
+ expect(
+ fmt.format({'NUM_FLOOR': 23}), 'Take the elevator to the 23rd floor.');
+ // Esoteric example.
+ expect(fmt.format({'NUM_FLOOR': 0}), 'Take the elevator to the 0th floor.');
+ });
+
+ ignoreTest('testOrdinalWithNegativeValue', () {
+ // TOFIX. Ordinal not supported in Dart
+ var fmt = MessageFormat('{NUM_FLOOR, selectordinal, '
+ 'one {Take the elevator to the #st floor.}'
+ 'two {Take the elevator to the #nd floor.}'
+ 'few {Take the elevator to the #rd floor.}'
+ 'other {Take the elevator to the #th floor.}}');
+
+ expect(
+ fmt.format({'NUM_FLOOR': -1}), 'Take the elevator to the -1st floor.');
+ expect(
+ fmt.format({'NUM_FLOOR': -2}), 'Take the elevator to the -2nd floor.');
+ expect(
+ fmt.format({'NUM_FLOOR': -3}), 'Take the elevator to the -3rd floor.');
+ expect(
+ fmt.format({'NUM_FLOOR': -4}), 'Take the elevator to the -4th floor.');
+ });
+
+ ignoreTest('testSimpleOrdinalWithIgnorePound', () {
+ // TOFIX. Ordinal not supported in Dart
+ var fmt = MessageFormat('{NUM_FLOOR, selectordinal, '
+ 'one {Take the elevator to the #st floor.}'
+ 'two {Take the elevator to the #nd floor.}'
+ 'few {Take the elevator to the #rd floor.}'
+ 'other {Take the elevator to the #th floor.}}');
+
+ expect(fmt.formatIgnoringPound({'NUM_FLOOR': 100}),
+ 'Take the elevator to the #th floor.');
+ });
+
+ ignoreTest('testMissingOrInvalidOrdinalParameter', () {
+ // TOFIX. Ordinal not supported in Dart
+ var fmt = MessageFormat('{SOME_NUM, selectordinal, other {result}}');
+
+ // Key name doesn't match A != SOME_NUM.
+ expect(
+ fmt.format({'A': '10'}), 'Undefined or invalid parameter - SOME_NUM');
+
+ // Value is not a number.
+ expect(fmt.format({'SOME_NUM': 'Value'}),
+ 'Undefined or invalid parameter - SOME_NUM');
+ });
+} // end of main
+
+// Disabling unit tests without having to comment the whole body.
+// Similar to @Ignore in JUnit
+void ignoreTest(description, body) {
+ print('\u001b[93mTest ignored: $description\u001b[m');
+}