123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617 |
- /*
- *
- * 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);
|