(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(type, url, callback, sync) { var xhr = new XMLHttpRequest(); var needParse; if (xhr.overrideMimeType) { xhr.overrideMimeType(type); } xhr.open('GET', url, !sync); if (type === 'application/json') { // Gecko 11.0+ forbids the use of the responseType attribute when // performing sync requests (NS_ERROR_DOM_INVALID_ACCESS_ERR). // We'll need to JSON.parse manually. if (sync) { needParse = true; } else { xhr.responseType = 'json'; } } xhr.addEventListener('load', function io_onload(e) { if (e.target.status === 200 || e.target.status === 0) { // Sinon.JS's FakeXHR doesn't have the response property var res = e.target.response || e.target.responseText; callback(null, needParse ? JSON.parse(res) : res); } 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) { if (e.name === 'NS_ERROR_FILE_NOT_FOUND') { // the app: protocol throws on 404, see https://bugzil.la/827243 callback(new L10nError('Not found: ' + url)); } else { throw e; } } }, load: function(url, callback, sync) { return io._load('text/plain', url, callback, sync); }, loadJSON: function(url, callback, sync) { return io._load('application/json', url, callback, sync); } }; 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 typeof n === typeof start && 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 MAX_PLACEABLES = 100; var PropertiesParser = { patterns: null, entryIds: null, init: function() { this.patterns = { comment: /^\s*#|^\s*$/, entity: /^([^=\s]+)\s*=\s*(.*)$/, multiline: /[^\\]\\$/, index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i, unicode: /\\u([0-9a-fA-F]{1,4})/g, entries: /[^\r\n]+/g, controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g, placeables: /\{\{\s*([^\s]*?)\s*\}\}/, }; }, parse: function(ctx, source) { if (!this.patterns) { this.init(); } var ast = []; this.entryIds = Object.create(null); var entries = source.match(this.patterns.entries); if (!entries) { return ast; } for (var i = 0; i < entries.length; i++) { var line = entries[i]; if (this.patterns.comment.test(line)) { continue; } while (this.patterns.multiline.test(line) && i < entries.length) { line = line.slice(0, -1) + entries[++i].trim(); } var entityMatch = line.match(this.patterns.entity); if (entityMatch) { try { this.parseEntity(entityMatch[1], entityMatch[2], ast); } catch (e) { if (ctx) { ctx._emitter.emit('parseerror', e); } else { throw e; } } } } return ast; }, parseEntity: function(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('.'); if (nameElements.length > 2) { throw new L10nError('Error in ID: "' + name + '".' + ' Nested attributes are not supported.'); } var attr; if (nameElements.length > 1) { name = nameElements[0]; attr = nameElements[1]; if (attr[0] === '$') { throw new L10nError('Attribute can\'t start with "$"', id); } } else { attr = null; } this.setEntityValue(name, attr, key, this.unescapeString(value), ast); }, setEntityValue: function(id, attr, key, rawValue, ast) { var pos, v; var value = rawValue.indexOf('{{') > -1 ? this.parseString(rawValue) : rawValue; if (attr) { pos = this.entryIds[id]; if (pos === undefined) { v = {$i: id}; if (key) { v[attr] = {}; v[attr][key] = value; } else { v[attr] = value; } ast.push(v); this.entryIds[id] = ast.length - 1; return; } if (key) { if (typeof(ast[pos][attr]) === 'string') { ast[pos][attr] = { $x: this.parseIndex(ast[pos][attr]), $v: {} }; } ast[pos][attr].$v[key] = value; return; } ast[pos][attr] = value; return; } // Hash value if (key) { pos = this.entryIds[id]; if (pos === undefined) { v = {}; v[key] = value; ast.push({$i: id, $v: v}); this.entryIds[id] = ast.length - 1; return; } if (typeof(ast[pos].$v) === 'string') { ast[pos].$x = this.parseIndex(ast[pos].$v); ast[pos].$v = {}; } ast[pos].$v[key] = value; return; } // simple value ast.push({$i: id, $v: value}); this.entryIds[id] = ast.length - 1; }, parseString: function(str) { var chunks = str.split(this.patterns.placeables); var complexStr = []; var len = chunks.length; var placeablesCount = (len - 1) / 2; if (placeablesCount >= MAX_PLACEABLES) { throw new L10nError('Too many placeables (' + placeablesCount + ', max allowed is ' + MAX_PLACEABLES + ')'); } for (var i = 0; i < chunks.length; i++) { if (chunks[i].length === 0) { continue; } if (i % 2 === 1) { complexStr.push({t: 'idOrVar', v: chunks[i]}); } else { complexStr.push(chunks[i]); } } return complexStr; }, unescapeString: function(str) { if (str.lastIndexOf('\\') !== -1) { str = str.replace(this.patterns.controlChars, '$1'); } return str.replace(this.patterns.unicode, function(match, token) { return unescape('%u' + '0000'.slice(token.length) + token); }); }, parseIndex: function(str) { var match = str.match(this.patterns.index); if (!match) { throw new L10nError('Malformed index'); } if (match[2]) { return [{t: 'idOrVar', v: match[1]}, match[2]]; } else { return [{t: 'idOrVar', v: match[1]}]; } } }; var KNOWN_MACROS = ['plural']; var MAX_PLACEABLE_LENGTH = 2500; var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g; // Matches characters outside of the Latin-1 character set var nonLatin1 = /[^\x01-\xFF]/; // Unicode bidi isolation characters var FSI = '\u2068'; var PDI = '\u2069'; function createEntry(node, env) { var keys = Object.keys(node); // the most common scenario: a simple string with no arguments if (typeof node.$v === 'string' && keys.length === 2) { return node.$v; } var attrs; /* jshint -W084 */ for (var i = 0, key; key = keys[i]; i++) { // skip $i (id), $v (value), $x (index) if (key[0] === '$') { continue; } if (!attrs) { attrs = Object.create(null); } attrs[key] = createAttribute(node[key], env, node.$i + '.' + key); } return { id: node.$i, value: node.$v !== undefined ? node.$v : null, index: node.$x || null, attrs: attrs || null, env: env, // the dirty guard prevents cyclic or recursive references dirty: false }; } function createAttribute(node, env, id) { if (typeof node === 'string') { return node; } return { id: id, value: node.$v || (node !== undefined ? node : null), index: node.$x || null, env: env, dirty: false }; } function format(args, entity) { if (typeof entity === 'string') { return [{}, entity]; } if (entity.dirty) { throw new L10nError('Cyclic reference detected: ' + entity.id); } entity.dirty = true; var rv; // if format 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 { rv = resolveValue({}, args, entity.env, entity.value, entity.index); } finally { entity.dirty = false; } return rv; } function resolveIdentifier(args, env, id) { if (KNOWN_MACROS.indexOf(id) > -1) { return [{}, env['__' + id]]; } if (args && args.hasOwnProperty(id)) { if (typeof args[id] === 'string' || (typeof args[id] === 'number' && !isNaN(args[id]))) { return [{}, args[id]]; } else { throw new L10nError('Arg must be a string or a number: ' + id); } } // XXX: special case for Node.js where still: // '__proto__' in Object.create(null) => true if (id in env && id !== '__proto__') { return format(args, env[id]); } throw new L10nError('Unknown reference: ' + id); } function subPlaceable(locals, args, env, id) { var res; try { res = resolveIdentifier(args, env, id); } catch (err) { return [{ error: err }, '{{ ' + id + ' }}']; } var value = res[1]; if (typeof value === 'number') { return res; } 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 + ')'); } if (locals.contextIsNonLatin1 || value.match(nonLatin1)) { // When dealing with non-Latin-1 text // we wrap substitutions in bidi isolate characters // to avoid bidi issues. res[1] = FSI + value + PDI; } return res; } return [{}, '{{ ' + id + ' }}']; } function interpolate(locals, args, env, arr) { return arr.reduce(function(prev, cur) { if (typeof cur === 'string') { return [prev[0], prev[1] + cur]; } else if (cur.t === 'idOrVar'){ var placeable = subPlaceable(locals, args, env, cur.v); return [prev[0], prev[1] + placeable[1]]; } }, [locals, '']); } function resolveSelector(args, env, expr, index) { var selectorName = index[0].v; var selector = resolveIdentifier(args, env, selectorName)[1]; if (typeof selector !== 'function') { // selector is a simple reference to an entity or args return selector; } var argValue = index[1] ? resolveIdentifier(args, env, index[1])[1] : undefined; if (selector === env.__plural) { // special cases for zero, one, two if they are defined on the hash if (argValue === 0 && 'zero' in expr) { return 'zero'; } if (argValue === 1 && 'one' in expr) { return 'one'; } if (argValue === 2 && 'two' in expr) { return 'two'; } } return selector(argValue); } function resolveValue(locals, args, env, expr, index) { if (!expr) { return [locals, expr]; } if (typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') { return [locals, expr]; } if (Array.isArray(expr)) { locals.contextIsNonLatin1 = expr.some(function($_) { return typeof($_) === 'string' && $_.match(nonLatin1); }); return interpolate(locals, args, env, expr); } // otherwise, it's a dict if (index) { // try to use the index in order to select the right dict member var selector = resolveSelector(args, env, expr, index); if (expr.hasOwnProperty(selector)) { return resolveValue(locals, args, env, expr[selector]); } } // if there was no index or no selector was found, try 'other' if ('other' in expr) { return resolveValue(locals, args, env, expr.other); } // XXX Specify entity id throw new L10nError('Unresolvable value'); } var Resolver = { createEntry: createEntry, format: format, rePlaceables: rePlaceables }; /* Utility functions */ // Recursively walk an AST node searching for content leaves function walkContent(node, fn) { if (typeof node === 'string') { return fn(node); } if (node.t === 'idOrVar') { return node; } var rv = Array.isArray(node) ? [] : {}; var keys = Object.keys(node); for (var i = 0, key; (key = keys[i]); i++) { // don't change identifier ($i) nor indices ($x) if (key === '$i' || key === '$x') { rv[key] = node[key]; } else { rv[key] = walkContent(node[key], fn); } } return rv; } /* Pseudolocalizations * * PSEUDO is a dict of strategies to be used to modify the English * context in order to create pseudolocalizations. These can be used by * developers to test the localizability of their code without having to * actually speak a foreign language. * * Currently, the following pseudolocales are supported: * * fr-x-psaccent - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ * * In Accented English all English letters are replaced by accented * Unicode counterparts which don't impair the readability of the content. * This allows developers to quickly test if any given string is being * correctly displayed in its 'translated' form. Additionally, simple * heuristics are used to make certain words longer to better simulate the * experience of international users. * * ar-x-psbidi - ɥsıʅƃuƎ ıpıԐ * * Bidi English is a fake RTL locale. All words are surrounded by * Unicode formatting marks forcing the RTL directionality of characters. * In addition, to make the reversed text easier to read, individual * letters are flipped. * * Note: The name above is hardcoded to be RTL in case code editors have * trouble with the RLO and PDF Unicode marks. In reality, it should be * surrounded by those marks as well. * * See https://bugzil.la/900182 for more information. * */ var reAlphas = /[a-zA-Z]/g; var reVowels = /[aeiouAEIOU]/g; // ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ + [\\]^_` + ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ var ACCENTED_MAP = '\u0226\u0181\u0187\u1E12\u1E16\u0191\u0193\u0126\u012A' + '\u0134\u0136\u013F\u1E3E\u0220\u01FE\u01A4\u024A\u0158' + '\u015E\u0166\u016C\u1E7C\u1E86\u1E8A\u1E8E\u1E90' + '[\\]^_`' + '\u0227\u0180\u0188\u1E13\u1E17\u0192\u0260\u0127\u012B' + '\u0135\u0137\u0140\u1E3F\u019E\u01FF\u01A5\u024B\u0159' + '\u015F\u0167\u016D\u1E7D\u1E87\u1E8B\u1E8F\u1E91'; // XXX Until https://bugzil.la/1007340 is fixed, ᗡℲ⅁⅂⅄ don't render correctly // on the devices. For now, use the following replacements: pɟפ˥ʎ // ∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅMXʎZ + [\\]ᵥ_, + ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz var FLIPPED_MAP = '\u2200\u0510\u2183p\u018E\u025F\u05E4HI\u017F' + '\u04FC\u02E5WNO\u0500\xD2\u1D1AS\u22A5\u2229\u0245' + '\uFF2DX\u028EZ' + '[\\]\u1D65_,' + '\u0250q\u0254p\u01DD\u025F\u0183\u0265\u0131\u027E' + '\u029E\u0285\u026Fuodb\u0279s\u0287n\u028C\u028Dx\u028Ez'; function makeLonger(val) { return val.replace(reVowels, function(match) { return match + match.toLowerCase(); }); } function replaceChars(map, val) { // Replace each Latin letter with a Unicode character from map return val.replace(reAlphas, function(match) { return map.charAt(match.charCodeAt(0) - 65); }); } var reWords = /[^\W0-9_]+/g; function makeRTL(val) { // Surround each word with Unicode formatting codes, RLO and PDF: // U+202E: RIGHT-TO-LEFT OVERRIDE (RLO) // U+202C: POP DIRECTIONAL FORMATTING (PDF) // See http://www.w3.org/International/questions/qa-bidi-controls return val.replace(reWords, function(match) { return '\u202e' + match + '\u202c'; }); } // strftime tokens (%a, %Eb), template {vars}, HTML entities (‪) // and HTML tags. var reExcluded = /(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/; function mapContent(fn, val) { if (!val) { return val; } var parts = val.split(reExcluded); var modified = parts.map(function(part) { if (reExcluded.test(part)) { return part; } return fn(part); }); return modified.join(''); } function Pseudo(id, name, charMap, modFn) { this.id = id; this.translate = mapContent.bind(null, function(val) { return replaceChars(charMap, modFn(val)); }); this.name = this.translate(name); } var PSEUDO = { 'fr-x-psaccent': new Pseudo('fr-x-psaccent', 'Runtime Accented', ACCENTED_MAP, makeLonger), 'ar-x-psbidi': new Pseudo('ar-x-psbidi', 'Runtime Bidi', FLIPPED_MAP, makeRTL) }; function Locale(id, ctx) { this.id = id; this.ctx = ctx; this.isReady = false; this.entries = Object.create(null); this.entries.__plural = getPluralRule(this.isPseudo() ? this.ctx.defaultLocale : id); } Locale.prototype.isPseudo = function() { return this.ctx.qps.indexOf(this.id) !== -1; }; var bindingsIO = { extra: function(id, ver, path, type, callback, errback) { if (type === 'properties') { type = 'text'; } navigator.mozApps.getLocalizationResource(id, ver, path, type). then(callback.bind(null, null), errback); }, app: function(id, ver, path, type, callback, errback, sync) { switch (type) { case 'properties': io.load(path, callback, sync); break; case 'json': io.loadJSON(path, callback, sync); break; } }, }; 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('fetcherror', 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 = PropertiesParser.parse(ctx, source); self.addAST(ast); } onL10nLoaded(err); } var idToFetch = this.isPseudo() ? ctx.defaultLocale : this.id; var appVersion = null; var source = 'app'; if (typeof(navigator) !== 'undefined') { source = navigator.mozL10n._config.localeSources[this.id] || 'app'; appVersion = navigator.mozL10n._config.appVersion; } for (var i = 0; i < ctx.resLinks.length; i++) { var resLink = decodeURI(ctx.resLinks[i]); var path = resLink.replace('{locale}', idToFetch); var type = path.substr(path.lastIndexOf('.') + 1); var cb; switch (type) { case 'json': cb = onJSONLoaded; break; case 'properties': cb = onPropLoaded; break; } bindingsIO[source](this.id, appVersion, path, type, cb, onL10nLoaded, sync); } }; function createPseudoEntry(node, entries) { return Resolver.createEntry( walkContent(node, PSEUDO[this.id].translate), entries); } Locale.prototype.addAST = function(ast) { /* jshint -W084 */ var createEntry = this.isPseudo() ? createPseudoEntry.bind(this) : Resolver.createEntry; for (var i = 0; i < ast.length; i++) { this.entries[ast[i].$i] = createEntry(ast[i], this.entries); } }; function Context(id) { this.id = id; this.isReady = false; this.isLoading = false; this.defaultLocale = 'en-US'; this.availableLocales = []; this.supportedLocales = []; this.qps = []; this.resLinks = []; this.locales = {}; this._emitter = new EventEmitter(); this._ready = new Promise(this.once.bind(this)); } // Getting translations function reportMissing(id, err) { this._emitter.emit('notfounderror', err); return id; } function getWithFallback(id) { /* jshint -W084 */ 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.entries[id]; if (entry === undefined) { cur++; reportMissing.call(this, id, new L10nError( '"' + id + '"' + ' not found in ' + loc + ' in ' + this.id, id, loc)); continue; } return entry; } throw new L10nError( '"' + id + '"' + ' missing from all supported locales in ' + this.id, id); } function formatTuple(args, entity) { try { return Resolver.format(args, entity); } catch (err) { this._emitter.emit('resolveerror', err); var locals = { error: err }; return [locals, entity.id]; } } function formatValue(args, entity) { if (typeof entity === 'string') { return entity; } // take the string value only return formatTuple.call(this, args, entity)[1]; } function formatEntity(args, entity) { var entityTuple = formatTuple.call(this, args, entity); var value = entityTuple[1]; var formatted = { value: value, attrs: null, }; if (entity.attrs) { formatted.attrs = Object.create(null); } for (var key in entity.attrs) { /* jshint -W089 */ var attrTuple = formatTuple.call(this, args, entity.attrs[key]); formatted.attrs[key] = attrTuple[1]; } return formatted; } function formatAsync(fn, id, args) { return this._ready.then( getWithFallback.bind(this, id)).then( fn.bind(this, args), reportMissing.bind(this, id)); } Context.prototype.formatValue = function(id, args) { return formatAsync.call(this, formatValue, id, args); }; Context.prototype.formatEntity = function(id, args) { return formatAsync.call(this, formatEntity, id, args); }; function legacyGet(fn, id, args) { if (!this.isReady) { throw new L10nError('Context not ready'); } var entry; try { entry = getWithFallback.call(this, id); } catch (err) { // Don't handle notfounderrors in individual locales in any special way if (err.loc) { throw err; } // For general notfounderrors, report them and return legacy fallback reportMissing.call(this, id, err); // XXX legacy compat; some Gaia code checks if returned value is falsy or // an empty string to know if a translation is available; this is bad and // will be fixed eventually in https://bugzil.la/1020138 return ''; } // If translation is broken use regular fallback-on-id approach return fn.call(this, args, entry); } Context.prototype.get = function(id, args) { return legacyGet.call(this, formatValue, id, args); }; Context.prototype.getEntity = function(id, args) { return legacyGet.call(this, formatEntity, id, args); }; Context.prototype.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) { var supportedLocale; // Find the first locale in the requested list that is supported. for (var i = 0; i < requested.length; i++) { var locale = requested[i]; if (available.indexOf(locale) !== -1) { supportedLocale = locale; break; } } if (!supportedLocale || supportedLocale === defaultLocale) { return [defaultLocale]; } return [supportedLocale, 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'); } Context.prototype.registerLocales = function(defLocale, available) { if (defLocale) { this.defaultLocale = defLocale; } /* jshint boss:true */ this.availableLocales = [this.defaultLocale]; this.qps = Object.keys(PSEUDO); if (available) { for (var i = 0, loc; loc = available[i]; i++) { if (this.availableLocales.indexOf(loc) === -1) { this.availableLocales.push(loc); var pos = this.qps.indexOf(loc); if (pos !== -1) { // remove from this context's runtime pseudolocales this.qps.splice(pos, 1); } } } } }; Context.prototype.requestLocales = function requestLocales() { if (this.isLoading && !this.isReady) { throw new L10nError('Context not ready'); } this.isLoading = true; var requested = Array.prototype.slice.call(arguments); if (requested.length === 0) { throw new L10nError('No locales requested'); } var supported = negotiate( this.availableLocales.concat(this.qps), requested, this.defaultLocale); // freeze only if the first language in the fallback chain is new if (this.supportedLocales[0] !== supported[0]) { freeze.call(this, supported); } }; // Events Context.prototype.addEventListener = function(type, listener) { this._emitter.addEventListener(type, listener); }; Context.prototype.removeEventListener = function(type, listener) { this._emitter.removeEventListener(type, listener); }; Context.prototype.ready = function(callback) { if (this.isReady) { setTimeout(callback); } this.addEventListener('ready', callback); }; Context.prototype.once = function(callback) { /* jshint -W068 */ if (this.isReady) { setTimeout(callback); return; } var callAndRemove = (function() { this.removeEventListener('ready', callAndRemove); callback(); }).bind(this); this.addEventListener('ready', callAndRemove); }; var allowed = { elements: [ 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u', 'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr' ], attributes: { global: [ 'title', 'aria-label', 'aria-valuetext', 'aria-moz-hint' ], a: [ 'download' ], area: [ 'download', 'alt' ], // value is special-cased in isAttrAllowed input: [ 'alt', 'placeholder' ], menuitem: [ 'label' ], menu: [ 'label' ], optgroup: [ 'label' ], option: [ 'label' ], track: [ 'label' ], img: [ 'alt' ], textarea: [ 'placeholder' ], th: [ 'abbr'] } }; var rtlList = ['ar', 'he', 'fa', 'ps', 'ar-x-psbidi', 'ur']; var nodeObserver = null; var pendingElements = null; var moConfig = { attributes: true, characterData: false, childList: true, subtree: true, attributeFilter: ['data-l10n-id', 'data-l10n-args'] }; // Public API navigator.mozL10n = { ctx: null, get: function get(id, ctxdata) { return navigator.mozL10n.ctx.get(id, ctxdata); }, formatValue: function(id, ctxdata) { return navigator.mozL10n.ctx.formatValue(id, ctxdata); }, formatEntity: function(id, ctxdata) { return navigator.mozL10n.ctx.formatEntity(id, ctxdata); }, translateFragment: function (fragment) { return translateFragment.call(navigator.mozL10n, fragment); }, setAttributes: setL10nAttributes, getAttributes: getL10nAttributes, 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]); } }, qps: PSEUDO, _config: { appVersion: null, localeSources: Object.create(null), isPretranslated: false, }, _getInternalAPI: function() { return { Error: L10nError, Context: Context, Locale: Locale, Resolver: Resolver, getPluralRule: getPluralRule, rePlaceables: rePlaceables, translateDocument: translateDocument, onMetaInjected: onMetaInjected, PropertiesParser: PropertiesParser, walkContent: walkContent, buildLocaleList: buildLocaleList }; } }; function getDirection(lang) { return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr'; } var readyStates = { loading: 0, interactive: 1, complete: 2 }; function whenInteractive(callback) { if (readyStates[document.readyState] >= readyStates.interactive) { callback(); return; } document.addEventListener('readystatechange', function l10n_onrsc() { if (readyStates[document.readyState] >= readyStates.interactive) { document.removeEventListener('readystatechange', l10n_onrsc); callback(); } }); } function initObserver() { nodeObserver = new MutationObserver(onMutations.bind(navigator.mozL10n)); nodeObserver.observe(document, moConfig); } function init(pretranslate) { if (!pretranslate) { // initialize MO early to collect nodes injected between now and when // resources are loaded because we're not going to translate the whole // document once l10n resources are ready initObserver(); } initResources.call(navigator.mozL10n); } function initResources() { /* jshint boss:true */ var meta = {}; var nodes = document.head .querySelectorAll('link[rel="localization"],' + 'meta[name="availableLanguages"],' + 'meta[name="defaultLanguage"],' + 'meta[name="appVersion"],' + 'script[type="application/l10n"]'); for (var i = 0, node; node = nodes[i]; i++) { var type = node.getAttribute('rel') || node.nodeName.toLowerCase(); switch (type) { case 'localization': this.ctx.resLinks.push(node.getAttribute('href')); break; case 'meta': onMetaInjected.call(this, node, meta); break; case 'script': onScriptInjected.call(this, node); break; } } var additionalLanguagesPromise; if (navigator.mozApps && navigator.mozApps.getAdditionalLanguages) { // if the environment supports langpacks, register extra languages… additionalLanguagesPromise = navigator.mozApps.getAdditionalLanguages().catch(function(e) { console.error('Error while loading getAdditionalLanguages', e); }); // …and listen to langpacks being added and removed document.addEventListener('additionallanguageschange', function(evt) { registerLocales.call(this, meta, evt.detail); this.ctx.requestLocales.apply( this.ctx, navigator.languages || [navigator.language]); }.bind(this)); } else { additionalLanguagesPromise = Promise.resolve(); } additionalLanguagesPromise.then(function(extraLangs) { registerLocales.call(this, meta, extraLangs); initLocale.call(this); }.bind(this)); } function registerLocales(meta, extraLangs) { var locales = buildLocaleList.call(this, meta, extraLangs); navigator.mozL10n._config.localeSources = locales[1]; this.ctx.registerLocales(locales[0], Object.keys(locales[1])); } function getMatchingLangpack(appVersion, langpacks) { for (var i = 0, langpack; (langpack = langpacks[i]); i++) { if (langpack.target === appVersion) { return langpack; } } return null; } function buildLocaleList(meta, extraLangs) { var loc, lp; var localeSources = Object.create(null); var defaultLocale = meta.defaultLanguage || this.ctx.defaultLocale; if (meta.availableLanguages) { for (loc in meta.availableLanguages) { localeSources[loc] = 'app'; } } if (extraLangs) { for (loc in extraLangs) { lp = getMatchingLangpack(this._config.appVersion, extraLangs[loc]); if (!lp) { continue; } if (!(loc in localeSources) || !meta.availableLanguages[loc] || parseInt(lp.revision) > meta.availableLanguages[loc]) { localeSources[loc] = 'extra'; } } } if (!(defaultLocale in localeSources)) { localeSources[defaultLocale] = 'app'; } return [defaultLocale, localeSources]; } function splitAvailableLanguagesString(str) { var langs = {}; str.split(',').forEach(function(lang) { // code:revision lang = lang.trim().split(':'); // if revision is missing, use NaN langs[lang[0]] = parseInt(lang[1]); }); return langs; } function onMetaInjected(node, meta) { switch (node.getAttribute('name')) { case 'availableLanguages': meta.availableLanguages = splitAvailableLanguagesString(node.getAttribute('content')); break; case 'defaultLanguage': meta.defaultLanguage = node.getAttribute('content'); break; case 'appVersion': navigator.mozL10n._config.appVersion = node.getAttribute('content'); break; } } function onScriptInjected(node) { var lang = node.getAttribute('lang'); var locale = this.ctx.getLocale(lang); locale.addAST(JSON.parse(node.textContent)); } function initLocale() { this.ctx.requestLocales.apply( this.ctx, navigator.languages || [navigator.language]); window.addEventListener('languagechange', function l10n_langchange() { this.ctx.requestLocales.apply( this.ctx, navigator.languages || [navigator.language]); }.bind(this)); } function localizeMutations(mutations) { var mutation; var targets = new Set(); for (var i = 0; i < mutations.length; i++) { mutation = mutations[i]; if (mutation.type === 'childList') { var addedNode; for (var j = 0; j < mutation.addedNodes.length; j++) { addedNode = mutation.addedNodes[j]; if (addedNode.nodeType !== Node.ELEMENT_NODE) { continue; } targets.add(addedNode); } } if (mutation.type === 'attributes') { targets.add(mutation.target); } } targets.forEach(function(target) { if (target.childElementCount) { translateFragment.call(this, target); } else if (target.hasAttribute('data-l10n-id')) { translateElement.call(this, target); } }, this); } function onMutations(mutations, self) { self.disconnect(); localizeMutations.call(this, mutations); self.observe(document, moConfig); } function onReady() { if (!navigator.mozL10n._config.isPretranslated) { translateDocument.call(this); } navigator.mozL10n._config.isPretranslated = false; if (pendingElements) { /* jshint boss:true */ for (var i = 0, element; element = pendingElements[i]; i++) { translateElement.call(this, element); } pendingElements = null; } if (!nodeObserver) { initObserver(); } fireLocalizedEvent.call(this); } function fireLocalizedEvent() { var event = new CustomEvent('localized', { 'bubbles': false, 'cancelable': false, 'detail': { 'language': this.ctx.supportedLocales[0] } }); window.dispatchEvent(event); } // match the opening angle bracket (<) in HTML tags, and HTML entities like // &, &, &. var reOverlay = /<|&#?\w+;/; var reHtml = /[&<>]/g; var htmlEntities = { '&': '&', '<': '<', '>': '>', }; function translateDocument() { document.documentElement.lang = this.language.code; document.documentElement.dir = this.language.direction; translateFragment.call(this, document.documentElement); } function translateFragment(element) { if (typeof element.hasAttribute === 'function' && element.hasAttribute('data-l10n-id')) { translateElement.call(this, element); } var nodes = getTranslatableChildren(element); for (var i = 0; i < nodes.length; i++ ) { translateElement.call(this, nodes[i]); } } function setL10nAttributes(element, id, args) { element.setAttribute('data-l10n-id', id); if (args) { element.setAttribute('data-l10n-args', JSON.stringify(args)); } } function getL10nAttributes(element) { return { id: element.getAttribute('data-l10n-id'), args: JSON.parse(element.getAttribute('data-l10n-args')) }; } function getTranslatableChildren(element) { return element ? element.querySelectorAll('*[data-l10n-id]') : []; } function camelCaseToDashed(string) { // XXX workaround for https://bugzil.la/1141934 if (string === 'ariaValueText') { return 'aria-valuetext'; } return string .replace(/[A-Z]/g, function (match) { return '-' + match.toLowerCase(); }) .replace(/^-/, ''); } function escapeL10nArgs(match) { return htmlEntities[match]; } function translateElement(element) { if (!this.ctx.isReady) { if (!pendingElements) { pendingElements = []; } pendingElements.push(element); return; } var l10nId = element.getAttribute('data-l10n-id'); if (!l10nId) { return false; } var l10nArgs = element.getAttribute('data-l10n-args'); var entity = this.ctx.getEntity( l10nId, l10nArgs ? JSON.parse(l10nArgs.replace(reHtml, escapeL10nArgs)) : undefined ); var value = entity.value; if (typeof value === 'string') { if (!reOverlay.test(value)) { element.textContent = value; } else { // start with an inert template element and move its children into // `element` but such that `element`'s own children are not replaced var translation = element.ownerDocument.createElement('template'); translation.innerHTML = value; // overlay the node with the DocumentFragment overlayElement(element, translation.content); } } for (var key in entity.attrs) { var attrName = camelCaseToDashed(key); if (isAttrAllowed({ name: attrName }, element)) { element.setAttribute(attrName, entity.attrs[key]); } } } // The goal of overlayElement is to move the children of `translationElement` // into `sourceElement` such that `sourceElement`'s own children are not // replaced, but onle have their text nodes and their attributes modified. // // We want to make it possible for localizers to apply text-level semantics to // the translations and make use of HTML entities. At the same time, we // don't trust translations so we need to filter unsafe elements and // attribtues out and we don't want to break the Web by replacing elements to // which third-party code might have created references (e.g. two-way // bindings in MVC frameworks). function overlayElement(sourceElement, translationElement) { var result = translationElement.ownerDocument.createDocumentFragment(); var k, attr; // take one node from translationElement at a time and check it against // the allowed list or try to match it with a corresponding element // in the source var childElement; while ((childElement = translationElement.childNodes[0])) { translationElement.removeChild(childElement); if (childElement.nodeType === Node.TEXT_NODE) { result.appendChild(childElement); continue; } var index = getIndexOfType(childElement); var sourceChild = getNthElementOfType(sourceElement, childElement, index); if (sourceChild) { // there is a corresponding element in the source, let's use it overlayElement(sourceChild, childElement); result.appendChild(sourceChild); continue; } if (isElementAllowed(childElement)) { var sanitizedChild = childElement.ownerDocument.createElement( childElement.nodeName); overlayElement(sanitizedChild, childElement); result.appendChild(sanitizedChild); continue; } // otherwise just take this child's textContent result.appendChild( document.createTextNode(childElement.textContent)); } // clear `sourceElement` and append `result` which by this time contains // `sourceElement`'s original children, overlayed with translation sourceElement.textContent = ''; sourceElement.appendChild(result); // if we're overlaying a nested element, translate the allowed // attributes; top-level attributes are handled in `translateElement` // XXX attributes previously set here for another language should be // cleared if a new language doesn't use them; https://bugzil.la/922577 if (translationElement.attributes) { for (k = 0, attr; (attr = translationElement.attributes[k]); k++) { if (isAttrAllowed(attr, sourceElement)) { sourceElement.setAttribute(attr.name, attr.value); } } } } // XXX the allowed list should be amendable; https://bugzil.la/922573 function isElementAllowed(element) { return allowed.elements.indexOf(element.tagName.toLowerCase()) !== -1; } function isAttrAllowed(attr, element) { var attrName = attr.name.toLowerCase(); var tagName = element.tagName.toLowerCase(); // is it a globally safe attribute? if (allowed.attributes.global.indexOf(attrName) !== -1) { return true; } // are there no allowed attributes for this element? if (!allowed.attributes[tagName]) { return false; } // is it allowed on this element? // XXX the allowed list should be amendable; https://bugzil.la/922573 if (allowed.attributes[tagName].indexOf(attrName) !== -1) { return true; } // special case for value on inputs with type button, reset, submit if (tagName === 'input' && attrName === 'value') { var type = element.type.toLowerCase(); if (type === 'submit' || type === 'button' || type === 'reset') { return true; } } return false; } // Get n-th immediate child of context that is of the same type as element. // XXX Use querySelector(':scope > ELEMENT:nth-of-type(index)'), when: // 1) :scope is widely supported in more browsers and 2) it works with // DocumentFragments. function getNthElementOfType(context, element, index) { /* jshint boss:true */ var nthOfType = 0; for (var i = 0, child; child = context.children[i]; i++) { if (child.nodeType === Node.ELEMENT_NODE && child.tagName === element.tagName) { if (nthOfType === index) { return child; } nthOfType++; } } return null; } // Get the index of the element among siblings of the same type. function getIndexOfType(element) { var index = 0; var child; while ((child = element.previousElementSibling)) { if (child.tagName === element.tagName) { index++; } } return index; } var DEBUG = false; navigator.mozL10n.ctx = new Context(window.document ? document.URL : null); navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n)); navigator.mozL10n.ctx.addEventListener('notfounderror', function reportMissingEntity(e) { if (DEBUG || e.loc === 'en-US') { console.warn(e.toString()); } }); if (DEBUG) { navigator.mozL10n.ctx.addEventListener('fetcherror', console.error.bind(console)); navigator.mozL10n.ctx.addEventListener('parseerror', console.error.bind(console)); navigator.mozL10n.ctx.addEventListener('resolveerror', console.error.bind(console)); } if (window.document) { navigator.mozL10n._config.isPretranslated = document.documentElement.lang === navigator.language; var forcePretranslate = !navigator.mozL10n._config.isPretranslated; whenInteractive(init.bind(navigator.mozL10n, forcePretranslate)); } document.l10n = { setAttributes: navigator.mozL10n.setAttributes, getAttributes: navigator.mozL10n.getAttributes, formatValue: function(id, args) { return navigator.mozL10n.formatValue(id, args); }, translateFragment: function (frag) { return Promise.resolve(navigator.mozL10n.translateFragment(frag)); }, ready: new Promise(function(resolve) { navigator.mozL10n.once(resolve); }), formatValues: function() { var keys = arguments; var resp = keys.map(function(key) { if (Array.isArray(key)) { return navigator.mozL10n.formatValue(key[0], key[1]); } return navigator.mozL10n.formatValue(key); }); return Promise.all(resp); }, requestLanguages: function(langs) { // XXX real l20n returns a promise navigator.mozL10n.ctx.requestLocales.apply( navigator.mozL10n.ctx, langs); }, pseudo: { 'fr-x-psaccent': { getName: function() { return Promise.resolve(navigator.mozL10n.qps['fr-x-psaccent'].name); }, processString: function(s) { return Promise.resolve( navigator.mozL10n.qps['fr-x-psaccent'].translate(s)); } }, 'ar-x-psbidi': { getName: function() { return Promise.resolve(navigator.mozL10n.qps['ar-x-psbidi'].name); }, processString: function(s) { return Promise.resolve( navigator.mozL10n.qps['ar-x-psbidi'].translate(s)); } } }, }; navigator.mozL10n.ready(function() { document.documentElement.setAttribute( 'langs', navigator.mozL10n.ctx.supportedLocales.join(' ')); }); navigator.mozL10n.once(function() { window.addEventListener('localized', function() { document.dispatchEvent(new CustomEvent('DOMRetranslated', { bubbles: false, cancelable: false })); }); }); })(this);