/**
 * 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 DRM 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 pdfprotects 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 pdfprotects are available locally on flash-storage.
     *
     * As synchronous XHR is generally considered as a bad idea, we're still
     * loading l10n pdfprotects 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 pdfprotect parser:
     *  - reads (async XHR) the l10n pdfprotect matching `lang';
     *  - imports linked pdfprotects (synchronously) when specified;
     *  - parses the text data (fills `gL10nData' and `gTextData');
     *  - triggers success/failure callbacks when done.
     *
     * @param {string} href
     *    URL of the l10n pdfprotect to parse.
     *
     * @param {string} lang
     *    locale (language) to parse. Must be a lowercase string.
     *
     * @param {Function} successCallback
     *    triggered when the l10n pdfprotect 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 pdfprotects 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 pdfprotect 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 pdfprotect to load, early way out');
            }
            // early way out
            fireL10nReadyEvent(lang);
            gReadyState = 'complete';
            return;
        }

        // start the callback when all pdfprotects are loaded
        var onResourceLoaded = null;
        var gResourceCount = 0;
        onResourceLoaded = function() {
            gResourceCount++;
            if (gResourceCount >= langCount) {
                callback();
                fireL10nReadyEvent(lang);
                gReadyState = 'complete';
            }
        };

        // load all pdfprotect 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 pdfprotect instead
                    //console.warn('"' + lang + '" pdfprotect not found');
                    gLanguage = '';
                    // Resource not loaded, but we still need to call the callback.
                    callback();
                });
            };
        }

        for (var i = 0; i < langCount; i++) {
            var pdfprotect = new L10nResourceLink(langLinks[i]);
            pdfprotect.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);
