messageformat.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. // Copyright 2010 The Closure Library Authors. All Rights Reserved
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  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" "," pluralStyle
  22. * | "selectordinal" "," ordinalStyle
  23. * | "select" "," selectStyle
  24. * pluralStyle := pluralFormatPattern
  25. * ordinalStyle := selectFormatPattern
  26. * selectStyle := selectFormatPattern
  27. * 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/messages
  33. * 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:1
  39. * =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'}) would
  46. * 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 produce
  57. * "Take the elevator to the 22nd floor".
  58. *
  59. * See messageformat_test.html for more examples.
  60. */
  61. goog.provide('goog.i18n.MessageFormat');
  62. goog.require('goog.array');
  63. goog.require('goog.asserts');
  64. goog.require('goog.i18n.CompactNumberFormatSymbols');
  65. goog.require('goog.i18n.NumberFormat');
  66. goog.require('goog.i18n.NumberFormatSymbols');
  67. goog.require('goog.i18n.ordinalRules');
  68. goog.require('goog.i18n.pluralRules');
  69. /**
  70. * Constructor of MessageFormat.
  71. * @param {string} pattern The pattern we parse and apply positional parameters
  72. * to.
  73. * @constructor
  74. * @final
  75. */
  76. goog.i18n.MessageFormat = function(pattern) {
  77. /**
  78. * The pattern we parse and apply positional parameters to.
  79. * @type {?string}
  80. * @private
  81. */
  82. this.pattern_ = pattern;
  83. /**
  84. * All encountered literals during parse stage. Indices tell us the order of
  85. * replacement.
  86. * @type {?Array<string>}
  87. * @private
  88. */
  89. this.initialLiterals_ = null;
  90. /**
  91. * Working array with all encountered literals during parse and format stages.
  92. * Indices tell us the order of replacement.
  93. * @type {?Array<string>}
  94. * @private
  95. */
  96. this.literals_ = null;
  97. /**
  98. * Input pattern gets parsed into objects for faster formatting.
  99. * @type {?Array<!Object>}
  100. * @private
  101. */
  102. this.parsedPattern_ = null;
  103. /**
  104. * Locale aware number formatter.
  105. * @type {!goog.i18n.NumberFormat}
  106. * @private
  107. */
  108. this.numberFormatter_ = goog.i18n.MessageFormat.getNumberFormatter_();
  109. };
  110. /**
  111. * Locale associated with the most recently created NumberFormat.
  112. * @type {?Object}
  113. * @private
  114. */
  115. goog.i18n.MessageFormat.numberFormatterSymbols_ = null;
  116. /**
  117. * Locale associated with the most recently created NumberFormat.
  118. * @type {?Object}
  119. * @private
  120. */
  121. goog.i18n.MessageFormat.compactNumberFormatterSymbols_ = null;
  122. /**
  123. * Locale aware number formatter. Reference to the most recently created
  124. * NumberFormat for sharing between MessageFormat instances.
  125. * @type {?goog.i18n.NumberFormat}
  126. * @private
  127. */
  128. goog.i18n.MessageFormat.numberFormatter_ = null;
  129. /**
  130. * Literal strings, including '', are replaced with \uFDDF_x_ for
  131. * parsing purposes, and recovered during format phase.
  132. * \uFDDF is a Unicode nonprinting character, not expected to be found in the
  133. * typical message.
  134. * @type {string}
  135. * @private
  136. */
  137. goog.i18n.MessageFormat.LITERAL_PLACEHOLDER_ = '\uFDDF_';
  138. /**
  139. * Marks a string and block during parsing.
  140. * @enum {number}
  141. * @private
  142. */
  143. goog.i18n.MessageFormat.Element_ = {
  144. STRING: 0,
  145. BLOCK: 1
  146. };
  147. /**
  148. * Block type.
  149. * @enum {number}
  150. * @private
  151. */
  152. goog.i18n.MessageFormat.BlockType_ = {
  153. PLURAL: 0,
  154. ORDINAL: 1,
  155. SELECT: 2,
  156. SIMPLE: 3,
  157. STRING: 4,
  158. UNKNOWN: 5
  159. };
  160. /**
  161. * Mandatory option in both select and plural form.
  162. * @type {string}
  163. * @private
  164. */
  165. goog.i18n.MessageFormat.OTHER_ = 'other';
  166. /**
  167. * Regular expression for looking for string literals.
  168. * @type {RegExp}
  169. * @private
  170. */
  171. goog.i18n.MessageFormat.REGEX_LITERAL_ = new RegExp("'([{}#].*?)'", 'g');
  172. /**
  173. * Regular expression for looking for '' in the message.
  174. * @type {RegExp}
  175. * @private
  176. */
  177. goog.i18n.MessageFormat.REGEX_DOUBLE_APOSTROPHE_ = new RegExp("''", 'g');
  178. /** @typedef {{ type: goog.i18n.MessageFormat.Element_, value: ? }} */
  179. goog.i18n.MessageFormat.TypeVal_;
  180. /**
  181. * Gets the a NumberFormat instance for the current locale.
  182. * If the locale is the same as the previous invocation, returns the same
  183. * NumberFormat instance. Otherwise, creates a new one.
  184. * @return {!goog.i18n.NumberFormat}
  185. * @private
  186. */
  187. goog.i18n.MessageFormat.getNumberFormatter_ = function() {
  188. var currentSymbols = goog.i18n.NumberFormatSymbols;
  189. var currentCompactSymbols = goog.i18n.CompactNumberFormatSymbols;
  190. if (goog.i18n.MessageFormat.numberFormatterSymbols_ !== currentSymbols ||
  191. goog.i18n.MessageFormat.compactNumberFormatterSymbols_ !==
  192. currentCompactSymbols) {
  193. goog.i18n.MessageFormat.numberFormatterSymbols_ = currentSymbols;
  194. goog.i18n.MessageFormat.compactNumberFormatterSymbols_ =
  195. currentCompactSymbols;
  196. goog.i18n.MessageFormat.numberFormatter_ =
  197. new goog.i18n.NumberFormat(goog.i18n.NumberFormat.Format.DECIMAL);
  198. }
  199. return /** @type {!goog.i18n.NumberFormat} */ (
  200. goog.i18n.MessageFormat.numberFormatter_);
  201. };
  202. /**
  203. * Formats a message, treating '#' with special meaning representing
  204. * the number (plural_variable - offset).
  205. * @param {!Object} namedParameters Parameters that either
  206. * influence the formatting or are used as actual data.
  207. * I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),
  208. * object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.
  209. * 1st parameter could mean 5 people, which could influence plural format,
  210. * and 2nd parameter is just a data to be printed out in proper position.
  211. * @return {string} Formatted message.
  212. */
  213. goog.i18n.MessageFormat.prototype.format = function(namedParameters) {
  214. return this.format_(namedParameters, false);
  215. };
  216. /**
  217. * Formats a message, treating '#' as literary character.
  218. * @param {!Object} namedParameters Parameters that either
  219. * influence the formatting or are used as actual data.
  220. * I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),
  221. * object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.
  222. * 1st parameter could mean 5 people, which could influence plural format,
  223. * and 2nd parameter is just a data to be printed out in proper position.
  224. * @return {string} Formatted message.
  225. */
  226. goog.i18n.MessageFormat.prototype.formatIgnoringPound = function(
  227. namedParameters) {
  228. return this.format_(namedParameters, true);
  229. };
  230. /**
  231. * Formats a message.
  232. * @param {!Object} namedParameters Parameters that either
  233. * influence the formatting or are used as actual data.
  234. * I.e. in call to fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'}),
  235. * object {'NUM_PEOPLE': 5, 'NAME': 'Angela'} holds positional parameters.
  236. * 1st parameter could mean 5 people, which could influence plural format,
  237. * and 2nd parameter is just a data to be printed out in proper position.
  238. * @param {boolean} ignorePound If true, treat '#' in plural messages as a
  239. * literary character, else treat it as an ICU syntax character, resolving
  240. * to the number (plural_variable - offset).
  241. * @return {string} Formatted message.
  242. * @private
  243. */
  244. goog.i18n.MessageFormat.prototype.format_ = function(
  245. namedParameters, ignorePound) {
  246. this.init_();
  247. if (!this.parsedPattern_ || this.parsedPattern_.length == 0) {
  248. return '';
  249. }
  250. this.literals_ = goog.array.clone(this.initialLiterals_);
  251. var result = [];
  252. this.formatBlock_(this.parsedPattern_, namedParameters, ignorePound, result);
  253. var message = result.join('');
  254. if (!ignorePound) {
  255. goog.asserts.assert(message.search('#') == -1, 'Not all # were replaced.');
  256. }
  257. while (this.literals_.length > 0) {
  258. message = message.replace(
  259. this.buildPlaceholder_(this.literals_), this.literals_.pop());
  260. }
  261. return message;
  262. };
  263. /**
  264. * Parses generic block and returns a formatted string.
  265. * @param {!Array<!goog.i18n.MessageFormat.TypeVal_>} parsedPattern
  266. * Holds parsed tree.
  267. * @param {!Object} namedParameters Parameters that either influence
  268. * the formatting or are used as actual data.
  269. * @param {boolean} ignorePound If true, treat '#' in plural messages as a
  270. * literary character, else treat it as an ICU syntax character, resolving
  271. * to the number (plural_variable - offset).
  272. * @param {!Array<string>} result Each formatting stage appends its product
  273. * to the result.
  274. * @private
  275. */
  276. goog.i18n.MessageFormat.prototype.formatBlock_ = function(
  277. parsedPattern, namedParameters, ignorePound, result) {
  278. for (var i = 0; i < parsedPattern.length; i++) {
  279. switch (parsedPattern[i].type) {
  280. case goog.i18n.MessageFormat.BlockType_.STRING:
  281. result.push(parsedPattern[i].value);
  282. break;
  283. case goog.i18n.MessageFormat.BlockType_.SIMPLE:
  284. var pattern = parsedPattern[i].value;
  285. this.formatSimplePlaceholder_(pattern, namedParameters, result);
  286. break;
  287. case goog.i18n.MessageFormat.BlockType_.SELECT:
  288. var pattern = parsedPattern[i].value;
  289. this.formatSelectBlock_(pattern, namedParameters, ignorePound, result);
  290. break;
  291. case goog.i18n.MessageFormat.BlockType_.PLURAL:
  292. var pattern = parsedPattern[i].value;
  293. this.formatPluralOrdinalBlock_(
  294. pattern, namedParameters, goog.i18n.pluralRules.select, ignorePound,
  295. result);
  296. break;
  297. case goog.i18n.MessageFormat.BlockType_.ORDINAL:
  298. var pattern = parsedPattern[i].value;
  299. this.formatPluralOrdinalBlock_(
  300. pattern, namedParameters, goog.i18n.ordinalRules.select,
  301. ignorePound, result);
  302. break;
  303. default:
  304. goog.asserts.fail('Unrecognized block type: ' + parsedPattern[i].type);
  305. }
  306. }
  307. };
  308. /**
  309. * Formats simple placeholder.
  310. * @param {!Object} parsedPattern JSON object containing placeholder info.
  311. * @param {!Object} namedParameters Parameters that are used as actual data.
  312. * @param {!Array<string>} result Each formatting stage appends its product
  313. * to the result.
  314. * @private
  315. */
  316. goog.i18n.MessageFormat.prototype.formatSimplePlaceholder_ = function(
  317. parsedPattern, namedParameters, result) {
  318. var value = namedParameters[parsedPattern];
  319. if (!goog.isDef(value)) {
  320. result.push('Undefined parameter - ' + parsedPattern);
  321. return;
  322. }
  323. // Don't push the value yet, it may contain any of # { } in it which
  324. // will break formatter. Insert a placeholder and replace at the end.
  325. this.literals_.push(value);
  326. result.push(this.buildPlaceholder_(this.literals_));
  327. };
  328. /**
  329. * Formats select block. Only one option is selected.
  330. * @param {!{argumentIndex:?}} parsedPattern JSON object containing select
  331. * block info.
  332. * @param {!Object} namedParameters Parameters that either influence
  333. * the formatting or are used as actual data.
  334. * @param {boolean} ignorePound If true, treat '#' in plural messages as a
  335. * literary character, else treat it as an ICU syntax character, resolving
  336. * to the number (plural_variable - offset).
  337. * @param {!Array<string>} result Each formatting stage appends its product
  338. * to the result.
  339. * @private
  340. */
  341. goog.i18n.MessageFormat.prototype.formatSelectBlock_ = function(
  342. parsedPattern, namedParameters, ignorePound, result) {
  343. var argumentIndex = parsedPattern.argumentIndex;
  344. if (!goog.isDef(namedParameters[argumentIndex])) {
  345. result.push('Undefined parameter - ' + argumentIndex);
  346. return;
  347. }
  348. var option = parsedPattern[namedParameters[argumentIndex]];
  349. if (!goog.isDef(option)) {
  350. option = parsedPattern[goog.i18n.MessageFormat.OTHER_];
  351. goog.asserts.assertArray(
  352. option, 'Invalid option or missing other option for select block.');
  353. }
  354. this.formatBlock_(option, namedParameters, ignorePound, result);
  355. };
  356. /**
  357. * Formats plural or selectordinal block. Only one option is selected and all #
  358. * are replaced.
  359. * @param {!{argumentIndex, argumentOffset}} parsedPattern JSON object
  360. * containing plural block info.
  361. * @param {!Object} namedParameters Parameters that either influence
  362. * the formatting or are used as actual data.
  363. * @param {function(number, number=):string} pluralSelector A select function
  364. * from goog.i18n.pluralRules or goog.i18n.ordinalRules which determines
  365. * which plural/ordinal form to use based on the input number's cardinality.
  366. * @param {boolean} ignorePound If true, treat '#' in plural messages as a
  367. * literary character, else treat it as an ICU syntax character, resolving
  368. * to the number (plural_variable - offset).
  369. * @param {!Array<string>} result Each formatting stage appends its product
  370. * to the result.
  371. * @private
  372. */
  373. goog.i18n.MessageFormat.prototype.formatPluralOrdinalBlock_ = function(
  374. parsedPattern, namedParameters, pluralSelector, ignorePound, result) {
  375. var argumentIndex = parsedPattern.argumentIndex;
  376. var argumentOffset = parsedPattern.argumentOffset;
  377. var pluralValue = +namedParameters[argumentIndex];
  378. if (isNaN(pluralValue)) {
  379. // TODO(user): Distinguish between undefined and invalid parameters.
  380. result.push('Undefined or invalid parameter - ' + argumentIndex);
  381. return;
  382. }
  383. var diff = pluralValue - argumentOffset;
  384. // Check if there is an exact match.
  385. var option = parsedPattern[namedParameters[argumentIndex]];
  386. if (!goog.isDef(option)) {
  387. goog.asserts.assert(diff >= 0, 'Argument index smaller than offset.');
  388. var item;
  389. if (this.numberFormatter_.getMinimumFractionDigits) { // number formatter?
  390. // If we know the number of fractional digits we can make better decisions
  391. // We can decide (for instance) between "1 dollar" and "1.00 dollars".
  392. item = pluralSelector(
  393. diff, this.numberFormatter_.getMinimumFractionDigits());
  394. } else {
  395. item = pluralSelector(diff);
  396. }
  397. goog.asserts.assertString(item, 'Invalid plural key.');
  398. option = parsedPattern[item];
  399. // If option is not provided fall back to "other".
  400. if (!goog.isDef(option)) {
  401. option = parsedPattern[goog.i18n.MessageFormat.OTHER_];
  402. }
  403. goog.asserts.assertArray(
  404. option, 'Invalid option or missing other option for plural block.');
  405. }
  406. var pluralResult = [];
  407. this.formatBlock_(option, namedParameters, ignorePound, pluralResult);
  408. var plural = pluralResult.join('');
  409. goog.asserts.assertString(plural, 'Empty block in plural.');
  410. if (ignorePound) {
  411. result.push(plural);
  412. } else {
  413. var localeAwareDiff = this.numberFormatter_.format(diff);
  414. result.push(plural.replace(/#/g, localeAwareDiff));
  415. }
  416. };
  417. /**
  418. * Set up the MessageFormat.
  419. * Parses input pattern into an array, for faster reformatting with
  420. * different input parameters.
  421. * Parsing is locale independent.
  422. * @private
  423. */
  424. goog.i18n.MessageFormat.prototype.init_ = function() {
  425. if (this.pattern_) {
  426. this.initialLiterals_ = [];
  427. var pattern = this.insertPlaceholders_(this.pattern_);
  428. this.parsedPattern_ = this.parseBlock_(pattern);
  429. this.pattern_ = null;
  430. }
  431. };
  432. /**
  433. * Replaces string literals with literal placeholders.
  434. * Literals are string of the form '}...', '{...' and '#...' where ... is
  435. * set of characters not containing '
  436. * Builds a dictionary so we can recover literals during format phase.
  437. * @param {string} pattern Pattern to clean up.
  438. * @return {string} Pattern with literals replaced with placeholders.
  439. * @private
  440. */
  441. goog.i18n.MessageFormat.prototype.insertPlaceholders_ = function(pattern) {
  442. var literals = this.initialLiterals_;
  443. var buildPlaceholder = goog.bind(this.buildPlaceholder_, this);
  444. // First replace '' with single quote placeholder since they can be found
  445. // inside other literals.
  446. pattern = pattern.replace(
  447. goog.i18n.MessageFormat.REGEX_DOUBLE_APOSTROPHE_, function() {
  448. literals.push("'");
  449. return buildPlaceholder(literals);
  450. });
  451. pattern = pattern.replace(
  452. goog.i18n.MessageFormat.REGEX_LITERAL_, function(match, text) {
  453. literals.push(text);
  454. return buildPlaceholder(literals);
  455. });
  456. return pattern;
  457. };
  458. /**
  459. * Breaks pattern into strings and top level {...} blocks.
  460. * @param {string} pattern (sub)Pattern to be broken.
  461. * @return {!Array<goog.i18n.MessageFormat.TypeVal_>}
  462. * @private
  463. */
  464. goog.i18n.MessageFormat.prototype.extractParts_ = function(pattern) {
  465. var prevPos = 0;
  466. var braceStack = [];
  467. var results = [];
  468. var braces = /[{}]/g;
  469. braces.lastIndex = 0; // lastIndex doesn't get set to 0 so we have to.
  470. var match;
  471. while (match = braces.exec(pattern)) {
  472. var pos = match.index;
  473. if (match[0] == '}') {
  474. var brace = braceStack.pop();
  475. goog.asserts.assert(
  476. goog.isDef(brace) && brace == '{', 'No matching { for }.');
  477. if (braceStack.length == 0) {
  478. // End of the block.
  479. var part = {};
  480. part.type = goog.i18n.MessageFormat.Element_.BLOCK;
  481. part.value = pattern.substring(prevPos, pos);
  482. results.push(part);
  483. prevPos = pos + 1;
  484. }
  485. } else {
  486. if (braceStack.length == 0) {
  487. var substring = pattern.substring(prevPos, pos);
  488. if (substring != '') {
  489. results.push({
  490. type: goog.i18n.MessageFormat.Element_.STRING,
  491. value: substring
  492. });
  493. }
  494. prevPos = pos + 1;
  495. }
  496. braceStack.push('{');
  497. }
  498. }
  499. // Take care of the final string, and check if the braceStack is empty.
  500. goog.asserts.assert(
  501. braceStack.length == 0, 'There are mismatched { or } in the pattern.');
  502. var substring = pattern.substring(prevPos);
  503. if (substring != '') {
  504. results.push(
  505. {type: goog.i18n.MessageFormat.Element_.STRING, value: substring});
  506. }
  507. return results;
  508. };
  509. /**
  510. * A regular expression to parse the plural block, extracting the argument
  511. * index and offset (if any).
  512. * @type {RegExp}
  513. * @private
  514. */
  515. goog.i18n.MessageFormat.PLURAL_BLOCK_RE_ =
  516. /^\s*(\w+)\s*,\s*plural\s*,(?:\s*offset:(\d+))?/;
  517. /**
  518. * A regular expression to parse the ordinal block, extracting the argument
  519. * index.
  520. * @type {RegExp}
  521. * @private
  522. */
  523. goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_ = /^\s*(\w+)\s*,\s*selectordinal\s*,/;
  524. /**
  525. * A regular expression to parse the select block, extracting the argument
  526. * index.
  527. * @type {RegExp}
  528. * @private
  529. */
  530. goog.i18n.MessageFormat.SELECT_BLOCK_RE_ = /^\s*(\w+)\s*,\s*select\s*,/;
  531. /**
  532. * Detects which type of a block is the pattern.
  533. * @param {string} pattern Content of the block.
  534. * @return {goog.i18n.MessageFormat.BlockType_} One of the block types.
  535. * @private
  536. */
  537. goog.i18n.MessageFormat.prototype.parseBlockType_ = function(pattern) {
  538. if (goog.i18n.MessageFormat.PLURAL_BLOCK_RE_.test(pattern)) {
  539. return goog.i18n.MessageFormat.BlockType_.PLURAL;
  540. }
  541. if (goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_.test(pattern)) {
  542. return goog.i18n.MessageFormat.BlockType_.ORDINAL;
  543. }
  544. if (goog.i18n.MessageFormat.SELECT_BLOCK_RE_.test(pattern)) {
  545. return goog.i18n.MessageFormat.BlockType_.SELECT;
  546. }
  547. if (/^\s*\w+\s*/.test(pattern)) {
  548. return goog.i18n.MessageFormat.BlockType_.SIMPLE;
  549. }
  550. return goog.i18n.MessageFormat.BlockType_.UNKNOWN;
  551. };
  552. /**
  553. * Parses generic block.
  554. * @param {string} pattern Content of the block to parse.
  555. * @return {!Array<!Object>} Subblocks marked as strings, select...
  556. * @private
  557. */
  558. goog.i18n.MessageFormat.prototype.parseBlock_ = function(pattern) {
  559. var result = [];
  560. var parts = this.extractParts_(pattern);
  561. for (var i = 0; i < parts.length; i++) {
  562. var block = {};
  563. if (goog.i18n.MessageFormat.Element_.STRING == parts[i].type) {
  564. block.type = goog.i18n.MessageFormat.BlockType_.STRING;
  565. block.value = parts[i].value;
  566. } else if (goog.i18n.MessageFormat.Element_.BLOCK == parts[i].type) {
  567. var blockType = this.parseBlockType_(parts[i].value);
  568. switch (blockType) {
  569. case goog.i18n.MessageFormat.BlockType_.SELECT:
  570. block.type = goog.i18n.MessageFormat.BlockType_.SELECT;
  571. block.value = this.parseSelectBlock_(parts[i].value);
  572. break;
  573. case goog.i18n.MessageFormat.BlockType_.PLURAL:
  574. block.type = goog.i18n.MessageFormat.BlockType_.PLURAL;
  575. block.value = this.parsePluralBlock_(parts[i].value);
  576. break;
  577. case goog.i18n.MessageFormat.BlockType_.ORDINAL:
  578. block.type = goog.i18n.MessageFormat.BlockType_.ORDINAL;
  579. block.value = this.parseOrdinalBlock_(parts[i].value);
  580. break;
  581. case goog.i18n.MessageFormat.BlockType_.SIMPLE:
  582. block.type = goog.i18n.MessageFormat.BlockType_.SIMPLE;
  583. block.value = parts[i].value;
  584. break;
  585. default:
  586. goog.asserts.fail(
  587. 'Unknown block type for pattern: ' + parts[i].value);
  588. }
  589. } else {
  590. goog.asserts.fail('Unknown part of the pattern.');
  591. }
  592. result.push(block);
  593. }
  594. return result;
  595. };
  596. /**
  597. * Parses a select type of a block and produces JSON object for it.
  598. * @param {string} pattern Subpattern that needs to be parsed as select pattern.
  599. * @return {!Object} Object with select block info.
  600. * @private
  601. */
  602. goog.i18n.MessageFormat.prototype.parseSelectBlock_ = function(pattern) {
  603. var argumentIndex = '';
  604. var replaceRegex = goog.i18n.MessageFormat.SELECT_BLOCK_RE_;
  605. pattern = pattern.replace(replaceRegex, function(string, name) {
  606. argumentIndex = name;
  607. return '';
  608. });
  609. var result = {};
  610. result.argumentIndex = argumentIndex;
  611. var parts = this.extractParts_(pattern);
  612. // Looking for (key block)+ sequence. One of the keys has to be "other".
  613. var pos = 0;
  614. while (pos < parts.length) {
  615. var key = parts[pos].value;
  616. goog.asserts.assertString(key, 'Missing select key element.');
  617. pos++;
  618. goog.asserts.assert(
  619. pos < parts.length, 'Missing or invalid select value element.');
  620. if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {
  621. var value = this.parseBlock_(parts[pos].value);
  622. } else {
  623. goog.asserts.fail('Expected block type.');
  624. }
  625. result[key.replace(/\s/g, '')] = value;
  626. pos++;
  627. }
  628. goog.asserts.assertArray(
  629. result[goog.i18n.MessageFormat.OTHER_],
  630. 'Missing other key in select statement.');
  631. return result;
  632. };
  633. /**
  634. * Parses a plural type of a block and produces JSON object for it.
  635. * @param {string} pattern Subpattern that needs to be parsed as plural pattern.
  636. * @return {!Object} Object with select block info.
  637. * @private
  638. */
  639. goog.i18n.MessageFormat.prototype.parsePluralBlock_ = function(pattern) {
  640. var argumentIndex = '';
  641. var argumentOffset = 0;
  642. var replaceRegex = goog.i18n.MessageFormat.PLURAL_BLOCK_RE_;
  643. pattern = pattern.replace(replaceRegex, function(string, name, offset) {
  644. argumentIndex = name;
  645. if (offset) {
  646. argumentOffset = parseInt(offset, 10);
  647. }
  648. return '';
  649. });
  650. var result = {};
  651. result.argumentIndex = argumentIndex;
  652. result.argumentOffset = argumentOffset;
  653. var parts = this.extractParts_(pattern);
  654. // Looking for (key block)+ sequence.
  655. var pos = 0;
  656. while (pos < parts.length) {
  657. var key = parts[pos].value;
  658. goog.asserts.assertString(key, 'Missing plural key element.');
  659. pos++;
  660. goog.asserts.assert(
  661. pos < parts.length, 'Missing or invalid plural value element.');
  662. if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {
  663. var value = this.parseBlock_(parts[pos].value);
  664. } else {
  665. goog.asserts.fail('Expected block type.');
  666. }
  667. result[key.replace(/\s*(?:=)?(\w+)\s*/, '$1')] = value;
  668. pos++;
  669. }
  670. goog.asserts.assertArray(
  671. result[goog.i18n.MessageFormat.OTHER_],
  672. 'Missing other key in plural statement.');
  673. return result;
  674. };
  675. /**
  676. * Parses an ordinal type of a block and produces JSON object for it.
  677. * For example the input string:
  678. * '{FOO, selectordinal, one {Message A}other {Message B}}'
  679. * Should result in the output object:
  680. * {
  681. * argumentIndex: 'FOO',
  682. * argumentOffest: 0,
  683. * one: [ { type: 4, value: 'Message A' } ],
  684. * other: [ { type: 4, value: 'Message B' } ]
  685. * }
  686. * @param {string} pattern Subpattern that needs to be parsed as plural pattern.
  687. * @return {!Object} Object with select block info.
  688. * @private
  689. */
  690. goog.i18n.MessageFormat.prototype.parseOrdinalBlock_ = function(pattern) {
  691. var argumentIndex = '';
  692. var replaceRegex = goog.i18n.MessageFormat.ORDINAL_BLOCK_RE_;
  693. pattern = pattern.replace(replaceRegex, function(string, name) {
  694. argumentIndex = name;
  695. return '';
  696. });
  697. var result = {};
  698. result.argumentIndex = argumentIndex;
  699. result.argumentOffset = 0;
  700. var parts = this.extractParts_(pattern);
  701. // Looking for (key block)+ sequence.
  702. var pos = 0;
  703. while (pos < parts.length) {
  704. var key = parts[pos].value;
  705. goog.asserts.assertString(key, 'Missing ordinal key element.');
  706. pos++;
  707. goog.asserts.assert(
  708. pos < parts.length, 'Missing or invalid ordinal value element.');
  709. if (goog.i18n.MessageFormat.Element_.BLOCK == parts[pos].type) {
  710. var value = this.parseBlock_(parts[pos].value);
  711. } else {
  712. goog.asserts.fail('Expected block type.');
  713. }
  714. result[key.replace(/\s*(?:=)?(\w+)\s*/, '$1')] = value;
  715. pos++;
  716. }
  717. goog.asserts.assertArray(
  718. result[goog.i18n.MessageFormat.OTHER_],
  719. 'Missing other key in selectordinal statement.');
  720. return result;
  721. };
  722. /**
  723. * Builds a placeholder from the last index of the array.
  724. * @param {!Array<string>} literals All literals encountered during parse.
  725. * @return {string} \uFDDF_ + last index + _.
  726. * @private
  727. */
  728. goog.i18n.MessageFormat.prototype.buildPlaceholder_ = function(literals) {
  729. goog.asserts.assert(literals.length > 0, 'Literal array is empty.');
  730. var index = (literals.length - 1).toString(10);
  731. return goog.i18n.MessageFormat.LITERAL_PLACEHOLDER_ + index + '_';
  732. };