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');
+}