text: /**
  * 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.
  */
  
  /*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
  
  
    /**
  * Debug helpers
  *
  * gDEBUG == 0: don't display any console message
  * gDEBUG == 1: display only warnings, not logs
  * gDEBUG == 2: display all console messages
  */
  
    var gDEBUG = 1;
  
    function consoleLog(message) {
      if (gDEBUG >= 2) {
        console.log('[l10n] ' + message);
      }
    };
  
    function consoleWarn(message) {
      if (gDEBUG) {
        console.warn('[l10n] ' + message);
      }
    };
  
  
    /**
  * 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) {
          consoleWarn('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, asynchronous) {
      onSuccess = onSuccess || function _onSuccess(data) {};
      onFailure = onFailure || function _onFailure() {
        consoleWarn(url + ' not found.');
      };
  
      var xhr = new XMLHttpRequest();
      xhr.open('GET', url, asynchronous);
      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.
  *
  * @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
      function parseProperties(text) {
        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) {
          var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
          var currentLang = '*';
          var genericLang = lang.replace(/-[a-z]+$/i, '');
          var skipLang = false;
          var match = '';
  
          for (var i = 0; i < entries.length; i++) {
            var line = entries[i];
  
            // comment or blank line?
            if (reComment.test(line))
              continue;
  
            // the extended syntax supports [lang] sections and @import rules
            if (extendedSyntax) {
              if (reSection.test(line)) { // section start?
                match = reSection.exec(line);
                currentLang = match[1];
                skipLang = (currentLang !== '*') &&
                    (currentLang !== lang) && (currentLang !== genericLang);
                continue;
              } else if (skipLang) {
                continue;
              }
              if (reImport.test(line)) { // @import rule?
                match = reImport.exec(line);
                loadImport(baseURL + match[1]); // load the resource synchronously
              }
            }
  
            // key-value pair
            var tmp = line.match(reSplit);
            if (tmp && tmp.length == 3) {
              dictionary[tmp[1]] = evalString(tmp[2]);
            }
          }
        }
  
        // import another *.properties file
        function loadImport(url) {
          xhrLoadText(url, function(content) {
            parseRawLines(content, false); // don't allow recursive imports
          }, null, false); // load synchronously
        }
  
        // fill the dictionary
        parseRawLines(text, true);
        return 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
        var data = parseProperties(response);
  
        // 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, gAsyncResourceLoading);
    };
  
    // load and parse all resources for the specified locale
    function loadLocale(lang, callback) {
      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) {
          consoleLog('using the embedded JSON directory, early way out');
          gL10nData = dict.locales[lang] || dict.locales[dict.default_locale];
          callback();
        } else {
          consoleLog('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;
        var type = link.type;
        this.load = function(lang, callback) {
          var applied = lang;
          parseResource(href, lang, callback, function() {
            consoleWarn(href + ' not found.');
            applied = '';
          });
          return applied; // return lang if found, an empty string if not found
        };
      }
  
      for (var i = 0; i < langCount; i++) {
        var resource = new l10nResourceLink(langLinks[i]);
        var rv = resource.load(lang, onResourceLoaded);
        if (rv != lang) { // lang not found, used default resource instead
          consoleWarn('"' + lang + '" resource not found');
          gLanguage = '';
        }
      }
    }
  
    // 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)) {
        consoleWarn('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) {
      var data = gL10nData[key];
      if (!data) {
        consoleWarn('#' + key + ' is undefined.');
      }
  
      /** 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*\}\}/;
      var match = reArgs.exec(str);
      while (match) {
        if (!match || match.length < 2)
          return str; // argument key not found
  
        var arg = match[1];
        var sub = '';
        if (args && arg in args) {
          sub = args[arg];
        } else if (arg in gL10nData) {
          sub = gL10nData[arg][gTextProp];
        } else {
          consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.');
          return str;
        }
  
        str = str.substring(0, match.index) + sub +
              str.substr(match.index + match[0].length);
        match = reArgs.exec(str);
      }
      return str;
    }
  
    // 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) {
        consoleWarn('#' + 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);
    }
  
  
    /**
  * Startup & Public API
  *
  * Warning: this part of the code contains browser-specific chunks --
  * that's where obsolete browsers, namely IE8 and earlier, are handled.
  *
  * Unlike the rest of the lib, this section is not shared with FirefoxOS/Gaia.
  */
  
    // load the default locale on startup
    function l10nStartup() {
      gReadyState = 'interactive';
  
      // most browsers expose the UI language as `navigator.language'
      // but IE uses `navigator.userLanguage' instead
      var userLocale = navigator.language || navigator.userLanguage;
      consoleLog('loading [' + userLocale + '] resources, ' +
          (gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
  
      // load the default locale and translate the document if required
      if (document.documentElement.lang === userLocale) {
        loadLocale(userLocale);
      } else {
        loadLocale(userLocale, translateFragment);
      }
    }
  
    // browser-specific startup
    if (document.addEventListener) { // modern browsers and IE9+
      if (document.readyState === 'loading') {
        // the document is not fully loaded yet: wait for DOMContentLoaded.
        document.addEventListener('DOMContentLoaded', l10nStartup);
      } else {
        // l10n.js is being loaded with <script defer> or <script async>,
        // the DOM is ready for parsing.
        window.setTimeout(l10nStartup);
      }
    } else if (window.attachEvent) { // IE8 and before (= oldIE)
      // TODO: check if jQuery is loaded (CSS selector + JSON + events)
  
      // dummy `console.log' and `console.warn' functions
      if (!window.console) {
        consoleLog = function(message) {}; // just ignore console.log calls
        consoleWarn = function(message) {
          if (gDEBUG) {
            alert('[l10n] ' + message); // vintage debugging, baby!
          }
        };
      }
  
      // XMLHttpRequest for IE6
      if (!window.XMLHttpRequest) {
        xhrLoadText = function(url, onSuccess, onFailure, asynchronous) {
          onSuccess = onSuccess || function _onSuccess(data) {};
          onFailure = onFailure || function _onFailure() {
            consoleWarn(url + ' not found.');
          };
          var xhr = new ActiveXObject('Microsoft.XMLHTTP');
          xhr.open('GET', url, asynchronous);
          xhr.onreadystatechange = function() {
            if (xhr.readyState == 4) {
              if (xhr.status == 200) {
                onSuccess(xhr.responseText);
              } else {
                onFailure();
              }
            }
          };
          xhr.send(null);
        }
      }
  
      // worst hack ever for IE6 and IE7
      if (!window.JSON) {
        getL10nAttributes = function(element) {
          if (!element)
            return {};
          var l10nId = element.getAttribute('data-l10n-id'),
              l10nArgs = element.getAttribute('data-l10n-args'),
              args = {};
          if (l10nArgs) try {
            args = eval(l10nArgs); // XXX yeah, I know...
          } catch (e) {
            consoleWarn('could not parse arguments for #' + l10nId);
          }
          return { id: l10nId, args: args };
        };
      }
  
      // override `getTranslatableChildren' and `getL10nResourceLinks'
      if (!document.querySelectorAll) {
        getTranslatableChildren = function(element) {
          if (!element)
            return [];
          var nodes = element.getElementsByTagName('*'),
              l10nElements = [],
              n = nodes.length;
          for (var i = 0; i < n; i++) {
            if (nodes[i].getAttribute('data-l10n-id'))
              l10nElements.push(nodes[i]);
          }
          return l10nElements;
        };
        getL10nResourceLinks = function() {
          var links = document.getElementsByTagName('link'),
              l10nLinks = [],
              n = links.length;
          for (var i = 0; i < n; i++) {
            if (links[i].type == 'application/l10n')
              l10nLinks.push(links[i]);
          }
          return l10nLinks;
        };
      }
  
      // override `getL10nDictionary'
      if (!window.JSON || !document.querySelectorAll) {
        getL10nDictionary = function() {
          var scripts = document.getElementsByName('script');
          for (var i = 0; i < scripts.length; i++) {
            if (scripts[i].type == 'application/l10n') {
              return eval(scripts[i].innerHTML);
            }
          }
          return null;
        };
      }
  
      // fire non-standard `localized' DOM events
      if (document.createEventObject && !document.createEvent) {
        fireL10nReadyEvent = function(lang) {
          // hack to simulate a custom event in IE:
          // to catch this event, add an event handler to `onpropertychange'
          document.documentElement.localized = 1;
        };
      }
  
      // startup for IE<9
      window.attachEvent('onload', function() {
        gTextProp = document.body.textContent ? 'textContent' : 'innerText';
        l10nStartup();
      });
    }
  
    // cross-browser API (sorry, oldIE doesn't support getters & setters)
    return {
      // get a localized string
      get: function(key, args, fallback) {
        var data = getL10nData(key, args) || fallback;
        if (data) { // XXX double-check this
          return gTextProp in data ? data[gTextProp] : '';
        }
        return '{{' + key + '}}';
      },
  
      // debug
      getData: function() { return gL10nData; },
      getText: function() { return gTextData; },
  
      // get|set the document language
      getLanguage: function() { return gLanguage; },
      setLanguage: function(lang) { loadLocale(lang, 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'];
        return (rtlList.indexOf(gLanguage) >= 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(callback);
        } else if (document.addEventListener) {
          document.addEventListener('localized', callback);
        } else if (document.attachEvent) {
          document.documentElement.attachEvent('onpropertychange', function(e) {
            if (e.propertyName === 'localized') {
              callback();
            }
          });
        }
      }
    };
  }) (window, document);
  
  // gettext-like shortcut for document.webL10n.get
  if (window._ === undefined) {
    var _ = document.webL10n.get;
  }