// Copyright 2010 The Closure Library Authors. All Rights Reserved
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Message/plural format library with locale support.
 *
 * Message format grammar:
 *
 * messageFormatPattern := string ( "{" messageFormatElement "}" string )*
 * messageFormatElement := argumentIndex [ "," elementFormat ]
 * elementFormat := "plural" "," pluralStyle
 *                  | "selectordinal" "," ordinalStyle
 *                  | "select" "," selectStyle
 * pluralStyle :=  pluralFormatPattern
 * ordinalStyle :=  selectFormatPattern
 * selectStyle :=  selectFormatPattern
 * pluralFormatPattern := [ "offset" ":" offsetIndex ] pluralForms*
 * selectFormatPattern := pluralForms*
 * pluralForms := stringKey "{" ( "{" messageFormatElement "}"|string )* "}"
 *
 * This is a subset of the ICU MessageFormatSyntax:
 *   http://userguide.icu-project.org/formatparse/messages
 * See also http://go/plurals and http://go/ordinals for internal details.
 *
 *
 * 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.
 *
 * OR:
 *
 * {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.}}
 *
 * Calling format({'NUM_FLOOR': 22}) would produce
 * "Take the elevator to the 22nd floor".
 *
 * See messageformat_test.html for more examples.
 */

goog.provide('goog.i18n.MessageFormat');

goog.require('goog.asserts');
goog.require('goog.i18n.NumberFormat');
goog.require('goog.i18n.ordinalRules');
goog.require('goog.i18n.pluralRules');



/**
 * Constructor of MessageFormat.
 * @param {string} pattern The pattern we parse and apply positional parameters
 *     to.
 * @constructor
 * @final
 */
goog.i18n.MessageFormat = function(pattern) {
  /**
   * All encountered literals during parse stage. Indices tell us the order of
   * replacement.
   * @type {!Array<string>}
   * @private
   */
  this.literals_ = [];

  /**
   * Input pattern gets parsed into objects for faster formatting.
   * @type {!Array<!Object>}
   * @private
   */
  this.parsedPattern_ = [];

  /**
   * Locale aware number formatter.
   * @type {goog.i18n.NumberFormat}
   * @private
   */
  this.numberFormatter_ = new goog.i18n.NumberFormat(
      goog.i18n.NumberFormat.Format.DECIMAL);

  this.parsePattern_(pattern);
};


/**
 * Literal strings, including '', are replaced with \uFDDF_x_ for
 * parsing purposes, and recovered during format phase.
 * \uFDDF is a Unicode nonprinting character, not expected to be found in the
 * typical message.
 * @type {string}
 * @private
 */
goog.i18n.MessageFormat.LITERAL_PLACEHOLDER_ = '\uFDDF_';


/**
 * Marks a string and block during parsing.
 * @enum {number}
 * @private
 */
goog.i18n.MessageFormat.Element_ = {
  STRING: 0,
  BLOCK: 1
};


/**
 * Block type.
 * @enum {number}
 * @private
 */
goog.i18n.MessageFormat.BlockType_ = {
  PLURAL: 0,
  ORDINAL: 1,
  SELECT: 2,
  SIMPLE: 3,
  STRING: 4,
  UNKNOWN: 5
};


/**
 * Mandatory option in both select and plural form.
 * @type {string}
 * @private
 */
goog.i18n.MessageFormat.OTHER_ = 'other';


/**
 * Regular expression for looking for string literals.
 * @type {RegExp}
 * @private
 */
goog.i18n.MessageFormat.REGEX_LITERAL_ = new RegExp("'([{}#].*?)'", 'g');


/**
 * Regular expression for looking for '' in the message.
 * @type {RegExp}
 * @private
 */
goog.i18n.MessageFormat.REGEX_DOUBLE_APOSTROPHE_ = new RegExp("''", 'g');


/**
 * Formats a message, treating '#' with special meaning representing
 * the number (plural_variable - offset).
 * @param {!Object} namedParameters Parameters that either
 *     influence the formatting or are used as actual data.
 *     I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),
 *     object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.
 *     1st parameter could mean 5 people, which could influence plural format,
 *     and 2nd parameter is just a data to be printed out in proper position.
 * @return {string} Formatted message.
 */
