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