Files
old-svijetlastrana-front/public/js/l20n.js
2018-05-07 16:07:00 +02:00

3525 lines
108 KiB
JavaScript
Executable File

(function(window, undefined) {
function define(name, payload) {
define.modules[name] = payload;
};
// un-instantiated modules
define.modules = {};
// instantiated modules
define.exports = {};
function normalize(path) {
var parts = path.split('/');
var normalized = [];
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '.') {
// don't add it to `normalized`
} else if (parts[i] == '..') {
normalized.pop();
} else {
normalized.push(parts[i]);
}
}
return normalized.join('/');
}
function join(a, b) {
return a ? a.trim().replace(/\/*$/, '/') + b.trim() : b.trim();
}
function dirname(path) {
return path ? path.split('/').slice(0, -1).join('/') : null;
}
function req(leaf, name) {
name = normalize(join(dirname(leaf), name));
if (name in define.exports) {
return define.exports[name];
}
if (!(name in define.modules)) {
throw new Error("Module not defined: " + name);
}
var module = define.modules[name];
if (typeof module == "function") {
var exports = {};
var reply = module(req.bind(null, name), exports, { id: name, uri: "" });
module = (reply !== undefined) ? reply : exports;
}
return define.exports[name] = module;
};
// for the top-level required modules, leaf is null
var require = req.bind(null, null);
define('l20n', function(require, exports) {
'use strict';
var Context = require('./l20n/context').Context;
var Parser = require('./l20n/parser').Parser;
var Compiler = require('./l20n/compiler').Compiler;
exports.Context = Context;
exports.Parser = Parser;
exports.Compiler = Compiler;
exports.getContext = function L20n_getContext(id) {
return new Context(id);
};
});
define('l20n/context', function(require, exports) {
'use strict';
var EventEmitter = require('./events').EventEmitter;
var Parser = require('./parser').Parser;
var Compiler = require('./compiler').Compiler;
var RetranslationManager = require('./retranslation').RetranslationManager;
// register globals with RetranslationManager
require('./platform/globals');
var io = require('./platform/io');
function Resource(id, parser) {
var self = this;
this.id = id;
this.resources = [];
this.source = null;
this.ast = {
type: 'LOL',
body: []
};
this.build = build;
var _imports_positions = [];
// absolute URLs start with a slash or contain a colon (for schema)
var reAbsolute = /^\/|:/;
function build(nesting, callback, sync) {
if (nesting >= 7) {
return callback(new ContextError('Too many nested imports.'));
}
if (self.source) {
// Bug 908826 - Don't artificially force asynchronicity when only using
// addResource
// https://bugzilla.mozilla.org/show_bug.cgi?id=908826
return setTimeout(function() {
parse();
});
} else {
io.load(self.id, parse, sync);
}
function parse(err, text) {
if (err) {
return callback(err);
} else if (text !== undefined) {
self.source = text;
}
self.ast = parser.parse(self.source);
buildImports();
}
function buildImports() {
var imports = self.ast.body.filter(function(elem, i) {
if (elem.type === 'ImportStatement') {
_imports_positions.push(i);
return true;
}
return false;
});
imports.forEach(function(imp) {
var uri = relativeToSelf(imp.uri.content);
var res = new Resource(uri, parser);
self.resources.push(res);
});
var importsToBuild = self.resources.length;
if (importsToBuild === 0) {
return callback();
}
self.resources.forEach(function(res) {
res.build(nesting + 1, resourceBuilt, sync);
});
function resourceBuilt(err) {
if (err) {
return callback(err);
}
importsToBuild--;
if (importsToBuild === 0) {
flatten();
}
}
}
function flatten() {
for (var i = self.resources.length - 1; i >= 0; i--) {
var pos = _imports_positions[i] || 0;
Array.prototype.splice.apply(self.ast.body,
[pos, 1].concat(self.resources[i].ast.body));
}
callback();
}
}
function relativeToSelf(url) {
if (self.id === null || reAbsolute.test(url)) {
return url;
}
var dirs = self.id.split('/')
.slice(0, -1)
.concat(url.split('/'))
.filter(function(elem) {
return elem !== '.';
});
return dirs.join('/');
}
}
function Locale(id, parser, compiler, emitter) {
this.id = id;
this.resources = [];
this.entries = null;
this.ast = {
type: 'LOL',
body: []
};
this.isReady = false;
this.build = build;
this.getEntry = getEntry;
this.hasResource = hasResource;
var self = this;
function build(callback) {
if (!callback) {
var sync = true;
}
var resourcesToBuild = self.resources.length;
if (resourcesToBuild === 0) {
throw new ContextError('Locale has no resources');
}
var resourcesWithErrors = 0;
self.resources.forEach(function(res) {
res.build(0, resourceBuilt, sync);
});
function resourceBuilt(err) {
if (err) {
resourcesWithErrors++;
emitter.emit(err instanceof ContextError ? 'error' : 'warning', err);
}
resourcesToBuild--;
if (resourcesToBuild === 0) {
if (resourcesWithErrors === self.resources.length) {
// XXX Bug 908780 - Decide what to do when all resources in
// a locale are missing or broken
// https://bugzilla.mozilla.org/show_bug.cgi?id=908780
emitter.emit('error',
new ContextError('Locale has no valid resources'));
}
flatten();
}
}
function flatten() {
self.ast.body = self.resources.reduce(function(prev, curr) {
return prev.concat(curr.ast.body);
}, self.ast.body);
compile();
}
function compile() {
self.entries = compiler.compile(self.ast);
self.isReady = true;
if (callback) {
callback();
}
}
}
function getEntry(id) {
/* jshint validthis: true */
if (this.entries.hasOwnProperty(id)) {
return this.entries[id];
}
return undefined;
}
function hasResource(uri) {
/* jshint validthis: true */
return this.resources.some(function(res) {
return res.id === uri;
});
}
}
function Context(id) {
this.id = id;
this.registerLocales = registerLocales;
this.registerLocaleNegotiator = registerLocaleNegotiator;
this.requestLocales = requestLocales;
this.addResource = addResource;
this.linkResource = linkResource;
this.updateData = updateData;
this.getSync = getSync;
this.getEntitySync = getEntitySync;
this.localize = localize;
this.ready = ready;
this.addEventListener = addEvent;
this.removeEventListener = removeEvent;
Object.defineProperty(this, 'supportedLocales', {
get: function() { return _fallbackChain.slice(); },
enumerable: true
});
var _data = {};
// language negotiator function
var _negotiator;
// registered and available languages
var _default = 'i-default';
var _registered = [_default];
var _requested = [];
var _fallbackChain = [];
// Locale objects corresponding to the registered languages
var _locales = {};
// URLs or text of resources (with information about the type) added via
// linkResource and addResource
var _reslinks = [];
var _isReady = false;
var _isFrozen = false;
var _emitter = new EventEmitter();
var _parser = new Parser();
var _compiler = new Compiler();
var _retr = new RetranslationManager();
var self = this;
_parser.addEventListener('error', error);
_compiler.addEventListener('error', warn);
_compiler.setGlobals(_retr.globals);
function extend(dst, src) {
Object.keys(src).forEach(function(key) {
if (src[key] === undefined) {
// un-define (remove) the property from dst
delete dst[key];
} else if (typeof src[key] !== 'object') {
// if the source property is a primitive, just copy it overwriting
// whatever the destination property is
dst[key] = src[key];
} else {
// if the source property is an object, deep-copy it recursively
if (typeof dst[key] !== 'object') {
dst[key] = {};
}
extend(dst[key], src[key]);
}
});
}
function updateData(obj) {
if (!obj || typeof obj !== 'object') {
throw new ContextError('Context data must be a non-null object');
}
extend(_data, obj);
}
function getSync(id, data) {
if (!_isReady) {
throw new ContextError('Context not ready');
}
return getFromLocale.call(self, 0, id, data).value;
}
function getEntitySync(id, data) {
if (!_isReady) {
throw new ContextError('Context not ready');
}
return getFromLocale.call(self, 0, id, data);
}
function localize(ids, callback) {
if (!callback) {
throw new ContextError('No callback passed');
}
return bindLocalize.call(self, ids, callback);
}
function ready(callback) {
if (_isReady) {
setTimeout(callback);
}
addEvent('ready', callback);
}
function bindLocalize(ids, callback, reason) {
/* jshint validthis: true */
var bound = {
// stop: fn
extend: function extend(newIds) {
for (var i = 0; i < newIds.length; i++) {
if (ids.indexOf(newIds[i]) === -1) {
ids.push(newIds[i]);
}
}
if (!_isReady) {
return;
}
var newMany = getMany.call(this, newIds);
// rebind the callback in `_retr`: append new globals seen used in
// `newIds` and overwrite the callback with a new one which has the
// updated `ids`
_retr.bindGet({
id: callback,
callback: bindLocalize.bind(this, ids, callback),
globals: Object.keys(newMany.globalsUsed)
}, true);
return newMany;
}.bind(this),
stop: function stop() {
_retr.unbindGet(callback);
}.bind(this)
};
// if the ctx isn't ready, bind the callback and return
if (!_isReady) {
_retr.bindGet({
id: callback,
callback: bindLocalize.bind(this, ids, callback),
globals: []
});
return bound;
}
// if the ctx is ready, retrieve the entities
var many = getMany.call(this, ids);
var l10n = {
entities: many.entities,
// `reason` might be undefined if context was ready before `localize`
// was called; in that case, we pass `locales` so that this scenario
// is transparent for the callback
reason: reason || { locales: _fallbackChain.slice() },
stop: function() {
_retr.unbindGet(callback);
}
};
_retr.bindGet({
id: callback,
callback: bindLocalize.bind(this, ids, callback),
globals: Object.keys(many.globalsUsed)
});
// callback may call bound.extend which will rebind it if needed; for
// this to work it needs to be called after _retr.bindGet above;
// otherwise bindGet would listen to globals passed initially in
// many.globalsUsed
callback(l10n);
return bound;
}
function getMany(ids) {
/* jshint validthis: true */
var many = {
entities: {},
globalsUsed: {}
};
for (var i = 0, id; id = ids[i]; i++) {
many.entities[id] = getEntitySync.call(this, id);
for (var global in many.entities[id].globals) {
if (many.entities[id].globals.hasOwnProperty(global)) {
many.globalsUsed[global] = true;
}
}
}
return many;
}
function getFromLocale(cur, id, data, prevSource) {
/* jshint validthis: true */
var loc = _fallbackChain[cur];
if (!loc) {
error(new RuntimeError('Unable to get translation', id,
_fallbackChain));
// imitate the return value of Compiler.Entity.get
return {
value: prevSource ? prevSource.source : id,
attributes: {},
globals: {},
locale: prevSource ? prevSource.loc : null
};
}
var locale = getLocale(loc);
if (!locale.isReady) {
// build without a callback, synchronously
locale.build(null);
}
var entry = locale.getEntry(id);
// if the entry is missing, just go to the next locale immediately
if (entry === undefined) {
warn(new TranslationError('Not found', id, _fallbackChain, locale));
return getFromLocale.call(this, cur + 1, id, data, prevSource);
}
// otherwise, try to get the value of the entry
var value;
try {
value = entry.get(getArgs.call(this, data));
} catch (e) {
if (e instanceof Compiler.RuntimeError) {
error(new TranslationError(e.message, id, _fallbackChain, locale));
if (e instanceof Compiler.ValueError) {
// salvage the source string which the compiler wasn't able to
// evaluate completely; this is still better than returning the
// identifer; prefer a source string from locales earlier in the
// fallback chain, if available
var source = prevSource || { source: e.source, loc: locale.id };
return getFromLocale.call(this, cur + 1, id, data, source);
}
return getFromLocale.call(this, cur + 1, id, data, prevSource);
} else {
throw error(e);
}
}
value.locale = locale.id;
return value;
}
function getArgs(extra) {
if (!extra) {
return _data;
}
var args = {};
// deep-clone _data first
extend(args, _data);
// overwrite args with the extra args passed to the `get` call
extend(args, extra);
return args;
}
function addResource(text) {
if (_isFrozen) {
throw new ContextError('Context is frozen');
}
_reslinks.push(['text', text]);
}
function add(text, locale) {
var res = new Resource(null, _parser);
res.source = text;
locale.resources.push(res);
}
function linkResource(uri) {
if (_isFrozen) {
throw new ContextError('Context is frozen');
}
_reslinks.push([typeof uri === 'function' ? 'template' : 'uri', uri]);
}
function link(uri, locale) {
if (!locale.hasResource(uri)) {
var res = new Resource(uri, _parser);
locale.resources.push(res);
}
}
function registerLocales(defLocale, available) {
if (_isFrozen) {
throw new ContextError('Context is frozen');
}
if (defLocale === undefined) {
return;
}
_default = defLocale;
_registered = [];
if (!available) {
available = [];
}
available.push(defLocale);
// uniquify `available` into `_registered`
available.forEach(function(loc) {
if (typeof loc !== 'string') {
throw new ContextError('Language codes must be strings');
}
if (_registered.indexOf(loc) === -1) {
_registered.push(loc);
}
});
}
function registerLocaleNegotiator(negotiator) {
if (_isFrozen) {
throw new ContextError('Context is frozen');
}
_negotiator = negotiator;
}
function getLocale(loc) {
if (_locales[loc]) {
return _locales[loc];
}
var locale = new Locale(loc, _parser, _compiler, _emitter);
_locales[loc] = locale;
// populate the locale with resources
for (var j = 0; j < _reslinks.length; j++) {
var res = _reslinks[j];
if (res[0] === 'text') {
// a resource added via addResource(String)
add(res[1], locale);
} else if (res[0] === 'uri') {
// a resource added via linkResource(String)
link(res[1], locale);
} else {
// a resource added via linkResource(Function); the function
// passed is a URL template and it takes the current locale's code
// as an argument
link(res[1](locale.id), locale);
}
}
return locale;
}
function requestLocales() {
if (_isFrozen && !_isReady) {
throw new ContextError('Context not ready');
}
if (_reslinks.length === 0) {
warn(new ContextError('Context has no resources; not freezing'));
return;
}
_isFrozen = true;
_requested = Array.prototype.slice.call(arguments);
if (_requested.length) {
_requested.forEach(function(loc) {
if (typeof loc !== 'string') {
throw new ContextError('Language codes must be strings');
}
});
}
// the whole language negotiation process can be asynchronous; for now
// we just use _registered as the list of all available locales, but in
// the future we might asynchronously try to query a language pack
// service of sorts for its own list of locales supported for this
// context
if (!_negotiator) {
var Intl = require('./intl').Intl;
_negotiator = Intl.prioritizeLocales;
}
var fallbackChain = _negotiator(_registered, _requested, _default,
freeze);
// if the negotiator returned something, freeze synchronously
if (fallbackChain) {
freeze(fallbackChain);
}
}
function freeze(fallbackChain) {
_fallbackChain = fallbackChain;
var locale = getLocale(_fallbackChain[0]);
if (locale.isReady) {
setReady();
} else {
locale.build(setReady);
}
}
function setReady() {
_isReady = true;
_retr.all(_fallbackChain.slice());
_emitter.emit('ready');
}
function addEvent(type, listener) {
_emitter.addEventListener(type, listener);
}
function removeEvent(type, listener) {
_emitter.removeEventListener(type, listener);
}
function warn(e) {
_emitter.emit('warning', e);
return e;
}
function error(e) {
_emitter.emit('error', e);
return e;
}
}
Context.Error = ContextError;
Context.RuntimeError = RuntimeError;
Context.TranslationError = TranslationError;
function ContextError(message) {
this.name = 'ContextError';
this.message = message;
}
ContextError.prototype = Object.create(Error.prototype);
ContextError.prototype.constructor = ContextError;
function RuntimeError(message, id, supported) {
ContextError.call(this, message);
this.name = 'RuntimeError';
this.entity = id;
this.supportedLocales = supported.slice();
this.message = id + ': ' + message + '; tried ' + supported.join(', ');
}
RuntimeError.prototype = Object.create(ContextError.prototype);
RuntimeError.prototype.constructor = RuntimeError;
function TranslationError(message, id, supported, locale) {
RuntimeError.call(this, message, id, supported);
this.name = 'TranslationError';
this.locale = locale.id;
this.message = '[' + this.locale + '] ' + id + ': ' + message;
}
TranslationError.prototype = Object.create(RuntimeError.prototype);
TranslationError.prototype.constructor = TranslationError;
exports.Context = Context;
});
define('l20n/events', function(require, exports) {
'use strict';
function EventEmitter() {
this._listeners = {};
}
EventEmitter.prototype.emit = function ee_emit() {
var args = Array.prototype.slice.call(arguments);
var type = args.shift();
if (!this._listeners[type]) {
return false;
}
var typeListeners = this._listeners[type].slice();
for (var i = 0; i < typeListeners.length; i++) {
typeListeners[i].apply(this, args);
}
return true;
};
EventEmitter.prototype.addEventListener = function ee_add(type, listener) {
if (!this._listeners[type]) {
this._listeners[type] = [];
}
this._listeners[type].push(listener);
return this;
};
EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) {
var typeListeners = this._listeners[type];
var pos = typeListeners.indexOf(listener);
if (pos === -1) {
return this;
}
typeListeners.splice(pos, 1);
return this;
};
exports.EventEmitter = EventEmitter;
});
define('l20n/parser', function(require, exports) {
'use strict';
var EventEmitter = require('./events').EventEmitter;
function Parser(throwOnErrors) {
/* Public */
this.parse = parse;
this.addEventListener = addEvent;
this.removeEventListener = removeEvent;
/* Private */
var MAX_PLACEABLES = 100;
var _source, _index, _length, _emitter;
var getLOL;
if (throwOnErrors) {
getLOL = getLOLPlain;
} else {
_emitter = new EventEmitter();
getLOL = getLOLWithRecover;
}
function getComment() {
_index += 2;
var start = _index;
var end = _source.indexOf('*/', start);
if (end === -1) {
throw error('Comment without closing tag');
}
_index = end + 2;
return {
type: 'Comment',
content: _source.slice(start, end)
};
}
function getAttributes() {
var attrs = [];
var attr, ws1, ch;
while (true) {
attr = getKVPWithIndex('Attribute');
attr.local = attr.key.name.charAt(0) === '_';
attrs.push(attr);
ws1 = getRequiredWS();
ch = _source.charAt(_index);
if (ch === '>') {
break;
} else if (!ws1) {
throw error('Expected ">"');
}
}
return attrs;
}
function getKVP(type) {
var key = getIdentifier();
getWS();
if (_source.charAt(_index) !== ':') {
throw error('Expected ":"');
}
++_index;
getWS();
return {
type: type,
key: key,
value: getValue()
};
}
function getKVPWithIndex(type) {
var key = getIdentifier();
var index = [];
if (_source.charAt(_index) === '[') {
++_index;
getWS();
index = getItemList(getExpression, ']');
}
getWS();
if (_source.charAt(_index) !== ':') {
throw error('Expected ":"');
}
++_index;
getWS();
return {
type: type,
key: key,
value: getValue(),
index: index
};
}
function getHash() {
++_index;
getWS();
var defItem, hi, comma, hash = [];
var hasDefItem = false;
while (true) {
defItem = false;
if (_source.charAt(_index) === '*') {
++_index;
if (hasDefItem) {
throw error('Default item redefinition forbidden');
}
defItem = true;
hasDefItem = true;
}
hi = getKVP('HashItem');
hi['default'] = defItem;
hash.push(hi);
getWS();
comma = _source.charAt(_index) === ',';
if (comma) {
++_index;
getWS();
}
if (_source.charAt(_index) === '}') {
++_index;
break;
}
if (!comma) {
throw error('Expected "}"');
}
}
return {
type: 'Hash',
content: hash
};
}
function _unescapeString() {
var ch = _source.charAt(++_index);
var cc;
if (ch === 'u') { // special case for unicode
var ucode = '';
for (var i = 0; i < 4; i++) {
ch = _source[++_index];
cc = ch.charCodeAt(0);
if ((cc > 96 && cc < 103) || // a-f
(cc > 64 && cc < 71) || // A-F
(cc > 47 && cc < 58)) { // 0-9
ucode += ch;
} else {
throw error('Illegal unicode escape sequence');
}
}
return String.fromCharCode(parseInt(ucode, 16));
}
return ch;
}
function getComplexString(opchar, opcharLen) {
var body = null;
var buf = '';
var placeables = 0;
var ch;
_index += opcharLen - 1;
var start = _index + 1;
walkChars:
while (true) {
ch = _source.charAt(++_index);
switch (ch) {
case '\\':
buf += _unescapeString();
break;
case '{':
/* We want to go to default unless {{ */
/* jshint -W086 */
if (_source.charAt(_index + 1) === '{') {
if (body === null) {
body = [];
}
if (placeables > MAX_PLACEABLES - 1) {
throw error('Too many placeables, maximum allowed is ' +
MAX_PLACEABLES);
}
if (buf) {
body.push({
type: 'String',
content: buf
});
}
_index += 2;
getWS();
body.push(getExpression());
getWS();
if (_source.charAt(_index) !== '}' ||
_source.charAt(_index + 1) !== '}') {
throw error('Expected "}}"');
}
_index += 1;
placeables++;
buf = '';
break;
}
default:
if (opcharLen === 1) {
if (ch === opchar) {
_index++;
break walkChars;
}
} else {
if (ch === opchar[0] &&
_source.charAt(_index + 1) === ch &&
_source.charAt(_index + 2) === ch) {
_index += 3;
break walkChars;
}
}
buf += ch;
if (_index + 1 >= _length) {
throw error('Unclosed string literal');
}
}
}
if (body === null) {
return {
type: 'String',
content: buf
};
}
if (buf.length) {
body.push({
type: 'String',
content: buf
});
}
return {
type: 'ComplexString',
content: body,
source: _source.slice(start, _index - opcharLen)
};
}
function getString(opchar, opcharLen) {
var opcharPos = _source.indexOf(opchar, _index + opcharLen);
var placeablePos, escPos, buf;
if (opcharPos === -1) {
throw error('Unclosed string literal');
}
buf = _source.slice(_index + opcharLen, opcharPos);
placeablePos = buf.indexOf('{{');
if (placeablePos !== -1) {
return getComplexString(opchar, opcharLen);
} else {
escPos = buf.indexOf('\\');
if (escPos !== -1) {
return getComplexString(opchar, opcharLen);
}
}
_index = opcharPos + opcharLen;
return {
type: 'String',
content: buf
};
}
function getValue(optional, ch) {
if (ch === undefined) {
ch = _source.charAt(_index);
}
if (ch === '\'' || ch === '"') {
if (ch === _source.charAt(_index + 1) &&
ch === _source.charAt(_index + 2)) {
return getString(ch + ch + ch, 3);
}
return getString(ch, 1);
}
if (ch === '{') {
return getHash();
}
if (!optional) {
throw error('Unknown value type');
}
return null;
}
function getRequiredWS() {
var pos = _index;
var cc = _source.charCodeAt(pos);
// space, \n, \t, \r
while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
cc = _source.charCodeAt(++_index);
}
return _index !== pos;
}
function getWS() {
var cc = _source.charCodeAt(_index);
// space, \n, \t, \r
while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
cc = _source.charCodeAt(++_index);
}
}
function getVariable() {
++_index;
return {
type: 'VariableExpression',
id: getIdentifier()
};
}
function getIdentifier() {
var index = _index;
var start = index;
var source = _source;
var cc = source.charCodeAt(start);
// a-zA-Z_
if ((cc < 97 || cc > 122) && (cc < 65 || cc > 90) && cc !== 95) {
throw error('Identifier has to start with [a-zA-Z_]');
}
cc = source.charCodeAt(++index);
while ((cc >= 97 && cc <= 122) || // a-z
(cc >= 65 && cc <= 90) || // A-Z
(cc >= 48 && cc <= 57) || // 0-9
cc === 95) { // _
cc = source.charCodeAt(++index);
}
_index = index;
return {
type: 'Identifier',
name: source.slice(start, index)
};
}
function getImportStatement() {
_index += 6;
if (_source.charAt(_index) !== '(') {
throw error('Expected "("');
}
++_index;
getWS();
var uri = getString(_source.charAt(_index), 1);
getWS();
if (_source.charAt(_index) !== ')') {
throw error('Expected ")"');
}
++_index;
return {
type: 'ImportStatement',
uri: uri
};
}
function getMacro(id) {
if (id.name.charAt(0) === '_') {
throw error('Macro ID cannot start with "_"');
}
++_index;
var idlist = getItemList(getVariable, ')');
getRequiredWS();
if (_source.charAt(_index) !== '{') {
throw error('Expected "{"');
}
++_index;
getWS();
var exp = getExpression();
getWS();
if (_source.charAt(_index) !== '}') {
throw error('Expected "}"');
}
++_index;
getWS();
if (_source.charCodeAt(_index) !== 62) {
throw error('Expected ">"');
}
++_index;
return {
type: 'Macro',
id: id,
args: idlist,
expression: exp
};
}
function getEntity(id, index) {
if (!getRequiredWS()) {
throw error('Expected white space');
}
var ch = _source.charAt(_index);
var value = getValue(true, ch);
var attrs = null;
if (value === null) {
if (ch === '>') {
throw error('Expected ">"');
}
attrs = getAttributes();
} else {
var ws1 = getRequiredWS();
if (_source.charAt(_index) !== '>') {
if (!ws1) {
throw error('Expected ">"');
}
attrs = getAttributes();
}
}
// skip '>'
++_index;
return {
type: 'Entity',
id: id,
value: value,
index: index,
attrs: attrs,
local: (id.name.charCodeAt(0) === 95) // _
};
}
function getEntry() {
var cc = _source.charCodeAt(_index);
// 60 == '<'
if (cc === 60) {
++_index;
var id = getIdentifier();
cc = _source.charCodeAt(_index);
// 40 == '('
if (cc === 40) {
return getMacro(id);
}
// 91 == '['
if (cc === 91) {
++_index;
return getEntity(id,
getItemList(getExpression, ']'));
}
return getEntity(id, null);
}
// 47, 42 == '/*'
if (_source.charCodeAt(_index) === 47 &&
_source.charCodeAt(_index + 1) === 42) {
return getComment();
}
if (_source.slice(_index, _index + 6) === 'import') {
return getImportStatement();
}
throw error('Invalid entry');
}
function getLOLWithRecover() {
var entries = [];
getWS();
while (_index < _length) {
try {
entries.push(getEntry());
} catch (e) {
if (e instanceof ParserError) {
_emitter.emit('error', e);
entries.push(recover());
} else {
throw e;
}
}
if (_index < _length) {
getWS();
}
}
return {
type: 'LOL',
body: entries
};
}
function getLOLPlain() {
var entries = [];
getWS();
while (_index < _length) {
entries.push(getEntry());
if (_index < _length) {
getWS();
}
}
return {
type: 'LOL',
body: entries
};
}
/* Public API functions */
function parse(string) {
_source = string;
_index = 0;
_length = _source.length;
return getLOL();
}
function addEvent(type, listener) {
if (!_emitter) {
throw new Error('Emitter not available');
}
return _emitter.addEventListener(type, listener);
}
function removeEvent(type, listener) {
if (!_emitter) {
throw new Error('Emitter not available');
}
return _emitter.removeEventListener(type, listener);
}
/* Expressions */
function getExpression() {
return getConditionalExpression();
}
function getPrefixExpression(token, cl, op, nxt) {
var exp = nxt();
var t, ch;
while (true) {
t = '';
getWS();
ch = _source.charAt(_index);
if (token[0].indexOf(ch) === -1) {
break;
}
t += ch;
++_index;
if (token.length > 1) {
ch = _source.charAt(_index);
if (token[1] === ch) {
++_index;
t += ch;
} else if (token[2]) {
--_index;
return exp;
}
}
getWS();
exp = {
type: cl,
operator: {
type: op,
token: t
},
left: exp,
right: nxt()
};
}
return exp;
}
function getPostfixExpression(token, cl, op, nxt) {
var cc = _source.charCodeAt(_index);
if (token.indexOf(cc) === -1) {
return nxt();
}
++_index;
getWS();
return {
type: cl,
operator: {
type: op,
token: String.fromCharCode(cc)
},
argument: getPostfixExpression(token, cl, op, nxt)
};
}
function getConditionalExpression() {
var exp = getOrExpression();
getWS();
if (_source.charCodeAt(_index) !== 63) { // ?
return exp;
}
++_index;
getWS();
var consequent = getExpression();
getWS();
if (_source.charCodeAt(_index) !== 58) { // :
throw error('Expected ":"');
}
++_index;
getWS();
return {
type: 'ConditionalExpression',
test: exp,
consequent: consequent,
alternate: getExpression()
};
}
function getOrExpression() {
return getPrefixExpression([['|'], '|', true],
'LogicalExpression',
'LogicalOperator',
getAndExpression);
}
function getAndExpression() {
return getPrefixExpression([['&'], '&', true],
'LogicalExpression',
'Logicalperator',
getEqualityExpression);
}
function getEqualityExpression() {
return getPrefixExpression([['=', '!'], '=', true],
'BinaryExpression',
'BinaryOperator',
getRelationalExpression);
}
function getRelationalExpression() {
return getPrefixExpression([['<', '>'], '=', false],
'BinaryExpression',
'BinaryOperator',
getAdditiveExpression);
}
function getAdditiveExpression() {
return getPrefixExpression([['+', '-']],
'BinaryExpression',
'BinaryOperator',
getModuloExpression);
}
function getModuloExpression() {
return getPrefixExpression([['%']],
'BinaryExpression',
'BinaryOperator',
getMultiplicativeExpression);
}
function getMultiplicativeExpression() {
return getPrefixExpression([['*']],
'BinaryExpression',
'BinaryOperator',
getDividiveExpression);
}
function getDividiveExpression() {
return getPrefixExpression([['/']],
'BinaryExpression',
'BinaryOperator',
getUnaryExpression);
}
function getUnaryExpression() {
return getPostfixExpression([43, 45, 33], // + - !
'UnaryExpression',
'UnaryOperator',
getMemberExpression);
}
function getCallExpression(callee) {
getWS();
return {
type: 'CallExpression',
callee: callee,
arguments: getItemList(getExpression, ')')
};
}
function getAttributeExpression(idref, computed) {
if (idref.type !== 'ParenthesisExpression' &&
idref.type !== 'Identifier' &&
idref.type !== 'ThisExpression') {
throw error('AttributeExpression must have Identifier, This or ' +
'Parenthesis as left node');
}
var exp;
if (computed) {
getWS();
exp = getExpression();
getWS();
if (_source.charAt(_index) !== ']') {
throw error('Expected "]"');
}
++_index;
return {
type: 'AttributeExpression',
expression: idref,
attribute: exp,
computed: true
};
}
exp = getIdentifier();
return {
type: 'AttributeExpression',
expression: idref,
attribute: exp,
computed: false
};
}
function getPropertyExpression(idref, computed) {
var exp;
if (computed) {
getWS();
exp = getExpression();
getWS();
if (_source.charAt(_index) !== ']') {
throw error('Expected "]"');
}
++_index;
return {
type: 'PropertyExpression',
expression: idref,
property: exp,
computed: true
};
}
exp = getIdentifier();
return {
type: 'PropertyExpression',
expression: idref,
property: exp,
computed: false
};
}
function getMemberExpression() {
var exp = getParenthesisExpression();
var cc;
// 46: '.'
// 40: '('
// 58: ':'
// 91: '['
while (true) {
cc = _source.charCodeAt(_index);
if (cc === 46 || cc === 91) { // . or [
++_index;
exp = getPropertyExpression(exp, cc === 91);
} else if (cc === 58 &&
_source.charCodeAt(_index + 1) === 58) { // ::
_index += 2;
if (_source.charCodeAt(_index) === 91) { // [
++_index;
exp = getAttributeExpression(exp, true);
} else {
exp = getAttributeExpression(exp, false);
}
} else if (cc === 40) { // (
++_index;
exp = getCallExpression(exp);
} else {
break;
}
}
return exp;
}
function getParenthesisExpression() {
// 40 == (
if (_source.charCodeAt(_index) === 40) {
++_index;
getWS();
var pexp = {
type: 'ParenthesisExpression',
expression: getExpression()
};
getWS();
if (_source.charCodeAt(_index) !== 41) {
throw error('Expected ")"');
}
++_index;
return pexp;
}
return getPrimaryExpression();
}
function getPrimaryExpression() {
var pos = _index;
var cc = _source.charCodeAt(pos);
// number
while (cc > 47 && cc < 58) {
cc = _source.charCodeAt(++pos);
}
if (pos > _index) {
var start = _index;
_index = pos;
return {
type: 'Number',
value: parseInt(_source.slice(start, pos), 10)
};
}
switch (cc) {
// value: '"{[
case 39:
case 34:
case 123:
case 91:
return getValue();
// variable: $
case 36:
return getVariable();
// globals: @
case 64:
++_index;
return {
type: 'GlobalsExpression',
id: getIdentifier()
};
// this: ~
case 126:
++_index;
return {
type: 'ThisExpression'
};
default:
return getIdentifier();
}
}
/* helper functions */
function getItemList(callback, closeChar) {
var ch;
getWS();
if (_source.charAt(_index) === closeChar) {
++_index;
return [];
}
var items = [];
while (true) {
items.push(callback());
getWS();
ch = _source.charAt(_index);
if (ch === ',') {
++_index;
getWS();
} else if (ch === closeChar) {
++_index;
break;
} else {
throw error('Expected "," or "' + closeChar + '"');
}
}
return items;
}
function error(message, pos) {
if (pos === undefined) {
pos = _index;
}
var start = _source.lastIndexOf('<', pos - 1);
var lastClose = _source.lastIndexOf('>', pos - 1);
start = lastClose > start ? lastClose + 1 : start;
var context = _source.slice(start, pos + 10);
var msg = message + ' at pos ' + pos + ': "' + context + '"';
return new ParserError(msg, pos, context);
}
// This code is being called whenever we
// hit ParserError.
//
// The strategy here is to find the closest entry opening
// and skip forward to it.
//
// It may happen that the entry opening is in fact part of expression,
// but this should just trigger another ParserError on the next char
// and we'll have to scan for entry opening again until we're successful
// or we run out of entry openings in the code.
function recover() {
var opening = _source.indexOf('<', _index);
var junk;
if (opening === -1) {
junk = {
'type': 'JunkEntry',
'content': _source.slice(_index)
};
_index = _length;
return junk;
}
junk = {
'type': 'JunkEntry',
'content': _source.slice(_index, opening)
};
_index = opening;
return junk;
}
}
/* ParserError class */
Parser.Error = ParserError;
function ParserError(message, pos, context) {
this.name = 'ParserError';
this.message = message;
this.pos = pos;
this.context = context;
}
ParserError.prototype = Object.create(Error.prototype);
ParserError.prototype.constructor = ParserError;
exports.Parser = Parser;
});
// This is L20n's on-the-fly compiler. It takes the AST produced by the parser
// and uses it to create a set of JavaScript objects and functions representing
// entities and macros and other expressions.
//
// The module defines a `Compiler` singleton with a single method: `compile`.
// The result of the compilation is stored on the `entries` object passed as
// the second argument to the `compile` function. The third argument is
// `globals`, an object whose properties provide information about the runtime
// environment, e.g., the current hour, operating system etc.
//
// Main concepts
// -------------
//
// **Entities** and **attributes** are objects which are publicly available.
// Their `toString` method is designed to be used by the L20n context to get
// a string value of the entity, given the context data passed to the method.
//
// All other symbols defined by the grammar are implemented as expression
// functions. The naming convention is:
//
// - capitalized first letters denote **expressions constructors**, e.g.
// `PropertyExpression`.
// - camel-case denotes **expression functions** returned by the
// constructors, e.g. `propertyExpression`.
//
// ### Constructors
//
// The constructor is called for every node in the AST. It stores the
// components of the expression which are constant and do not depend on the
// calling context (an example of the latter would be the data passed by the
// developer to the `toString` method).
//
// ### Expression functions
//
// The constructor, when called, returns an expression function, which, in
// turn, is called every time the expression needs to be evaluated. The
// evaluation call is context-dependend. Every expression function takes two
// mandatory arguments and one optional one:
//
// - `locals`, which stores the information about the currently evaluated
// entity (`locals.__this__`). It also stores the arguments passed to macros.
// - `ctxdata`, which is an object with data passed to the context by the
// developer. The developer can define data on the context, or pass it on
// a per-call basis.
// - `key` (optional), which is a number or a string passed to a `HashLiteral`
// expression denoting the member of the hash to return. The member will be
// another expression function which can then be evaluated further.
//
//
// Bubbling up the new _current_ entity
// ------------------------------------
//
// Every expression function returns an array [`newLocals`, `evaluatedValue`].
// The reason for this, and in particular for returning `newLocals`, is
// important for understanding how the compiler works.
//
// In most of the cases. `newLocals` will be the same as the original `locals`
// passed to the expression function during the evaluation call. In some
// cases, however, `newLocals.__this__` will reference a different entity than
// `locals.__this__` did. On runtime, as the compiler traverses the AST and
// goes deeper into individual branches, when it hits an `identifier` and
// evaluates it to an entity, it needs to **bubble up** this find back to the
// top expressions in the chain. This is so that the evaluation of the
// top-most expressions in the branch (root being at the very top of the tree)
// takes into account the new value of `__this__`.
//
// To illustrate this point, consider the following example.
//
// Two entities, `brandName` and `about` are defined as such:
//
// <brandName {
// short: "Firefox",
// long: "Mozilla {{ ~ }}"
// }>
// <about "About {{ brandName.long }}">
//
// Notice two `complexString`s: `about` references `brandName.long`, and
// `brandName.long` references its own entity via `~`. This `~` (meaning, the
// current entity) must always reference `brandName`, even when called from
// `about`.
//
// The AST for the `about` entity looks like this:
//
// [Entity]
// .id[Identifier]
// .name[unicode "about"]
// .index
// .value[ComplexString] <1>
// .content
// [String] <2>
// .content[unicode "About "]
// [PropertyExpression] <3>
// .expression[Identifier] <4>
// .name[unicode "brandName"]
// .property[Identifier]
// .name[unicode "long"]
// .computed[bool=False]
// .attrs
// .local[bool=False]
//
// During the compilation the compiler will walk the AST top-down to the
// deepest terminal leaves and will use expression constructors to create
// expression functions for the components. For instance, for `about`'s value,
// the compiler will call `ComplexString()` to create an expression function
// `complexString` <1> which will be assigned to the entity's value. The
// `ComplexString` construtor, before it returns the `complexString` <1>, will
// in turn call other expression constructors to create `content`:
// a `stringLiteral` and a `propertyExpression`. The `PropertyExpression`
// contructor will do the same, etc...
//
// When `entity.getString(ctxdata)` is called by a third-party code, we need to
// resolve the whole `complexString` <1> to return a single string value. This
// is what **resolving** means and it involves some recursion. On the other
// hand, **evaluating** means _to call the expression once and use what it
// returns_.
//
// The identifier expression sets `locals.__this__` to the current entity,
// `about`, and tells the `complexString` <1> to _resolve_ itself.
//
// In order to resolve the `complexString` <1>, we start by resolving its first
// member <2> to a string. As we resolve deeper down, we bubble down `locals`
// set by `toString`. The first member of `content` turns out to simply be
// a string that reads `About `.
//
// On to the second member, the propertyExpression <3>. We bubble down
// `locals` again and proceed to evaluate the `expression` field, which is an
// `identifier`. Note that we don't _resolve_ it to a string; we _evaluate_ it
// to something that can be further used in other expressions, in this case, an
// **entity** called `brandName`.
//
// Had we _resolved_ the `propertyExpression`, it would have resolve to
// a string, and it would have been impossible to access the `long` member.
// This leads us to an important concept: the compiler _resolves_ expressions
// when it expects a primitive value (a string, a number, a bool). On the
// other hand, it _evaluates_ expressions (calls them only once) when it needs
// to work with them further, e.g. in order to access a member of the hash.
//
// This also explains why in the above example, once the compiler hits the
// `brandName` identifier and changes the value of `locals.__this__` to the
// `brandName` entity, this value doesn't bubble up all the way up to the
// `about` entity. All components of any `complexString` are _resolved_ by the
// compiler until a primitive value is returned. This logic lives in the
// `_resolve` function.
define('l20n/compiler', function(require, exports) {
// TODO change newcap to true?
/* jshint strict: false, newcap: false */
var EventEmitter = require('./events').EventEmitter;
function Compiler() {
// Public
this.compile = compile;
this.setGlobals = setGlobals;
this.addEventListener = addEvent;
this.removeEventListener = removeEvent;
// Private
var MAX_PLACEABLE_LENGTH = 2500;
var _emitter = new EventEmitter();
var _globals = null;
var _references = {
globals: {}
};
var _entryTypes = {
Entity: Entity,
Macro: Macro
};
// Public API functions
function compile(ast, env) {
if (!env) {
env = {};
}
for (var i = 0, entry; entry = ast.body[i]; i++) {
var Constructor = _entryTypes[entry.type];
if (Constructor) {
try {
env[entry.id.name] = new Constructor(entry, env);
} catch (e) {
// rethrow non-compiler errors;
requireCompilerError(e);
// or, just ignore the error; it's been already emitted
}
}
}
return env;
}
function setGlobals(globals) {
_globals = globals;
return true;
}
function addEvent(type, listener) {
return _emitter.addEventListener(type, listener);
}
function removeEvent(type, listener) {
return _emitter.removeEventListener(type, listener);
}
// utils
function emit(Ctor, message, entry, source) {
var e = new Ctor(message, entry, source);
_emitter.emit('error', e);
return e;
}
// The Entity object.
function Entity(node, env) {
this.id = node.id.name;
this.env = env;
this.local = node.local || false;
this.index = null;
this.attributes = null;
this.publicAttributes = null;
var i;
if (node.index) {
this.index = [];
for (i = 0; i < node.index.length; i++) {
this.index.push(IndexExpression(node.index[i], this));
}
}
if (node.attrs) {
this.attributes = {};
this.publicAttributes = [];
for (i = 0; i < node.attrs.length; i++) {
var attr = node.attrs[i];
this.attributes[attr.key.name] = new Attribute(attr, this);
if (!attr.local) {
this.publicAttributes.push(attr.key.name);
}
}
}
// Bug 817610 - Optimize a fast path for String entities in the Compiler
if (node.value && node.value.type === 'String') {
this.value = node.value.content;
} else {
this.value = LazyExpression(node.value, this, this.index);
}
}
Entity.prototype.getString = function E_getString(ctxdata) {
try {
var locals = {
__this__: this,
__env__: this.env
};
return _resolve(this.value, locals, ctxdata);
} catch (e) {
requireCompilerError(e);
// `ValueErrors` are not emitted in `StringLiteral` where they are
// created, because if the string in question is being evaluated in an
// index, we'll emit an `IndexError` instead. To avoid duplication,
// `ValueErrors` are only be emitted if they actually make it to
// here. See `IndexExpression` for an example of why they wouldn't.
if (e instanceof ValueError) {
_emitter.emit('error', e);
}
throw e;
}
};
Entity.prototype.get = function E_get(ctxdata) {
// reset `_references` to an empty state
_references.globals = {};
// evaluate the entity and its attributes; if any globals are used in
// the process, `toString` will populate `_references.globals`
// accordingly.
var entity = {
value: this.getString(ctxdata),
attributes: {}
};
if (this.publicAttributes) {
entity.attributes = {};
for (var i = 0, attr; attr = this.publicAttributes[i]; i++) {
entity.attributes[attr] = this.attributes[attr].getString(ctxdata);
}
}
entity.globals = _references.globals;
return entity;
};
function Attribute(node, entity) {
this.key = node.key.name;
this.local = node.local || false;
this.index = null;
if (node.index) {
this.index = [];
for (var i = 0; i < node.index.length; i++) {
this.index.push(IndexExpression(node.index[i], this));
}
}
if (node.value && node.value.type === 'String') {
this.value = node.value.content;
} else {
this.value = LazyExpression(node.value, entity, this.index);
}
this.entity = entity;
}
Attribute.prototype.getString = function A_getString(ctxdata) {
try {
var locals = {
__this__: this.entity,
__env__: this.entity.env
};
return _resolve(this.value, locals, ctxdata);
} catch (e) {
requireCompilerError(e);
if (e instanceof ValueError) {
_emitter.emit('error', e);
}
throw e;
}
};
function Macro(node, env) {
this.id = node.id.name;
this.env = env;
this.local = node.local || false;
this.expression = LazyExpression(node.expression, this);
this.args = node.args;
}
Macro.prototype._call = function M_call(args, ctxdata) {
var locals = {
__this__: this,
__env__: this.env
};
// the number of arguments passed must equal the macro's arity
if (this.args.length !== args.length) {
throw new RuntimeError(this.id + '() takes exactly ' +
this.args.length + ' argument(s) (' +
args.length + ' given)');
}
for (var i = 0; i < this.args.length; i++) {
locals[this.args[i].id.name] = args[i];
}
var final = this.expression(locals, ctxdata);
locals = final[0];
final = final[1];
return [locals, _resolve(final, locals, ctxdata)];
};
var EXPRESSION_TYPES = {
// Primary expressions.
'Identifier': Identifier,
'ThisExpression': ThisExpression,
'VariableExpression': VariableExpression,
'GlobalsExpression': GlobalsExpression,
// Value expressions.
'Number': NumberLiteral,
'String': StringLiteral,
'Hash': HashLiteral,
'HashItem': Expression,
'ComplexString': ComplexString,
// Logical expressions.
'UnaryExpression': UnaryExpression,
'BinaryExpression': BinaryExpression,
'LogicalExpression': LogicalExpression,
'ConditionalExpression': ConditionalExpression,
// Member expressions.
'CallExpression': CallExpression,
'PropertyExpression': PropertyExpression,
'AttributeExpression': AttributeExpression,
'ParenthesisExpression': ParenthesisExpression
};
// The 'dispatcher' expression constructor. Other expression constructors
// call this to create expression functions for their components. For
// instance, `ConditionalExpression` calls `Expression` to create
// expression functions for its `test`, `consequent` and `alternate`
// symbols.
function Expression(node, entry, index) {
// An entity can have no value. It will be resolved to `null`.
if (!node) {
return null;
}
if (!EXPRESSION_TYPES[node.type]) {
throw emit('CompilationError', 'Unknown expression type' + node.type);
}
if (index) {
index = index.slice();
}
return EXPRESSION_TYPES[node.type](node, entry, index);
}
function LazyExpression(node, entry, index) {
// An entity can have no value. It will be resolved to `null`.
if (!node) {
return null;
}
var expr;
return function(locals, ctxdata, prop) {
if (!expr) {
expr = Expression(node, entry, index);
}
return expr(locals, ctxdata, prop);
};
}
function _resolve(expr, locals, ctxdata) {
// Bail out early if it's a primitive value or `null`. This is exactly
// what we want.
if (typeof expr === 'string' ||
typeof expr === 'boolean' ||
typeof expr === 'number' ||
!expr) {
return expr;
}
// Check if `expr` is an Entity or an Attribute
if (expr.value !== undefined) {
return _resolve(expr.value, locals, ctxdata);
}
// Check if `expr` is an expression
if (typeof expr === 'function') {
var current = expr(locals, ctxdata);
locals = current[0];
current = current[1];
return _resolve(current, locals, ctxdata);
}
// Throw if `expr` is a macro
if (expr.expression) {
throw new RuntimeError('Uncalled macro: ' + expr.id);
}
// Throw if `expr` is a non-primitive from ctxdata or a global
throw new RuntimeError('Cannot resolve ctxdata or global of type ' +
typeof expr);
}
function Identifier(node) {
var name = node.name;
return function identifier(locals) {
if (!locals.__env__.hasOwnProperty(name)) {
throw new RuntimeError('Reference to an unknown entry: ' + name);
}
// The only thing we care about here is the new `__this__` so we
// discard any other local variables. Note that because this is an
// assignment to a local variable, the original `locals` passed is not
// changed.
locals = {
__this__: locals.__env__[name],
__env__: locals.__env__
};
return [locals, locals.__this__];
};
}
function ThisExpression() {
return function thisExpression(locals) {
return [locals, locals.__this__];
};
}
function VariableExpression(node) {
var name = node.id.name;
return function variableExpression(locals, ctxdata) {
if (locals.hasOwnProperty(name)) {
// locals[name] is already a [locals, value] tuple on its own
return locals[name];
}
if (!ctxdata || !ctxdata.hasOwnProperty(name)) {
throw new RuntimeError('Reference to an unknown variable: ' + name);
}
return [locals, ctxdata[name]];
};
}
function GlobalsExpression(node) {
var name = node.id.name;
return function globalsExpression(locals) {
if (!_globals) {
throw new RuntimeError('No globals set (tried @' + name + ')');
}
if (!_globals.hasOwnProperty(name)) {
throw new RuntimeError('Reference to an unknown global: ' + name);
}
var value;
try {
value = _globals[name].get();
} catch (e) {
throw new RuntimeError('Cannot evaluate global ' + name);
}
_references.globals[name] = true;
return [locals, value];
};
}
function NumberLiteral(node) {
return function numberLiteral(locals) {
return [locals, node.value];
};
}
function StringLiteral(node) {
return function stringLiteral(locals, ctxdata, key) {
// if a key was passed, throw; checking arguments is more reliable
// than testing the value of key because if the key comes from context
// data it can be any type, also undefined
if (key !== undefined) {
throw new RuntimeError('Cannot get property of a string: ' + key);
}
return [locals, node.content];
};
}
function ComplexString(node, entry) {
var content = [];
for (var i = 0; i < node.content.length; i++) {
content.push(Expression(node.content[i], entry));
}
// Every complexString needs to have its own `dirty` flag whose state
// persists across multiple calls to the given complexString. On the
// other hand, `dirty` must not be shared by all complexStrings. Hence
// the need to define `dirty` as a variable available in the closure.
// Note that the anonymous function is a self-invoked one and it returns
// the closure immediately.
return (function() {
var dirty = false;
return function complexString(locals, ctxdata, key) {
if (key !== undefined) {
throw new RuntimeError('Cannot get property of a string: ' + key);
}
if (dirty) {
throw new RuntimeError('Cyclic reference detected');
}
dirty = true;
var parts = [];
try {
for (var i = 0; i < content.length; i++) {
var part = _resolve(content[i], locals, ctxdata);
if (typeof part !== 'string' && typeof part !== 'number') {
throw new RuntimeError('Placeables must be strings or ' +
'numbers');
}
if (part.length > MAX_PLACEABLE_LENGTH) {
throw new RuntimeError('Placeable has too many characters, ' +
'maximum allowed is ' +
MAX_PLACEABLE_LENGTH);
}
parts.push(part);
}
} catch (e) {
requireCompilerError(e);
// only throw, don't emit yet. If the `ValueError` makes it to
// `getString()` it will be emitted there. It might, however, be
// cought by `IndexExpression` and changed into a `IndexError`.
// See `IndexExpression` for more explanation.
throw new ValueError(e.message, entry, node.source);
} finally {
dirty = false;
}
return [locals, parts.join('')];
};
})();
}
function IndexExpression(node, entry) {
var expression = Expression(node, entry);
// This is analogous to `ComplexString` in that an individual index can
// only be visited once during the resolution of an Entity. `dirty` is
// set in a closure context of the returned function.
return (function() {
var dirty = false;
return function indexExpression(locals, ctxdata) {
if (dirty) {
throw new RuntimeError('Cyclic reference detected');
}
dirty = true;
var retval;
try {
// We need to resolve `expression` here so that we catch errors
// thrown deep within. Without `_resolve` we might end up with an
// unresolved Entity object, and no "Cyclic reference detected"
// error would be thown.
retval = _resolve(expression, locals, ctxdata);
} catch (e) {
// If it's an `IndexError` thrown deeper within `expression`, it
// has already been emitted by its `indexExpression`. We can
// safely re-throw it here.
if (e instanceof IndexError) {
throw e;
}
// Otherwise, make sure it's a `RuntimeError` or a `ValueError` and
// throw and emit an `IndexError`.
//
// If it's a `ValueError` we want to replace it by an `IndexError`
// here so that `ValueErrors` from the index don't make their way
// up to the context. The context only cares about ValueErrors
// thrown by the value of the entity it has requested, not entities
// used in the index.
//
// To illustrate this point with an example, consider the following
// two strings, where `foo` is a missing entity.
//
// <prompt1["remove"] {
// remove: "Remove {{ foo }}?",
// keep: "Keep {{ foo }}?"
// }>
//
// `prompt1` will throw a `ValueError`. The context can use it to
// display the source of the entity, i.e. `Remove {{ foo }}?`. The
// index resolved properly, so at least we know that we're showing
// the right variant of the entity.
//
// <prompt2["{{ foo }}"] {
// remove: "Remove file?",
// keep: "Keep file?"
// }>
//
// On the other hand, `prompt2` will throw an `IndexError`. This
// is a more serious scenario for the context. We should not
// assume that we know which variant to show to the user. In fact,
// in the above (much contrived, but still) example, showing the
// incorrect variant will likely lead to data loss. The context
// should be more strict in this case and should not try to recover
// from this error too hard.
requireCompilerError(e);
throw emit(IndexError, e.message, entry);
} finally {
dirty = false;
}
return [locals, retval];
};
})();
}
function HashLiteral(node, entry, index) {
var content = {};
// if absent, `defaultKey` and `defaultIndex` are undefined
var defaultKey;
var defaultIndex = index ? index.shift() : undefined;
for (var i = 0; i < node.content.length; i++) {
var elem = node.content[i];
// use `elem.value` to skip `HashItem` and create the value right away
content[elem.key.name] = Expression(elem.value, entry, index);
if (elem.default) {
defaultKey = elem.key.name;
}
}
return function hashLiteral(locals, ctxdata, prop) {
var keysToTry = [prop, defaultIndex, defaultKey];
var keysTried = [];
for (var i = 0; i < keysToTry.length; i++) {
var key = _resolve(keysToTry[i], locals, ctxdata);
if (key === undefined) {
continue;
}
if (typeof key !== 'string') {
throw emit(IndexError, 'Index must be a string', entry);
}
keysTried.push(key);
if (content.hasOwnProperty(key)) {
return [locals, content[key]];
}
}
// If no valid key was found, throw an `IndexError`
var message;
if (keysTried.length) {
message = 'Hash key lookup failed ' +
'(tried "' + keysTried.join('", "') + '").';
} else {
message = 'Hash key lookup failed.';
}
throw emit(IndexError, message, entry);
};
}
function UnaryOperator(token, entry) {
if (token === '-') return function negativeOperator(argument) {
if (typeof argument !== 'number') {
throw new RuntimeError('The unary - operator takes a number');
}
return -argument;
};
if (token === '+') return function positiveOperator(argument) {
if (typeof argument !== 'number') {
throw new RuntimeError('The unary + operator takes a number');
}
return +argument;
};
if (token === '!') return function notOperator(argument) {
if (typeof argument !== 'boolean') {
throw new RuntimeError('The ! operator takes a boolean');
}
return !argument;
};
throw emit(CompilationError, 'Unknown token: ' + token, entry);
}
function BinaryOperator(token, entry) {
if (token === '==') return function equalOperator(left, right) {
if ((typeof left !== 'number' || typeof right !== 'number') &&
(typeof left !== 'string' || typeof right !== 'string')) {
throw new RuntimeError('The == operator takes two numbers or ' +
'two strings');
}
return left === right;
};
if (token === '!=') return function notEqualOperator(left, right) {
if ((typeof left !== 'number' || typeof right !== 'number') &&
(typeof left !== 'string' || typeof right !== 'string')) {
throw new RuntimeError('The != operator takes two numbers or ' +
'two strings');
}
return left !== right;
};
if (token === '<') return function lessThanOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The < operator takes two numbers');
}
return left < right;
};
if (token === '<=') return function lessThanEqualOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The <= operator takes two numbers');
}
return left <= right;
};
if (token === '>') return function greaterThanOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The > operator takes two numbers');
}
return left > right;
};
if (token === '>=') {
return function greaterThanEqualOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The >= operator takes two numbers');
}
return left >= right;
};
}
if (token === '+') return function addOperator(left, right) {
if ((typeof left !== 'number' || typeof right !== 'number') &&
(typeof left !== 'string' || typeof right !== 'string')) {
throw new RuntimeError('The + operator takes two numbers or ' +
'two strings');
}
return left + right;
};
if (token === '-') return function substractOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The - operator takes two numbers');
}
return left - right;
};
if (token === '*') return function multiplyOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The * operator takes two numbers');
}
return left * right;
};
if (token === '/') return function devideOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The / operator takes two numbers');
}
if (right === 0) {
throw new RuntimeError('Division by zero not allowed.');
}
return left / right;
};
if (token === '%') return function moduloOperator(left, right) {
if (typeof left !== 'number' || typeof right !== 'number') {
throw new RuntimeError('The % operator takes two numbers');
}
if (right === 0) {
throw new RuntimeError('Modulo zero not allowed.');
}
return left % right;
};
throw emit(CompilationError, 'Unknown token: ' + token, entry);
}
function LogicalOperator(token, entry) {
if (token === '&&') return function andOperator(left, right) {
if (typeof left !== 'boolean' || typeof right !== 'boolean') {
throw new RuntimeError('The && operator takes two booleans');
}
return left && right;
};
if (token === '||') return function orOperator(left, right) {
if (typeof left !== 'boolean' || typeof right !== 'boolean') {
throw new RuntimeError('The || operator takes two booleans');
}
return left || right;
};
throw emit(CompilationError, 'Unknown token: ' + token, entry);
}
function UnaryExpression(node, entry) {
var operator = UnaryOperator(node.operator.token, entry);
var argument = Expression(node.argument, entry);
return function unaryExpression(locals, ctxdata) {
return [locals, operator(_resolve(argument, locals, ctxdata))];
};
}
function BinaryExpression(node, entry) {
var left = Expression(node.left, entry);
var operator = BinaryOperator(node.operator.token, entry);
var right = Expression(node.right, entry);
return function binaryExpression(locals, ctxdata) {
return [locals, operator(
_resolve(left, locals, ctxdata),
_resolve(right, locals, ctxdata)
)];
};
}
function LogicalExpression(node, entry) {
var left = Expression(node.left, entry);
var operator = LogicalOperator(node.operator.token, entry);
var right = Expression(node.right, entry);
return function logicalExpression(locals, ctxdata) {
return [locals, operator(
_resolve(left, locals, ctxdata),
_resolve(right, locals, ctxdata)
)];
};
}
function ConditionalExpression(node, entry) {
var test = Expression(node.test, entry);
var consequent = Expression(node.consequent, entry);
var alternate = Expression(node.alternate, entry);
return function conditionalExpression(locals, ctxdata) {
var tested = _resolve(test, locals, ctxdata);
if (typeof tested !== 'boolean') {
throw new RuntimeError('Conditional expressions must test a ' +
'boolean');
}
if (tested === true) {
return consequent(locals, ctxdata);
}
return alternate(locals, ctxdata);
};
}
function CallExpression(node, entry) {
var callee = Expression(node.callee, entry);
var args = [];
for (var i = 0; i < node.arguments.length; i++) {
args.push(Expression(node.arguments[i], entry));
}
return function callExpression(locals, ctxdata) {
var evaluated_args = [];
for (var i = 0; i < args.length; i++) {
evaluated_args.push(args[i](locals, ctxdata));
}
// callee is an expression pointing to a macro, e.g. an identifier
var macro = callee(locals, ctxdata);
locals = macro[0];
macro = macro[1];
if (!macro.expression) {
throw new RuntimeError('Expected a macro, got a non-callable.');
}
// Rely entirely on the platform implementation to detect recursion.
// `Macro::_call` assigns `evaluated_args` to members of `locals`.
return macro._call(evaluated_args, ctxdata);
};
}
function PropertyExpression(node, entry) {
var expression = Expression(node.expression, entry);
var property = node.computed ?
Expression(node.property, entry) :
node.property.name;
return function propertyExpression(locals, ctxdata) {
var prop = _resolve(property, locals, ctxdata);
if (typeof prop !== 'string') {
throw new RuntimeError('Property name must evaluate to a string: ' +
prop);
}
var parent = expression(locals, ctxdata);
locals = parent[0];
parent = parent[1];
// At this point, `parent` can be anything and we need to do some
// type-checking to handle erros gracefully (bug 883664) and securely
// (bug 815962).
// If `parent` is an Entity or an Attribute, `locals` has been
// correctly set up by Identifier
if (parent && parent.value !== undefined) {
if (typeof parent.value !== 'function') {
throw new RuntimeError('Cannot get property of a ' +
typeof parent.value + ': ' + prop);
}
return parent.value(locals, ctxdata, prop);
}
// If it's a hashLiteral or stringLiteral inside a hash, just call it
if (typeof parent === 'function') {
return parent(locals, ctxdata, prop);
}
if (parent && parent.expression) {
throw new RuntimeError('Cannot get property of a macro: ' + prop);
}
// If `parent` is an object passed by the developer to the context
// (i.e., `expression` was a `VariableExpression`) or a global, return
// the member of the object corresponding to `prop`
if (typeof parent === 'object') {
if (parent === null) {
throw new RuntimeError('Cannot get property of a null: ' + prop);
}
if (Array.isArray(parent)) {
throw new RuntimeError('Cannot get property of an array: ' + prop);
}
if (!parent.hasOwnProperty(prop)) {
throw new RuntimeError(prop + ' is not defined on the object.');
}
return [locals, parent[prop]];
}
// otherwise it's a primitive
throw new RuntimeError('Cannot get property of a ' + typeof parent +
': ' + prop);
};
}
function AttributeExpression(node, entry) {
var expression = Expression(node.expression, entry);
var attribute = node.computed ?
Expression(node.attribute, entry) :
node.attribute.name;
return function attributeExpression(locals, ctxdata) {
var attr = _resolve(attribute, locals, ctxdata);
var entity = expression(locals, ctxdata);
locals = entity[0];
entity = entity[1];
if (!entity.attributes) {
throw new RuntimeError('Cannot get attribute of a non-entity: ' +
attr);
}
if (!entity.attributes.hasOwnProperty(attr)) {
throw new RuntimeError(entity.id + ' has no attribute ' + attr);
}
return [locals, entity.attributes[attr]];
};
}
function ParenthesisExpression(node, entry) {
return Expression(node.expression, entry);
}
}
Compiler.Error = CompilerError;
Compiler.CompilationError = CompilationError;
Compiler.RuntimeError = RuntimeError;
Compiler.ValueError = ValueError;
Compiler.IndexError = IndexError;
// `CompilerError` is a general class of errors emitted by the Compiler.
function CompilerError(message) {
this.name = 'CompilerError';
this.message = message;
}
CompilerError.prototype = Object.create(Error.prototype);
CompilerError.prototype.constructor = CompilerError;
// `CompilationError` extends `CompilerError`. It's a class of errors
// which happen during compilation of the AST.
function CompilationError(message, entry) {
CompilerError.call(this, message);
this.name = 'CompilationError';
this.entry = entry.id;
}
CompilationError.prototype = Object.create(CompilerError.prototype);
CompilationError.prototype.constructor = CompilationError;
// `RuntimeError` extends `CompilerError`. It's a class of errors which
// happen during the evaluation of entries, i.e. when you call
// `entity.toString()`.
function RuntimeError(message) {
CompilerError.call(this, message);
this.name = 'RuntimeError';
}
RuntimeError.prototype = Object.create(CompilerError.prototype);
RuntimeError.prototype.constructor = RuntimeError;
// `ValueError` extends `RuntimeError`. It's a class of errors which
// happen during the composition of a ComplexString value. It's easier to
// recover from than an `IndexError` because at least we know that we're
// showing the correct member of the hash.
function ValueError(message, entry, source) {
RuntimeError.call(this, message);
this.name = 'ValueError';
this.entry = entry.id;
this.source = source;
}
ValueError.prototype = Object.create(RuntimeError.prototype);
ValueError.prototype.constructor = ValueError;
// `IndexError` extends `RuntimeError`. It's a class of errors which
// happen during the lookup of a hash member. It's harder to recover
// from than `ValueError` because we en dup not knowing which variant of the
// entity value to show and in case the meanings are divergent, the
// consequences for the user can be serious.
function IndexError(message, entry) {
RuntimeError.call(this, message);
this.name = 'IndexError';
this.entry = entry.id;
}
IndexError.prototype = Object.create(RuntimeError.prototype);
IndexError.prototype.constructor = IndexError;
function requireCompilerError(e) {
if (!(e instanceof CompilerError)) {
throw e;
}
}
exports.Compiler = Compiler;
});
define('l20n/retranslation', function(require, exports) {
'use strict';
function RetranslationManager() {
var _usage = [];
var _counter = {};
var _callbacks = [];
this.bindGet = bindGet;
this.unbindGet = unbindGet;
this.all = all;
this.globals = {};
RetranslationManager._constructors.forEach(function(ctor) {
initGlobal.call(this, ctor);
}, this);
function initGlobal(GlobalCtor) {
/* jshint validthis: true */
var global = new GlobalCtor();
this.globals[global.id] = global;
if (!global.activate) {
return;
}
_counter[global.id] = 0;
global.addEventListener('change', function(id) {
for (var i = 0; i < _usage.length; i++) {
if (_usage[i] && _usage[i].globals.indexOf(id) !== -1) {
// invoke the callback with the reason
_usage[i].callback({
global: global.id
});
}
}
});
}
function bindGet(get, isRebind) {
/* jshint validthis: true */
var i;
// store the callback in case we want to retranslate the whole context
var inCallbacks;
for (i = 0; i < _callbacks.length; i++) {
if (_callbacks[i].id === get.id) {
inCallbacks = true;
break;
}
}
if (!inCallbacks) {
_callbacks.push(get);
} else if (isRebind) {
_callbacks[i] = get;
}
// handle the global usage
var bound;
for (i = 0; i < _usage.length; i++) {
if (_usage[i] && _usage[i].id === get.id) {
bound = _usage[i];
break;
}
}
var added;
if (!bound) {
// it's the first time we see this get
if (get.globals.length !== 0) {
_usage.push(get);
get.globals.forEach(function(id) {
if (!this.globals[id].activate) {
return;
}
_counter[id]++;
this.globals[id].activate();
}, this);
}
} else if (isRebind) {
// if we rebinding the callback, don't remove globals
// because we're just adding new entities to the bind
bound.callback = get.callback;
added = get.globals.filter(function(id) {
return this.globals[id].activate && bound.globals.indexOf(id) === -1;
}, this);
added.forEach(function(id) {
_counter[id]++;
this.globals[id].activate();
}, this);
bound.globals = bound.globals.concat(added);
} else if (get.globals.length === 0) {
// after a retranslation, no globals were used; remove the callback
delete _usage[i];
} else {
// see which globals were added and which ones were removed
added = get.globals.filter(function(id) {
return this.globals[id].activate && bound.globals.indexOf(id) === -1;
}, this);
added.forEach(function(id) {
_counter[id]++;
this.globals[id].activate();
}, this);
var removed = bound.globals.filter(function(id) {
return this.globals[id].activate && get.globals.indexOf(id) === -1;
}, this);
removed.forEach(function(id) {
_counter[id]--;
if (_counter[id] === 0) {
this.globals[id].deactivate();
}
}, this);
bound.globals = get.globals;
}
}
function unbindGet(id) {
/* jshint validthis: true */
var bound;
var usagePos = -1;
for (var i = 0; i < _usage.length; i++) {
if (_usage[i] && _usage[i].id === id) {
bound = _usage[i];
usagePos = i;
break;
}
}
if (bound) {
bound.globals.forEach(function(id) {
if (this.globals[id].activate) {
_counter[id]--;
if (_counter[id] === 0) {
this.globals[id].deactivate();
}
}
}, this);
_usage.splice(usagePos, 1);
for (i = 0; i < _callbacks.length; i++) {
if (_callbacks[i].id === id) {
_callbacks.splice(i, 1);
break;
}
}
}
}
function all(locales) {
for (var i = 0; i < _callbacks.length; i++) {
// invoke the callback with the reason
_callbacks[i].callback({
locales: locales
});
}
}
}
RetranslationManager._constructors = [];
RetranslationManager.registerGlobal = function(ctor) {
RetranslationManager._constructors.push(ctor);
};
RetranslationManager.deregisterGlobal = function(ctor) {
var pos = RetranslationManager._constructors.indexOf(ctor);
if (pos !== -1) {
RetranslationManager._constructors.splice(pos, 1);
}
};
exports.RetranslationManager = RetranslationManager;
});
define('l20n/platform/globals', function(require, exports) {
'use strict';
var EventEmitter = require('../events').EventEmitter;
var RetranslationManager = require('../retranslation').RetranslationManager;
function Global() {
this.id = null;
this._emitter = new EventEmitter();
this.value = null;
this.isActive = false;
}
Global.prototype._get = function _get() {
throw new Error('Not implemented');
};
Global.prototype.get = function get() {
// invalidate the cached value if the global is not active; active
// globals handle `value` automatically in `onchange()`
if (!this.value || !this.isActive) {
this.value = this._get();
}
return this.value;
};
Global.prototype.addEventListener = function(type, listener) {
if (type !== 'change') {
throw 'Unknown event type';
}
this._emitter.addEventListener(type, listener);
};
// XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=865226
// We want to have @screen.width, but since we can't get it from compiler, we
// call it @screen and in order to keep API forward-compatible with 1.0 we
// return an object with key width to
// make it callable as @screen.width
function ScreenGlobal() {
Global.call(this);
this.id = 'screen';
this._get = _get;
this.activate = function activate() {
if (!this.isActive) {
window.addEventListener('resize', onchange);
this.isActive = true;
}
};
this.deactivate = function deactivate() {
window.removeEventListener('resize', onchange);
this.value = null;
this.isActive = false;
};
function _get() {
return {
width: {
px: document.body.clientWidth
}
};
}
var self = this;
function onchange() {
self.value = _get();
self._emitter.emit('change', self.id);
}
}
ScreenGlobal.prototype = Object.create(Global.prototype);
ScreenGlobal.prototype.constructor = ScreenGlobal;
function OSGlobal() {
Global.call(this);
this.id = 'os';
this.get = get;
function get() {
if (/^MacIntel/.test(navigator.platform)) {
return 'mac';
}
if (/^Linux/.test(navigator.platform)) {
return 'linux';
}
if (/^Win/.test(navigator.platform)) {
return 'win';
}
return 'unknown';
}
}
OSGlobal.prototype = Object.create(Global.prototype);
OSGlobal.prototype.constructor = OSGlobal;
function HourGlobal() {
Global.call(this);
this.id = 'hour';
this._get = _get;
this.activate = function activate() {
if (!this.isActive) {
var time = new Date();
I = setTimeout(function() {
onchange();
I = setInterval(onchange, interval);
}, interval - (time.getTime() % interval));
this.isActive = true;
}
};
this.deactivate = function deactivate() {
clearInterval(I);
this.value = null;
this.isActive = false;
};
function _get() {
var time = new Date();
return time.getHours();
}
var self = this;
var interval = 60 * 60 * 1000;
var I = null;
function onchange() {
var time = new Date();
if (time.getHours() !== self.value) {
self.value = time.getHours();
self._emitter.emit('change', self.id);
}
}
}
HourGlobal.prototype = Object.create(Global.prototype);
HourGlobal.prototype.constructor = HourGlobal;
RetranslationManager.registerGlobal(ScreenGlobal);
RetranslationManager.registerGlobal(OSGlobal);
RetranslationManager.registerGlobal(HourGlobal);
exports.Global = Global;
});
define('l20n/platform/io', function(require, exports) {
'use strict';
exports.load = function load(url, callback, sync) {
var xhr = new XMLHttpRequest();
if (xhr.overrideMimeType) {
xhr.overrideMimeType('text/plain');
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 0) {
callback(null, xhr.responseText);
} else {
var ex = new IOError('Not found: ' + url);
callback(ex);
}
}
};
xhr.open('GET', url, !sync);
xhr.send('');
};
function IOError(message) {
this.name = 'IOError';
this.message = message;
}
IOError.prototype = Object.create(Error.prototype);
IOError.prototype.constructor = IOError;
exports.Error = IOError;
});
define('l20n/intl', function(require, exports, module) {
'use strict';
if (!String.prototype.startsWith) {
Object.defineProperty(String.prototype, 'startsWith', {
enumerable: false,
configurable: false,
writable: false,
value: function (searchString, position) {
position = position || 0;
return this.indexOf(searchString, position) === position;
}
});
}
var unicodeLocaleExtensionSequence = "-u(-[a-z0-9]{2,8})+";
var unicodeLocaleExtensionSequenceRE = new RegExp(unicodeLocaleExtensionSequence);
var unicodeLocaleExtensionSequenceGlobalRE = new RegExp(unicodeLocaleExtensionSequence, "g");
var langTagMappings = {};
var langSubtagMappings = {};
var extlangMappings = {};
/**
* Regular expression defining BCP 47 language tags.
*
* Spec: RFC 5646 section 2.1.
*/
var languageTagRE = (function () {
// RFC 5234 section B.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
var ALPHA = "[a-zA-Z]";
// DIGIT = %x30-39
// ; 0-9
var DIGIT = "[0-9]";
// RFC 5646 section 2.1
// alphanum = (ALPHA / DIGIT) ; letters and numbers
var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")";
// regular = "art-lojban" ; these tags match the 'langtag'
// / "cel-gaulish" ; production, but their subtags
// / "no-bok" ; are not extended language
// / "no-nyn" ; or variant subtags: their meaning
// / "zh-guoyu" ; is defined by their registration
// / "zh-hakka" ; and all of these are deprecated
// / "zh-min" ; in favor of a more modern
// / "zh-min-nan" ; subtag or sequence of subtags
// / "zh-xiang"
var regular = "(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)";
// irregular = "en-GB-oed" ; irregular tags do not match
// / "i-ami" ; the 'langtag' production and
// / "i-bnn" ; would not otherwise be
// / "i-default" ; considered 'well-formed'
// / "i-enochian" ; These tags are all valid,
// / "i-hak" ; but most are deprecated
// / "i-klingon" ; in favor of more modern
// / "i-lux" ; subtags or subtag
// / "i-mingo" ; combination
// / "i-navajo"
// / "i-pwn"
// / "i-tao"
// / "i-tay"
// / "i-tsu"
// / "sgn-BE-FR"
// / "sgn-BE-NL"
// / "sgn-CH-DE"
var irregular = "(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)";
// grandfathered = irregular ; non-redundant tags registered
// / regular ; during the RFC 3066 era
var grandfathered = "(?:" + irregular + "|" + regular + ")";
// privateuse = "x" 1*("-" (1*8alphanum))
var privateuse = "(?:x(?:-[a-z0-9]{1,8})+)";
// singleton = DIGIT ; 0 - 9
// / %x41-57 ; A - W
// / %x59-5A ; Y - Z
// / %x61-77 ; a - w
// / %x79-7A ; y - z
var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])";
// extension = singleton 1*("-" (2*8alphanum))
var extension = "(?:" + singleton + "(?:-" + alphanum + "{2,8})+)";
// variant = 5*8alphanum ; registered variants
// / (DIGIT 3alphanum)
var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))";
// region = 2ALPHA ; ISO 3166-1 code
// / 3DIGIT ; UN M.49 code
var region = "(?:" + ALPHA + "{2}|" + DIGIT + "{3})";
// script = 4ALPHA ; ISO 15924 code
var script = "(?:" + ALPHA + "{4})";
// extlang = 3ALPHA ; selected ISO 639 codes
// *2("-" 3ALPHA) ; permanently reserved
var extlang = "(?:" + ALPHA + "{3}(?:-" + ALPHA + "{3}){0,2})";
// language = 2*3ALPHA ; shortest ISO 639 code
// ["-" extlang] ; sometimes followed by
// ; extended language subtags
// / 4ALPHA ; or reserved for future use
// / 5*8ALPHA ; or registered language subtag
var language = "(?:" + ALPHA + "{2,3}(?:-" + extlang + ")?|" + ALPHA + "{4}|" + ALPHA + "{5,8})";
// langtag = language
// ["-" script]
// ["-" region]
// *("-" variant)
// *("-" extension)
// ["-" privateuse]
var langtag = language + "(?:-" + script + ")?(?:-" + region + ")?(?:-" +
variant + ")*(?:-" + extension + ")*(?:-" + privateuse + ")?";
// Language-Tag = langtag ; normal language tags
// / privateuse ; private use tag
// / grandfathered ; grandfathered tags
var languageTag = "^(?:" + langtag + "|" + privateuse + "|" + grandfathered + ")$";
// Language tags are case insensitive (RFC 5646 section 2.1.1).
return new RegExp(languageTag, "i");
}());
var duplicateVariantRE = (function () {
// RFC 5234 section B.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
var ALPHA = "[a-zA-Z]";
// DIGIT = %x30-39
// ; 0-9
var DIGIT = "[0-9]";
// RFC 5646 section 2.1
// alphanum = (ALPHA / DIGIT) ; letters and numbers
var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")";
// variant = 5*8alphanum ; registered variants
// / (DIGIT 3alphanum)
var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))";
// Match a langtag that contains a duplicate variant.
var duplicateVariant =
// Match everything in a langtag prior to any variants, and maybe some
// of the variants as well (which makes this pattern inefficient but
// not wrong, for our purposes);
"(?:" + alphanum + "{2,8}-)+" +
// a variant, parenthesised so that we can refer back to it later;
"(" + variant + ")-" +
// zero or more subtags at least two characters long (thus stopping
// before extension and privateuse components);
"(?:" + alphanum + "{2,8}-)*" +
// and the same variant again
"\\1" +
// ...but not followed by any characters that would turn it into a
// different subtag.
"(?!" + alphanum + ")";
// Language tags are case insensitive (RFC 5646 section 2.1.1), but for
// this regular expression that's covered by having its character classes
// list both upper- and lower-case characters.
return new RegExp(duplicateVariant);
}());
var duplicateSingletonRE = (function () {
// RFC 5234 section B.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
var ALPHA = "[a-zA-Z]";
// DIGIT = %x30-39
// ; 0-9
var DIGIT = "[0-9]";
// RFC 5646 section 2.1
// alphanum = (ALPHA / DIGIT) ; letters and numbers
var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")";
// singleton = DIGIT ; 0 - 9
// / %x41-57 ; A - W
// / %x59-5A ; Y - Z
// / %x61-77 ; a - w
// / %x79-7A ; y - z
var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])";
// Match a langtag that contains a duplicate singleton.
var duplicateSingleton =
// Match a singleton subtag, parenthesised so that we can refer back to
// it later;
"-(" + singleton + ")-" +
// then zero or more subtags;
"(?:" + alphanum + "+-)*" +
// and the same singleton again
"\\1" +
// ...but not followed by any characters that would turn it into a
// different subtag.
"(?!" + alphanum + ")";
// Language tags are case insensitive (RFC 5646 section 2.1.1), but for
// this regular expression that's covered by having its character classes
// list both upper- and lower-case characters.
return new RegExp(duplicateSingleton);
}());
/**
* Verifies that the given string is a well-formed BCP 47 language tag
* with no duplicate variant or singleton subtags.
*
* Spec: ECMAScript Internationalization API Specification, 6.2.2.
*/
function IsStructurallyValidLanguageTag(locale) {
if (!languageTagRE.test(locale))
return false;
// Before checking for duplicate variant or singleton subtags with
// regular expressions, we have to get private use subtag sequences
// out of the picture.
if (locale.startsWith("x-"))
return true;
var pos = locale.indexOf("-x-");
if (pos !== -1)
locale = locale.substring(0, pos);
// Check for duplicate variant or singleton subtags.
return !duplicateVariantRE.test(locale) &&
!duplicateSingletonRE.test(locale);
}
/**
* Canonicalizes the given structurally valid BCP 47 language tag, including
* regularized case of subtags. For example, the language tag
* Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE, where
*
* Zh ; 2*3ALPHA
* -NAN ; ["-" extlang]
* -haNS ; ["-" script]
* -bu ; ["-" region]
* -variant2 ; *("-" variant)
* -Variant1
* -u-ca-chinese ; *("-" extension)
* -t-Zh-laTN
* -x-PRIVATE ; ["-" privateuse]
*
* becomes nan-Hans-mm-variant2-variant1-t-zh-latn-u-ca-chinese-x-private
*
* Spec: ECMAScript Internationalization API Specification, 6.2.3.
* Spec: RFC 5646, section 4.5.
*/
function CanonicalizeLanguageTag(locale) {
// The input
// "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE"
// will be used throughout this method to illustrate how it works.
// Language tags are compared and processed case-insensitively, so
// technically it's not necessary to adjust case. But for easier processing,
// and because the canonical form for most subtags is lower case, we start
// with lower case for all.
// "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" ->
// "zh-nan-hans-bu-variant2-variant1-u-ca-chinese-t-zh-latn-x-private"
locale = locale.toLowerCase();
// Handle mappings for complete tags.
if (langTagMappings && langTagMappings.hasOwnProperty(locale))
return langTagMappings[locale];
var subtags = locale.split("-");
var i = 0;
// Handle the standard part: All subtags before the first singleton or "x".
// "zh-nan-hans-bu-variant2-variant1"
while (i < subtags.length) {
var subtag = subtags[i];
// If we reach the start of an extension sequence or private use part,
// we're done with this loop. We have to check for i > 0 because for
// irregular language tags, such as i-klingon, the single-character
// subtag "i" is not the start of an extension sequence.
// In the example, we break at "u".
if (subtag.length === 1 && (i > 0 || subtag === "x"))
break;
if (subtag.length === 4) {
// 4-character subtags are script codes; their first character
// needs to be capitalized. "hans" -> "Hans"
subtag = subtag[0].toUpperCase() +
subtag.substring(1);
} else if (i !== 0 && subtag.length === 2) {
// 2-character subtags that are not in initial position are region
// codes; they need to be upper case. "bu" -> "BU"
subtag = subtag.toUpperCase();
}
if (langSubtagMappings.hasOwnProperty(subtag)) {
// Replace deprecated subtags with their preferred values.
// "BU" -> "MM"
// This has to come after we capitalize region codes because
// otherwise some language and region codes could be confused.
// For example, "in" is an obsolete language code for Indonesian,
// but "IN" is the country code for India.
// Note that the script generating langSubtagMappings makes sure
// that no regular subtag mapping will replace an extlang code.
subtag = langSubtagMappings[subtag];
} else if (extlangMappings.hasOwnProperty(subtag)) {
// Replace deprecated extlang subtags with their preferred values,
// and remove the preceding subtag if it's a redundant prefix.
// "zh-nan" -> "nan"
// Note that the script generating extlangMappings makes sure that
// no extlang mapping will replace a normal language code.
subtag = extlangMappings[subtag].preferred;
if (i === 1 && extlangMappings[subtag].prefix === subtags[0]) {
subtags.shift();
i--;
}
}
subtags[i] = subtag;
i++;
}
var normal = subtags.slice(0, i).join("-");
// Extension sequences are sorted by their singleton characters.
// "u-ca-chinese-t-zh-latn" -> "t-zh-latn-u-ca-chinese"
var extensions = [];
while (i < subtags.length && subtags[i] !== "x") {
var extensionStart = i;
i++;
while (i < subtags.length && subtags[i].length > 1)
i++;
var extension = sybtags.slice(extensionStart, i).join("-");
extensions.push(extension);
}
extensions.sort();
// Private use sequences are left as is. "x-private"
var privateUse = "";
if (i < subtags.length)
privateUse = subtags.slice(i).join("-");
// Put everything back together.
var canonical = normal;
if (extensions.length > 0)
canonical += "-" + extensions.join("-");
if (privateUse.length > 0) {
// Be careful of a Language-Tag that is entirely privateuse.
if (canonical.length > 0)
canonical += "-" + privateUse;
else
canonical = privateUse;
}
return canonical;
}
/**
* Canonicalizes a locale list.
*
* Spec: ECMAScript Internationalization API Specification, 9.2.1.
*/
function CanonicalizeLocaleList(locales) {
if (locales === undefined)
return [];
var seen = [];
if (typeof locales === "string")
locales = [locales];
var O = locales;
var len = O.length;
var k = 0;
while (k < len) {
// Don't call ToString(k) - SpiderMonkey is faster with integers.
var kPresent = k in O;
if (kPresent) {
var kValue = O[k];
if (!(typeof kValue === "string" || typeof kValue === "object"))
ThrowError(JSMSG_INVALID_LOCALES_ELEMENT);
var tag = kValue;
if (!IsStructurallyValidLanguageTag(tag))
ThrowError(JSMSG_INVALID_LANGUAGE_TAG, tag);
tag = CanonicalizeLanguageTag(tag);
if (seen.indexOf(tag) === -1)
seen.push(tag);
}
k++;
}
return seen;
}
/**
* Compares a BCP 47 language tag against the locales in availableLocales
* and returns the best available match. Uses the fallback
* mechanism of RFC 4647, section 3.4.
*
* Spec: ECMAScript Internationalization API Specification, 9.2.2.
* Spec: RFC 4647, section 3.4.
*/
function BestAvailableLocale(availableLocales, locale) {
var candidate = locale;
while (true) {
if (availableLocales.indexOf(candidate) !== -1)
return candidate;
var pos = candidate.lastIndexOf('-');
if (pos === -1)
return undefined;
if (pos >= 2 && candidate[pos - 2] === "-")
pos -= 2;
candidate = candidate.substring(0, pos);
}
}
/**
* Returns the subset of availableLocales for which requestedLocales has a
* matching (possibly fallback) locale. Locales appear in the same order in the
* returned list as in the input list.
*
* This function is a slight modification of the LookupSupprtedLocales algorithm
* The difference is in step 4d where instead of adding requested locale,
* we're adding availableLocale to the subset.
*
* This allows us to directly use returned subset to pool resources.
*
* Spec: ECMAScript Internationalization API Specification, 9.2.6.
*/
function LookupAvailableLocales(availableLocales, requestedLocales) {
// Steps 1-2.
var len = requestedLocales.length;
var subset = [];
// Steps 3-4.
var k = 0;
while (k < len) {
// Steps 4.a-b.
var locale = requestedLocales[k];
var noExtensionsLocale = locale.replace(unicodeLocaleExtensionSequenceGlobalRE, "");
// Step 4.c-d.
var availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
if (availableLocale !== undefined)
// in LookupSupportedLocales it pushes locale here
subset.push(availableLocale);
// Step 4.e.
k++;
}
// Steps 5-6.
return subset.slice(0);
}
function PrioritizeLocales(availableLocales,
requestedLocales,
defaultLocale) {
availableLocales = CanonicalizeLocaleList(availableLocales);
requestedLocales = CanonicalizeLocaleList(requestedLocales);
var result = LookupAvailableLocales(availableLocales, requestedLocales);
if (!defaultLocale) {
return result;
}
// if default locale is not present in result,
// add it to the end of fallback chain
defaultLocale = CanonicalizeLanguageTag(defaultLocale);
if (result.indexOf(defaultLocale) === -1) {
result.push(defaultLocale);
}
return result;
}
exports.Intl = {
prioritizeLocales: PrioritizeLocales
};
});
// attach the L20n singleton to the global object
window.L20n = require('l20n');
// close the function defined in build/prefix/microrequire.js
})(window);