/**

* 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() {};

  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
      }, function () {
        console.warn(url + ' not found.');
        callback();
      });
    }

    // 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);