Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1571 lines
36 KiB
JavaScript
Executable file
1571 lines
36 KiB
JavaScript
Executable file
// 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', 'ar-x-psbidi', '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.bind(console));
|
|
navigator.mozL10n.ctx.addEventListener('warning', console.warn.bind(console));
|
|
}
|
|
|
|
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);
|
|
|