// This is the Gaia version of l20n: https://github.com/l20n/l20n.js // l20n is Apache 2.0 licensed: https://github.com/l20n/l20n.js/blob/master/LICENSE // You can find the latest build for Gaia here: https://github.com/mozilla-b2g/gaia/blob/master/shared/js/l10n.js (function(window, undefined) { 'use strict'; /* jshint validthis:true */ function L10nError(message, id, loc) { this.name = 'L10nError'; this.message = message; this.id = id; this.loc = loc; } L10nError.prototype = Object.create(Error.prototype); L10nError.prototype.constructor = L10nError; /* jshint browser:true */ var io = { load: function load(url, callback, sync) { var xhr = new XMLHttpRequest(); if (xhr.overrideMimeType) { xhr.overrideMimeType('text/plain'); } xhr.open('GET', url, !sync); xhr.addEventListener('load', function io_load(e) { if (e.target.status === 200 || e.target.status === 0) { callback(null, e.target.responseText); } else { callback(new L10nError('Not found: ' + url)); } }); xhr.addEventListener('error', callback); xhr.addEventListener('timeout', callback); // the app: protocol throws on 404, see https://bugzil.la/827243 try { xhr.send(null); } catch (e) { callback(new L10nError('Not found: ' + url)); } }, loadJSON: function loadJSON(url, callback) { var xhr = new XMLHttpRequest(); if (xhr.overrideMimeType) { xhr.overrideMimeType('application/json'); } xhr.open('GET', url); xhr.responseType = 'json'; xhr.addEventListener('load', function io_loadjson(e) { if (e.target.status === 200 || e.target.status === 0) { callback(null, e.target.response); } else { callback(new L10nError('Not found: ' + url)); } }); xhr.addEventListener('error', callback); xhr.addEventListener('timeout', callback); // the app: protocol throws on 404, see https://bugzil.la/827243 try { xhr.send(null); } catch (e) { callback(new L10nError('Not found: ' + url)); } } }; function EventEmitter() {} EventEmitter.prototype.emit = function ee_emit() { if (!this._listeners) { return; } var args = Array.prototype.slice.call(arguments); var type = args.shift(); if (!this._listeners[type]) { return; } var typeListeners = this._listeners[type].slice(); for (var i = 0; i < typeListeners.length; i++) { typeListeners[i].apply(this, args); } }; EventEmitter.prototype.addEventListener = function ee_add(type, listener) { if (!this._listeners) { this._listeners = {}; } if (!(type in this._listeners)) { this._listeners[type] = []; } this._listeners[type].push(listener); }; EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) { if (!this._listeners) { return; } var typeListeners = this._listeners[type]; var pos = typeListeners.indexOf(listener); if (pos === -1) { return; } typeListeners.splice(pos, 1); }; function getPluralRule(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() { 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)) { return function() { return 'other'; }; } return pluralRules[index]; } var nestedProps = ['style', 'dataset']; var parsePatterns; function parse(ctx, source) { var ast = {}; if (!parsePatterns) { parsePatterns = { comment: /^\s*#|^\s*$/, entity: /^([^=\s]+)\s*=\s*(.+)$/, multiline: /[^\\]\\$/, macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i, unicode: /\\u([0-9a-fA-F]{1,4})/g, entries: /[\r\n]+/, controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g }; } var entries = source.split(parsePatterns.entries); for (var i = 0; i < entries.length; i++) { var line = entries[i]; if (parsePatterns.comment.test(line)) { continue; } while (parsePatterns.multiline.test(line) && i < entries.length) { line = line.slice(0, -1) + entries[++i].trim(); } var entityMatch = line.match(parsePatterns.entity); if (entityMatch) { try { parseEntity(entityMatch[1], entityMatch[2], ast); } catch (e) { if (ctx) { ctx._emitter.emit('error', e); } else { throw e; } } } } return ast; } function setEntityValue(id, attr, key, value, ast) { var obj = ast; var prop = id; if (attr) { if (!(id in obj)) { obj[id] = {}; } if (typeof(obj[id]) === 'string') { obj[id] = {'_': obj[id]}; } obj = obj[id]; prop = attr; } if (!key) { obj[prop] = value; return; } if (!(prop in obj)) { obj[prop] = {'_': {}}; } else if (typeof(obj[prop]) === 'string') { obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}}; } obj[prop]._[key] = value; } function parseEntity(id, value, ast) { var name, key; var pos = id.indexOf('['); if (pos !== -1) { name = id.substr(0, pos); key = id.substring(pos + 1, id.length - 1); } else { name = id; key = null; } var nameElements = name.split('.'); var attr; if (nameElements.length > 1) { var attrElements = []; attrElements.push(nameElements.pop()); if (nameElements.length > 1) { // Usually the last dot separates an attribute from an id // // In case when there are more than one dot in the id // and the second to last item is "style" or "dataset" then the last two // items are becoming the attribute. // // ex. // id.style.color = foo => // // id: // style.color: foo // // id.other.color = foo => // // id.other: // color: foo if (nestedProps.indexOf(nameElements[nameElements.length - 1]) !== -1) { attrElements.push(nameElements.pop()); } } name = nameElements.join('.'); attr = attrElements.reverse().join('.'); } else { attr = null; } setEntityValue(name, attr, key, unescapeString(value), ast); } function unescapeControlCharacters(str) { return str.replace(parsePatterns.controlChars, '$1'); } function unescapeUnicode(str) { return str.replace(parsePatterns.unicode, function(match, token) { return unescape('%u' + '0000'.slice(token.length) + token); }); } function unescapeString(str) { if (str.lastIndexOf('\\') !== -1) { str = unescapeControlCharacters(str); } return unescapeUnicode(str); } function parseMacro(str) { var match = str.match(parsePatterns.macro); if (!match) { throw new L10nError('Malformed macro'); } return [match[1], match[2]]; } var MAX_PLACEABLE_LENGTH = 2500; var MAX_PLACEABLES = 100; var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g; function Entity(id, node, env) { this.id = id; this.env = env; // the dirty guard prevents cyclic or recursive references from other // Entities; see Entity.prototype.resolve this.dirty = false; if (typeof node === 'string') { this.value = node; } else { // it's either a hash or it has attrs, or both for (var key in node) { if (node.hasOwnProperty(key) && key[0] !== '_') { if (!this.attributes) { this.attributes = {}; } this.attributes[key] = new Entity(this.id + '.' + key, node[key], env); } } this.value = node._ || null; this.index = node._index; } } Entity.prototype.resolve = function E_resolve(ctxdata) { if (this.dirty) { return undefined; } this.dirty = true; var val; // if resolve fails, we want the exception to bubble up and stop the whole // resolving process; however, we still need to clean up the dirty flag try { val = resolve(ctxdata, this.env, this.value, this.index); } finally { this.dirty = false; } return val; }; Entity.prototype.toString = function E_toString(ctxdata) { try { return this.resolve(ctxdata); } catch (e) { return undefined; } }; Entity.prototype.valueOf = function E_valueOf(ctxdata) { if (!this.attributes) { return this.toString(ctxdata); } var entity = { value: this.toString(ctxdata), attributes: {} }; for (var key in this.attributes) { if (this.attributes.hasOwnProperty(key)) { entity.attributes[key] = this.attributes[key].toString(ctxdata); } } return entity; }; function subPlaceable(ctxdata, env, match, id) { if (ctxdata && ctxdata.hasOwnProperty(id) && (typeof ctxdata[id] === 'string' || (typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) { return ctxdata[id]; } if (env.hasOwnProperty(id)) { if (!(env[id] instanceof Entity)) { env[id] = new Entity(id, env[id], env); } var value = env[id].resolve(ctxdata); if (typeof value === 'string') { // prevent Billion Laughs attacks if (value.length >= MAX_PLACEABLE_LENGTH) { throw new L10nError('Too many characters in placeable (' + value.length + ', max allowed is ' + MAX_PLACEABLE_LENGTH + ')'); } return value; } } return match; } function interpolate(ctxdata, env, str) { var placeablesCount = 0; var value = str.replace(rePlaceables, function(match, id) { // prevent Quadratic Blowup attacks if (placeablesCount++ >= MAX_PLACEABLES) { throw new L10nError('Too many placeables (' + placeablesCount + ', max allowed is ' + MAX_PLACEABLES + ')'); } return subPlaceable(ctxdata, env, match, id); }); placeablesCount = 0; return value; } function resolve(ctxdata, env, expr, index) { if (typeof expr === 'string') { return interpolate(ctxdata, env, expr); } if (typeof expr === 'boolean' || typeof expr === 'number' || !expr) { return expr; } // otherwise, it's a dict if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) { var argValue = ctxdata[index[1]]; // special cases for zero, one, two if they are defined on the hash if (argValue === 0 && 'zero' in expr) { return resolve(ctxdata, env, expr.zero); } if (argValue === 1 && 'one' in expr) { return resolve(ctxdata, env, expr.one); } if (argValue === 2 && 'two' in expr) { return resolve(ctxdata, env, expr.two); } var selector = env.__plural(argValue); if (expr.hasOwnProperty(selector)) { return resolve(ctxdata, env, expr[selector]); } } // if there was no index or no selector was found, try 'other' if ('other' in expr) { return resolve(ctxdata, env, expr.other); } return undefined; } function compile(env, ast) { env = env || {}; for (var id in ast) { if (ast.hasOwnProperty(id)) { env[id] = new Entity(id, ast[id], env); } } return env; } function Locale(id, ctx) { this.id = id; this.ctx = ctx; this.isReady = false; this.entries = { __plural: getPluralRule(id) }; } Locale.prototype.getEntry = function L_getEntry(id) { /* jshint -W093 */ var entries = this.entries; if (!entries.hasOwnProperty(id)) { return undefined; } if (entries[id] instanceof Entity) { return entries[id]; } return entries[id] = new Entity(id, entries[id], entries); }; Locale.prototype.build = function L_build(callback) { var sync = !callback; var ctx = this.ctx; var self = this; var l10nLoads = ctx.resLinks.length; function onL10nLoaded(err) { if (err) { ctx._emitter.emit('error', err); } if (--l10nLoads <= 0) { self.isReady = true; if (callback) { callback(); } } } if (l10nLoads === 0) { onL10nLoaded(); return; } function onJSONLoaded(err, json) { if (!err && json) { self.addAST(json); } onL10nLoaded(err); } function onPropLoaded(err, source) { if (!err && source) { var ast = parse(ctx, source); self.addAST(ast); } onL10nLoaded(err); } for (var i = 0; i < ctx.resLinks.length; i++) { var path = ctx.resLinks[i].replace('{{locale}}', this.id); var type = path.substr(path.lastIndexOf('.') + 1); switch (type) { case 'json': io.loadJSON(path, onJSONLoaded, sync); break; case 'properties': io.load(path, onPropLoaded, sync); break; } } }; Locale.prototype.addAST = function(ast) { for (var id in ast) { if (ast.hasOwnProperty(id)) { this.entries[id] = ast[id]; } } }; Locale.prototype.getEntity = function(id, ctxdata) { var entry = this.getEntry(id); if (!entry) { return null; } return entry.valueOf(ctxdata); }; function Context(id) { this.id = id; this.isReady = false; this.isLoading = false; this.supportedLocales = []; this.resLinks = []; this.locales = {}; this._emitter = new EventEmitter(); // Getting translations function getWithFallback(id) { /* jshint -W084 */ if (!this.isReady) { throw new L10nError('Context not ready'); } var cur = 0; var loc; var locale; while (loc = this.supportedLocales[cur]) { locale = this.getLocale(loc); if (!locale.isReady) { // build without callback, synchronously locale.build(null); } var entry = locale.getEntry(id); if (entry === undefined) { cur++; warning.call(this, new L10nError(id + ' not found in ' + loc, id, loc)); continue; } return entry; } error.call(this, new L10nError(id + ' not found', id)); return null; } this.get = function get(id, ctxdata) { var entry = getWithFallback.call(this, id); if (entry === null) { return ''; } return entry.toString(ctxdata) || ''; }; this.getEntity = function getEntity(id, ctxdata) { var entry = getWithFallback.call(this, id); if (entry === null) { return null; } return entry.valueOf(ctxdata); }; // Helpers this.getLocale = function getLocale(code) { /* jshint -W093 */ var locales = this.locales; if (locales[code]) { return locales[code]; } return locales[code] = new Locale(code, this); }; // Getting ready function negotiate(available, requested, defaultLocale) { if (available.indexOf(requested[0]) === -1 || requested[0] === defaultLocale) { return [defaultLocale]; } else { return [requested[0], defaultLocale]; } } function freeze(supported) { var locale = this.getLocale(supported[0]); if (locale.isReady) { setReady.call(this, supported); } else { locale.build(setReady.bind(this, supported)); } } function setReady(supported) { this.supportedLocales = supported; this.isReady = true; this._emitter.emit('ready'); } this.requestLocales = function requestLocales() { if (this.isLoading && !this.isReady) { throw new L10nError('Context not ready'); } this.isLoading = true; var requested = Array.prototype.slice.call(arguments); var supported = negotiate(requested.concat('en-US'), requested, 'en-US'); freeze.call(this, supported); }; // Events this.addEventListener = function addEventListener(type, listener) { this._emitter.addEventListener(type, listener); }; this.removeEventListener = function removeEventListener(type, listener) { this._emitter.removeEventListener(type, listener); }; this.ready = function ready(callback) { if (this.isReady) { setTimeout(callback); } this.addEventListener('ready', callback); }; this.once = function once(callback) { /* jshint -W068 */ if (this.isReady) { setTimeout(callback); return; } var callAndRemove = (function() { this.removeEventListener('ready', callAndRemove); callback(); }).bind(this); this.addEventListener('ready', callAndRemove); }; // Errors function warning(e) { this._emitter.emit('warning', e); return e; } function error(e) { this._emitter.emit('error', e); return e; } } /* jshint -W104 */ var DEBUG = false; var isPretranslated = false; var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur']; // Public API navigator.mozL10n = { ctx: new Context(), get: function get(id, ctxdata) { return navigator.mozL10n.ctx.get(id, ctxdata); }, localize: function localize(element, id, args) { return localizeElement.call(navigator.mozL10n, element, id, args); }, translate: function translate(element) { return translateFragment.call(navigator.mozL10n, element); }, ready: function ready(callback) { return navigator.mozL10n.ctx.ready(callback); }, once: function once(callback) { return navigator.mozL10n.ctx.once(callback); }, get readyState() { return navigator.mozL10n.ctx.isReady ? 'complete' : 'loading'; }, language: { set code(lang) { navigator.mozL10n.ctx.requestLocales(lang); }, get code() { return navigator.mozL10n.ctx.supportedLocales[0]; }, get direction() { return getDirection(navigator.mozL10n.ctx.supportedLocales[0]); } }, _getInternalAPI: function() { return { Error: L10nError, Context: Context, Locale: Locale, Entity: Entity, getPluralRule: getPluralRule, rePlaceables: rePlaceables, getTranslatableChildren: getTranslatableChildren, getL10nAttributes: getL10nAttributes, loadINI: loadINI, fireLocalizedEvent: fireLocalizedEvent, parse: parse, compile: compile }; } }; navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n)); if (DEBUG) { navigator.mozL10n.ctx.addEventListener('error', console.error); navigator.mozL10n.ctx.addEventListener('warning', console.warn); } function getDirection(lang) { return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr'; } var readyStates = { 'loading': 0, 'interactive': 1, 'complete': 2 }; function waitFor(state, callback) { state = readyStates[state]; if (readyStates[document.readyState] >= state) { callback(); return; } document.addEventListener('readystatechange', function l10n_onrsc() { if (readyStates[document.readyState] >= state) { document.removeEventListener('readystatechange', l10n_onrsc); callback(); } }); } if (window.document) { isPretranslated = (document.documentElement.lang === navigator.language); // this is a special case for netError bug; see https://bugzil.la/444165 if (document.documentElement.dataset.noCompleteBug) { pretranslate.call(navigator.mozL10n); return; } if (isPretranslated) { waitFor('interactive', function() { window.setTimeout(initResources.bind(navigator.mozL10n)); }); } else { if (document.readyState === 'complete') { window.setTimeout(initResources.bind(navigator.mozL10n)); } else { waitFor('interactive', pretranslate.bind(navigator.mozL10n)); } } } function pretranslate() { /* jshint -W068 */ if (inlineLocalization.call(this)) { waitFor('interactive', (function() { window.setTimeout(initResources.bind(this)); }).bind(this)); } else { initResources.call(this); } } function inlineLocalization() { var script = document.documentElement .querySelector('script[type="application/l10n"]' + '[lang="' + navigator.language + '"]'); if (!script) { return false; } var locale = this.ctx.getLocale(navigator.language); // the inline localization is happenning very early, when the ctx is not // yet ready and when the resources haven't been downloaded yet; add the // inlined JSON directly to the current locale locale.addAST(JSON.parse(script.innerHTML)); // localize the visible DOM var l10n = { ctx: locale, language: { code: locale.id, direction: getDirection(locale.id) } }; translateFragment.call(l10n); // the visible DOM is now pretranslated isPretranslated = true; return true; } function initResources() { var resLinks = document.head .querySelectorAll('link[type="application/l10n"]'); var iniLinks = []; var i; for (i = 0; i < resLinks.length; i++) { var link = resLinks[i]; var url = link.getAttribute('href'); var type = url.substr(url.lastIndexOf('.') + 1); if (type === 'ini') { iniLinks.push(url); } this.ctx.resLinks.push(url); } var iniLoads = iniLinks.length; if (iniLoads === 0) { initLocale.call(this); return; } function onIniLoaded(err) { if (err) { this.ctx._emitter.emit('error', err); } if (--iniLoads === 0) { initLocale.call(this); } } for (i = 0; i < iniLinks.length; i++) { loadINI.call(this, iniLinks[i], onIniLoaded.bind(this)); } } function initLocale() { this.ctx.requestLocales(navigator.language); window.addEventListener('languagechange', function l10n_langchange() { navigator.mozL10n.language.code = navigator.language; }); } function onReady() { if (!isPretranslated) { this.translate(); } isPretranslated = false; fireLocalizedEvent.call(this); } function fireLocalizedEvent() { var event = new CustomEvent('localized', { 'bubbles': false, 'cancelable': false, 'detail': { 'language': this.ctx.supportedLocales[0] } }); window.dispatchEvent(event); } /* jshint -W104 */ function loadINI(url, callback) { var ctx = this.ctx; io.load(url, function(err, source) { var pos = ctx.resLinks.indexOf(url); if (err) { // remove the ini link from resLinks ctx.resLinks.splice(pos, 1); return callback(err); } if (!source) { ctx.resLinks.splice(pos, 1); return callback(new Error('Empty file: ' + url)); } var patterns = parseINI(source, url).resources.map(function(x) { return x.replace('en-US', '{{locale}}'); }); ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns)); callback(); }); } function relativePath(baseUrl, url) { if (url[0] === '/') { return url; } var dirs = baseUrl.split('/') .slice(0, -1) .concat(url.split('/')) .filter(function(path) { return path !== '.'; }); return dirs.join('/'); } var iniPatterns = { 'section': /^\s*\[(.*)\]\s*$/, 'import': /^\s*@import\s+url\((.*)\)\s*$/i, 'entry': /[\r\n]+/ }; function parseINI(source, iniPath) { var entries = source.split(iniPatterns.entry); var locales = ['en-US']; var genericSection = true; var uris = []; var match; for (var i = 0; i < entries.length; i++) { var line = entries[i]; // we only care about en-US resources if (genericSection && iniPatterns['import'].test(line)) { match = iniPatterns['import'].exec(line); var uri = relativePath(iniPath, match[1]); uris.push(uri); continue; } // but we need the list of all locales in the ini, too if (iniPatterns.section.test(line)) { genericSection = false; match = iniPatterns.section.exec(line); locales.push(match[1]); } } return { locales: locales, resources: uris }; } /* jshint -W104 */ function translateFragment(element) { if (!element) { element = document.documentElement; document.documentElement.lang = this.language.code; document.documentElement.dir = this.language.direction; } translateElement.call(this, element); var nodes = getTranslatableChildren(element); for (var i = 0; i < nodes.length; i++ ) { translateElement.call(this, nodes[i]); } } function getTranslatableChildren(element) { return element ? element.querySelectorAll('*[data-l10n-id]') : []; } function localizeElement(element, id, args) { if (!element) { return; } if (!id) { element.removeAttribute('data-l10n-id'); element.removeAttribute('data-l10n-args'); setTextContent(element, ''); return; } element.setAttribute('data-l10n-id', id); if (args && typeof args === 'object') { element.setAttribute('data-l10n-args', JSON.stringify(args)); } else { element.removeAttribute('data-l10n-args'); } if (this.ctx.isReady) { translateElement.call(this, element); } } function getL10nAttributes(element) { if (!element) { return {}; } var l10nId = element.getAttribute('data-l10n-id'); var l10nArgs = element.getAttribute('data-l10n-args'); var args = l10nArgs ? JSON.parse(l10nArgs) : null; return {id: l10nId, args: args}; } function translateElement(element) { var l10n = getL10nAttributes(element); if (!l10n.id) { return; } var entity = this.ctx.getEntity(l10n.id, l10n.args); if (!entity) { return; } if (typeof entity === 'string') { setTextContent(element, entity); return true; } if (entity.value) { setTextContent(element, entity.value); } for (var key in entity.attributes) { if (entity.attributes.hasOwnProperty(key)) { var attr = entity.attributes[key]; var pos = key.indexOf('.'); if (pos !== -1) { element[key.substr(0, pos)][key.substr(pos + 1)] = attr; } else if (key === 'ariaLabel') { element.setAttribute('aria-label', attr); } else { element[key] = attr; } } } return true; } function setTextContent(element, text) { // standard case: no element children if (!element.firstElementChild) { element.textContent = text; return; } // this element has element children: replace the content of the first // (non-blank) child textNode and clear other child textNodes var found = false; var reNotBlank = /\S/; for (var child = element.firstChild; child; child = child.nextSibling) { if (child.nodeType === Node.TEXT_NODE && reNotBlank.test(child.nodeValue)) { if (found) { child.nodeValue = ''; } else { child.nodeValue = text; found = true; } } } // if no (non-empty) textNode is found, insert a textNode before the // element's first child. if (!found) { element.insertBefore(document.createTextNode(text), element.firstChild); } } })(this);