l10n.js 27 KB

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