l10n.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. /**
  2. * Copyright (c) 2011-2013 Fabien Cazenave, Mozilla.
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to
  6. * deal in the Software without restriction, including without limitation the
  7. * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. * sell copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. * IN THE SOFTWARE.
  21. */
  22. /*
  23. Additional modifications for PDF.js project:
  24. - Disables language initialization on page loading.
  25. - Disables document translation on page loading.
  26. - Removes consoleWarn and consoleLog and use console.log/warn directly.
  27. - Removes window._ assignment.
  28. - Remove compatibility code for OldIE.
  29. - Replaces `String.prototype.substr()` with `String.prototype.substring()`.
  30. - Replaces one `Node.insertBefore()` with `Element.prepend()`.
  31. - Removes `fireL10nReadyEvent` since the "localized" event it dispatches
  32. is unused and may clash with an identically named event in the viewer.
  33. */
  34. /*jshint browser: true, devel: true, es5: true, globalstrict: true */
  35. 'use strict';
  36. document.webL10n = (function(window, document, undefined) {
  37. var gL10nData = {};
  38. var gTextData = '';
  39. var gTextProp = 'textContent';
  40. var gLanguage = '';
  41. var gMacros = {};
  42. var gReadyState = 'loading';
  43. /**
  44. * Synchronously loading l10n resources significantly minimizes flickering
  45. * from displaying the app with non-localized strings and then updating the
  46. * strings. Although this will block all script execution on this page, we
  47. * expect that the l10n resources are available locally on flash-storage.
  48. *
  49. * As synchronous XHR is generally considered as a bad idea, we're still
  50. * loading l10n resources asynchronously -- but we keep this in a setting,
  51. * just in case... and applications using this library should hide their
  52. * content until the `localized' event happens.
  53. */
  54. var gAsyncResourceLoading = true; // read-only
  55. /**
  56. * DOM helpers for the so-called "HTML API".
  57. *
  58. * These functions are written for modern browsers. For old versions of IE,
  59. * they're overridden in the 'startup' section at the end of this file.
  60. */
  61. function getL10nResourceLinks() {
  62. return document.querySelectorAll('link[type="application/l10n"]');
  63. }
  64. function getL10nDictionary() {
  65. var script = document.querySelector('script[type="application/l10n"]');
  66. // TODO: support multiple and external JSON dictionaries
  67. return script ? JSON.parse(script.innerHTML) : null;
  68. }
  69. function getTranslatableChildren(element) {
  70. return element ? element.querySelectorAll('*[data-l10n-id]') : [];
  71. }
  72. function getL10nAttributes(element) {
  73. if (!element)
  74. return {};
  75. var l10nId = element.getAttribute('data-l10n-id');
  76. var l10nArgs = element.getAttribute('data-l10n-args');
  77. var args = {};
  78. if (l10nArgs) {
  79. try {
  80. args = JSON.parse(l10nArgs);
  81. } catch (e) {
  82. console.warn('could not parse arguments for #' + l10nId);
  83. }
  84. }
  85. return { id: l10nId, args: args };
  86. }
  87. function xhrLoadText(url, onSuccess, onFailure) {
  88. onSuccess = onSuccess || function _onSuccess(data) {};
  89. onFailure = onFailure || function _onFailure() {};
  90. var xhr = new XMLHttpRequest();
  91. xhr.open('GET', url, gAsyncResourceLoading);
  92. if (xhr.overrideMimeType) {
  93. xhr.overrideMimeType('text/plain; charset=utf-8');
  94. }
  95. xhr.onreadystatechange = function() {
  96. if (xhr.readyState == 4) {
  97. if (xhr.status == 200 || xhr.status === 0) {
  98. onSuccess(xhr.responseText);
  99. } else {
  100. onFailure();
  101. }
  102. }
  103. };
  104. xhr.onerror = onFailure;
  105. xhr.ontimeout = onFailure;
  106. // in Firefox OS with the app:// protocol, trying to XHR a non-existing
  107. // URL will raise an exception here -- hence this ugly try...catch.
  108. try {
  109. xhr.send(null);
  110. } catch (e) {
  111. onFailure();
  112. }
  113. }
  114. /**
  115. * l10n resource parser:
  116. * - reads (async XHR) the l10n resource matching `lang';
  117. * - imports linked resources (synchronously) when specified;
  118. * - parses the text data (fills `gL10nData' and `gTextData');
  119. * - triggers success/failure callbacks when done.
  120. *
  121. * @param {string} href
  122. * URL of the l10n resource to parse.
  123. *
  124. * @param {string} lang
  125. * locale (language) to parse. Must be a lowercase string.
  126. *
  127. * @param {Function} successCallback
  128. * triggered when the l10n resource has been successfully parsed.
  129. *
  130. * @param {Function} failureCallback
  131. * triggered when the an error has occurred.
  132. *
  133. * @return {void}
  134. * uses the following global variables: gL10nData, gTextData, gTextProp.
  135. */
  136. function parseResource(href, lang, successCallback, failureCallback) {
  137. var baseURL = href.replace(/[^\/]*$/, '') || './';
  138. // handle escaped characters (backslashes) in a string
  139. function evalString(text) {
  140. if (text.lastIndexOf('\\') < 0)
  141. return text;
  142. return text.replace(/\\\\/g, '\\')
  143. .replace(/\\n/g, '\n')
  144. .replace(/\\r/g, '\r')
  145. .replace(/\\t/g, '\t')
  146. .replace(/\\b/g, '\b')
  147. .replace(/\\f/g, '\f')
  148. .replace(/\\{/g, '{')
  149. .replace(/\\}/g, '}')
  150. .replace(/\\"/g, '"')
  151. .replace(/\\'/g, "'");
  152. }
  153. // parse *.properties text data into an l10n dictionary
  154. // If gAsyncResourceLoading is false, then the callback will be called
  155. // synchronously. Otherwise it is called asynchronously.
  156. function parseProperties(text, parsedPropertiesCallback) {
  157. var dictionary = {};
  158. // token expressions
  159. var reBlank = /^\s*|\s*$/;
  160. var reComment = /^\s*#|^\s*$/;
  161. var reSection = /^\s*\[(.*)\]\s*$/;
  162. var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
  163. var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
  164. // parse the *.properties file into an associative array
  165. function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) {
  166. var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
  167. var currentLang = '*';
  168. var genericLang = lang.split('-', 1)[0];
  169. var skipLang = false;
  170. var match = '';
  171. function nextEntry() {
  172. // Use infinite loop instead of recursion to avoid reaching the
  173. // maximum recursion limit for content with many lines.
  174. while (true) {
  175. if (!entries.length) {
  176. parsedRawLinesCallback();
  177. return;
  178. }
  179. var line = entries.shift();
  180. // comment or blank line?
  181. if (reComment.test(line))
  182. continue;
  183. // the extended syntax supports [lang] sections and @import rules
  184. if (extendedSyntax) {
  185. match = reSection.exec(line);
  186. if (match) { // section start?
  187. // RFC 4646, section 4.4, "All comparisons MUST be performed
  188. // in a case-insensitive manner."
  189. currentLang = match[1].toLowerCase();
  190. skipLang = (currentLang !== '*') &&
  191. (currentLang !== lang) && (currentLang !== genericLang);
  192. continue;
  193. } else if (skipLang) {
  194. continue;
  195. }
  196. match = reImport.exec(line);
  197. if (match) { // @import rule?
  198. loadImport(baseURL + match[1], nextEntry);
  199. return;
  200. }
  201. }
  202. // key-value pair
  203. var tmp = line.match(reSplit);
  204. if (tmp && tmp.length == 3) {
  205. dictionary[tmp[1]] = evalString(tmp[2]);
  206. }
  207. }
  208. }
  209. nextEntry();
  210. }
  211. // import another *.properties file
  212. function loadImport(url, callback) {
  213. xhrLoadText(url, function(content) {
  214. parseRawLines(content, false, callback); // don't allow recursive imports
  215. }, function () {
  216. console.warn(url + ' not found.');
  217. callback();
  218. });
  219. }
  220. // fill the dictionary
  221. parseRawLines(text, true, function() {
  222. parsedPropertiesCallback(dictionary);
  223. });
  224. }
  225. // load and parse l10n data (warning: global variables are used here)
  226. xhrLoadText(href, function(response) {
  227. gTextData += response; // mostly for debug
  228. // parse *.properties text data into an l10n dictionary
  229. parseProperties(response, function(data) {
  230. // find attribute descriptions, if any
  231. for (var key in data) {
  232. var id, prop, index = key.lastIndexOf('.');
  233. if (index > 0) { // an attribute has been specified
  234. id = key.substring(0, index);
  235. prop = key.substring(index + 1);
  236. } else { // no attribute: assuming text content by default
  237. id = key;
  238. prop = gTextProp;
  239. }
  240. if (!gL10nData[id]) {
  241. gL10nData[id] = {};
  242. }
  243. gL10nData[id][prop] = data[key];
  244. }
  245. // trigger callback
  246. if (successCallback) {
  247. successCallback();
  248. }
  249. });
  250. }, failureCallback);
  251. }
  252. // load and parse all resources for the specified locale
  253. function loadLocale(lang, callback) {
  254. // RFC 4646, section 2.1 states that language tags have to be treated as
  255. // case-insensitive. Convert to lowercase for case-insensitive comparisons.
  256. if (lang) {
  257. lang = lang.toLowerCase();
  258. }
  259. callback = callback || function _callback() {};
  260. clear();
  261. gLanguage = lang;
  262. // check all <link type="application/l10n" href="..." /> nodes
  263. // and load the resource files
  264. var langLinks = getL10nResourceLinks();
  265. var langCount = langLinks.length;
  266. if (langCount === 0) {
  267. // we might have a pre-compiled dictionary instead
  268. var dict = getL10nDictionary();
  269. if (dict && dict.locales && dict.default_locale) {
  270. console.log('using the embedded JSON directory, early way out');
  271. gL10nData = dict.locales[lang];
  272. if (!gL10nData) {
  273. var defaultLocale = dict.default_locale.toLowerCase();
  274. for (var anyCaseLang in dict.locales) {
  275. anyCaseLang = anyCaseLang.toLowerCase();
  276. if (anyCaseLang === lang) {
  277. gL10nData = dict.locales[lang];
  278. break;
  279. } else if (anyCaseLang === defaultLocale) {
  280. gL10nData = dict.locales[defaultLocale];
  281. }
  282. }
  283. }
  284. callback();
  285. } else {
  286. console.log('no resource to load, early way out');
  287. }
  288. // early way out
  289. gReadyState = 'complete';
  290. return;
  291. }
  292. // start the callback when all resources are loaded
  293. var onResourceLoaded = null;
  294. var gResourceCount = 0;
  295. onResourceLoaded = function() {
  296. gResourceCount++;
  297. if (gResourceCount >= langCount) {
  298. callback();
  299. gReadyState = 'complete';
  300. }
  301. };
  302. // load all resource files
  303. function L10nResourceLink(link) {
  304. var href = link.href;
  305. // Note: If |gAsyncResourceLoading| is false, then the following callbacks
  306. // are synchronously called.
  307. this.load = function(lang, callback) {
  308. parseResource(href, lang, callback, function() {
  309. console.warn(href + ' not found.');
  310. // lang not found, used default resource instead
  311. console.warn('"' + lang + '" resource not found');
  312. gLanguage = '';
  313. // Resource not loaded, but we still need to call the callback.
  314. callback();
  315. });
  316. };
  317. }
  318. for (var i = 0; i < langCount; i++) {
  319. var resource = new L10nResourceLink(langLinks[i]);
  320. resource.load(lang, onResourceLoaded);
  321. }
  322. }
  323. // clear all l10n data
  324. function clear() {
  325. gL10nData = {};
  326. gTextData = '';
  327. gLanguage = '';
  328. // TODO: clear all non predefined macros.
  329. // There's no such macro /yet/ but we're planning to have some...
  330. }
  331. /**
  332. * Get rules for plural forms (shared with JetPack), see:
  333. * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
  334. * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
  335. *
  336. * @param {string} lang
  337. * locale (language) used.
  338. *
  339. * @return {Function}
  340. * returns a function that gives the plural form name for a given integer:
  341. * var fun = getPluralRules('en');
  342. * fun(1) -> 'one'
  343. * fun(0) -> 'other'
  344. * fun(1000) -> 'other'.
  345. */
  346. function getPluralRules(lang) {
  347. var locales2rules = {
  348. 'af': 3,
  349. 'ak': 4,
  350. 'am': 4,
  351. 'ar': 1,
  352. 'asa': 3,
  353. 'az': 0,
  354. 'be': 11,
  355. 'bem': 3,
  356. 'bez': 3,
  357. 'bg': 3,
  358. 'bh': 4,
  359. 'bm': 0,
  360. 'bn': 3,
  361. 'bo': 0,
  362. 'br': 20,
  363. 'brx': 3,
  364. 'bs': 11,
  365. 'ca': 3,
  366. 'cgg': 3,
  367. 'chr': 3,
  368. 'cs': 12,
  369. 'cy': 17,
  370. 'da': 3,
  371. 'de': 3,
  372. 'dv': 3,
  373. 'dz': 0,
  374. 'ee': 3,
  375. 'el': 3,
  376. 'en': 3,
  377. 'eo': 3,
  378. 'es': 3,
  379. 'et': 3,
  380. 'eu': 3,
  381. 'fa': 0,
  382. 'ff': 5,
  383. 'fi': 3,
  384. 'fil': 4,
  385. 'fo': 3,
  386. 'fr': 5,
  387. 'fur': 3,
  388. 'fy': 3,
  389. 'ga': 8,
  390. 'gd': 24,
  391. 'gl': 3,
  392. 'gsw': 3,
  393. 'gu': 3,
  394. 'guw': 4,
  395. 'gv': 23,
  396. 'ha': 3,
  397. 'haw': 3,
  398. 'he': 2,
  399. 'hi': 4,
  400. 'hr': 11,
  401. 'hu': 0,
  402. 'id': 0,
  403. 'ig': 0,
  404. 'ii': 0,
  405. 'is': 3,
  406. 'it': 3,
  407. 'iu': 7,
  408. 'ja': 0,
  409. 'jmc': 3,
  410. 'jv': 0,
  411. 'ka': 0,
  412. 'kab': 5,
  413. 'kaj': 3,
  414. 'kcg': 3,
  415. 'kde': 0,
  416. 'kea': 0,
  417. 'kk': 3,
  418. 'kl': 3,
  419. 'km': 0,
  420. 'kn': 0,
  421. 'ko': 0,
  422. 'ksb': 3,
  423. 'ksh': 21,
  424. 'ku': 3,
  425. 'kw': 7,
  426. 'lag': 18,
  427. 'lb': 3,
  428. 'lg': 3,
  429. 'ln': 4,
  430. 'lo': 0,
  431. 'lt': 10,
  432. 'lv': 6,
  433. 'mas': 3,
  434. 'mg': 4,
  435. 'mk': 16,
  436. 'ml': 3,
  437. 'mn': 3,
  438. 'mo': 9,
  439. 'mr': 3,
  440. 'ms': 0,
  441. 'mt': 15,
  442. 'my': 0,
  443. 'nah': 3,
  444. 'naq': 7,
  445. 'nb': 3,
  446. 'nd': 3,
  447. 'ne': 3,
  448. 'nl': 3,
  449. 'nn': 3,
  450. 'no': 3,
  451. 'nr': 3,
  452. 'nso': 4,
  453. 'ny': 3,
  454. 'nyn': 3,
  455. 'om': 3,
  456. 'or': 3,
  457. 'pa': 3,
  458. 'pap': 3,
  459. 'pl': 13,
  460. 'ps': 3,
  461. 'pt': 3,
  462. 'rm': 3,
  463. 'ro': 9,
  464. 'rof': 3,
  465. 'ru': 11,
  466. 'rwk': 3,
  467. 'sah': 0,
  468. 'saq': 3,
  469. 'se': 7,
  470. 'seh': 3,
  471. 'ses': 0,
  472. 'sg': 0,
  473. 'sh': 11,
  474. 'shi': 19,
  475. 'sk': 12,
  476. 'sl': 14,
  477. 'sma': 7,
  478. 'smi': 7,
  479. 'smj': 7,
  480. 'smn': 7,
  481. 'sms': 7,
  482. 'sn': 3,
  483. 'so': 3,
  484. 'sq': 3,
  485. 'sr': 11,
  486. 'ss': 3,
  487. 'ssy': 3,
  488. 'st': 3,
  489. 'sv': 3,
  490. 'sw': 3,
  491. 'syr': 3,
  492. 'ta': 3,
  493. 'te': 3,
  494. 'teo': 3,
  495. 'th': 0,
  496. 'ti': 4,
  497. 'tig': 3,
  498. 'tk': 3,
  499. 'tl': 4,
  500. 'tn': 3,
  501. 'to': 0,
  502. 'tr': 0,
  503. 'ts': 3,
  504. 'tzm': 22,
  505. 'uk': 11,
  506. 'ur': 3,
  507. 've': 3,
  508. 'vi': 0,
  509. 'vun': 3,
  510. 'wa': 4,
  511. 'wae': 3,
  512. 'wo': 0,
  513. 'xh': 3,
  514. 'xog': 3,
  515. 'yo': 0,
  516. 'zh': 0,
  517. 'zu': 3
  518. };
  519. // utility functions for plural rules methods
  520. function isIn(n, list) {
  521. return list.indexOf(n) !== -1;
  522. }
  523. function isBetween(n, start, end) {
  524. return start <= n && n <= end;
  525. }
  526. // list of all plural rules methods:
  527. // map an integer to the plural form name to use
  528. var pluralRules = {
  529. '0': function(n) {
  530. return 'other';
  531. },
  532. '1': function(n) {
  533. if ((isBetween((n % 100), 3, 10)))
  534. return 'few';
  535. if (n === 0)
  536. return 'zero';
  537. if ((isBetween((n % 100), 11, 99)))
  538. return 'many';
  539. if (n == 2)
  540. return 'two';
  541. if (n == 1)
  542. return 'one';
  543. return 'other';
  544. },
  545. '2': function(n) {
  546. if (n !== 0 && (n % 10) === 0)
  547. return 'many';
  548. if (n == 2)
  549. return 'two';
  550. if (n == 1)
  551. return 'one';
  552. return 'other';
  553. },
  554. '3': function(n) {
  555. if (n == 1)
  556. return 'one';
  557. return 'other';
  558. },
  559. '4': function(n) {
  560. if ((isBetween(n, 0, 1)))
  561. return 'one';
  562. return 'other';
  563. },
  564. '5': function(n) {
  565. if ((isBetween(n, 0, 2)) && n != 2)
  566. return 'one';
  567. return 'other';
  568. },
  569. '6': function(n) {
  570. if (n === 0)
  571. return 'zero';
  572. if ((n % 10) == 1 && (n % 100) != 11)
  573. return 'one';
  574. return 'other';
  575. },
  576. '7': function(n) {
  577. if (n == 2)
  578. return 'two';
  579. if (n == 1)
  580. return 'one';
  581. return 'other';
  582. },
  583. '8': function(n) {
  584. if ((isBetween(n, 3, 6)))
  585. return 'few';
  586. if ((isBetween(n, 7, 10)))
  587. return 'many';
  588. if (n == 2)
  589. return 'two';
  590. if (n == 1)
  591. return 'one';
  592. return 'other';
  593. },
  594. '9': function(n) {
  595. if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
  596. return 'few';
  597. if (n == 1)
  598. return 'one';
  599. return 'other';
  600. },
  601. '10': function(n) {
  602. if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
  603. return 'few';
  604. if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
  605. return 'one';
  606. return 'other';
  607. },
  608. '11': function(n) {
  609. if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
  610. return 'few';
  611. if ((n % 10) === 0 ||
  612. (isBetween((n % 10), 5, 9)) ||
  613. (isBetween((n % 100), 11, 14)))
  614. return 'many';
  615. if ((n % 10) == 1 && (n % 100) != 11)
  616. return 'one';
  617. return 'other';
  618. },
  619. '12': function(n) {
  620. if ((isBetween(n, 2, 4)))
  621. return 'few';
  622. if (n == 1)
  623. return 'one';
  624. return 'other';
  625. },
  626. '13': function(n) {
  627. if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
  628. return 'few';
  629. if (n != 1 && (isBetween((n % 10), 0, 1)) ||
  630. (isBetween((n % 10), 5, 9)) ||
  631. (isBetween((n % 100), 12, 14)))
  632. return 'many';
  633. if (n == 1)
  634. return 'one';
  635. return 'other';
  636. },
  637. '14': function(n) {
  638. if ((isBetween((n % 100), 3, 4)))
  639. return 'few';
  640. if ((n % 100) == 2)
  641. return 'two';
  642. if ((n % 100) == 1)
  643. return 'one';
  644. return 'other';
  645. },
  646. '15': function(n) {
  647. if (n === 0 || (isBetween((n % 100), 2, 10)))
  648. return 'few';
  649. if ((isBetween((n % 100), 11, 19)))
  650. return 'many';
  651. if (n == 1)
  652. return 'one';
  653. return 'other';
  654. },
  655. '16': function(n) {
  656. if ((n % 10) == 1 && n != 11)
  657. return 'one';
  658. return 'other';
  659. },
  660. '17': function(n) {
  661. if (n == 3)
  662. return 'few';
  663. if (n === 0)
  664. return 'zero';
  665. if (n == 6)
  666. return 'many';
  667. if (n == 2)
  668. return 'two';
  669. if (n == 1)
  670. return 'one';
  671. return 'other';
  672. },
  673. '18': function(n) {
  674. if (n === 0)
  675. return 'zero';
  676. if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
  677. return 'one';
  678. return 'other';
  679. },
  680. '19': function(n) {
  681. if ((isBetween(n, 2, 10)))
  682. return 'few';
  683. if ((isBetween(n, 0, 1)))
  684. return 'one';
  685. return 'other';
  686. },
  687. '20': function(n) {
  688. if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
  689. isBetween((n % 100), 10, 19) ||
  690. isBetween((n % 100), 70, 79) ||
  691. isBetween((n % 100), 90, 99)
  692. ))
  693. return 'few';
  694. if ((n % 1000000) === 0 && n !== 0)
  695. return 'many';
  696. if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
  697. return 'two';
  698. if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
  699. return 'one';
  700. return 'other';
  701. },
  702. '21': function(n) {
  703. if (n === 0)
  704. return 'zero';
  705. if (n == 1)
  706. return 'one';
  707. return 'other';
  708. },
  709. '22': function(n) {
  710. if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
  711. return 'one';
  712. return 'other';
  713. },
  714. '23': function(n) {
  715. if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
  716. return 'one';
  717. return 'other';
  718. },
  719. '24': function(n) {
  720. if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
  721. return 'few';
  722. if (isIn(n, [2, 12]))
  723. return 'two';
  724. if (isIn(n, [1, 11]))
  725. return 'one';
  726. return 'other';
  727. }
  728. };
  729. // return a function that gives the plural form name for a given integer
  730. var index = locales2rules[lang.replace(/-.*$/, '')];
  731. if (!(index in pluralRules)) {
  732. console.warn('plural form unknown for [' + lang + ']');
  733. return function() { return 'other'; };
  734. }
  735. return pluralRules[index];
  736. }
  737. // pre-defined 'plural' macro
  738. gMacros.plural = function(str, param, key, prop) {
  739. var n = parseFloat(param);
  740. if (isNaN(n))
  741. return str;
  742. // TODO: support other properties (l20n still doesn't...)
  743. if (prop != gTextProp)
  744. return str;
  745. // initialize _pluralRules
  746. if (!gMacros._pluralRules) {
  747. gMacros._pluralRules = getPluralRules(gLanguage);
  748. }
  749. var index = '[' + gMacros._pluralRules(n) + ']';
  750. // try to find a [zero|one|two] key if it's defined
  751. if (n === 0 && (key + '[zero]') in gL10nData) {
  752. str = gL10nData[key + '[zero]'][prop];
  753. } else if (n == 1 && (key + '[one]') in gL10nData) {
  754. str = gL10nData[key + '[one]'][prop];
  755. } else if (n == 2 && (key + '[two]') in gL10nData) {
  756. str = gL10nData[key + '[two]'][prop];
  757. } else if ((key + index) in gL10nData) {
  758. str = gL10nData[key + index][prop];
  759. } else if ((key + '[other]') in gL10nData) {
  760. str = gL10nData[key + '[other]'][prop];
  761. }
  762. return str;
  763. };
  764. /**
  765. * l10n dictionary functions
  766. */
  767. // fetch an l10n object, warn if not found, apply `args' if possible
  768. function getL10nData(key, args, fallback) {
  769. var data = gL10nData[key];
  770. if (!data) {
  771. console.warn('#' + key + ' is undefined.');
  772. if (!fallback) {
  773. return null;
  774. }
  775. data = fallback;
  776. }
  777. /** This is where l10n expressions should be processed.
  778. * The plan is to support C-style expressions from the l20n project;
  779. * until then, only two kinds of simple expressions are supported:
  780. * {[ index ]} and {{ arguments }}.
  781. */
  782. var rv = {};
  783. for (var prop in data) {
  784. var str = data[prop];
  785. str = substIndexes(str, args, key, prop);
  786. str = substArguments(str, args, key);
  787. rv[prop] = str;
  788. }
  789. return rv;
  790. }
  791. // replace {[macros]} with their values
  792. function substIndexes(str, args, key, prop) {
  793. var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
  794. var reMatch = reIndex.exec(str);
  795. if (!reMatch || !reMatch.length)
  796. return str;
  797. // an index/macro has been found
  798. // Note: at the moment, only one parameter is supported
  799. var macroName = reMatch[1];
  800. var paramName = reMatch[2];
  801. var param;
  802. if (args && paramName in args) {
  803. param = args[paramName];
  804. } else if (paramName in gL10nData) {
  805. param = gL10nData[paramName];
  806. }
  807. // there's no macro parser yet: it has to be defined in gMacros
  808. if (macroName in gMacros) {
  809. var macro = gMacros[macroName];
  810. str = macro(str, param, key, prop);
  811. }
  812. return str;
  813. }
  814. // replace {{arguments}} with their values
  815. function substArguments(str, args, key) {
  816. var reArgs = /\{\{\s*(.+?)\s*\}\}/g;
  817. return str.replace(reArgs, function(matched_text, arg) {
  818. if (args && arg in args) {
  819. return args[arg];
  820. }
  821. if (arg in gL10nData) {
  822. return gL10nData[arg];
  823. }
  824. console.log('argument {{' + arg + '}} for #' + key + ' is undefined.');
  825. return matched_text;
  826. });
  827. }
  828. // translate an HTML element
  829. function translateElement(element) {
  830. var l10n = getL10nAttributes(element);
  831. if (!l10n.id)
  832. return;
  833. // get the related l10n object
  834. var data = getL10nData(l10n.id, l10n.args);
  835. if (!data) {
  836. console.warn('#' + l10n.id + ' is undefined.');
  837. return;
  838. }
  839. // translate element (TODO: security checks?)
  840. if (data[gTextProp]) { // XXX
  841. if (getChildElementCount(element) === 0) {
  842. element[gTextProp] = data[gTextProp];
  843. } else {
  844. // this element has element children: replace the content of the first
  845. // (non-empty) child textNode and clear other child textNodes
  846. var children = element.childNodes;
  847. var found = false;
  848. for (var i = 0, l = children.length; i < l; i++) {
  849. if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
  850. if (found) {
  851. children[i].nodeValue = '';
  852. } else {
  853. children[i].nodeValue = data[gTextProp];
  854. found = true;
  855. }
  856. }
  857. }
  858. // if no (non-empty) textNode is found, insert a textNode before the
  859. // first element child.
  860. if (!found) {
  861. var textNode = document.createTextNode(data[gTextProp]);
  862. element.prepend(textNode);
  863. }
  864. }
  865. delete data[gTextProp];
  866. }
  867. for (var k in data) {
  868. element[k] = data[k];
  869. }
  870. }
  871. // webkit browsers don't currently support 'children' on SVG elements...
  872. function getChildElementCount(element) {
  873. if (element.children) {
  874. return element.children.length;
  875. }
  876. if (typeof element.childElementCount !== 'undefined') {
  877. return element.childElementCount;
  878. }
  879. var count = 0;
  880. for (var i = 0; i < element.childNodes.length; i++) {
  881. count += element.nodeType === 1 ? 1 : 0;
  882. }
  883. return count;
  884. }
  885. // translate an HTML subtree
  886. function translateFragment(element) {
  887. element = element || document.documentElement;
  888. // check all translatable children (= w/ a `data-l10n-id' attribute)
  889. var children = getTranslatableChildren(element);
  890. var elementCount = children.length;
  891. for (var i = 0; i < elementCount; i++) {
  892. translateElement(children[i]);
  893. }
  894. // translate element itself if necessary
  895. translateElement(element);
  896. }
  897. return {
  898. // get a localized string
  899. get: function(key, args, fallbackString) {
  900. var index = key.lastIndexOf('.');
  901. var prop = gTextProp;
  902. if (index > 0) { // An attribute has been specified
  903. prop = key.substring(index + 1);
  904. key = key.substring(0, index);
  905. }
  906. var fallback;
  907. if (fallbackString) {
  908. fallback = {};
  909. fallback[prop] = fallbackString;
  910. }
  911. var data = getL10nData(key, args, fallback);
  912. if (data && prop in data) {
  913. return data[prop];
  914. }
  915. return '{{' + key + '}}';
  916. },
  917. // debug
  918. getData: function() { return gL10nData; },
  919. getText: function() { return gTextData; },
  920. // get|set the document language
  921. getLanguage: function() { return gLanguage; },
  922. setLanguage: function(lang, callback) {
  923. loadLocale(lang, function() {
  924. if (callback)
  925. callback();
  926. });
  927. },
  928. // get the direction (ltr|rtl) of the current language
  929. getDirection: function() {
  930. // http://www.w3.org/International/questions/qa-scripts
  931. // Arabic, Hebrew, Farsi, Pashto, Urdu
  932. var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
  933. var shortCode = gLanguage.split('-', 1)[0];
  934. return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';
  935. },
  936. // translate an element or document fragment
  937. translate: translateFragment,
  938. // this can be used to prevent race conditions
  939. getReadyState: function() { return gReadyState; },
  940. ready: function(callback) {
  941. if (!callback) {
  942. return;
  943. } else if (gReadyState == 'complete' || gReadyState == 'interactive') {
  944. window.setTimeout(function() {
  945. callback();
  946. });
  947. } else if (document.addEventListener) {
  948. document.addEventListener('localized', function once() {
  949. document.removeEventListener('localized', once);
  950. callback();
  951. });
  952. }
  953. }
  954. };
  955. }) (window, document);