goog.i18n.MessageFormat.prototype.format = function(namedParameters) {
  return this.format_(namedParameters, false);
};


/**
 * Formats a message, treating '#' as literary character.
 * @param {!Object} namedParameters Parameters that either
 *     influence the formatting or are used as actual data.
 *     I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),
 *     object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.
 *     1st parameter could mean 5 people, which could influence plural format,
 *     and 2nd parameter is just a data to be printed out in proper position.
 * @return {string} Formatted message.
 */
goog.i18n.MessageFormat.prototype.formatIgnoringPound =
    function(namedParameters) {
  return this.format_(namedParameters, true);
};


/**
 * Formats a message.
 * @param {!Object} namedParameters Parameters that either
 *     influence the formatting or are used as actual data.
 *     I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),
 *     object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.
 *     1st parameter could mean 5 people, which could influence plural format,
 *     and 2nd parameter is just a data to be printed out in proper position.
 * @param {boolean} ignorePound If true, treat '#' in plural messages as a
 *     literary character, else treat it as an ICU syntax character, resolving
 *     to the number (plural_variable - offset).
 * @return {string} Formatted message.
 * @private
 */
goog.i18n.MessageFormat.prototype.format_ =
    function(namedParameters, ignorePound) {
  if (this.parsedPattern_.length == 0) {
    return '';
  }

  var result = [];
  this.formatBlock_(this.parsedPattern_, namedParameters, ignorePound, result);
  var message = result.join('');

  if (!ignorePound) {
    goog.asserts.assert(message.search('#') == -1, 'Not all # were replaced.');
  }

  while (this.literals_.length > 0) {
    message = message.replace(this.buildPlaceholder_(this.literals_),
                              this.literals_.pop());
  }

  return message;
};


/**
 * Parses generic block and returns a formatted string.
 * @param {!Array<!Object>} parsedPattern Holds parsed tree.
 * @param {!Object} namedParameters Parameters that either influence
 *     the formatting or are used as actual data.
 * @param {boolean} ignorePound If true, treat '#' in plural messages as a
 *     literary character, else treat it as an ICU syntax character, resolving
 *     to the number (plural_variable - offset).
 * @param {!Array<!string>} result Each formatting stage appends its product
 *     to the result.
 * @private
 */
goog.i18n.MessageFormat.prototype.formatBlock_ = function(
    parsedPattern, namedParameters, ignorePound, result) {
  for (var i = 0; i < parsedPattern.length; i++) {
    switch (parsedPattern[i].type) {
      case goog.i18n.MessageFormat.BlockType_.STRING:
        result.push(parsedPattern[i].value);
        break;
      case goog.i18n.MessageFormat.BlockType_.SIMPLE:
        var pattern = parsedPattern[i].value;
        this.formatSimplePlaceholder_(pattern, namedParameters, result);
        break;
      case goog.i18n.MessageFormat.BlockType_.SELECT:
        var pattern = parsedPattern[i].value;
        this.formatSelectBlock_(pattern, namedParameters, ignorePound, result);
        break;
      case goog.i18n.MessageFormat.BlockType_.PLURAL:
        var pattern = parsedPattern[i].value;
        this.formatPluralOrdinalBlock_(pattern,
                                       namedParameters,
                                       goog.i18n.pluralRules.select,
                                       ignorePound,
                                       result);
        break;
      case goog.i18n.MessageFormat.BlockType_.ORDINAL:
        var pattern = parsedPattern[i].value;
        this.formatPluralOrdinalBlock_(pattern,
                                       namedParameters,
                                       goog.i18n.ordinalRules.select,
                                       ignorePound,
                                       result);
        break;
      default:
        goog.asserts.fail('Unrecognized block type: ' + parsedPattern[i].type);
    }
  }
};


/**
 * Formats simple placeholder.
 * @param {!Object} parsedPattern JSON object containing placeholder info.
 * @param {!Object} namedParameters Parameters that are used as actual data.
 * @param {!Array<!string>} result Each formatting stage appends its product
 *     to the result.
 * @private
 */
