not really known
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1029 lines
28 KiB

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