1571 lines
36 KiB
JavaScript
1571 lines
36 KiB
JavaScript
// This is the Gaia version of l20n: https://github.com/l20n/l20n.js
|
|
// l20n is Apache 2.0 licensed: https://github.com/l20n/l20n.js/blob/master/LICENSE
|
|
// You can find the latest build for Gaia here: https://github.com/mozilla-b2g/gaia/blob/master/shared/js/l10n.js
|
|
(function(window, undefined) {
|
|
'use strict';
|
|
|
|
/* jshint validthis:true */
|
|
function L10nError(message, id, loc) {
|
|
this.name = 'L10nError';
|
|
this.message = message;
|
|
this.id = id;
|
|
this.loc = loc;
|
|
}
|
|
L10nError.prototype = Object.create(Error.prototype);
|
|
L10nError.prototype.constructor = L10nError;
|
|
|
|
|
|
/* jshint browser:true */
|
|
|
|
var io = {
|
|
load: function load(url, callback, sync) {
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
if (xhr.overrideMimeType) {
|
|
xhr.overrideMimeType('text/plain');
|
|
}
|
|
|
|
xhr.open('GET', url, !sync);
|
|
|
|
xhr.addEventListener('load', function io_load(e) {
|
|
if (e.target.status === 200 || e.target.status === 0) {
|
|
callback(null, e.target.responseText);
|
|
} else {
|
|
callback(new L10nError('Not found: ' + url));
|
|
}
|
|
});
|
|
xhr.addEventListener('error', callback);
|
|
xhr.addEventListener('timeout', callback);
|
|
|
|
// the app: protocol throws on 404, see https://bugzil.la/827243
|
|
try {
|
|
xhr.send(null);
|
|
} catch (e) {
|
|
callback(new L10nError('Not found: ' + url));
|
|
}
|
|
},
|
|
|
|
loadJSON: function loadJSON(url, callback) {
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
if (xhr.overrideMimeType) {
|
|
xhr.overrideMimeType('application/json');
|
|
}
|
|
|
|
xhr.open('GET', url);
|
|
|
|
xhr.responseType = 'json';
|
|
xhr.addEventListener('load', function io_loadjson(e) {
|
|
if (e.target.status === 200 || e.target.status === 0) {
|
|
callback(null, e.target.response);
|
|
} else {
|
|
callback(new L10nError('Not found: ' + url));
|
|
}
|
|
});
|
|
xhr.addEventListener('error', callback);
|
|
xhr.addEventListener('timeout', callback);
|
|
|
|
// the app: protocol throws on 404, see https://bugzil.la/827243
|
|
try {
|
|
xhr.send(null);
|
|
} catch (e) {
|
|
callback(new L10nError('Not found: ' + url));
|
|
}
|
|
}
|
|
};
|
|
|
|
function EventEmitter() {}
|
|
|
|
EventEmitter.prototype.emit = function ee_emit() {
|
|
if (!this._listeners) {
|
|
return;
|
|
}
|
|
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var type = args.shift();
|
|
if (!this._listeners[type]) {
|
|
return;
|
|
}
|
|
|
|
var typeListeners = this._listeners[type].slice();
|
|
for (var i = 0; i < typeListeners.length; i++) {
|
|
typeListeners[i].apply(this, args);
|
|
}
|
|
};
|
|
|
|
EventEmitter.prototype.addEventListener = function ee_add(type, listener) {
|
|
if (!this._listeners) {
|
|
this._listeners = {};
|
|
}
|
|
if (!(type in this._listeners)) {
|
|
this._listeners[type] = [];
|
|
}
|
|
this._listeners[type].push(listener);
|
|
};
|
|
|
|
EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) {
|
|
if (!this._listeners) {
|
|
return;
|
|
}
|
|
|
|
var typeListeners = this._listeners[type];
|
|
var pos = typeListeners.indexOf(listener);
|
|
if (pos === -1) {
|
|
return;
|
|
}
|
|
|
|
typeListeners.splice(pos, 1);
|
|
};
|
|
|
|
|
|
function getPluralRule(lang) {
|
|
var locales2rules = {
|
|
'af': 3,
|
|
'ak': 4,
|
|
'am': 4,
|
|
'ar': 1,
|
|
'asa': 3,
|
|
'az': 0,
|
|
'be': 11,
|
|
'bem': 3,
|
|
'bez': 3,
|
|
'bg': 3,
|
|
'bh': 4,
|
|
'bm': 0,
|
|
'bn': 3,
|
|
'bo': 0,
|
|
'br': 20,
|
|
'brx': 3,
|
|
'bs': 11,
|
|
'ca': 3,
|
|
'cgg': 3,
|
|
'chr': 3,
|
|
'cs': 12,
|
|
'cy': 17,
|
|
'da': 3,
|
|
'de': 3,
|
|
'dv': 3,
|
|
'dz': 0,
|
|
'ee': 3,
|
|
'el': 3,
|
|
'en': 3,
|
|
'eo': 3,
|
|
'es': 3,
|
|
'et': 3,
|
|
'eu': 3,
|
|
'fa': 0,
|
|
'ff': 5,
|
|
'fi': 3,
|
|
'fil': 4,
|
|
'fo': 3,
|
|
'fr': 5,
|
|
'fur': 3,
|
|
'fy': 3,
|
|
'ga': 8,
|
|
'gd': 24,
|
|
'gl': 3,
|
|
'gsw': 3,
|
|
'gu': 3,
|
|
'guw': 4,
|
|
'gv': 23,
|
|
'ha': 3,
|
|
'haw': 3,
|
|
'he': 2,
|
|
'hi': 4,
|
|
'hr': 11,
|
|
'hu': 0,
|
|
'id': 0,
|
|
'ig': 0,
|
|
'ii': 0,
|
|
'is': 3,
|
|
'it': 3,
|
|
'iu': 7,
|
|
'ja': 0,
|
|
'jmc': 3,
|
|
'jv': 0,
|
|
'ka': 0,
|
|
'kab': 5,
|
|
'kaj': 3,
|
|
'kcg': 3,
|
|
'kde': 0,
|
|
'kea': 0,
|
|
'kk': 3,
|
|
'kl': 3,
|
|
'km': 0,
|
|
'kn': 0,
|
|
'ko': 0,
|
|
'ksb': 3,
|
|
'ksh': 21,
|
|
'ku': 3,
|
|
'kw': 7,
|
|
'lag': 18,
|
|
'lb': 3,
|
|
'lg': 3,
|
|
'ln': 4,
|
|
'lo': 0,
|
|
'lt': 10,
|
|
'lv': 6,
|
|
'mas': 3,
|
|
'mg': 4,
|
|
'mk': 16,
|
|
'ml': 3,
|
|
'mn': 3,
|
|
'mo': 9,
|
|
'mr': 3,
|
|
'ms': 0,
|
|
'mt': 15,
|
|
'my': 0,
|
|
'nah': 3,
|
|
'naq': 7,
|
|
'nb': 3,
|
|
'nd': 3,
|
|
'ne': 3,
|
|
'nl': 3,
|
|
'nn': 3,
|
|
'no': 3,
|
|
'nr': 3,
|
|
'nso': 4,
|
|
'ny': 3,
|
|
'nyn': 3,
|
|
'om': 3,
|
|
'or': 3,
|
|
'pa': 3,
|
|
'pap': 3,
|
|
'pl': 13,
|
|
'ps': 3,
|
|
'pt': 3,
|
|
'rm': 3,
|
|
'ro': 9,
|
|
'rof': 3,
|
|
'ru': 11,
|
|
'rwk': 3,
|
|
'sah': 0,
|
|
'saq': 3,
|
|
'se': 7,
|
|
'seh': 3,
|
|
'ses': 0,
|
|
'sg': 0,
|
|
'sh': 11,
|
|
'shi': 19,
|
|
'sk': 12,
|
|
'sl': 14,
|
|
'sma': 7,
|
|
'smi': 7,
|
|
'smj': 7,
|
|
'smn': 7,
|
|
'sms': 7,
|
|
'sn': 3,
|
|
'so': 3,
|
|
'sq': 3,
|
|
'sr': 11,
|
|
'ss': 3,
|
|
'ssy': 3,
|
|
'st': 3,
|
|
'sv': 3,
|
|
'sw': 3,
|
|
'syr': 3,
|
|
'ta': 3,
|
|
'te': 3,
|
|
'teo': 3,
|
|
'th': 0,
|
|
'ti': 4,
|
|
'tig': 3,
|
|
'tk': 3,
|
|
'tl': 4,
|
|
'tn': 3,
|
|
'to': 0,
|
|
'tr': 0,
|
|
'ts': 3,
|
|
'tzm': 22,
|
|
'uk': 11,
|
|
'ur': 3,
|
|
've': 3,
|
|
'vi': 0,
|
|
'vun': 3,
|
|
'wa': 4,
|
|
'wae': 3,
|
|
'wo': 0,
|
|
'xh': 3,
|
|
'xog': 3,
|
|
'yo': 0,
|
|
'zh': 0,
|
|
'zu': 3
|
|
};
|
|
|
|
// utility functions for plural rules methods
|
|
function isIn(n, list) {
|
|
return list.indexOf(n) !== -1;
|
|
}
|
|
function isBetween(n, start, end) {
|
|
return start <= n && n <= end;
|
|
}
|
|
|
|
// list of all plural rules methods:
|
|
// map an integer to the plural form name to use
|
|
var pluralRules = {
|
|
'0': function() {
|
|
return 'other';
|
|
},
|
|
'1': function(n) {
|
|
if ((isBetween((n % 100), 3, 10))) {
|
|
return 'few';
|
|
}
|
|
if (n === 0) {
|
|
return 'zero';
|
|
}
|
|
if ((isBetween((n % 100), 11, 99))) {
|
|
return 'many';
|
|
}
|
|
if (n === 2) {
|
|
return 'two';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'2': function(n) {
|
|
if (n !== 0 && (n % 10) === 0) {
|
|
return 'many';
|
|
}
|
|
if (n === 2) {
|
|
return 'two';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'3': function(n) {
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'4': function(n) {
|
|
if ((isBetween(n, 0, 1))) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'5': function(n) {
|
|
if ((isBetween(n, 0, 2)) && n !== 2) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'6': function(n) {
|
|
if (n === 0) {
|
|
return 'zero';
|
|
}
|
|
if ((n % 10) === 1 && (n % 100) !== 11) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'7': function(n) {
|
|
if (n === 2) {
|
|
return 'two';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'8': function(n) {
|
|
if ((isBetween(n, 3, 6))) {
|
|
return 'few';
|
|
}
|
|
if ((isBetween(n, 7, 10))) {
|
|
return 'many';
|
|
}
|
|
if (n === 2) {
|
|
return 'two';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'9': function(n) {
|
|
if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) {
|
|
return 'few';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'10': function(n) {
|
|
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) {
|
|
return 'few';
|
|
}
|
|
if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'11': function(n) {
|
|
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
|
|
return 'few';
|
|
}
|
|
if ((n % 10) === 0 ||
|
|
(isBetween((n % 10), 5, 9)) ||
|
|
(isBetween((n % 100), 11, 14))) {
|
|
return 'many';
|
|
}
|
|
if ((n % 10) === 1 && (n % 100) !== 11) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'12': function(n) {
|
|
if ((isBetween(n, 2, 4))) {
|
|
return 'few';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'13': function(n) {
|
|
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
|
|
return 'few';
|
|
}
|
|
if (n !== 1 && (isBetween((n % 10), 0, 1)) ||
|
|
(isBetween((n % 10), 5, 9)) ||
|
|
(isBetween((n % 100), 12, 14))) {
|
|
return 'many';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'14': function(n) {
|
|
if ((isBetween((n % 100), 3, 4))) {
|
|
return 'few';
|
|
}
|
|
if ((n % 100) === 2) {
|
|
return 'two';
|
|
}
|
|
if ((n % 100) === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'15': function(n) {
|
|
if (n === 0 || (isBetween((n % 100), 2, 10))) {
|
|
return 'few';
|
|
}
|
|
if ((isBetween((n % 100), 11, 19))) {
|
|
return 'many';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'16': function(n) {
|
|
if ((n % 10) === 1 && n !== 11) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'17': function(n) {
|
|
if (n === 3) {
|
|
return 'few';
|
|
}
|
|
if (n === 0) {
|
|
return 'zero';
|
|
}
|
|
if (n === 6) {
|
|
return 'many';
|
|
}
|
|
if (n === 2) {
|
|
return 'two';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'18': function(n) {
|
|
if (n === 0) {
|
|
return 'zero';
|
|
}
|
|
if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'19': function(n) {
|
|
if ((isBetween(n, 2, 10))) {
|
|
return 'few';
|
|
}
|
|
if ((isBetween(n, 0, 1))) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'20': function(n) {
|
|
if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !(
|
|
isBetween((n % 100), 10, 19) ||
|
|
isBetween((n % 100), 70, 79) ||
|
|
isBetween((n % 100), 90, 99)
|
|
)) {
|
|
return 'few';
|
|
}
|
|
if ((n % 1000000) === 0 && n !== 0) {
|
|
return 'many';
|
|
}
|
|
if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) {
|
|
return 'two';
|
|
}
|
|
if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'21': function(n) {
|
|
if (n === 0) {
|
|
return 'zero';
|
|
}
|
|
if (n === 1) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'22': function(n) {
|
|
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'23': function(n) {
|
|
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
},
|
|
'24': function(n) {
|
|
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) {
|
|
return 'few';
|
|
}
|
|
if (isIn(n, [2, 12])) {
|
|
return 'two';
|
|
}
|
|
if (isIn(n, [1, 11])) {
|
|
return 'one';
|
|
}
|
|
return 'other';
|
|
}
|
|
};
|
|
|
|
// return a function that gives the plural form name for a given integer
|
|
var index = locales2rules[lang.replace(/-.*$/, '')];
|
|
if (!(index in pluralRules)) {
|
|
return function() { return 'other'; };
|
|
}
|
|
return pluralRules[index];
|
|
}
|
|
|
|
|
|
|
|
|
|
var nestedProps = ['style', 'dataset'];
|
|
|
|
var parsePatterns;
|
|
|
|
function parse(ctx, source) {
|
|
var ast = {};
|
|
|
|
if (!parsePatterns) {
|
|
parsePatterns = {
|
|
comment: /^\s*#|^\s*$/,
|
|
entity: /^([^=\s]+)\s*=\s*(.+)$/,
|
|
multiline: /[^\\]\\$/,
|
|
macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i,
|
|
unicode: /\\u([0-9a-fA-F]{1,4})/g,
|
|
entries: /[\r\n]+/,
|
|
controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g
|
|
};
|
|
}
|
|
|
|
var entries = source.split(parsePatterns.entries);
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var line = entries[i];
|
|
|
|
if (parsePatterns.comment.test(line)) {
|
|
continue;
|
|
}
|
|
|
|
while (parsePatterns.multiline.test(line) && i < entries.length) {
|
|
line = line.slice(0, -1) + entries[++i].trim();
|
|
}
|
|
|
|
var entityMatch = line.match(parsePatterns.entity);
|
|
if (entityMatch) {
|
|
try {
|
|
parseEntity(entityMatch[1], entityMatch[2], ast);
|
|
} catch (e) {
|
|
if (ctx) {
|
|
ctx._emitter.emit('error', e);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
function setEntityValue(id, attr, key, value, ast) {
|
|
var obj = ast;
|
|
var prop = id;
|
|
|
|
if (attr) {
|
|
if (!(id in obj)) {
|
|
obj[id] = {};
|
|
}
|
|
if (typeof(obj[id]) === 'string') {
|
|
obj[id] = {'_': obj[id]};
|
|
}
|
|
obj = obj[id];
|
|
prop = attr;
|
|
}
|
|
|
|
if (!key) {
|
|
obj[prop] = value;
|
|
return;
|
|
}
|
|
|
|
if (!(prop in obj)) {
|
|
obj[prop] = {'_': {}};
|
|
} else if (typeof(obj[prop]) === 'string') {
|
|
obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}};
|
|
}
|
|
obj[prop]._[key] = value;
|
|
}
|
|
|
|
function parseEntity(id, value, ast) {
|
|
var name, key;
|
|
|
|
var pos = id.indexOf('[');
|
|
if (pos !== -1) {
|
|
name = id.substr(0, pos);
|
|
key = id.substring(pos + 1, id.length - 1);
|
|
} else {
|
|
name = id;
|
|
key = null;
|
|
}
|
|
|
|
var nameElements = name.split('.');
|
|
|
|
var attr;
|
|
if (nameElements.length > 1) {
|
|
var attrElements = [];
|
|
attrElements.push(nameElements.pop());
|
|
if (nameElements.length > 1) {
|
|
// Usually the last dot separates an attribute from an id
|
|
//
|
|
// In case when there are more than one dot in the id
|
|
// and the second to last item is "style" or "dataset" then the last two
|
|
// items are becoming the attribute.
|
|
//
|
|
// ex.
|
|
// id.style.color = foo =>
|
|
//
|
|
// id:
|
|
// style.color: foo
|
|
//
|
|
// id.other.color = foo =>
|
|
//
|
|
// id.other:
|
|
// color: foo
|
|
if (nestedProps.indexOf(nameElements[nameElements.length - 1]) !== -1) {
|
|
attrElements.push(nameElements.pop());
|
|
}
|
|
}
|
|
name = nameElements.join('.');
|
|
attr = attrElements.reverse().join('.');
|
|
} else {
|
|
attr = null;
|
|
}
|
|
|
|
setEntityValue(name, attr, key, unescapeString(value), ast);
|
|
}
|
|
|
|
function unescapeControlCharacters(str) {
|
|
return str.replace(parsePatterns.controlChars, '$1');
|
|
}
|
|
|
|
function unescapeUnicode(str) {
|
|
return str.replace(parsePatterns.unicode, function(match, token) {
|
|
return unescape('%u' + '0000'.slice(token.length) + token);
|
|
});
|
|
}
|
|
|
|
function unescapeString(str) {
|
|
if (str.lastIndexOf('\\') !== -1) {
|
|
str = unescapeControlCharacters(str);
|
|
}
|
|
return unescapeUnicode(str);
|
|
}
|
|
|
|
function parseMacro(str) {
|
|
var match = str.match(parsePatterns.macro);
|
|
if (!match) {
|
|
throw new L10nError('Malformed macro');
|
|
}
|
|
return [match[1], match[2]];
|
|
}
|
|
|
|
|
|
|
|
var MAX_PLACEABLE_LENGTH = 2500;
|
|
var MAX_PLACEABLES = 100;
|
|
var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g;
|
|
|
|
function Entity(id, node, env) {
|
|
this.id = id;
|
|
this.env = env;
|
|
// the dirty guard prevents cyclic or recursive references from other
|
|
// Entities; see Entity.prototype.resolve
|
|
this.dirty = false;
|
|
if (typeof node === 'string') {
|
|
this.value = node;
|
|
} else {
|
|
// it's either a hash or it has attrs, or both
|
|
for (var key in node) {
|
|
if (node.hasOwnProperty(key) && key[0] !== '_') {
|
|
if (!this.attributes) {
|
|
this.attributes = {};
|
|
}
|
|
this.attributes[key] = new Entity(this.id + '.' + key, node[key],
|
|
env);
|
|
}
|
|
}
|
|
this.value = node._ || null;
|
|
this.index = node._index;
|
|
}
|
|
}
|
|
|
|
Entity.prototype.resolve = function E_resolve(ctxdata) {
|
|
if (this.dirty) {
|
|
return undefined;
|
|
}
|
|
|
|
this.dirty = true;
|
|
var val;
|
|
// if resolve fails, we want the exception to bubble up and stop the whole
|
|
// resolving process; however, we still need to clean up the dirty flag
|
|
try {
|
|
val = resolve(ctxdata, this.env, this.value, this.index);
|
|
} finally {
|
|
this.dirty = false;
|
|
}
|
|
return val;
|
|
};
|
|
|
|
Entity.prototype.toString = function E_toString(ctxdata) {
|
|
try {
|
|
return this.resolve(ctxdata);
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
Entity.prototype.valueOf = function E_valueOf(ctxdata) {
|
|
if (!this.attributes) {
|
|
return this.toString(ctxdata);
|
|
}
|
|
|
|
var entity = {
|
|
value: this.toString(ctxdata),
|
|
attributes: {}
|
|
};
|
|
|
|
for (var key in this.attributes) {
|
|
if (this.attributes.hasOwnProperty(key)) {
|
|
entity.attributes[key] = this.attributes[key].toString(ctxdata);
|
|
}
|
|
}
|
|
|
|
return entity;
|
|
};
|
|
|
|
function subPlaceable(ctxdata, env, match, id) {
|
|
if (ctxdata && ctxdata.hasOwnProperty(id) &&
|
|
(typeof ctxdata[id] === 'string' ||
|
|
(typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) {
|
|
return ctxdata[id];
|
|
}
|
|
|
|
if (env.hasOwnProperty(id)) {
|
|
if (!(env[id] instanceof Entity)) {
|
|
env[id] = new Entity(id, env[id], env);
|
|
}
|
|
var value = env[id].resolve(ctxdata);
|
|
if (typeof value === 'string') {
|
|
// prevent Billion Laughs attacks
|
|
if (value.length >= MAX_PLACEABLE_LENGTH) {
|
|
throw new L10nError('Too many characters in placeable (' +
|
|
value.length + ', max allowed is ' +
|
|
MAX_PLACEABLE_LENGTH + ')');
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
function interpolate(ctxdata, env, str) {
|
|
var placeablesCount = 0;
|
|
var value = str.replace(rePlaceables, function(match, id) {
|
|
// prevent Quadratic Blowup attacks
|
|
if (placeablesCount++ >= MAX_PLACEABLES) {
|
|
throw new L10nError('Too many placeables (' + placeablesCount +
|
|
', max allowed is ' + MAX_PLACEABLES + ')');
|
|
}
|
|
return subPlaceable(ctxdata, env, match, id);
|
|
});
|
|
placeablesCount = 0;
|
|
return value;
|
|
}
|
|
|
|
function resolve(ctxdata, env, expr, index) {
|
|
if (typeof expr === 'string') {
|
|
return interpolate(ctxdata, env, expr);
|
|
}
|
|
|
|
if (typeof expr === 'boolean' ||
|
|
typeof expr === 'number' ||
|
|
!expr) {
|
|
return expr;
|
|
}
|
|
|
|
// otherwise, it's a dict
|
|
|
|
if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) {
|
|
var argValue = ctxdata[index[1]];
|
|
|
|
// special cases for zero, one, two if they are defined on the hash
|
|
if (argValue === 0 && 'zero' in expr) {
|
|
return resolve(ctxdata, env, expr.zero);
|
|
}
|
|
if (argValue === 1 && 'one' in expr) {
|
|
return resolve(ctxdata, env, expr.one);
|
|
}
|
|
if (argValue === 2 && 'two' in expr) {
|
|
return resolve(ctxdata, env, expr.two);
|
|
}
|
|
|
|
var selector = env.__plural(argValue);
|
|
if (expr.hasOwnProperty(selector)) {
|
|
return resolve(ctxdata, env, expr[selector]);
|
|
}
|
|
}
|
|
|
|
// if there was no index or no selector was found, try 'other'
|
|
if ('other' in expr) {
|
|
return resolve(ctxdata, env, expr.other);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function compile(env, ast) {
|
|
env = env || {};
|
|
for (var id in ast) {
|
|
if (ast.hasOwnProperty(id)) {
|
|
env[id] = new Entity(id, ast[id], env);
|
|
}
|
|
}
|
|
return env;
|
|
}
|
|
|
|
|
|
|
|
function Locale(id, ctx) {
|
|
this.id = id;
|
|
this.ctx = ctx;
|
|
this.isReady = false;
|
|
this.entries = {
|
|
__plural: getPluralRule(id)
|
|
};
|
|
}
|
|
|
|
Locale.prototype.getEntry = function L_getEntry(id) {
|
|
/* jshint -W093 */
|
|
|
|
var entries = this.entries;
|
|
|
|
if (!entries.hasOwnProperty(id)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (entries[id] instanceof Entity) {
|
|
return entries[id];
|
|
}
|
|
|
|
return entries[id] = new Entity(id, entries[id], entries);
|
|
};
|
|
|
|
Locale.prototype.build = function L_build(callback) {
|
|
var sync = !callback;
|
|
var ctx = this.ctx;
|
|
var self = this;
|
|
|
|
var l10nLoads = ctx.resLinks.length;
|
|
|
|
function onL10nLoaded(err) {
|
|
if (err) {
|
|
ctx._emitter.emit('error', err);
|
|
}
|
|
if (--l10nLoads <= 0) {
|
|
self.isReady = true;
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (l10nLoads === 0) {
|
|
onL10nLoaded();
|
|
return;
|
|
}
|
|
|
|
function onJSONLoaded(err, json) {
|
|
if (!err && json) {
|
|
self.addAST(json);
|
|
}
|
|
onL10nLoaded(err);
|
|
}
|
|
|
|
function onPropLoaded(err, source) {
|
|
if (!err && source) {
|
|
var ast = parse(ctx, source);
|
|
self.addAST(ast);
|
|
}
|
|
onL10nLoaded(err);
|
|
}
|
|
|
|
|
|
for (var i = 0; i < ctx.resLinks.length; i++) {
|
|
var path = ctx.resLinks[i].replace('{{locale}}', this.id);
|
|
var type = path.substr(path.lastIndexOf('.') + 1);
|
|
|
|
switch (type) {
|
|
case 'json':
|
|
io.loadJSON(path, onJSONLoaded, sync);
|
|
break;
|
|
case 'properties':
|
|
io.load(path, onPropLoaded, sync);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
Locale.prototype.addAST = function(ast) {
|
|
for (var id in ast) {
|
|
if (ast.hasOwnProperty(id)) {
|
|
this.entries[id] = ast[id];
|
|
}
|
|
}
|
|
};
|
|
|
|
Locale.prototype.getEntity = function(id, ctxdata) {
|
|
var entry = this.getEntry(id);
|
|
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
return entry.valueOf(ctxdata);
|
|
};
|
|
|
|
|
|
|
|
function Context(id) {
|
|
|
|
this.id = id;
|
|
this.isReady = false;
|
|
this.isLoading = false;
|
|
|
|
this.supportedLocales = [];
|
|
this.resLinks = [];
|
|
this.locales = {};
|
|
|
|
this._emitter = new EventEmitter();
|
|
|
|
|
|
// Getting translations
|
|
|
|
function getWithFallback(id) {
|
|
/* jshint -W084 */
|
|
|
|
if (!this.isReady) {
|
|
throw new L10nError('Context not ready');
|
|
}
|
|
|
|
var cur = 0;
|
|
var loc;
|
|
var locale;
|
|
while (loc = this.supportedLocales[cur]) {
|
|
locale = this.getLocale(loc);
|
|
if (!locale.isReady) {
|
|
// build without callback, synchronously
|
|
locale.build(null);
|
|
}
|
|
var entry = locale.getEntry(id);
|
|
if (entry === undefined) {
|
|
cur++;
|
|
warning.call(this, new L10nError(id + ' not found in ' + loc, id,
|
|
loc));
|
|
continue;
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
error.call(this, new L10nError(id + ' not found', id));
|
|
return null;
|
|
}
|
|
|
|
this.get = function get(id, ctxdata) {
|
|
var entry = getWithFallback.call(this, id);
|
|
if (entry === null) {
|
|
return '';
|
|
}
|
|
|
|
return entry.toString(ctxdata) || '';
|
|
};
|
|
|
|
this.getEntity = function getEntity(id, ctxdata) {
|
|
var entry = getWithFallback.call(this, id);
|
|
if (entry === null) {
|
|
return null;
|
|
}
|
|
|
|
return entry.valueOf(ctxdata);
|
|
};
|
|
|
|
|
|
// Helpers
|
|
|
|
this.getLocale = function getLocale(code) {
|
|
/* jshint -W093 */
|
|
|
|
var locales = this.locales;
|
|
if (locales[code]) {
|
|
return locales[code];
|
|
}
|
|
|
|
return locales[code] = new Locale(code, this);
|
|
};
|
|
|
|
|
|
// Getting ready
|
|
|
|
function negotiate(available, requested, defaultLocale) {
|
|
if (available.indexOf(requested[0]) === -1 ||
|
|
requested[0] === defaultLocale) {
|
|
return [defaultLocale];
|
|
} else {
|
|
return [requested[0], defaultLocale];
|
|
}
|
|
}
|
|
|
|
function freeze(supported) {
|
|
var locale = this.getLocale(supported[0]);
|
|
if (locale.isReady) {
|
|
setReady.call(this, supported);
|
|
} else {
|
|
locale.build(setReady.bind(this, supported));
|
|
}
|
|
}
|
|
|
|
function setReady(supported) {
|
|
this.supportedLocales = supported;
|
|
this.isReady = true;
|
|
this._emitter.emit('ready');
|
|
}
|
|
|
|
this.requestLocales = function requestLocales() {
|
|
if (this.isLoading && !this.isReady) {
|
|
throw new L10nError('Context not ready');
|
|
}
|
|
|
|
this.isLoading = true;
|
|
var requested = Array.prototype.slice.call(arguments);
|
|
|
|
var supported = negotiate(requested.concat('en-US'), requested, 'en-US');
|
|
freeze.call(this, supported);
|
|
};
|
|
|
|
|
|
// Events
|
|
|
|
this.addEventListener = function addEventListener(type, listener) {
|
|
this._emitter.addEventListener(type, listener);
|
|
};
|
|
|
|
this.removeEventListener = function removeEventListener(type, listener) {
|
|
this._emitter.removeEventListener(type, listener);
|
|
};
|
|
|
|
this.ready = function ready(callback) {
|
|
if (this.isReady) {
|
|
setTimeout(callback);
|
|
}
|
|
this.addEventListener('ready', callback);
|
|
};
|
|
|
|
this.once = function once(callback) {
|
|
/* jshint -W068 */
|
|
if (this.isReady) {
|
|
setTimeout(callback);
|
|
return;
|
|
}
|
|
|
|
var callAndRemove = (function() {
|
|
this.removeEventListener('ready', callAndRemove);
|
|
callback();
|
|
}).bind(this);
|
|
this.addEventListener('ready', callAndRemove);
|
|
};
|
|
|
|
|
|
// Errors
|
|
|
|
function warning(e) {
|
|
this._emitter.emit('warning', e);
|
|
return e;
|
|
}
|
|
|
|
function error(e) {
|
|
this._emitter.emit('error', e);
|
|
return e;
|
|
}
|
|
}
|
|
|
|
|
|
/* jshint -W104 */
|
|
|
|
var DEBUG = false;
|
|
var isPretranslated = false;
|
|
var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur'];
|
|
|
|
// Public API
|
|
|
|
navigator.mozL10n = {
|
|
ctx: new Context(),
|
|
get: function get(id, ctxdata) {
|
|
return navigator.mozL10n.ctx.get(id, ctxdata);
|
|
},
|
|
localize: function localize(element, id, args) {
|
|
return localizeElement.call(navigator.mozL10n, element, id, args);
|
|
},
|
|
translate: function translate(element) {
|
|
return translateFragment.call(navigator.mozL10n, element);
|
|
},
|
|
ready: function ready(callback) {
|
|
return navigator.mozL10n.ctx.ready(callback);
|
|
},
|
|
once: function once(callback) {
|
|
return navigator.mozL10n.ctx.once(callback);
|
|
},
|
|
get readyState() {
|
|
return navigator.mozL10n.ctx.isReady ? 'complete' : 'loading';
|
|
},
|
|
language: {
|
|
set code(lang) {
|
|
navigator.mozL10n.ctx.requestLocales(lang);
|
|
},
|
|
get code() {
|
|
return navigator.mozL10n.ctx.supportedLocales[0];
|
|
},
|
|
get direction() {
|
|
return getDirection(navigator.mozL10n.ctx.supportedLocales[0]);
|
|
}
|
|
},
|
|
_getInternalAPI: function() {
|
|
return {
|
|
Error: L10nError,
|
|
Context: Context,
|
|
Locale: Locale,
|
|
Entity: Entity,
|
|
getPluralRule: getPluralRule,
|
|
rePlaceables: rePlaceables,
|
|
getTranslatableChildren: getTranslatableChildren,
|
|
getL10nAttributes: getL10nAttributes,
|
|
loadINI: loadINI,
|
|
fireLocalizedEvent: fireLocalizedEvent,
|
|
parse: parse,
|
|
compile: compile
|
|
};
|
|
}
|
|
};
|
|
|
|
navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n));
|
|
|
|
if (DEBUG) {
|
|
navigator.mozL10n.ctx.addEventListener('error', console.error);
|
|
navigator.mozL10n.ctx.addEventListener('warning', console.warn);
|
|
}
|
|
|
|
function getDirection(lang) {
|
|
return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr';
|
|
}
|
|
|
|
var readyStates = {
|
|
'loading': 0,
|
|
'interactive': 1,
|
|
'complete': 2
|
|
};
|
|
|
|
function waitFor(state, callback) {
|
|
state = readyStates[state];
|
|
if (readyStates[document.readyState] >= state) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
document.addEventListener('readystatechange', function l10n_onrsc() {
|
|
if (readyStates[document.readyState] >= state) {
|
|
document.removeEventListener('readystatechange', l10n_onrsc);
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (window.document) {
|
|
isPretranslated = (document.documentElement.lang === navigator.language);
|
|
|
|
// this is a special case for netError bug; see https://bugzil.la/444165
|
|
if (document.documentElement.dataset.noCompleteBug) {
|
|
pretranslate.call(navigator.mozL10n);
|
|
return;
|
|
}
|
|
|
|
|
|
if (isPretranslated) {
|
|
waitFor('interactive', function() {
|
|
window.setTimeout(initResources.bind(navigator.mozL10n));
|
|
});
|
|
} else {
|
|
if (document.readyState === 'complete') {
|
|
window.setTimeout(initResources.bind(navigator.mozL10n));
|
|
} else {
|
|
waitFor('interactive', pretranslate.bind(navigator.mozL10n));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function pretranslate() {
|
|
/* jshint -W068 */
|
|
if (inlineLocalization.call(this)) {
|
|
waitFor('interactive', (function() {
|
|
window.setTimeout(initResources.bind(this));
|
|
}).bind(this));
|
|
} else {
|
|
initResources.call(this);
|
|
}
|
|
}
|
|
|
|
function inlineLocalization() {
|
|
var script = document.documentElement
|
|
.querySelector('script[type="application/l10n"]' +
|
|
'[lang="' + navigator.language + '"]');
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
|
|
var locale = this.ctx.getLocale(navigator.language);
|
|
// the inline localization is happenning very early, when the ctx is not
|
|
// yet ready and when the resources haven't been downloaded yet; add the
|
|
// inlined JSON directly to the current locale
|
|
locale.addAST(JSON.parse(script.innerHTML));
|
|
// localize the visible DOM
|
|
var l10n = {
|
|
ctx: locale,
|
|
language: {
|
|
code: locale.id,
|
|
direction: getDirection(locale.id)
|
|
}
|
|
};
|
|
translateFragment.call(l10n);
|
|
// the visible DOM is now pretranslated
|
|
isPretranslated = true;
|
|
return true;
|
|
}
|
|
|
|
function initResources() {
|
|
var resLinks = document.head
|
|
.querySelectorAll('link[type="application/l10n"]');
|
|
var iniLinks = [];
|
|
var i;
|
|
|
|
for (i = 0; i < resLinks.length; i++) {
|
|
var link = resLinks[i];
|
|
var url = link.getAttribute('href');
|
|
var type = url.substr(url.lastIndexOf('.') + 1);
|
|
if (type === 'ini') {
|
|
iniLinks.push(url);
|
|
}
|
|
this.ctx.resLinks.push(url);
|
|
}
|
|
|
|
var iniLoads = iniLinks.length;
|
|
if (iniLoads === 0) {
|
|
initLocale.call(this);
|
|
return;
|
|
}
|
|
|
|
function onIniLoaded(err) {
|
|
if (err) {
|
|
this.ctx._emitter.emit('error', err);
|
|
}
|
|
if (--iniLoads === 0) {
|
|
initLocale.call(this);
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < iniLinks.length; i++) {
|
|
loadINI.call(this, iniLinks[i], onIniLoaded.bind(this));
|
|
}
|
|
}
|
|
|
|
function initLocale() {
|
|
this.ctx.requestLocales(navigator.language);
|
|
window.addEventListener('languagechange', function l10n_langchange() {
|
|
navigator.mozL10n.language.code = navigator.language;
|
|
});
|
|
}
|
|
|
|
function onReady() {
|
|
if (!isPretranslated) {
|
|
this.translate();
|
|
}
|
|
isPretranslated = false;
|
|
|
|
fireLocalizedEvent.call(this);
|
|
}
|
|
|
|
function fireLocalizedEvent() {
|
|
var event = new CustomEvent('localized', {
|
|
'bubbles': false,
|
|
'cancelable': false,
|
|
'detail': {
|
|
'language': this.ctx.supportedLocales[0]
|
|
}
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
/* jshint -W104 */
|
|
|
|
function loadINI(url, callback) {
|
|
var ctx = this.ctx;
|
|
io.load(url, function(err, source) {
|
|
var pos = ctx.resLinks.indexOf(url);
|
|
|
|
if (err) {
|
|
// remove the ini link from resLinks
|
|
ctx.resLinks.splice(pos, 1);
|
|
return callback(err);
|
|
}
|
|
|
|
if (!source) {
|
|
ctx.resLinks.splice(pos, 1);
|
|
return callback(new Error('Empty file: ' + url));
|
|
}
|
|
|
|
var patterns = parseINI(source, url).resources.map(function(x) {
|
|
return x.replace('en-US', '{{locale}}');
|
|
});
|
|
ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns));
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function relativePath(baseUrl, url) {
|
|
if (url[0] === '/') {
|
|
return url;
|
|
}
|
|
|
|
var dirs = baseUrl.split('/')
|
|
.slice(0, -1)
|
|
.concat(url.split('/'))
|
|
.filter(function(path) {
|
|
return path !== '.';
|
|
});
|
|
|
|
return dirs.join('/');
|
|
}
|
|
|
|
var iniPatterns = {
|
|
'section': /^\s*\[(.*)\]\s*$/,
|
|
'import': /^\s*@import\s+url\((.*)\)\s*$/i,
|
|
'entry': /[\r\n]+/
|
|
};
|
|
|
|
function parseINI(source, iniPath) {
|
|
var entries = source.split(iniPatterns.entry);
|
|
var locales = ['en-US'];
|
|
var genericSection = true;
|
|
var uris = [];
|
|
var match;
|
|
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var line = entries[i];
|
|
// we only care about en-US resources
|
|
if (genericSection && iniPatterns['import'].test(line)) {
|
|
match = iniPatterns['import'].exec(line);
|
|
var uri = relativePath(iniPath, match[1]);
|
|
uris.push(uri);
|
|
continue;
|
|
}
|
|
|
|
// but we need the list of all locales in the ini, too
|
|
if (iniPatterns.section.test(line)) {
|
|
genericSection = false;
|
|
match = iniPatterns.section.exec(line);
|
|
locales.push(match[1]);
|
|
}
|
|
}
|
|
return {
|
|
locales: locales,
|
|
resources: uris
|
|
};
|
|
}
|
|
|
|
/* jshint -W104 */
|
|
|
|
function translateFragment(element) {
|
|
if (!element) {
|
|
element = document.documentElement;
|
|
document.documentElement.lang = this.language.code;
|
|
document.documentElement.dir = this.language.direction;
|
|
}
|
|
translateElement.call(this, element);
|
|
|
|
var nodes = getTranslatableChildren(element);
|
|
for (var i = 0; i < nodes.length; i++ ) {
|
|
translateElement.call(this, nodes[i]);
|
|
}
|
|
}
|
|
|
|
function getTranslatableChildren(element) {
|
|
return element ? element.querySelectorAll('*[data-l10n-id]') : [];
|
|
}
|
|
|
|
function localizeElement(element, id, args) {
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
if (!id) {
|
|
element.removeAttribute('data-l10n-id');
|
|
element.removeAttribute('data-l10n-args');
|
|
setTextContent(element, '');
|
|
return;
|
|
}
|
|
|
|
element.setAttribute('data-l10n-id', id);
|
|
if (args && typeof args === 'object') {
|
|
element.setAttribute('data-l10n-args', JSON.stringify(args));
|
|
} else {
|
|
element.removeAttribute('data-l10n-args');
|
|
}
|
|
|
|
if (this.ctx.isReady) {
|
|
translateElement.call(this, element);
|
|
}
|
|
}
|
|
|
|
function getL10nAttributes(element) {
|
|
if (!element) {
|
|
return {};
|
|
}
|
|
|
|
var l10nId = element.getAttribute('data-l10n-id');
|
|
var l10nArgs = element.getAttribute('data-l10n-args');
|
|
|
|
var args = l10nArgs ? JSON.parse(l10nArgs) : null;
|
|
|
|
return {id: l10nId, args: args};
|
|
}
|
|
|
|
|
|
|
|
function translateElement(element) {
|
|
var l10n = getL10nAttributes(element);
|
|
|
|
if (!l10n.id) {
|
|
return;
|
|
}
|
|
|
|
var entity = this.ctx.getEntity(l10n.id, l10n.args);
|
|
|
|
if (!entity) {
|
|
return;
|
|
}
|
|
|
|
if (typeof entity === 'string') {
|
|
setTextContent(element, entity);
|
|
return true;
|
|
}
|
|
|
|
if (entity.value) {
|
|
setTextContent(element, entity.value);
|
|
}
|
|
|
|
for (var key in entity.attributes) {
|
|
if (entity.attributes.hasOwnProperty(key)) {
|
|
var attr = entity.attributes[key];
|
|
var pos = key.indexOf('.');
|
|
if (pos !== -1) {
|
|
element[key.substr(0, pos)][key.substr(pos + 1)] = attr;
|
|
} else if (key === 'ariaLabel') {
|
|
element.setAttribute('aria-label', attr);
|
|
} else {
|
|
element[key] = attr;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function setTextContent(element, text) {
|
|
// standard case: no element children
|
|
if (!element.firstElementChild) {
|
|
element.textContent = text;
|
|
return;
|
|
}
|
|
|
|
// this element has element children: replace the content of the first
|
|
// (non-blank) child textNode and clear other child textNodes
|
|
var found = false;
|
|
var reNotBlank = /\S/;
|
|
for (var child = element.firstChild; child; child = child.nextSibling) {
|
|
if (child.nodeType === Node.TEXT_NODE &&
|
|
reNotBlank.test(child.nodeValue)) {
|
|
if (found) {
|
|
child.nodeValue = '';
|
|
} else {
|
|
child.nodeValue = text;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
// if no (non-empty) textNode is found, insert a textNode before the
|
|
// element's first child.
|
|
if (!found) {
|
|
element.insertBefore(document.createTextNode(text), element.firstChild);
|
|
}
|
|
}
|
|
|
|
})(this);
|
|
|