goog.i18n.MessageFormat.prototype.formatSimplePlaceholder_ = function(
    parsedPattern, namedParameters, result) {
  var value = namedParameters[parsedPattern];
  if (!goog.isDef(value)) {
    result.push('Undefined parameter - ' + parsedPattern);
    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.
  this.literals_.push(value);
  result.push(this.buildPlaceholder_(this.literals_));
};


/**
 * Formats select block. Only one option is selected.
 * @param {!Object} parsedPattern JSON object containing select block info.
 * @param {!Object} namedParameters Parameters that either influence
 *     the formatting or are used as actual data.
 * @param {boolean} ignorePound If true, treat '#' in plural messages as a
 *     literary character, else treat it as an ICU syntax character, resolving
 *     to the number (plural_variable - offset).
 * @param {!Array<!string>} result Each formatting stage appends its product
 *     to the result.
 * @private
 */
goog.i18n.MessageFormat.prototype.formatSelectBlock_ = function(
    parsedPattern, namedParameters, ignorePound, result) {
  var argumentIndex = parsedPattern.argumentIndex;
  if (!goog.isDef(namedParameters[argumentIndex])) {
    result.push('Undefined parameter - ' + argumentIndex);
    return;
  }

  var option = parsedPattern[namedParameters[argumentIndex]];
  if (!goog.isDef(option)) {
    option = parsedPattern[goog.i18n.MessageFormat.OTHER_];
    goog.asserts.assertArray(
        option, 'Invalid option or missing other option for select block.');
  }

  this.formatBlock_(option, namedParameters, ignorePound, result);
};


/**
 * Formats plural or selectordinal block. Only one option is selected and all #
 * are replaced.
 * @param {!Object} parsedPattern JSON object containing plural block info.
 * @param {!Object} namedParameters Parameters that either influence
 *     the formatting or are used as actual data.
 * @param {!function(number, number=):string} pluralSelector  A select function
 *     from goog.i18n.pluralRules or goog.i18n.ordinalRules which determines
 *     which plural/ordinal form to use based on the input number's cardinality.
 * @param {boolean} ignorePound If true, treat '#' in plural messages as a
 *     literary character, else treat it as an ICU syntax character, resolving
 *     to the number (plural_variable - offset).
 * @param {!Array<!string>} result Each formatting stage appends its product
 *     to the result.
 * @private
 */
goog.i18n.MessageFormat.prototype.formatPluralOrdinalBlock_ = function(
    parsedPattern, namedParameters, pluralSelector, ignorePound, result) {
  var argumentIndex = parsedPattern.argumentIndex;
  var argumentOffset = parsedPattern.argumentOffset;
  var pluralValue = +namedParameters[argumentIndex];
  if (isNaN(pluralValue)) {
    // TODO(user): Distinguish between undefined and invalid parameters.
    result.push('Undefined or invalid parameter - ' + argumentIndex);
    return;
  }
  var diff = pluralValue - argumentOffset;

  // Check if there is an exact match.
  var option = parsedPattern[namedParameters[argumentIndex]];
  if (!goog.isDef(option)) {
    goog.asserts.assert(diff >= 0, 'Argument index smaller than offset.');
    var item;
    if (this.numberFormatter_.getMinimumFractionDigits) { // number formatter?
      // If we know the number of fractional digits we can make better decisions
      // We can decide (for instance) between "1 dollar" and "1.00 dollars".
      item = pluralSelector(diff,
          this.numberFormatter_.getMinimumFractionDigits());
    } else {
      item = pluralSelector(diff);
    }
    goog.asserts.assertString(item, 'Invalid plural key.');

    option = parsedPattern[item];

    // If option is not provided fall back to "other".
    if (!goog.isDef(option)) {
      option = parsedPattern[goog.i18n.MessageFormat.OTHER_];
    }

    goog.asserts.assertArray(
        option, 'Invalid option or missing other option for plural block.');
  }

  var pluralResult = [];
  this.formatBlock_(option, namedParameters, ignorePound, pluralResult);
  var plural = pluralResult.join('');
  goog.asserts.assertString(plural, 'Empty block in plural.');
  if (ignorePound) {
    result.push(plural);
  } else {
    var localeAwareDiff = this.numberFormatter_.format(diff);
    result.push(plural.replace(/#/g, localeAwareDiff));
  }
};


/**
 * Parses input pattern into an array, for faster reformatting with
 * different input parameters.
 * Parsing is locale independent.
 * @param {string} pattern MessageFormat pattern to parse.
 * @private
 */
goog.i18n.MessageFormat.prototype.parsePattern_ = function(pattern) {
  if (pattern) {
    pattern = this.insertPlaceholders_(pattern);

    this.parsedPattern_ = this.parseBlock_(pattern);
  }
};


/**
 * Replaces string literals with literal placeholders.
 * 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.
 * @param {string} pattern Pattern to clean up.
 * @return {string} Pattern with literals replaced with placeholders.
 * @private
 */
goog.i18n.MessageFormat.prototype.insertPlaceholders_ = function(pattern) {
  var literals = this.literals_;
  var buildPlaceholder = goog.bind(this.buildPlaceholder_, this);

  // First replace '' with single quote placeholder since they can be found
  // inside other literals.
  pattern = pattern.replace(
      goog.i18n.MessageFormat.REGEX_DOUBLE_APOSTROPHE_,
      function() {
        literals.push("'");
        return buildPlaceholder(literals);
      });

  pattern = pattern.replace(
      goog.i18n.MessageFormat.REGEX_LITERAL_,
      function(match, text) {
        literals.push(text);
        return buildPlaceholder(literals);
      });

  return pattern;
};


/**
 * Breaks pattern into strings and top level {...} blocks.
 * @param {string} pattern (sub)Pattern to be broken.
 * @return {!Array<Object>} Each item is {type, value}.
 * @private
 */
goog.i18n.MessageFormat.prototype.extractParts_ = function(pattern) {
  var prevPos = 0;
  var inBlock = false;
  var braceStack = [];
  var results = [];

  var braces = /[{}]/g;
  braces.lastIndex = 0;  // lastIndex doesn't get set to 0 so we have to.
  var match;

  while (match = braces.exec(pattern)) {
    var pos = match.index;
    if (match[0] == '}') {
      var brace = braceStack.pop();
      goog.asserts.assert(goog.isDef(brace) && brace == '{',
                          'No matching { for }.');

      if (braceStack.length == 0) {
        // End of the block.
        var part = {};
        part.type = goog.i18n.MessageFormat.Element_.BLOCK;
        part.value = pattern.substring(prevPos, pos);
        results.push(part);
        prevPos = pos + 1;
        inBlock = false;
      }
    } else {
      if (braceStack.length == 0) {
        inBlock = true;
        var substring = pattern.substring(prevPos, pos);
        if (substring != '') {
          results.push({
            type: goog.i18n.MessageFormat.Element_.STRING,
            value: substring
          });
        }
        prevPos = pos + 1;
      }
      braceStack.push('{');
    }
  }

  // Take care of the final string, and check if the braceStack is empty.
  goog.asserts.assert(braceStack.length == 0,
                      'There are mismatched { or } in the pattern.');

  var substring = pattern.substring(prevPos);
  if (substring != '') {
    results.push({
      type: goog.i18n.MessageFormat.Element_.STRING,
      value: substring
    });
  }

  return results;
};


/**
 * A regular expression to parse the plural block, extracting the argument
 * index and offset (if any).
 * @type {RegExp}
 * @private
 */
goog.i18n.MessageFormat.PLURAL_BLOCK_RE_ =
    /^\s*(\w+)\s*,\s*plural\s*,(?:\s*offset:(\d+))?/;


/**
 * A regular expression to parse the ordinal block, extracting the argument
 * index.
 * @type {RegExp}
 * @private
 */
goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_ = /^\s*(\w+)\s*,\s*selectordinal\s*,/;


/**
 * A regular expression to parse the select block, extracting the argument
 * index.
 * @type {RegExp}
 * @private
 */
goog.i18n.MessageFormat.SELECT_BLOCK_RE_ = /^\s*(\w+)\s*,\s*select\s*,/;


/**
 * Detects which type of a block is the pattern.
 * @param {string} pattern Content of the block.
 * @return {goog.i18n.MessageFormat.BlockType_} One of the block types.
 * @private
 */
goog.i18n.MessageFormat.prototype.parseBlockType_ = function(pattern) {
  if (goog.i18n.MessageFormat.PLURAL_BLOCK_RE_.test(pattern)) {
    return goog.i18n.MessageFormat.BlockType_.PLURAL;
  }

  if (goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_.test(pattern)) {
    return goog.i18n.MessageFormat.BlockType_.ORDINAL;
  }

  if (goog.i18n.MessageFormat.SELECT_BLOCK_RE_.test(pattern)) {
    return goog.i18n.MessageFormat.BlockType_.SELECT;
  }

  if (/^\s*\w+\s*/.test(pattern)) {
    return goog.i18n.MessageFormat.BlockType_.SIMPLE;
  }

  return goog.i18n.MessageFormat.BlockType_.UNKNOWN;
};


/**
 * Parses generic block.
 * @param {string} pattern Content of the block to parse.
 * @return {!Array<!Object>} Subblocks marked as strings, select...
 * @private
 */
goog.i18n.MessageFormat.prototype.parseBlock_ = function(pattern) {
  var result = [];
  var parts = this.extractParts_(pattern);
  for (var i = 0; i < parts.length; i++) {
    var block = {};
    if (goog.i18n.MessageFormat.Element_.STRING == parts[i].type) {
      block.type = goog.i18n.MessageFormat.BlockType_.STRING;
      block.value = parts[i].value;
    } else if (goog.i18n.MessageFormat.Element_.BLOCK == parts[i].type) {
      var blockType = this.parseBlockType_(parts[i].value);

      switch (blockType) {
        case goog.i18n.MessageFormat.BlockType_.SELECT:
          block.type = goog.i18n.MessageFormat.BlockType_.SELECT;
          block.value = this.parseSelectBlock_(parts[i].value);
          break;
        case goog.i18n.MessageFormat.BlockType_.PLURAL:
          block.type = goog.i18n.MessageFormat.BlockType_.PLURAL;
          block.value = this.parsePluralBlock_(parts[i].value);
          break;
        case goog.i18n.MessageFormat.BlockType_.ORDINAL:
          block.type = goog.i18n.MessageFormat.BlockType_.ORDINAL;
          block.value = this.parseOrdinalBlock_(parts[i].value);
          break;
        case goog.i18n.MessageFormat.BlockType_.SIMPLE:
          block.type = goog.i18n.MessageFormat.BlockType_.SIMPLE;
          block.value = parts[i].value;
          break;
        default:
          goog.asserts.fail(
              'Unknown block type for pattern: ' + parts[i].value);
      }
    } else {
      goog.asserts.fail('Unknown part of the pattern.');
    }
    result.push(block);
  }

  return result;
};


/**
 * Parses a select type of a block and produces JSON object for it.
 * @param {string} pattern Subpattern that needs to be parsed as select pattern.
 * @return {!Object} Object with select block info.
 * @private
 */
goog.i18n.MessageFormat.prototype.parseSelectBlock_ = function(pattern) {
  var argumentIndex = '';
  var replaceRegex = goog.i18n.MessageFormat.SELECT_BLOCK_RE_;
  pattern = pattern.replace(replaceRegex, function(string, name) {
    argumentIndex = name;
    return '';
  });
  var result = {};
  result.argumentIndex = argumentIndex;

  var parts = this.extractParts_(pattern);
  // Looking for (key block)+ sequence. One of the keys has to be "other".
  var pos = 0;
  while (pos < parts.length) {
    var key = parts[pos].value;
    goog.asserts.assertString(key, 'Missing select key element.');

    pos++;
    goog.asserts.assert(pos < parts.length,
                        'Missing or invalid select value element.');

    if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {
      var value = this.parseBlock_(parts[pos].value);
    } else {
      goog.asserts.fail('Expected block type.');
    }
    result[key.replace(/\s/g, '')] = value;
    pos++;
  }

  goog.asserts.assertArray(result[goog.i18n.MessageFormat.OTHER_],
                           'Missing other key in select statement.');
  return result;
};


/**
 * Parses a plural type of a block and produces JSON object for it.
 * @param {string} pattern Subpattern that needs to be parsed as plural pattern.
 * @return {!Object} Object with select block info.
 * @private
 */
goog.i18n.MessageFormat.prototype.parsePluralBlock_ = function(pattern) {
  var argumentIndex = '';
  var argumentOffset = 0;
  var replaceRegex = goog.i18n.MessageFormat.PLURAL_BLOCK_RE_;
  pattern = pattern.replace(replaceRegex, function(string, name, offset) {
    argumentIndex = name;
    if (offset) {
      argumentOffset = parseInt(offset, 10);
    }
    return '';
  });

  var result = {};
  result.argumentIndex = argumentIndex;
  result.argumentOffset = argumentOffset;

  var parts = this.extractParts_(pattern);
  // Looking for (key block)+ sequence.
  var pos = 0;
  while (pos < parts.length) {
    var key = parts[pos].value;
    goog.asserts.assertString(key, 'Missing plural key element.');

    pos++;
    goog.asserts.assert(pos < parts.length,
                        'Missing or invalid plural value element.');

    if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {
      var value = this.parseBlock_(parts[pos].value);
    } else {
      goog.asserts.fail('Expected block type.');
    }
    result[key.replace(/\s*(?:=)?(\w+)\s*/, '$1')] = value;
    pos++;
  }

  goog.asserts.assertArray(result[goog.i18n.MessageFormat.OTHER_],
                           'Missing other key in plural statement.');

  return result;
};


/**
 * Parses an ordinal type of a block and produces JSON object for it.
 * For example the input string:
 *  '{FOO, selectordinal, one {Message A}other {Message B}}'
 * Should result in the output object:
 * {
 *   argumentIndex: 'FOO',
 *   argumentOffest: 0,
 *   one: [ { type: 4, value: 'Message A' } ],
 *   other: [ { type: 4, value: 'Message B' } ]
 * }
 * @param {string} pattern Subpattern that needs to be parsed as plural pattern.
 * @return {!Object} Object with select block info.
 * @private
 */
goog.i18n.MessageFormat.prototype.parseOrdinalBlock_ = function(pattern) {
  var argumentIndex = '';
  var replaceRegex = goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_;
  pattern = pattern.replace(replaceRegex, function(string, name) {
    argumentIndex = name;
    return '';
  });

  var result = {};
  result.argumentIndex = argumentIndex;
  result.argumentOffset = 0;

  var parts = this.extractParts_(pattern);
  // Looking for (key block)+ sequence.
  var pos = 0;
  while (pos < parts.length) {
    var key = parts[pos].value;
    goog.asserts.assertString(key, 'Missing ordinal key element.');

    pos++;
    goog.asserts.assert(pos < parts.length,
                        'Missing or invalid ordinal value element.');

    if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {
      var value = this.parseBlock_(parts[pos].value);
    } else {
      goog.asserts.fail('Expected block type.');
    }
    result[key.replace(/\s*(?:=)?(\w+)\s*/, '$1')] = value;
    pos++;
  }

  goog.asserts.assertArray(result[goog.i18n.MessageFormat.OTHER_],
                           'Missing other key in selectordinal statement.');

  return result;
};


/**
 * Builds a placeholder from the last index of the array.
 * @param {!Array<string>} literals All literals encountered during parse.
 * @return {string} \uFDDF_ + last index + _.
 * @private
 */
goog.i18n.MessageFormat.prototype.buildPlaceholder_ = function(literals) {
  goog.asserts.assert(literals.length > 0, 'Literal array is empty.');

  var index = (literals.length - 1).toString(10);
  return goog.i18n.MessageFormat.LITERAL_PLACEHOLDER_ + index + '_';
};
