| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033 | /** * Copyright (c) 2011-2013 Fabien Cazenave, Mozilla. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. *//*  Additional modifications for PDF.js project:    - Disables language initialization on page loading;    - Removes consoleWarn and consoleLog and use console.log/warn directly.    - Removes window._ assignment.    - Remove compatibility code for OldIE.*//*jshint browser: true, devel: true, es5: true, globalstrict: true */'use strict';document.webL10n = (function(window, document, undefined) {  var gL10nData = {};  var gTextData = '';  var gTextProp = 'textContent';  var gLanguage = '';  var gMacros = {};  var gReadyState = 'loading';  /**   * Synchronously loading l10n resources significantly minimizes flickering   * from displaying the app with non-localized strings and then updating the   * strings. Although this will block all script execution on this page, we   * expect that the l10n resources are available locally on flash-storage.   *   * As synchronous XHR is generally considered as a bad idea, we're still   * loading l10n resources asynchronously -- but we keep this in a setting,   * just in case... and applications using this library should hide their   * content until the `localized' event happens.   */  var gAsyncResourceLoading = true; // read-only  /**   * DOM helpers for the so-called "HTML API".   *   * These functions are written for modern browsers. For old versions of IE,   * they're overridden in the 'startup' section at the end of this file.   */  function getL10nResourceLinks() {    return document.querySelectorAll('link[type="application/l10n"]');  }  function getL10nDictionary() {    var script = document.querySelector('script[type="application/l10n"]');    // TODO: support multiple and external JSON dictionaries    return script ? JSON.parse(script.innerHTML) : null;  }  function getTranslatableChildren(element) {    return element ? element.querySelectorAll('*[data-l10n-id]') : [];  }  function getL10nAttributes(element) {    if (!element)      return {};    var l10nId = element.getAttribute('data-l10n-id');    var l10nArgs = element.getAttribute('data-l10n-args');    var args = {};    if (l10nArgs) {      try {        args = JSON.parse(l10nArgs);      } catch (e) {        console.warn('could not parse arguments for #' + l10nId);      }    }    return { id: l10nId, args: args };  }  function fireL10nReadyEvent(lang) {    var evtObject = document.createEvent('Event');    evtObject.initEvent('localized', true, false);    evtObject.language = lang;    document.dispatchEvent(evtObject);  }  function xhrLoadText(url, onSuccess, onFailure) {    onSuccess = onSuccess || function _onSuccess(data) {};    onFailure = onFailure || function _onFailure() {      console.warn(url + ' not found.');    };    var xhr = new XMLHttpRequest();    xhr.open('GET', url, gAsyncResourceLoading);    if (xhr.overrideMimeType) {      xhr.overrideMimeType('text/plain; charset=utf-8');    }    xhr.onreadystatechange = function() {      if (xhr.readyState == 4) {        if (xhr.status == 200 || xhr.status === 0) {          onSuccess(xhr.responseText);        } else {          onFailure();        }      }    };    xhr.onerror = onFailure;    xhr.ontimeout = onFailure;    // in Firefox OS with the app:// protocol, trying to XHR a non-existing    // URL will raise an exception here -- hence this ugly try...catch.    try {      xhr.send(null);    } catch (e) {      onFailure();    }  }  /**   * l10n resource parser:   *  - reads (async XHR) the l10n resource matching `lang';   *  - imports linked resources (synchronously) when specified;   *  - parses the text data (fills `gL10nData' and `gTextData');   *  - triggers success/failure callbacks when done.   *   * @param {string} href   *    URL of the l10n resource to parse.   *   * @param {string} lang   *    locale (language) to parse. Must be a lowercase string.   *   * @param {Function} successCallback   *    triggered when the l10n resource has been successully parsed.   *   * @param {Function} failureCallback   *    triggered when the an error has occured.   *   * @return {void}   *    uses the following global variables: gL10nData, gTextData, gTextProp.   */  function parseResource(href, lang, successCallback, failureCallback) {    var baseURL = href.replace(/[^\/]*$/, '') || './';    // handle escaped characters (backslashes) in a string    function evalString(text) {      if (text.lastIndexOf('\\') < 0)        return text;      return text.replace(/\\\\/g, '\\')                 .replace(/\\n/g, '\n')                 .replace(/\\r/g, '\r')                 .replace(/\\t/g, '\t')                 .replace(/\\b/g, '\b')                 .replace(/\\f/g, '\f')                 .replace(/\\{/g, '{')                 .replace(/\\}/g, '}')                 .replace(/\\"/g, '"')                 .replace(/\\'/g, "'");    }    // parse *.properties text data into an l10n dictionary    // If gAsyncResourceLoading is false, then the callback will be called    // synchronously. Otherwise it is called asynchronously.    function parseProperties(text, parsedPropertiesCallback) {      var dictionary = {};      // token expressions      var reBlank = /^\s*|\s*$/;      var reComment = /^\s*#|^\s*$/;      var reSection = /^\s*\[(.*)\]\s*$/;      var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;      var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'      // parse the *.properties file into an associative array      function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) {        var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);        var currentLang = '*';        var genericLang = lang.split('-', 1)[0];        var skipLang = false;        var match = '';        function nextEntry() {          // Use infinite loop instead of recursion to avoid reaching the          // maximum recursion limit for content with many lines.          while (true) {            if (!entries.length) {              parsedRawLinesCallback();              return;            }            var line = entries.shift();            // comment or blank line?            if (reComment.test(line))              continue;            // the extended syntax supports [lang] sections and @import rules            if (extendedSyntax) {              match = reSection.exec(line);              if (match) { // section start?                // RFC 4646, section 4.4, "All comparisons MUST be performed                // in a case-insensitive manner."                currentLang = match[1].toLowerCase();                skipLang = (currentLang !== '*') &&                    (currentLang !== lang) && (currentLang !== genericLang);                continue;              } else if (skipLang) {                continue;              }              match = reImport.exec(line);              if (match) { // @import rule?                loadImport(baseURL + match[1], nextEntry);                return;              }            }            // key-value pair            var tmp = line.match(reSplit);            if (tmp && tmp.length == 3) {              dictionary[tmp[1]] = evalString(tmp[2]);            }          }        }        nextEntry();      }      // import another *.properties file      function loadImport(url, callback) {        xhrLoadText(url, function(content) {          parseRawLines(content, false, callback); // don't allow recursive imports        }, null);      }      // fill the dictionary      parseRawLines(text, true, function() {        parsedPropertiesCallback(dictionary);      });    }    // load and parse l10n data (warning: global variables are used here)    xhrLoadText(href, function(response) {      gTextData += response; // mostly for debug      // parse *.properties text data into an l10n dictionary      parseProperties(response, function(data) {        // find attribute descriptions, if any        for (var key in data) {          var id, prop, index = key.lastIndexOf('.');          if (index > 0) { // an attribute has been specified            id = key.substring(0, index);            prop = key.substr(index + 1);          } else { // no attribute: assuming text content by default            id = key;            prop = gTextProp;          }          if (!gL10nData[id]) {            gL10nData[id] = {};          }          gL10nData[id][prop] = data[key];        }        // trigger callback        if (successCallback) {          successCallback();        }      });    }, failureCallback);  }  // load and parse all resources for the specified locale  function loadLocale(lang, callback) {    // RFC 4646, section 2.1 states that language tags have to be treated as    // case-insensitive. Convert to lowercase for case-insensitive comparisons.    if (lang) {      lang = lang.toLowerCase();    }    callback = callback || function _callback() {};    clear();    gLanguage = lang;    // check all <link type="application/l10n" href="..." /> nodes    // and load the resource files    var langLinks = getL10nResourceLinks();    var langCount = langLinks.length;    if (langCount === 0) {      // we might have a pre-compiled dictionary instead      var dict = getL10nDictionary();      if (dict && dict.locales && dict.default_locale) {        console.log('using the embedded JSON directory, early way out');        gL10nData = dict.locales[lang];        if (!gL10nData) {          var defaultLocale = dict.default_locale.toLowerCase();          for (var anyCaseLang in dict.locales) {            anyCaseLang = anyCaseLang.toLowerCase();            if (anyCaseLang === lang) {              gL10nData = dict.locales[lang];              break;            } else if (anyCaseLang === defaultLocale) {              gL10nData = dict.locales[defaultLocale];            }          }        }        callback();      } else {        console.log('no resource to load, early way out');      }      // early way out      fireL10nReadyEvent(lang);      gReadyState = 'complete';      return;    }    // start the callback when all resources are loaded    var onResourceLoaded = null;    var gResourceCount = 0;    onResourceLoaded = function() {      gResourceCount++;      if (gResourceCount >= langCount) {        callback();        fireL10nReadyEvent(lang);        gReadyState = 'complete';      }    };    // load all resource files    function L10nResourceLink(link) {      var href = link.href;      // Note: If |gAsyncResourceLoading| is false, then the following callbacks      // are synchronously called.      this.load = function(lang, callback) {        parseResource(href, lang, callback, function() {          console.warn(href + ' not found.');          // lang not found, used default resource instead          console.warn('"' + lang + '" resource not found');          gLanguage = '';          // Resource not loaded, but we still need to call the callback.          callback();        });      };    }    for (var i = 0; i < langCount; i++) {      var resource = new L10nResourceLink(langLinks[i]);      resource.load(lang, onResourceLoaded);    }  }  // clear all l10n data  function clear() {    gL10nData = {};    gTextData = '';    gLanguage = '';    // TODO: clear all non predefined macros.    // There's no such macro /yet/ but we're planning to have some...  }  /**   * Get rules for plural forms (shared with JetPack), see:   * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html   * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p   *   * @param {string} lang   *    locale (language) used.   *   * @return {Function}   *    returns a function that gives the plural form name for a given integer:   *       var fun = getPluralRules('en');   *       fun(1)    -> 'one'   *       fun(0)    -> 'other'   *       fun(1000) -> 'other'.   */  function getPluralRules(lang) {    var locales2rules = {      'af': 3,      'ak': 4,      'am': 4,      'ar': 1,      'asa': 3,      'az': 0,      'be': 11,      'bem': 3,      'bez': 3,      'bg': 3,      'bh': 4,      'bm': 0,      'bn': 3,      'bo': 0,      'br': 20,      'brx': 3,      'bs': 11,      'ca': 3,      'cgg': 3,      'chr': 3,      'cs': 12,      'cy': 17,      'da': 3,      'de': 3,      'dv': 3,      'dz': 0,      'ee': 3,      'el': 3,      'en': 3,      'eo': 3,      'es': 3,      'et': 3,      'eu': 3,      'fa': 0,      'ff': 5,      'fi': 3,      'fil': 4,      'fo': 3,      'fr': 5,      'fur': 3,      'fy': 3,      'ga': 8,      'gd': 24,      'gl': 3,      'gsw': 3,      'gu': 3,      'guw': 4,      'gv': 23,      'ha': 3,      'haw': 3,      'he': 2,      'hi': 4,      'hr': 11,      'hu': 0,      'id': 0,      'ig': 0,      'ii': 0,      'is': 3,      'it': 3,      'iu': 7,      'ja': 0,      'jmc': 3,      'jv': 0,      'ka': 0,      'kab': 5,      'kaj': 3,      'kcg': 3,      'kde': 0,      'kea': 0,      'kk': 3,      'kl': 3,      'km': 0,      'kn': 0,      'ko': 0,      'ksb': 3,      'ksh': 21,      'ku': 3,      'kw': 7,      'lag': 18,      'lb': 3,      'lg': 3,      'ln': 4,      'lo': 0,      'lt': 10,      'lv': 6,      'mas': 3,      'mg': 4,      'mk': 16,      'ml': 3,      'mn': 3,      'mo': 9,      'mr': 3,      'ms': 0,      'mt': 15,      'my': 0,      'nah': 3,      'naq': 7,      'nb': 3,      'nd': 3,      'ne': 3,      'nl': 3,      'nn': 3,      'no': 3,      'nr': 3,      'nso': 4,      'ny': 3,      'nyn': 3,      'om': 3,      'or': 3,      'pa': 3,      'pap': 3,      'pl': 13,      'ps': 3,      'pt': 3,      'rm': 3,      'ro': 9,      'rof': 3,      'ru': 11,      'rwk': 3,      'sah': 0,      'saq': 3,      'se': 7,      'seh': 3,      'ses': 0,      'sg': 0,      'sh': 11,      'shi': 19,      'sk': 12,      'sl': 14,      'sma': 7,      'smi': 7,      'smj': 7,      'smn': 7,      'sms': 7,      'sn': 3,      'so': 3,      'sq': 3,      'sr': 11,      'ss': 3,      'ssy': 3,      'st': 3,      'sv': 3,      'sw': 3,      'syr': 3,      'ta': 3,      'te': 3,      'teo': 3,      'th': 0,      'ti': 4,      'tig': 3,      'tk': 3,      'tl': 4,      'tn': 3,      'to': 0,      'tr': 0,      'ts': 3,      'tzm': 22,      'uk': 11,      'ur': 3,      've': 3,      'vi': 0,      'vun': 3,      'wa': 4,      'wae': 3,      'wo': 0,      'xh': 3,      'xog': 3,      'yo': 0,      'zh': 0,      'zu': 3    };    // utility functions for plural rules methods    function isIn(n, list) {      return list.indexOf(n) !== -1;    }    function isBetween(n, start, end) {      return start <= n && n <= end;    }    // list of all plural rules methods:    // map an integer to the plural form name to use    var pluralRules = {      '0': function(n) {        return 'other';      },      '1': function(n) {        if ((isBetween((n % 100), 3, 10)))          return 'few';        if (n === 0)          return 'zero';        if ((isBetween((n % 100), 11, 99)))          return 'many';        if (n == 2)          return 'two';        if (n == 1)          return 'one';        return 'other';      },      '2': function(n) {        if (n !== 0 && (n % 10) === 0)          return 'many';        if (n == 2)          return 'two';        if (n == 1)          return 'one';        return 'other';      },      '3': function(n) {        if (n == 1)          return 'one';        return 'other';      },      '4': function(n) {        if ((isBetween(n, 0, 1)))          return 'one';        return 'other';      },      '5': function(n) {        if ((isBetween(n, 0, 2)) && n != 2)          return 'one';        return 'other';      },      '6': function(n) {        if (n === 0)          return 'zero';        if ((n % 10) == 1 && (n % 100) != 11)          return 'one';        return 'other';      },      '7': function(n) {        if (n == 2)          return 'two';        if (n == 1)          return 'one';        return 'other';      },      '8': function(n) {        if ((isBetween(n, 3, 6)))          return 'few';        if ((isBetween(n, 7, 10)))          return 'many';        if (n == 2)          return 'two';        if (n == 1)          return 'one';        return 'other';      },      '9': function(n) {        if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))          return 'few';        if (n == 1)          return 'one';        return 'other';      },      '10': function(n) {        if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))          return 'few';        if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))          return 'one';        return 'other';      },      '11': function(n) {        if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))          return 'few';        if ((n % 10) === 0 ||            (isBetween((n % 10), 5, 9)) ||            (isBetween((n % 100), 11, 14)))          return 'many';        if ((n % 10) == 1 && (n % 100) != 11)          return 'one';        return 'other';      },      '12': function(n) {        if ((isBetween(n, 2, 4)))          return 'few';        if (n == 1)          return 'one';        return 'other';      },      '13': function(n) {        if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))          return 'few';        if (n != 1 && (isBetween((n % 10), 0, 1)) ||            (isBetween((n % 10), 5, 9)) ||            (isBetween((n % 100), 12, 14)))          return 'many';        if (n == 1)          return 'one';        return 'other';      },      '14': function(n) {        if ((isBetween((n % 100), 3, 4)))          return 'few';        if ((n % 100) == 2)          return 'two';        if ((n % 100) == 1)          return 'one';        return 'other';      },      '15': function(n) {        if (n === 0 || (isBetween((n % 100), 2, 10)))          return 'few';        if ((isBetween((n % 100), 11, 19)))          return 'many';        if (n == 1)          return 'one';        return 'other';      },      '16': function(n) {        if ((n % 10) == 1 && n != 11)          return 'one';        return 'other';      },      '17': function(n) {        if (n == 3)          return 'few';        if (n === 0)          return 'zero';        if (n == 6)          return 'many';        if (n == 2)          return 'two';        if (n == 1)          return 'one';        return 'other';      },      '18': function(n) {        if (n === 0)          return 'zero';        if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)          return 'one';        return 'other';      },      '19': function(n) {        if ((isBetween(n, 2, 10)))          return 'few';        if ((isBetween(n, 0, 1)))          return 'one';        return 'other';      },      '20': function(n) {        if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(            isBetween((n % 100), 10, 19) ||            isBetween((n % 100), 70, 79) ||            isBetween((n % 100), 90, 99)            ))          return 'few';        if ((n % 1000000) === 0 && n !== 0)          return 'many';        if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))          return 'two';        if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))          return 'one';        return 'other';      },      '21': function(n) {        if (n === 0)          return 'zero';        if (n == 1)          return 'one';        return 'other';      },      '22': function(n) {        if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))          return 'one';        return 'other';      },      '23': function(n) {        if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)          return 'one';        return 'other';      },      '24': function(n) {        if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))          return 'few';        if (isIn(n, [2, 12]))          return 'two';        if (isIn(n, [1, 11]))          return 'one';        return 'other';      }    };    // return a function that gives the plural form name for a given integer    var index = locales2rules[lang.replace(/-.*$/, '')];    if (!(index in pluralRules)) {      console.warn('plural form unknown for [' + lang + ']');      return function() { return 'other'; };    }    return pluralRules[index];  }  // pre-defined 'plural' macro  gMacros.plural = function(str, param, key, prop) {    var n = parseFloat(param);    if (isNaN(n))      return str;    // TODO: support other properties (l20n still doesn't...)    if (prop != gTextProp)      return str;    // initialize _pluralRules    if (!gMacros._pluralRules) {      gMacros._pluralRules = getPluralRules(gLanguage);    }    var index = '[' + gMacros._pluralRules(n) + ']';    // try to find a [zero|one|two] key if it's defined    if (n === 0 && (key + '[zero]') in gL10nData) {      str = gL10nData[key + '[zero]'][prop];    } else if (n == 1 && (key + '[one]') in gL10nData) {      str = gL10nData[key + '[one]'][prop];    } else if (n == 2 && (key + '[two]') in gL10nData) {      str = gL10nData[key + '[two]'][prop];    } else if ((key + index) in gL10nData) {      str = gL10nData[key + index][prop];    } else if ((key + '[other]') in gL10nData) {      str = gL10nData[key + '[other]'][prop];    }    return str;  };  /**   * l10n dictionary functions   */  // fetch an l10n object, warn if not found, apply `args' if possible  function getL10nData(key, args, fallback) {    var data = gL10nData[key];    if (!data) {      console.warn('#' + key + ' is undefined.');      if (!fallback) {        return null;      }      data = fallback;    }    /** This is where l10n expressions should be processed.      * The plan is to support C-style expressions from the l20n project;      * until then, only two kinds of simple expressions are supported:      *   {[ index ]} and {{ arguments }}.      */    var rv = {};    for (var prop in data) {      var str = data[prop];      str = substIndexes(str, args, key, prop);      str = substArguments(str, args, key);      rv[prop] = str;    }    return rv;  }  // replace {[macros]} with their values  function substIndexes(str, args, key, prop) {    var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;    var reMatch = reIndex.exec(str);    if (!reMatch || !reMatch.length)      return str;    // an index/macro has been found    // Note: at the moment, only one parameter is supported    var macroName = reMatch[1];    var paramName = reMatch[2];    var param;    if (args && paramName in args) {      param = args[paramName];    } else if (paramName in gL10nData) {      param = gL10nData[paramName];    }    // there's no macro parser yet: it has to be defined in gMacros    if (macroName in gMacros) {      var macro = gMacros[macroName];      str = macro(str, param, key, prop);    }    return str;  }  // replace {{arguments}} with their values  function substArguments(str, args, key) {    var reArgs = /\{\{\s*(.+?)\s*\}\}/g;    return str.replace(reArgs, function(matched_text, arg) {      if (args && arg in args) {        return args[arg];      }      if (arg in gL10nData) {        return gL10nData[arg];      }      console.log('argument {{' + arg + '}} for #' + key + ' is undefined.');      return matched_text;    });  }  // translate an HTML element  function translateElement(element) {    var l10n = getL10nAttributes(element);    if (!l10n.id)      return;    // get the related l10n object    var data = getL10nData(l10n.id, l10n.args);    if (!data) {      console.warn('#' + l10n.id + ' is undefined.');      return;    }    // translate element (TODO: security checks?)    if (data[gTextProp]) { // XXX      if (getChildElementCount(element) === 0) {        element[gTextProp] = data[gTextProp];      } else {        // this element has element children: replace the content of the first        // (non-empty) child textNode and clear other child textNodes        var children = element.childNodes;        var found = false;        for (var i = 0, l = children.length; i < l; i++) {          if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {            if (found) {              children[i].nodeValue = '';            } else {              children[i].nodeValue = data[gTextProp];              found = true;            }          }        }        // if no (non-empty) textNode is found, insert a textNode before the        // first element child.        if (!found) {          var textNode = document.createTextNode(data[gTextProp]);          element.insertBefore(textNode, element.firstChild);        }      }      delete data[gTextProp];    }    for (var k in data) {      element[k] = data[k];    }  }  // webkit browsers don't currently support 'children' on SVG elements...  function getChildElementCount(element) {    if (element.children) {      return element.children.length;    }    if (typeof element.childElementCount !== 'undefined') {      return element.childElementCount;    }    var count = 0;    for (var i = 0; i < element.childNodes.length; i++) {      count += element.nodeType === 1 ? 1 : 0;    }    return count;  }  // translate an HTML subtree  function translateFragment(element) {    element = element || document.documentElement;    // check all translatable children (= w/ a `data-l10n-id' attribute)    var children = getTranslatableChildren(element);    var elementCount = children.length;    for (var i = 0; i < elementCount; i++) {      translateElement(children[i]);    }    // translate element itself if necessary    translateElement(element);  }  return {    // get a localized string    get: function(key, args, fallbackString) {      var index = key.lastIndexOf('.');      var prop = gTextProp;      if (index > 0) { // An attribute has been specified        prop = key.substr(index + 1);        key = key.substring(0, index);      }      var fallback;      if (fallbackString) {        fallback = {};        fallback[prop] = fallbackString;      }      var data = getL10nData(key, args, fallback);      if (data && prop in data) {        return data[prop];      }      return '{{' + key + '}}';    },    // debug    getData: function() { return gL10nData; },    getText: function() { return gTextData; },    // get|set the document language    getLanguage: function() { return gLanguage; },    setLanguage: function(lang, callback) {      loadLocale(lang, function() {        if (callback)          callback();        translateFragment();      });    },    // get the direction (ltr|rtl) of the current language    getDirection: function() {      // http://www.w3.org/International/questions/qa-scripts      // Arabic, Hebrew, Farsi, Pashto, Urdu      var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];      var shortCode = gLanguage.split('-', 1)[0];      return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';    },    // translate an element or document fragment    translate: translateFragment,    // this can be used to prevent race conditions    getReadyState: function() { return gReadyState; },    ready: function(callback) {      if (!callback) {        return;      } else if (gReadyState == 'complete' || gReadyState == 'interactive') {        window.setTimeout(function() {          callback();        });      } else if (document.addEventListener) {        document.addEventListener('localized', function once() {          document.removeEventListener('localized', once);          callback();        });      }    }  };}) (window, document);
 |