blob: a2cd3285079822fc9ab8bf5f28ca23f7865477a8 [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* global unescape */
(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 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 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('.');
if (nameElements.length > 2) {
throw new Error('Error in ID: "' + name + '".' +
' Nested attributes are not supported.');
}
var attr;
if (nameElements.length > 1) {
name = nameElements[0];
attr = nameElements[1];
} 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;
}
}
var DEBUG = false;
var isPretranslated = false;
var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur'];
var nodeObserver = false;
var moConfig = {
attributes: true,
characterData: false,
childList: true,
subtree: true,
attributeFilter: ['data-l10n-id', 'data-l10n-args']
};
// 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 () {
// XXX: Remove after removing obsolete calls. Bugs 992473 and 1020136
},
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]);
}
},
_getInternalAPI: function() {
return {
Error: L10nError,
Context: Context,
Locale: Locale,
Entity: Entity,
getPluralRule: getPluralRule,
rePlaceables: rePlaceables,
getTranslatableChildren: getTranslatableChildren,
translateDocument: translateDocument,
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)
}
};
translateDocument.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 = [];
for (var 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 localizeMutations(mutations) {
var mutation;
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;
}
if (addedNode.childElementCount) {
translateFragment.call(this, addedNode);
} else if (addedNode.hasAttribute('data-l10n-id')) {
translateElement.call(this, addedNode);
}
}
}
if (mutation.type === 'attributes') {
translateElement.call(this, mutation.target);
}
}
}
function onMutations(mutations, self) {
self.disconnect();
localizeMutations.call(this, mutations);
self.observe(document, moConfig);
}
function onReady() {
if (!isPretranslated) {
translateDocument.call(this);
}
isPretranslated = false;
if (!nodeObserver) {
nodeObserver = new MutationObserver(onMutations.bind(this));
nodeObserver.observe(document, moConfig);
}
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 translateDocument() {
document.documentElement.lang = this.language.code;
document.documentElement.dir = this.language.direction;
translateFragment.call(this, document.documentElement);
}
function translateFragment(element) {
if (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 localizeElement(element, id, args) {
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');
}
}
function translateElement(element) {
var l10n = getL10nAttributes(element);
if (!l10n.id) {
return false;
}
var entity = this.ctx.getEntity(l10n.id, l10n.args);
if (!entity) {
return false;
}
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];
if (key === 'ariaLabel') {
element.setAttribute('aria-label', attr);
} else if (key === 'innerHTML') {
// XXX: to be removed once bug 994357 lands
element.innerHTML = attr;
} else {
element.setAttribute(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);