l10n.js 29 KB

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