VulpesOS/gaia/shared/js/l10n.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

2205 lines
57 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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 (&#x202a;)
// 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
// &amp;, &#0038;, &#x0026;.
var reOverlay = /<|&#?\w+;/;
var reHtml = /[&<>]/g;
var htmlEntities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
};
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);