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.

1157 lines
32 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. document.webL10n = (function(window, document, undefined) {
  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 = 0;
  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 + ' is undefined.');
  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, key);
  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, key) {
  793. var reArgs = /\{\{\s*(.+?)\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 (args && arg in args) {
  801. sub = args[arg];
  802. } else if (arg in gL10nData) {
  803. sub = gL10nData[arg][gTextProp];
  804. } else {
  805. consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.');
  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 + ' is undefined.');
  823. return;
  824. }
  825. // translate element (TODO: security checks?)
  826. if (data[gTextProp]) { // XXX
  827. if (getChildElementCount(element) === 0) {
  828. element[gTextProp] = data[gTextProp];
  829. } else {
  830. // this element has element children: replace the content of the first
  831. // (non-empty) child textNode and clear other child textNodes
  832. var children = element.childNodes;
  833. var found = false;
  834. for (var i = 0, l = children.length; i < l; i++) {
  835. if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
  836. if (found) {
  837. children[i].nodeValue = '';
  838. } else {
  839. children[i].nodeValue = data[gTextProp];
  840. found = true;
  841. }
  842. }
  843. }
  844. // if no (non-empty) textNode is found, insert a textNode before the
  845. // first element child.
  846. if (!found) {
  847. var textNode = document.createTextNode(data[gTextProp]);
  848. element.insertBefore(textNode, element.firstChild);
  849. }
  850. }
  851. delete data[gTextProp];
  852. }
  853. for (var k in data) {
  854. element[k] = data[k];
  855. }
  856. }
  857. // webkit browsers don't currently support 'children' on SVG elements...
  858. function getChildElementCount(element) {
  859. if (element.children) {
  860. return element.children.length;
  861. }
  862. if (typeof element.childElementCount !== 'undefined') {
  863. return element.childElementCount;
  864. }
  865. var count = 0;
  866. for (var i = 0; i < element.childNodes.length; i++) {
  867. count += element.nodeType === 1 ? 1 : 0;
  868. }
  869. return count;
  870. }
  871. // translate an HTML subtree
  872. function translateFragment(element) {
  873. element = element || document.documentElement;
  874. // check all translatable children (= w/ a `data-l10n-id' attribute)
  875. var children = getTranslatableChildren(element);
  876. var elementCount = children.length;
  877. for (var i = 0; i < elementCount; i++) {
  878. translateElement(children[i]);
  879. }
  880. // translate element itself if necessary
  881. translateElement(element);
  882. }
  883. /**
  884. * Startup & Public API
  885. *
  886. * Warning: this part of the code contains browser-specific chunks --
  887. * that's where obsolete browsers, namely IE8 and earlier, are handled.
  888. *
  889. * Unlike the rest of the lib, this section is not shared with FirefoxOS/Gaia.
  890. */
  891. // load the default locale on startup
  892. function l10nStartup() {
  893. gReadyState = 'interactive';
  894. // most browsers expose the UI language as `navigator.language'
  895. // but IE uses `navigator.userLanguage' instead
  896. var userLocale = navigator.language || navigator.userLanguage;
  897. consoleLog('loading [' + userLocale + '] resources, ' +
  898. (gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
  899. // load the default locale and translate the document if required
  900. if (document.documentElement.lang === userLocale) {
  901. loadLocale(userLocale);
  902. } else {
  903. loadLocale(userLocale, translateFragment);
  904. }
  905. }
  906. // browser-specific startup
  907. if (document.addEventListener) { // modern browsers and IE9+
  908. if (document.readyState === 'loading') {
  909. // the document is not fully loaded yet: wait for DOMContentLoaded.
  910. document.addEventListener('DOMContentLoaded', l10nStartup);
  911. } else {
  912. // l10n.js is being loaded with <script defer> or <script async>,
  913. // the DOM is ready for parsing.
  914. window.setTimeout(l10nStartup);
  915. }
  916. } else if (window.attachEvent) { // IE8 and before (= oldIE)
  917. // TODO: check if jQuery is loaded (CSS selector + JSON + events)
  918. // dummy `console.log' and `console.warn' functions
  919. if (!window.console) {
  920. consoleLog = function(message) {}; // just ignore console.log calls
  921. consoleWarn = function(message) {
  922. if (gDEBUG) {
  923. alert('[l10n] ' + message); // vintage debugging, baby!
  924. }
  925. };
  926. }
  927. // XMLHttpRequest for IE6
  928. if (!window.XMLHttpRequest) {
  929. xhrLoadText = function(url, onSuccess, onFailure, asynchronous) {
  930. onSuccess = onSuccess || function _onSuccess(data) {};
  931. onFailure = onFailure || function _onFailure() {
  932. consoleWarn(url + ' not found.');
  933. };
  934. var xhr = new ActiveXObject('Microsoft.XMLHTTP');
  935. xhr.open('GET', url, asynchronous);
  936. xhr.onreadystatechange = function() {
  937. if (xhr.readyState == 4) {
  938. if (xhr.status == 200) {
  939. onSuccess(xhr.responseText);
  940. } else {
  941. onFailure();
  942. }
  943. }
  944. };
  945. xhr.send(null);
  946. }
  947. }
  948. // worst hack ever for IE6 and IE7
  949. if (!window.JSON) {
  950. getL10nAttributes = function(element) {
  951. if (!element)
  952. return {};
  953. var l10nId = element.getAttribute('data-l10n-id'),
  954. l10nArgs = element.getAttribute('data-l10n-args'),
  955. args = {};
  956. if (l10nArgs) try {
  957. args = eval(l10nArgs); // XXX yeah, I know...
  958. } catch (e) {
  959. consoleWarn('could not parse arguments for #' + l10nId);
  960. }
  961. return { id: l10nId, args: args };
  962. };
  963. }
  964. // override `getTranslatableChildren' and `getL10nResourceLinks'
  965. if (!document.querySelectorAll) {
  966. getTranslatableChildren = function(element) {
  967. if (!element)
  968. return [];
  969. var nodes = element.getElementsByTagName('*'),
  970. l10nElements = [],
  971. n = nodes.length;
  972. for (var i = 0; i < n; i++) {
  973. if (nodes[i].getAttribute('data-l10n-id'))
  974. l10nElements.push(nodes[i]);
  975. }
  976. return l10nElements;
  977. };
  978. getL10nResourceLinks = function() {
  979. var links = document.getElementsByTagName('link'),
  980. l10nLinks = [],
  981. n = links.length;
  982. for (var i = 0; i < n; i++) {
  983. if (links[i].type == 'application/l10n')
  984. l10nLinks.push(links[i]);
  985. }
  986. return l10nLinks;
  987. };
  988. }
  989. // override `getL10nDictionary'
  990. if (!window.JSON || !document.querySelectorAll) {
  991. getL10nDictionary = function() {
  992. var scripts = document.getElementsByName('script');
  993. for (var i = 0; i < scripts.length; i++) {
  994. if (scripts[i].type == 'application/l10n') {
  995. return eval(scripts[i].innerHTML);
  996. }
  997. }
  998. return null;
  999. };
  1000. }
  1001. // fire non-standard `localized' DOM events
  1002. if (document.createEventObject && !document.createEvent) {
  1003. fireL10nReadyEvent = function(lang) {
  1004. // hack to simulate a custom event in IE:
  1005. // to catch this event, add an event handler to `onpropertychange'
  1006. document.documentElement.localized = 1;
  1007. };
  1008. }
  1009. // startup for IE<9
  1010. window.attachEvent('onload', function() {
  1011. gTextProp = document.body.textContent ? 'textContent' : 'innerText';
  1012. l10nStartup();
  1013. });
  1014. }
  1015. // cross-browser API (sorry, oldIE doesn't support getters & setters)
  1016. return {
  1017. // get a localized string
  1018. get: function(key, args, fallback) {
  1019. var data = getL10nData(key, args) || fallback;
  1020. if (data) { // XXX double-check this
  1021. return gTextProp in data ? data[gTextProp] : '';
  1022. }
  1023. return '{{' + key + '}}';
  1024. },
  1025. // debug
  1026. getData: function() { return gL10nData; },
  1027. getText: function() { return gTextData; },
  1028. // get|set the document language
  1029. getLanguage: function() { return gLanguage; },
  1030. setLanguage: function(lang) { loadLocale(lang, translateFragment); },
  1031. // get the direction (ltr|rtl) of the current language
  1032. getDirection: function() {
  1033. // http://www.w3.org/International/questions/qa-scripts
  1034. // Arabic, Hebrew, Farsi, Pashto, Urdu
  1035. var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
  1036. return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
  1037. },
  1038. // translate an element or document fragment
  1039. translate: translateFragment,
  1040. // this can be used to prevent race conditions
  1041. getReadyState: function() { return gReadyState; },
  1042. ready: function(callback) {
  1043. if (!callback) {
  1044. return;
  1045. } else if (gReadyState == 'complete' || gReadyState == 'interactive') {
  1046. window.setTimeout(callback);
  1047. } else if (document.addEventListener) {
  1048. document.addEventListener('localized', callback);
  1049. } else if (document.attachEvent) {
  1050. document.documentElement.attachEvent('onpropertychange', function(e) {
  1051. if (e.propertyName === 'localized') {
  1052. callback();
  1053. }
  1054. });
  1055. }
  1056. }
  1057. };
  1058. }) (window, document);
  1059. // gettext-like shortcut for document.webL10n.get
  1060. if (window._ === undefined) {
  1061. var _ = document.webL10n.get;
  1062. }