Path: blob/trunk/third_party/closure/goog/i18n/messageformat.js
2868 views
// Copyright 2010 The Closure Library Authors. All Rights Reserved1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Message/plural format library with locale support.16*17* Message format grammar:18*19* messageFormatPattern := string ( "{" messageFormatElement "}" string )*20* messageFormatElement := argumentIndex [ "," elementFormat ]21* elementFormat := "plural" "," pluralStyle22* | "selectordinal" "," ordinalStyle23* | "select" "," selectStyle24* pluralStyle := pluralFormatPattern25* ordinalStyle := selectFormatPattern26* selectStyle := selectFormatPattern27* pluralFormatPattern := [ "offset" ":" offsetIndex ] pluralForms*28* selectFormatPattern := pluralForms*29* pluralForms := stringKey "{" ( "{" messageFormatElement "}"|string )* "}"30*31* This is a subset of the ICU MessageFormatSyntax:32* http://userguide.icu-project.org/formatparse/messages33* See also http://go/plurals and http://go/ordinals for internal details.34*35*36* Message example:37*38* I see {NUM_PEOPLE, plural, offset:139* =0 {no one at all}40* =1 {{WHO}}41* one {{WHO} and one other person}42* other {{WHO} and # other people}}43* in {PLACE}.44*45* Calling format({'NUM_PEOPLE': 2, 'WHO': 'Mark', 'PLACE': 'Athens'}) would46* produce "I see Mark and one other person in Athens." as output.47*48* OR:49*50* {NUM_FLOOR, selectordinal,51* one {Take the elevator to the #st floor.}52* two {Take the elevator to the #nd floor.}53* few {Take the elevator to the #rd floor.}54* other {Take the elevator to the #th floor.}}55*56* Calling format({'NUM_FLOOR': 22}) would produce57* "Take the elevator to the 22nd floor".58*59* See messageformat_test.html for more examples.60*/6162goog.provide('goog.i18n.MessageFormat');6364goog.require('goog.array');65goog.require('goog.asserts');66goog.require('goog.i18n.CompactNumberFormatSymbols');67goog.require('goog.i18n.NumberFormat');68goog.require('goog.i18n.NumberFormatSymbols');69goog.require('goog.i18n.ordinalRules');70goog.require('goog.i18n.pluralRules');71727374/**75* Constructor of MessageFormat.76* @param {string} pattern The pattern we parse and apply positional parameters77* to.78* @constructor79* @final80*/81goog.i18n.MessageFormat = function(pattern) {82/**83* The pattern we parse and apply positional parameters to.84* @type {?string}85* @private86*/87this.pattern_ = pattern;8889/**90* All encountered literals during parse stage. Indices tell us the order of91* replacement.92* @type {?Array<string>}93* @private94*/95this.initialLiterals_ = null;9697/**98* Working array with all encountered literals during parse and format stages.99* Indices tell us the order of replacement.100* @type {?Array<string>}101* @private102*/103this.literals_ = null;104105/**106* Input pattern gets parsed into objects for faster formatting.107* @type {?Array<!Object>}108* @private109*/110this.parsedPattern_ = null;111112/**113* Locale aware number formatter.114* @type {!goog.i18n.NumberFormat}115* @private116*/117this.numberFormatter_ = goog.i18n.MessageFormat.getNumberFormatter_();118};119120121/**122* Locale associated with the most recently created NumberFormat.123* @type {?Object}124* @private125*/126goog.i18n.MessageFormat.numberFormatterSymbols_ = null;127128129/**130* Locale associated with the most recently created NumberFormat.131* @type {?Object}132* @private133*/134goog.i18n.MessageFormat.compactNumberFormatterSymbols_ = null;135136137/**138* Locale aware number formatter. Reference to the most recently created139* NumberFormat for sharing between MessageFormat instances.140* @type {?goog.i18n.NumberFormat}141* @private142*/143goog.i18n.MessageFormat.numberFormatter_ = null;144145146/**147* Literal strings, including '', are replaced with \uFDDF_x_ for148* parsing purposes, and recovered during format phase.149* \uFDDF is a Unicode nonprinting character, not expected to be found in the150* typical message.151* @type {string}152* @private153*/154goog.i18n.MessageFormat.LITERAL_PLACEHOLDER_ = '\uFDDF_';155156157/**158* Marks a string and block during parsing.159* @enum {number}160* @private161*/162goog.i18n.MessageFormat.Element_ = {163STRING: 0,164BLOCK: 1165};166167168/**169* Block type.170* @enum {number}171* @private172*/173goog.i18n.MessageFormat.BlockType_ = {174PLURAL: 0,175ORDINAL: 1,176SELECT: 2,177SIMPLE: 3,178STRING: 4,179UNKNOWN: 5180};181182183/**184* Mandatory option in both select and plural form.185* @type {string}186* @private187*/188goog.i18n.MessageFormat.OTHER_ = 'other';189190191/**192* Regular expression for looking for string literals.193* @type {RegExp}194* @private195*/196goog.i18n.MessageFormat.REGEX_LITERAL_ = new RegExp("'([{}#].*?)'", 'g');197198199/**200* Regular expression for looking for '' in the message.201* @type {RegExp}202* @private203*/204goog.i18n.MessageFormat.REGEX_DOUBLE_APOSTROPHE_ = new RegExp("''", 'g');205206/** @typedef {{ type: goog.i18n.MessageFormat.Element_, value: ? }} */207goog.i18n.MessageFormat.TypeVal_;208209210/**211* Gets the a NumberFormat instance for the current locale.212* If the locale is the same as the previous invocation, returns the same213* NumberFormat instance. Otherwise, creates a new one.214* @return {!goog.i18n.NumberFormat}215* @private216*/217goog.i18n.MessageFormat.getNumberFormatter_ = function() {218var currentSymbols = goog.i18n.NumberFormatSymbols;219var currentCompactSymbols = goog.i18n.CompactNumberFormatSymbols;220221if (goog.i18n.MessageFormat.numberFormatterSymbols_ !== currentSymbols ||222goog.i18n.MessageFormat.compactNumberFormatterSymbols_ !==223currentCompactSymbols) {224goog.i18n.MessageFormat.numberFormatterSymbols_ = currentSymbols;225goog.i18n.MessageFormat.compactNumberFormatterSymbols_ =226currentCompactSymbols;227goog.i18n.MessageFormat.numberFormatter_ =228new goog.i18n.NumberFormat(goog.i18n.NumberFormat.Format.DECIMAL);229}230231return /** @type {!goog.i18n.NumberFormat} */ (232goog.i18n.MessageFormat.numberFormatter_);233};234235236/**237* Formats a message, treating '#' with special meaning representing238* the number (plural_variable - offset).239* @param {!Object} namedParameters Parameters that either240* influence the formatting or are used as actual data.241* I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),242* object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.243* 1st parameter could mean 5 people, which could influence plural format,244* and 2nd parameter is just a data to be printed out in proper position.245* @return {string} Formatted message.246*/247goog.i18n.MessageFormat.prototype.format = function(namedParameters) {248return this.format_(namedParameters, false);249};250251252/**253* Formats a message, treating '#' as literary character.254* @param {!Object} namedParameters Parameters that either255* influence the formatting or are used as actual data.256* I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),257* object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.258* 1st parameter could mean 5 people, which could influence plural format,259* and 2nd parameter is just a data to be printed out in proper position.260* @return {string} Formatted message.261*/262goog.i18n.MessageFormat.prototype.formatIgnoringPound = function(263namedParameters) {264return this.format_(namedParameters, true);265};266267268/**269* Formats a message.270* @param {!Object} namedParameters Parameters that either271* influence the formatting or are used as actual data.272* I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),273* object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.274* 1st parameter could mean 5 people, which could influence plural format,275* and 2nd parameter is just a data to be printed out in proper position.276* @param {boolean} ignorePound If true, treat '#' in plural messages as a277* literary character, else treat it as an ICU syntax character, resolving278* to the number (plural_variable - offset).279* @return {string} Formatted message.280* @private281*/282goog.i18n.MessageFormat.prototype.format_ = function(283namedParameters, ignorePound) {284this.init_();285if (!this.parsedPattern_ || this.parsedPattern_.length == 0) {286return '';287}288this.literals_ = goog.array.clone(this.initialLiterals_);289290var result = [];291this.formatBlock_(this.parsedPattern_, namedParameters, ignorePound, result);292var message = result.join('');293294if (!ignorePound) {295goog.asserts.assert(message.search('#') == -1, 'Not all # were replaced.');296}297298while (this.literals_.length > 0) {299message = message.replace(300this.buildPlaceholder_(this.literals_), this.literals_.pop());301}302303return message;304};305306307/**308* Parses generic block and returns a formatted string.309* @param {!Array<!goog.i18n.MessageFormat.TypeVal_>} parsedPattern310* Holds parsed tree.311* @param {!Object} namedParameters Parameters that either influence312* the formatting or are used as actual data.313* @param {boolean} ignorePound If true, treat '#' in plural messages as a314* literary character, else treat it as an ICU syntax character, resolving315* to the number (plural_variable - offset).316* @param {!Array<string>} result Each formatting stage appends its product317* to the result.318* @private319*/320goog.i18n.MessageFormat.prototype.formatBlock_ = function(321parsedPattern, namedParameters, ignorePound, result) {322for (var i = 0; i < parsedPattern.length; i++) {323switch (parsedPattern[i].type) {324case goog.i18n.MessageFormat.BlockType_.STRING:325result.push(parsedPattern[i].value);326break;327case goog.i18n.MessageFormat.BlockType_.SIMPLE:328var pattern = parsedPattern[i].value;329this.formatSimplePlaceholder_(pattern, namedParameters, result);330break;331case goog.i18n.MessageFormat.BlockType_.SELECT:332var pattern = parsedPattern[i].value;333this.formatSelectBlock_(pattern, namedParameters, ignorePound, result);334break;335case goog.i18n.MessageFormat.BlockType_.PLURAL:336var pattern = parsedPattern[i].value;337this.formatPluralOrdinalBlock_(338pattern, namedParameters, goog.i18n.pluralRules.select, ignorePound,339result);340break;341case goog.i18n.MessageFormat.BlockType_.ORDINAL:342var pattern = parsedPattern[i].value;343this.formatPluralOrdinalBlock_(344pattern, namedParameters, goog.i18n.ordinalRules.select,345ignorePound, result);346break;347default:348goog.asserts.fail('Unrecognized block type: ' + parsedPattern[i].type);349}350}351};352353354/**355* Formats simple placeholder.356* @param {!Object} parsedPattern JSON object containing placeholder info.357* @param {!Object} namedParameters Parameters that are used as actual data.358* @param {!Array<string>} result Each formatting stage appends its product359* to the result.360* @private361*/362goog.i18n.MessageFormat.prototype.formatSimplePlaceholder_ = function(363parsedPattern, namedParameters, result) {364var value = namedParameters[parsedPattern];365if (!goog.isDef(value)) {366result.push('Undefined parameter - ' + parsedPattern);367return;368}369370// Don't push the value yet, it may contain any of # { } in it which371// will break formatter. Insert a placeholder and replace at the end.372this.literals_.push(value);373result.push(this.buildPlaceholder_(this.literals_));374};375376377/**378* Formats select block. Only one option is selected.379* @param {!{argumentIndex:?}} parsedPattern JSON object containing select380* block info.381* @param {!Object} namedParameters Parameters that either influence382* the formatting or are used as actual data.383* @param {boolean} ignorePound If true, treat '#' in plural messages as a384* literary character, else treat it as an ICU syntax character, resolving385* to the number (plural_variable - offset).386* @param {!Array<string>} result Each formatting stage appends its product387* to the result.388* @private389*/390goog.i18n.MessageFormat.prototype.formatSelectBlock_ = function(391parsedPattern, namedParameters, ignorePound, result) {392var argumentIndex = parsedPattern.argumentIndex;393if (!goog.isDef(namedParameters[argumentIndex])) {394result.push('Undefined parameter - ' + argumentIndex);395return;396}397398var option = parsedPattern[namedParameters[argumentIndex]];399if (!goog.isDef(option)) {400option = parsedPattern[goog.i18n.MessageFormat.OTHER_];401goog.asserts.assertArray(402option, 'Invalid option or missing other option for select block.');403}404405this.formatBlock_(option, namedParameters, ignorePound, result);406};407408409/**410* Formats plural or selectordinal block. Only one option is selected and all #411* are replaced.412* @param {!{argumentIndex, argumentOffset}} parsedPattern JSON object413* containing plural block info.414* @param {!Object} namedParameters Parameters that either influence415* the formatting or are used as actual data.416* @param {function(number, number=):string} pluralSelector A select function417* from goog.i18n.pluralRules or goog.i18n.ordinalRules which determines418* which plural/ordinal form to use based on the input number's cardinality.419* @param {boolean} ignorePound If true, treat '#' in plural messages as a420* literary character, else treat it as an ICU syntax character, resolving421* to the number (plural_variable - offset).422* @param {!Array<string>} result Each formatting stage appends its product423* to the result.424* @private425*/426goog.i18n.MessageFormat.prototype.formatPluralOrdinalBlock_ = function(427parsedPattern, namedParameters, pluralSelector, ignorePound, result) {428var argumentIndex = parsedPattern.argumentIndex;429var argumentOffset = parsedPattern.argumentOffset;430var pluralValue = +namedParameters[argumentIndex];431if (isNaN(pluralValue)) {432// TODO(user): Distinguish between undefined and invalid parameters.433result.push('Undefined or invalid parameter - ' + argumentIndex);434return;435}436var diff = pluralValue - argumentOffset;437438// Check if there is an exact match.439var option = parsedPattern[namedParameters[argumentIndex]];440if (!goog.isDef(option)) {441goog.asserts.assert(diff >= 0, 'Argument index smaller than offset.');442var item;443if (this.numberFormatter_.getMinimumFractionDigits) { // number formatter?444// If we know the number of fractional digits we can make better decisions445// We can decide (for instance) between "1 dollar" and "1.00 dollars".446item = pluralSelector(447diff, this.numberFormatter_.getMinimumFractionDigits());448} else {449item = pluralSelector(diff);450}451goog.asserts.assertString(item, 'Invalid plural key.');452453option = parsedPattern[item];454455// If option is not provided fall back to "other".456if (!goog.isDef(option)) {457option = parsedPattern[goog.i18n.MessageFormat.OTHER_];458}459460goog.asserts.assertArray(461option, 'Invalid option or missing other option for plural block.');462}463464var pluralResult = [];465this.formatBlock_(option, namedParameters, ignorePound, pluralResult);466var plural = pluralResult.join('');467goog.asserts.assertString(plural, 'Empty block in plural.');468if (ignorePound) {469result.push(plural);470} else {471var localeAwareDiff = this.numberFormatter_.format(diff);472result.push(plural.replace(/#/g, localeAwareDiff));473}474};475476477/**478* Set up the MessageFormat.479* Parses input pattern into an array, for faster reformatting with480* different input parameters.481* Parsing is locale independent.482* @private483*/484goog.i18n.MessageFormat.prototype.init_ = function() {485if (this.pattern_) {486this.initialLiterals_ = [];487var pattern = this.insertPlaceholders_(this.pattern_);488489this.parsedPattern_ = this.parseBlock_(pattern);490this.pattern_ = null;491}492};493494495/**496* Replaces string literals with literal placeholders.497* Literals are string of the form '}...', '{...' and '#...' where ... is498* set of characters not containing '499* Builds a dictionary so we can recover literals during format phase.500* @param {string} pattern Pattern to clean up.501* @return {string} Pattern with literals replaced with placeholders.502* @private503*/504goog.i18n.MessageFormat.prototype.insertPlaceholders_ = function(pattern) {505var literals = this.initialLiterals_;506var buildPlaceholder = goog.bind(this.buildPlaceholder_, this);507508// First replace '' with single quote placeholder since they can be found509// inside other literals.510pattern = pattern.replace(511goog.i18n.MessageFormat.REGEX_DOUBLE_APOSTROPHE_, function() {512literals.push("'");513return buildPlaceholder(literals);514});515516pattern = pattern.replace(517goog.i18n.MessageFormat.REGEX_LITERAL_, function(match, text) {518literals.push(text);519return buildPlaceholder(literals);520});521522return pattern;523};524525526/**527* Breaks pattern into strings and top level {...} blocks.528* @param {string} pattern (sub)Pattern to be broken.529* @return {!Array<goog.i18n.MessageFormat.TypeVal_>}530* @private531*/532goog.i18n.MessageFormat.prototype.extractParts_ = function(pattern) {533var prevPos = 0;534var braceStack = [];535var results = [];536537var braces = /[{}]/g;538braces.lastIndex = 0; // lastIndex doesn't get set to 0 so we have to.539var match;540541while (match = braces.exec(pattern)) {542var pos = match.index;543if (match[0] == '}') {544var brace = braceStack.pop();545goog.asserts.assert(546goog.isDef(brace) && brace == '{', 'No matching { for }.');547548if (braceStack.length == 0) {549// End of the block.550var part = {};551part.type = goog.i18n.MessageFormat.Element_.BLOCK;552part.value = pattern.substring(prevPos, pos);553results.push(part);554prevPos = pos + 1;555}556} else {557if (braceStack.length == 0) {558var substring = pattern.substring(prevPos, pos);559if (substring != '') {560results.push({561type: goog.i18n.MessageFormat.Element_.STRING,562value: substring563});564}565prevPos = pos + 1;566}567braceStack.push('{');568}569}570571// Take care of the final string, and check if the braceStack is empty.572goog.asserts.assert(573braceStack.length == 0, 'There are mismatched { or } in the pattern.');574575var substring = pattern.substring(prevPos);576if (substring != '') {577results.push(578{type: goog.i18n.MessageFormat.Element_.STRING, value: substring});579}580581return results;582};583584585/**586* A regular expression to parse the plural block, extracting the argument587* index and offset (if any).588* @type {RegExp}589* @private590*/591goog.i18n.MessageFormat.PLURAL_BLOCK_RE_ =592/^\s*(\w+)\s*,\s*plural\s*,(?:\s*offset:(\d+))?/;593594595/**596* A regular expression to parse the ordinal block, extracting the argument597* index.598* @type {RegExp}599* @private600*/601goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_ = /^\s*(\w+)\s*,\s*selectordinal\s*,/;602603604/**605* A regular expression to parse the select block, extracting the argument606* index.607* @type {RegExp}608* @private609*/610goog.i18n.MessageFormat.SELECT_BLOCK_RE_ = /^\s*(\w+)\s*,\s*select\s*,/;611612613/**614* Detects which type of a block is the pattern.615* @param {string} pattern Content of the block.616* @return {goog.i18n.MessageFormat.BlockType_} One of the block types.617* @private618*/619goog.i18n.MessageFormat.prototype.parseBlockType_ = function(pattern) {620if (goog.i18n.MessageFormat.PLURAL_BLOCK_RE_.test(pattern)) {621return goog.i18n.MessageFormat.BlockType_.PLURAL;622}623624if (goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_.test(pattern)) {625return goog.i18n.MessageFormat.BlockType_.ORDINAL;626}627628if (goog.i18n.MessageFormat.SELECT_BLOCK_RE_.test(pattern)) {629return goog.i18n.MessageFormat.BlockType_.SELECT;630}631632if (/^\s*\w+\s*/.test(pattern)) {633return goog.i18n.MessageFormat.BlockType_.SIMPLE;634}635636return goog.i18n.MessageFormat.BlockType_.UNKNOWN;637};638639640/**641* Parses generic block.642* @param {string} pattern Content of the block to parse.643* @return {!Array<!Object>} Subblocks marked as strings, select...644* @private645*/646goog.i18n.MessageFormat.prototype.parseBlock_ = function(pattern) {647var result = [];648var parts = this.extractParts_(pattern);649for (var i = 0; i < parts.length; i++) {650var block = {};651if (goog.i18n.MessageFormat.Element_.STRING == parts[i].type) {652block.type = goog.i18n.MessageFormat.BlockType_.STRING;653block.value = parts[i].value;654} else if (goog.i18n.MessageFormat.Element_.BLOCK == parts[i].type) {655var blockType = this.parseBlockType_(parts[i].value);656657switch (blockType) {658case goog.i18n.MessageFormat.BlockType_.SELECT:659block.type = goog.i18n.MessageFormat.BlockType_.SELECT;660block.value = this.parseSelectBlock_(parts[i].value);661break;662case goog.i18n.MessageFormat.BlockType_.PLURAL:663block.type = goog.i18n.MessageFormat.BlockType_.PLURAL;664block.value = this.parsePluralBlock_(parts[i].value);665break;666case goog.i18n.MessageFormat.BlockType_.ORDINAL:667block.type = goog.i18n.MessageFormat.BlockType_.ORDINAL;668block.value = this.parseOrdinalBlock_(parts[i].value);669break;670case goog.i18n.MessageFormat.BlockType_.SIMPLE:671block.type = goog.i18n.MessageFormat.BlockType_.SIMPLE;672block.value = parts[i].value;673break;674default:675goog.asserts.fail(676'Unknown block type for pattern: ' + parts[i].value);677}678} else {679goog.asserts.fail('Unknown part of the pattern.');680}681result.push(block);682}683684return result;685};686687688/**689* Parses a select type of a block and produces JSON object for it.690* @param {string} pattern Subpattern that needs to be parsed as select pattern.691* @return {!Object} Object with select block info.692* @private693*/694goog.i18n.MessageFormat.prototype.parseSelectBlock_ = function(pattern) {695var argumentIndex = '';696var replaceRegex = goog.i18n.MessageFormat.SELECT_BLOCK_RE_;697pattern = pattern.replace(replaceRegex, function(string, name) {698argumentIndex = name;699return '';700});701var result = {};702result.argumentIndex = argumentIndex;703704var parts = this.extractParts_(pattern);705// Looking for (key block)+ sequence. One of the keys has to be "other".706var pos = 0;707while (pos < parts.length) {708var key = parts[pos].value;709goog.asserts.assertString(key, 'Missing select key element.');710711pos++;712goog.asserts.assert(713pos < parts.length, 'Missing or invalid select value element.');714715if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {716var value = this.parseBlock_(parts[pos].value);717} else {718goog.asserts.fail('Expected block type.');719}720result[key.replace(/\s/g, '')] = value;721pos++;722}723724goog.asserts.assertArray(725result[goog.i18n.MessageFormat.OTHER_],726'Missing other key in select statement.');727return result;728};729730731/**732* Parses a plural type of a block and produces JSON object for it.733* @param {string} pattern Subpattern that needs to be parsed as plural pattern.734* @return {!Object} Object with select block info.735* @private736*/737goog.i18n.MessageFormat.prototype.parsePluralBlock_ = function(pattern) {738var argumentIndex = '';739var argumentOffset = 0;740var replaceRegex = goog.i18n.MessageFormat.PLURAL_BLOCK_RE_;741pattern = pattern.replace(replaceRegex, function(string, name, offset) {742argumentIndex = name;743if (offset) {744argumentOffset = parseInt(offset, 10);745}746return '';747});748749var result = {};750result.argumentIndex = argumentIndex;751result.argumentOffset = argumentOffset;752753var parts = this.extractParts_(pattern);754// Looking for (key block)+ sequence.755var pos = 0;756while (pos < parts.length) {757var key = parts[pos].value;758goog.asserts.assertString(key, 'Missing plural key element.');759760pos++;761goog.asserts.assert(762pos < parts.length, 'Missing or invalid plural value element.');763764if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {765var value = this.parseBlock_(parts[pos].value);766} else {767goog.asserts.fail('Expected block type.');768}769result[key.replace(/\s*(?:=)?(\w+)\s*/, '$1')] = value;770pos++;771}772773goog.asserts.assertArray(774result[goog.i18n.MessageFormat.OTHER_],775'Missing other key in plural statement.');776777return result;778};779780781/**782* Parses an ordinal type of a block and produces JSON object for it.783* For example the input string:784* '{FOO, selectordinal, one {Message A}other {Message B}}'785* Should result in the output object:786* {787* argumentIndex: 'FOO',788* argumentOffest: 0,789* one: [ { type: 4, value: 'Message A' } ],790* other: [ { type: 4, value: 'Message B' } ]791* }792* @param {string} pattern Subpattern that needs to be parsed as plural pattern.793* @return {!Object} Object with select block info.794* @private795*/796goog.i18n.MessageFormat.prototype.parseOrdinalBlock_ = function(pattern) {797var argumentIndex = '';798var replaceRegex = goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_;799pattern = pattern.replace(replaceRegex, function(string, name) {800argumentIndex = name;801return '';802});803804var result = {};805result.argumentIndex = argumentIndex;806result.argumentOffset = 0;807808var parts = this.extractParts_(pattern);809// Looking for (key block)+ sequence.810var pos = 0;811while (pos < parts.length) {812var key = parts[pos].value;813goog.asserts.assertString(key, 'Missing ordinal key element.');814815pos++;816goog.asserts.assert(817pos < parts.length, 'Missing or invalid ordinal value element.');818819if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {820var value = this.parseBlock_(parts[pos].value);821} else {822goog.asserts.fail('Expected block type.');823}824result[key.replace(/\s*(?:=)?(\w+)\s*/, '$1')] = value;825pos++;826}827828goog.asserts.assertArray(829result[goog.i18n.MessageFormat.OTHER_],830'Missing other key in selectordinal statement.');831832return result;833};834835836/**837* Builds a placeholder from the last index of the array.838* @param {!Array<string>} literals All literals encountered during parse.839* @return {string} \uFDDF_ + last index + _.840* @private841*/842goog.i18n.MessageFormat.prototype.buildPlaceholder_ = function(literals) {843goog.asserts.assert(literals.length > 0, 'Literal array is empty.');844845var index = (literals.length - 1).toString(10);846return goog.i18n.MessageFormat.LITERAL_PLACEHOLDER_ + index + '_';847};848849850