Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2205 lines
57 KiB
JavaScript
Executable file
2205 lines
57 KiB
JavaScript
Executable file
(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);
|