(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: // // // // // 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` 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. // // // // 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);