Created
March 20, 2017 16:10
-
-
Save galdiolo/cfb65c383e4db8fec4866e17c2321dc3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } | |
(function () { | |
'use strict'; | |
function emit(listeners) { | |
var _this = this; | |
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | |
args[_key - 1] = arguments[_key]; | |
} | |
var type = args.shift(); | |
if (listeners['*']) { | |
listeners['*'].slice().forEach(function (listener) { | |
return listener.apply(_this, args); | |
}); | |
} | |
if (listeners[type]) { | |
listeners[type].slice().forEach(function (listener) { | |
return listener.apply(_this, args); | |
}); | |
} | |
} | |
function addEventListener(listeners, type, listener) { | |
if (!(type in listeners)) { | |
listeners[type] = []; | |
} | |
listeners[type].push(listener); | |
} | |
function removeEventListener(listeners, type, listener) { | |
var typeListeners = listeners[type]; | |
var pos = typeListeners.indexOf(listener); | |
if (pos === -1) { | |
return; | |
} | |
typeListeners.splice(pos, 1); | |
} | |
var Client = (function () { | |
function Client(remote) { | |
_classCallCheck(this, Client); | |
this.id = this; | |
this.remote = remote; | |
var listeners = {}; | |
this.on = function () { | |
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { | |
args[_key2] = arguments[_key2]; | |
} | |
return addEventListener.apply(undefined, [listeners].concat(args)); | |
}; | |
this.emit = function () { | |
for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { | |
args[_key3] = arguments[_key3]; | |
} | |
return emit.apply(undefined, [listeners].concat(args)); | |
}; | |
} | |
Client.prototype.method = function method(name) { | |
var _remote; | |
for (var _len4 = arguments.length, args = Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) { | |
args[_key4 - 1] = arguments[_key4]; | |
} | |
return (_remote = this.remote)[name].apply(_remote, args); | |
}; | |
return Client; | |
})(); | |
function broadcast(type, data) { | |
Array.from(this.ctxs.keys()).forEach(function (client) { | |
return client.emit(type, data); | |
}); | |
} | |
function L10nError(message, id, lang) { | |
this.name = 'L10nError'; | |
this.message = message; | |
this.id = id; | |
this.lang = lang; | |
} | |
L10nError.prototype = Object.create(Error.prototype); | |
L10nError.prototype.constructor = L10nError; | |
var HTTP_STATUS_CODE_OK = 200; | |
function load(type, url) { | |
return new Promise(function (resolve, reject) { | |
var xhr = new XMLHttpRequest(); | |
if (xhr.overrideMimeType) { | |
xhr.overrideMimeType(type); | |
} | |
xhr.open('GET', url, true); | |
if (type === 'application/json') { | |
xhr.responseType = 'json'; | |
} | |
xhr.addEventListener('load', function (e) { | |
if (e.target.status === HTTP_STATUS_CODE_OK || e.target.status === 0) { | |
resolve(e.target.response); | |
} else { | |
reject(new L10nError('Not found: ' + url)); | |
} | |
}); | |
xhr.addEventListener('error', reject); | |
xhr.addEventListener('timeout', reject); | |
try { | |
xhr.send(null); | |
} catch (e) { | |
if (e.name === 'NS_ERROR_FILE_NOT_FOUND') { | |
reject(new L10nError('Not found: ' + url)); | |
} else { | |
throw e; | |
} | |
} | |
}); | |
} | |
var io = { | |
extra: function (code, ver, path, type) { | |
return navigator.mozApps.getLocalizationResource(code, ver, path, type); | |
}, | |
app: function (code, ver, path, type) { | |
switch (type) { | |
case 'text': | |
return load('text/plain', path); | |
case 'json': | |
return load('application/json', path); | |
default: | |
throw new L10nError('Unknown file type: ' + type); | |
} | |
} | |
}; | |
function fetchResource(res, _ref4) { | |
var code = _ref4.code; | |
var src = _ref4.src; | |
var ver = _ref4.ver; | |
var url = res.replace('{locale}', code); | |
var type = res.endsWith('.json') ? 'json' : 'text'; | |
return io[src](code, ver, url, type); | |
} | |
var KNOWN_MACROS = ['plural']; | |
var MAX_PLACEABLE_LENGTH = 2500; | |
var FSI = ''; | |
var PDI = ''; | |
var resolutionChain = new WeakSet(); | |
function format(ctx, lang, args, entity) { | |
if (typeof entity === 'string') { | |
return [{}, entity]; | |
} | |
if (resolutionChain.has(entity)) { | |
throw new L10nError('Cyclic reference detected'); | |
} | |
resolutionChain.add(entity); | |
var rv = undefined; | |
try { | |
rv = resolveValue({}, ctx, lang, args, entity.value, entity.index); | |
} finally { | |
resolutionChain.delete(entity); | |
} | |
return rv; | |
} | |
function resolveIdentifier(ctx, lang, args, id) { | |
if (KNOWN_MACROS.indexOf(id) > -1) { | |
return [{}, ctx._getMacro(lang, id)]; | |
} | |
if (args && args.hasOwnProperty(id)) { | |
if (typeof args[id] === 'string' || typeof args[id] === 'number' && !isNaN(args[id])) { | |
return [{}, args[id]]; | |
} else { | |
throw new L10nError('Arg must be a string or a number: ' + id); | |
} | |
} | |
if (id === '__proto__') { | |
throw new L10nError('Illegal id: ' + id); | |
} | |
var entity = ctx._getEntity(lang, id); | |
if (entity) { | |
return format(ctx, lang, args, entity); | |
} | |
throw new L10nError('Unknown reference: ' + id); | |
} | |
function subPlaceable(locals, ctx, lang, args, id) { | |
var newLocals = undefined, | |
value = undefined; | |
try { | |
var _resolveIdentifier = resolveIdentifier(ctx, lang, args, id); | |
newLocals = _resolveIdentifier[0]; | |
value = _resolveIdentifier[1]; | |
} catch (err) { | |
return [{ error: err }, FSI + '{{ ' + id + ' }}' + PDI]; | |
} | |
if (typeof value === 'number') { | |
var formatter = ctx._getNumberFormatter(lang); | |
return [newLocals, formatter.format(value)]; | |
} | |
if (typeof value === 'string') { | |
if (value.length >= MAX_PLACEABLE_LENGTH) { | |
throw new L10nError('Too many characters in placeable (' + value.length + ', max allowed is ' + MAX_PLACEABLE_LENGTH + ')'); | |
} | |
return [newLocals, FSI + value + PDI]; | |
} | |
return [{}, FSI + '{{ ' + id + ' }}' + PDI]; | |
} | |
function interpolate(locals, ctx, lang, args, arr) { | |
return arr.reduce(function (_ref5, cur) { | |
var localsSeq = _ref5[0]; | |
var valueSeq = _ref5[1]; | |
if (typeof cur === 'string') { | |
return [localsSeq, valueSeq + cur]; | |
} else { | |
var _subPlaceable = subPlaceable(locals, ctx, lang, args, cur.name); | |
var value = _subPlaceable[1]; | |
return [localsSeq, valueSeq + value]; | |
} | |
}, [locals, '']); | |
} | |
function resolveSelector(ctx, lang, args, expr, index) { | |
var selectorName = undefined; | |
if (index[0].type === 'call' && index[0].expr.type === 'prop' && index[0].expr.expr.name === 'cldr') { | |
selectorName = 'plural'; | |
} else { | |
selectorName = index[0].name; | |
} | |
var selector = resolveIdentifier(ctx, lang, args, selectorName)[1]; | |
if (typeof selector !== 'function') { | |
return selector; | |
} | |
var argValue = index[0].args ? resolveIdentifier(ctx, lang, args, index[0].args[0].name)[1] : undefined; | |
if (selectorName === 'plural') { | |
if (argValue === 0 && 'zero' in expr) { | |
return 'zero'; | |
} | |
if (argValue === 1 && 'one' in expr) { | |
return 'one'; | |
} | |
if (argValue === 2 && 'two' in expr) { | |
return 'two'; | |
} | |
} | |
return selector(argValue); | |
} | |
function resolveValue(locals, ctx, lang, args, expr, index) { | |
if (!expr) { | |
return [locals, expr]; | |
} | |
if (typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') { | |
return [locals, expr]; | |
} | |
if (Array.isArray(expr)) { | |
return interpolate(locals, ctx, lang, args, expr); | |
} | |
if (index) { | |
var selector = resolveSelector(ctx, lang, args, expr, index); | |
if (selector in expr) { | |
return resolveValue(locals, ctx, lang, args, expr[selector]); | |
} | |
} | |
var defaultKey = expr.__default || 'other'; | |
if (defaultKey in expr) { | |
return resolveValue(locals, ctx, lang, args, expr[defaultKey]); | |
} | |
throw new L10nError('Unresolvable value'); | |
} | |
var locales2rules = { | |
'af': 3, | |
'ak': 4, | |
'am': 4, | |
'ar': 1, | |
'asa': 3, | |
'az': 0, | |
'be': 11, | |
'bem': 3, | |
'bez': 3, | |
'bg': 3, | |
'bh': 4, | |
'bm': 0, | |
'bn': 3, | |
'bo': 0, | |
'br': 20, | |
'brx': 3, | |
'bs': 11, | |
'ca': 3, | |
'cgg': 3, | |
'chr': 3, | |
'cs': 12, | |
'cy': 17, | |
'da': 3, | |
'de': 3, | |
'dv': 3, | |
'dz': 0, | |
'ee': 3, | |
'el': 3, | |
'en': 3, | |
'eo': 3, | |
'es': 3, | |
'et': 3, | |
'eu': 3, | |
'fa': 0, | |
'ff': 5, | |
'fi': 3, | |
'fil': 4, | |
'fo': 3, | |
'fr': 5, | |
'fur': 3, | |
'fy': 3, | |
'ga': 8, | |
'gd': 24, | |
'gl': 3, | |
'gsw': 3, | |
'gu': 3, | |
'guw': 4, | |
'gv': 23, | |
'ha': 3, | |
'haw': 3, | |
'he': 2, | |
'hi': 4, | |
'hr': 11, | |
'hu': 0, | |
'id': 0, | |
'ig': 0, | |
'ii': 0, | |
'is': 3, | |
'it': 3, | |
'iu': 7, | |
'ja': 0, | |
'jmc': 3, | |
'jv': 0, | |
'ka': 0, | |
'kab': 5, | |
'kaj': 3, | |
'kcg': 3, | |
'kde': 0, | |
'kea': 0, | |
'kk': 3, | |
'kl': 3, | |
'km': 0, | |
'kn': 0, | |
'ko': 0, | |
'ksb': 3, | |
'ksh': 21, | |
'ku': 3, | |
'kw': 7, | |
'lag': 18, | |
'lb': 3, | |
'lg': 3, | |
'ln': 4, | |
'lo': 0, | |
'lt': 10, | |
'lv': 6, | |
'mas': 3, | |
'mg': 4, | |
'mk': 16, | |
'ml': 3, | |
'mn': 3, | |
'mo': 9, | |
'mr': 3, | |
'ms': 0, | |
'mt': 15, | |
'my': 0, | |
'nah': 3, | |
'naq': 7, | |
'nb': 3, | |
'nd': 3, | |
'ne': 3, | |
'nl': 3, | |
'nn': 3, | |
'no': 3, | |
'nr': 3, | |
'nso': 4, | |
'ny': 3, | |
'nyn': 3, | |
'om': 3, | |
'or': 3, | |
'pa': 3, | |
'pap': 3, | |
'pl': 13, | |
'ps': 3, | |
'pt': 3, | |
'rm': 3, | |
'ro': 9, | |
'rof': 3, | |
'ru': 11, | |
'rwk': 3, | |
'sah': 0, | |
'saq': 3, | |
'se': 7, | |
'seh': 3, | |
'ses': 0, | |
'sg': 0, | |
'sh': 11, | |
'shi': 19, | |
'sk': 12, | |
'sl': 14, | |
'sma': 7, | |
'smi': 7, | |
'smj': 7, | |
'smn': 7, | |
'sms': 7, | |
'sn': 3, | |
'so': 3, | |
'sq': 3, | |
'sr': 11, | |
'ss': 3, | |
'ssy': 3, | |
'st': 3, | |
'sv': 3, | |
'sw': 3, | |
'syr': 3, | |
'ta': 3, | |
'te': 3, | |
'teo': 3, | |
'th': 0, | |
'ti': 4, | |
'tig': 3, | |
'tk': 3, | |
'tl': 4, | |
'tn': 3, | |
'to': 0, | |
'tr': 0, | |
'ts': 3, | |
'tzm': 22, | |
'uk': 11, | |
'ur': 3, | |
've': 3, | |
'vi': 0, | |
'vun': 3, | |
'wa': 4, | |
'wae': 3, | |
'wo': 0, | |
'xh': 3, | |
'xog': 3, | |
'yo': 0, | |
'zh': 0, | |
'zu': 3 | |
}; | |
function isIn(n, list) { | |
return list.indexOf(n) !== -1; | |
} | |
function isBetween(n, start, end) { | |
return typeof n === typeof start && start <= n && n <= end; | |
} | |
var pluralRules = { | |
'0': function () { | |
return 'other'; | |
}, | |
'1': function (n) { | |
if (isBetween(n % 100, 3, 10)) { | |
return 'few'; | |
} | |
if (n === 0) { | |
return 'zero'; | |
} | |
if (isBetween(n % 100, 11, 99)) { | |
return 'many'; | |
} | |
if (n === 2) { | |
return 'two'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'2': function (n) { | |
if (n !== 0 && n % 10 === 0) { | |
return 'many'; | |
} | |
if (n === 2) { | |
return 'two'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'3': function (n) { | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'4': function (n) { | |
if (isBetween(n, 0, 1)) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'5': function (n) { | |
if (isBetween(n, 0, 2) && n !== 2) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'6': function (n) { | |
if (n === 0) { | |
return 'zero'; | |
} | |
if (n % 10 === 1 && n % 100 !== 11) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'7': function (n) { | |
if (n === 2) { | |
return 'two'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'8': function (n) { | |
if (isBetween(n, 3, 6)) { | |
return 'few'; | |
} | |
if (isBetween(n, 7, 10)) { | |
return 'many'; | |
} | |
if (n === 2) { | |
return 'two'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'9': function (n) { | |
if (n === 0 || n !== 1 && isBetween(n % 100, 1, 19)) { | |
return 'few'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'10': function (n) { | |
if (isBetween(n % 10, 2, 9) && !isBetween(n % 100, 11, 19)) { | |
return 'few'; | |
} | |
if (n % 10 === 1 && !isBetween(n % 100, 11, 19)) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'11': function (n) { | |
if (isBetween(n % 10, 2, 4) && !isBetween(n % 100, 12, 14)) { | |
return 'few'; | |
} | |
if (n % 10 === 0 || isBetween(n % 10, 5, 9) || isBetween(n % 100, 11, 14)) { | |
return 'many'; | |
} | |
if (n % 10 === 1 && n % 100 !== 11) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'12': function (n) { | |
if (isBetween(n, 2, 4)) { | |
return 'few'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'13': function (n) { | |
if (isBetween(n % 10, 2, 4) && !isBetween(n % 100, 12, 14)) { | |
return 'few'; | |
} | |
if (n !== 1 && isBetween(n % 10, 0, 1) || isBetween(n % 10, 5, 9) || isBetween(n % 100, 12, 14)) { | |
return 'many'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'14': function (n) { | |
if (isBetween(n % 100, 3, 4)) { | |
return 'few'; | |
} | |
if (n % 100 === 2) { | |
return 'two'; | |
} | |
if (n % 100 === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'15': function (n) { | |
if (n === 0 || isBetween(n % 100, 2, 10)) { | |
return 'few'; | |
} | |
if (isBetween(n % 100, 11, 19)) { | |
return 'many'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'16': function (n) { | |
if (n % 10 === 1 && n !== 11) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'17': function (n) { | |
if (n === 3) { | |
return 'few'; | |
} | |
if (n === 0) { | |
return 'zero'; | |
} | |
if (n === 6) { | |
return 'many'; | |
} | |
if (n === 2) { | |
return 'two'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'18': function (n) { | |
if (n === 0) { | |
return 'zero'; | |
} | |
if (isBetween(n, 0, 2) && n !== 0 && n !== 2) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'19': function (n) { | |
if (isBetween(n, 2, 10)) { | |
return 'few'; | |
} | |
if (isBetween(n, 0, 1)) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'20': function (n) { | |
if ((isBetween(n % 10, 3, 4) || n % 10 === 9) && !(isBetween(n % 100, 10, 19) || isBetween(n % 100, 70, 79) || isBetween(n % 100, 90, 99))) { | |
return 'few'; | |
} | |
if (n % 1000000 === 0 && n !== 0) { | |
return 'many'; | |
} | |
if (n % 10 === 2 && !isIn(n % 100, [12, 72, 92])) { | |
return 'two'; | |
} | |
if (n % 10 === 1 && !isIn(n % 100, [11, 71, 91])) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'21': function (n) { | |
if (n === 0) { | |
return 'zero'; | |
} | |
if (n === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'22': function (n) { | |
if (isBetween(n, 0, 1) || isBetween(n, 11, 99)) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'23': function (n) { | |
if (isBetween(n % 10, 1, 2) || n % 20 === 0) { | |
return 'one'; | |
} | |
return 'other'; | |
}, | |
'24': function (n) { | |
if (isBetween(n, 3, 10) || isBetween(n, 13, 19)) { | |
return 'few'; | |
} | |
if (isIn(n, [2, 12])) { | |
return 'two'; | |
} | |
if (isIn(n, [1, 11])) { | |
return 'one'; | |
} | |
return 'other'; | |
} | |
}; | |
function getPluralRule(code) { | |
var index = locales2rules[code.replace(/-.*$/, '')]; | |
if (!(index in pluralRules)) { | |
return function () { | |
return 'other'; | |
}; | |
} | |
return pluralRules[index]; | |
} | |
var L20nIntl = typeof Intl !== 'undefined' ? Intl : { | |
NumberFormat: function () { | |
return { | |
format: function (v) { | |
return v; | |
} | |
}; | |
} | |
}; | |
var Context = (function () { | |
function Context(env, langs, resIds) { | |
var _this2 = this; | |
_classCallCheck(this, Context); | |
this.langs = langs; | |
this.resIds = resIds; | |
this.env = env; | |
this.emit = function (type, evt) { | |
return env.emit(type, evt, _this2); | |
}; | |
} | |
Context.prototype._formatTuple = function _formatTuple(lang, args, entity, id, key) { | |
try { | |
return format(this, lang, args, entity); | |
} catch (err) { | |
err.id = key ? id + '::' + key : id; | |
err.lang = lang; | |
this.emit('resolveerror', err); | |
return [{ error: err }, err.id]; | |
} | |
}; | |
Context.prototype._formatEntity = function _formatEntity(lang, args, entity, id) { | |
var _formatTuple2 = this._formatTuple(lang, args, entity, id); | |
var value = _formatTuple2[1]; | |
var formatted = { | |
value: value, | |
attrs: null | |
}; | |
if (entity.attrs) { | |
formatted.attrs = Object.create(null); | |
for (var key in entity.attrs) { | |
var _formatTuple3 = this._formatTuple(lang, args, entity.attrs[key], id, key); | |
var attrValue = _formatTuple3[1]; | |
formatted.attrs[key] = attrValue; | |
} | |
} | |
return formatted; | |
}; | |
Context.prototype._formatValue = function _formatValue(lang, args, entity, id) { | |
return this._formatTuple(lang, args, entity, id)[1]; | |
}; | |
Context.prototype.fetch = function fetch() { | |
var _this3 = this; | |
var langs = arguments.length <= 0 || arguments[0] === undefined ? this.langs : arguments[0]; | |
if (langs.length === 0) { | |
return Promise.resolve(langs); | |
} | |
return Promise.all(this.resIds.map(function (resId) { | |
return _this3.env._getResource(langs[0], resId); | |
})).then(function () { | |
return langs; | |
}); | |
}; | |
Context.prototype._resolve = function _resolve(langs, keys, formatter, prevResolved) { | |
var _this4 = this; | |
var lang = langs[0]; | |
if (!lang) { | |
return reportMissing.call(this, keys, formatter, prevResolved); | |
} | |
var hasUnresolved = false; | |
var resolved = keys.map(function (key, i) { | |
if (prevResolved && prevResolved[i] !== undefined) { | |
return prevResolved[i]; | |
} | |
var _ref6 = Array.isArray(key) ? key : [key, undefined]; | |
var id = _ref6[0]; | |
var args = _ref6[1]; | |
var entity = _this4._getEntity(lang, id); | |
if (entity) { | |
return formatter.call(_this4, lang, args, entity, id); | |
} | |
_this4.emit('notfounderror', new L10nError('"' + id + '" not found in ' + lang.code, id, lang)); | |
hasUnresolved = true; | |
}); | |
if (!hasUnresolved) { | |
return resolved; | |
} | |
return this.fetch(langs.slice(1)).then(function (nextLangs) { | |
return _this4._resolve(nextLangs, keys, formatter, resolved); | |
}); | |
}; | |
Context.prototype.formatEntities = function formatEntities() { | |
var _this5 = this; | |
for (var _len5 = arguments.length, keys = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { | |
keys[_key5] = arguments[_key5]; | |
} | |
return this.fetch().then(function (langs) { | |
return _this5._resolve(langs, keys, _this5._formatEntity); | |
}); | |
}; | |
Context.prototype.formatValues = function formatValues() { | |
var _this6 = this; | |
for (var _len6 = arguments.length, keys = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { | |
keys[_key6] = arguments[_key6]; | |
} | |
return this.fetch().then(function (langs) { | |
return _this6._resolve(langs, keys, _this6._formatValue); | |
}); | |
}; | |
Context.prototype._getEntity = function _getEntity(lang, id) { | |
var cache = this.env.resCache; | |
for (var i = 0, resId = undefined; resId = this.resIds[i]; i++) { | |
var resource = cache.get(resId + lang.code + lang.src); | |
if (resource instanceof L10nError) { | |
continue; | |
} | |
if (id in resource) { | |
return resource[id]; | |
} | |
} | |
return undefined; | |
}; | |
Context.prototype._getNumberFormatter = function _getNumberFormatter(lang) { | |
if (!this.env.numberFormatters) { | |
this.env.numberFormatters = new Map(); | |
} | |
if (!this.env.numberFormatters.has(lang)) { | |
var formatter = L20nIntl.NumberFormat(lang); | |
this.env.numberFormatters.set(lang, formatter); | |
return formatter; | |
} | |
return this.env.numberFormatters.get(lang); | |
}; | |
Context.prototype._getMacro = function _getMacro(lang, id) { | |
switch (id) { | |
case 'plural': | |
return getPluralRule(lang.code); | |
default: | |
return undefined; | |
} | |
}; | |
return Context; | |
})(); | |
function reportMissing(keys, formatter, resolved) { | |
var _this7 = this; | |
var missingIds = new Set(); | |
keys.forEach(function (key, i) { | |
if (resolved && resolved[i] !== undefined) { | |
return; | |
} | |
var id = Array.isArray(key) ? key[0] : key; | |
missingIds.add(id); | |
resolved[i] = formatter === _this7._formatValue ? id : { value: id, attrs: null }; | |
}); | |
this.emit('notfounderror', new L10nError('"' + Array.from(missingIds).join(', ') + '"' + ' not found in any language', missingIds)); | |
return resolved; | |
} | |
var MAX_PLACEABLES = 100; | |
var PropertiesParser = { | |
patterns: null, | |
entryIds: null, | |
emit: null, | |
init: function () { | |
this.patterns = { | |
comment: /^\s*#|^\s*$/, | |
entity: /^([^=\s]+)\s*=\s*(.*)$/, | |
multiline: /[^\\]\\$/, | |
index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i, | |
unicode: /\\u([0-9a-fA-F]{1,4})/g, | |
entries: /[^\r\n]+/g, | |
controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g, | |
placeables: /\{\{\s*([^\s]*?)\s*\}\}/ | |
}; | |
}, | |
parse: function (emit, source) { | |
if (!this.patterns) { | |
this.init(); | |
} | |
this.emit = emit; | |
var entries = {}; | |
var lines = source.match(this.patterns.entries); | |
if (!lines) { | |
return entries; | |
} | |
for (var i = 0; i < lines.length; i++) { | |
var line = lines[i]; | |
if (this.patterns.comment.test(line)) { | |
continue; | |
} | |
while (this.patterns.multiline.test(line) && i < lines.length) { | |
line = line.slice(0, -1) + lines[++i].trim(); | |
} | |
var entityMatch = line.match(this.patterns.entity); | |
if (entityMatch) { | |
try { | |
this.parseEntity(entityMatch[1], entityMatch[2], entries); | |
} catch (e) { | |
if (!this.emit) { | |
throw e; | |
} | |
} | |
} | |
} | |
return entries; | |
}, | |
parseEntity: function (id, value, entries) { | |
var name = undefined, | |
key = undefined; | |
var pos = id.indexOf('['); | |
if (pos !== -1) { | |
name = id.substr(0, pos); | |
key = id.substring(pos + 1, id.length - 1); | |
} else { | |
name = id; | |
key = null; | |
} | |
var nameElements = name.split('.'); | |
if (nameElements.length > 2) { | |
throw this.error('Error in ID: "' + name + '".' + ' Nested attributes are not supported.'); | |
} | |
var attr = undefined; | |
if (nameElements.length > 1) { | |
name = nameElements[0]; | |
attr = nameElements[1]; | |
if (attr[0] === '$') { | |
throw this.error('Attribute can\'t start with "$"'); | |
} | |
} else { | |
attr = null; | |
} | |
this.setEntityValue(name, attr, key, this.unescapeString(value), entries); | |
}, | |
setEntityValue: function (id, attr, key, rawValue, entries) { | |
var value = rawValue.indexOf('{{') > -1 ? this.parseString(rawValue) : rawValue; | |
var isSimpleValue = typeof value === 'string'; | |
var root = entries; | |
var isSimpleNode = typeof entries[id] === 'string'; | |
if (!entries[id] && (attr || key || !isSimpleValue)) { | |
entries[id] = Object.create(null); | |
isSimpleNode = false; | |
} | |
if (attr) { | |
if (isSimpleNode) { | |
var val = entries[id]; | |
entries[id] = Object.create(null); | |
entries[id].value = val; | |
} | |
if (!entries[id].attrs) { | |
entries[id].attrs = Object.create(null); | |
} | |
if (!entries[id].attrs && !isSimpleValue) { | |
entries[id].attrs[attr] = Object.create(null); | |
} | |
root = entries[id].attrs; | |
id = attr; | |
} | |
if (key) { | |
isSimpleNode = false; | |
if (typeof root[id] === 'string') { | |
var val = root[id]; | |
root[id] = Object.create(null); | |
root[id].index = this.parseIndex(val); | |
root[id].value = Object.create(null); | |
} | |
root = root[id].value; | |
id = key; | |
isSimpleValue = true; | |
} | |
if (isSimpleValue) { | |
if (id in root) { | |
throw this.error('Duplicated id: ' + id); | |
} | |
root[id] = value; | |
} else { | |
if (!root[id]) { | |
root[id] = Object.create(null); | |
} | |
root[id].value = value; | |
} | |
}, | |
parseString: function (str) { | |
var chunks = str.split(this.patterns.placeables); | |
var complexStr = []; | |
var len = chunks.length; | |
var placeablesCount = (len - 1) / 2; | |
if (placeablesCount >= MAX_PLACEABLES) { | |
throw this.error('Too many placeables (' + placeablesCount + ', max allowed is ' + MAX_PLACEABLES + ')'); | |
} | |
for (var i = 0; i < chunks.length; i++) { | |
if (chunks[i].length === 0) { | |
continue; | |
} | |
if (i % 2 === 1) { | |
complexStr.push({ type: 'idOrVar', name: chunks[i] }); | |
} else { | |
complexStr.push(chunks[i]); | |
} | |
} | |
return complexStr; | |
}, | |
unescapeString: function (str) { | |
if (str.lastIndexOf('\\') !== -1) { | |
str = str.replace(this.patterns.controlChars, '$1'); | |
} | |
return str.replace(this.patterns.unicode, function (match, token) { | |
return String.fromCodePoint(parseInt(token, 16)); | |
}); | |
}, | |
parseIndex: function (str) { | |
var match = str.match(this.patterns.index); | |
if (!match) { | |
throw new L10nError('Malformed index'); | |
} | |
if (match[2]) { | |
return [{ | |
type: 'call', | |
expr: { | |
type: 'prop', | |
expr: { | |
type: 'glob', | |
name: 'cldr' | |
}, | |
prop: 'plural', | |
cmpt: false | |
}, args: [{ | |
type: 'idOrVar', | |
name: match[2] | |
}] | |
}]; | |
} else { | |
return [{ type: 'idOrVar', name: match[1] }]; | |
} | |
}, | |
error: function (msg) { | |
var type = arguments.length <= 1 || arguments[1] === undefined ? 'parsererror' : arguments[1]; | |
var err = new L10nError(msg); | |
if (this.emit) { | |
this.emit(type, err); | |
} | |
return err; | |
} | |
}; | |
var MAX_PLACEABLES$1 = 100; | |
var L20nParser = { | |
parse: function (emit, string) { | |
this._source = string; | |
this._index = 0; | |
this._length = string.length; | |
this.entries = Object.create(null); | |
this.emit = emit; | |
return this.getResource(); | |
}, | |
getResource: function () { | |
this.getWS(); | |
while (this._index < this._length) { | |
try { | |
this.getEntry(); | |
} catch (e) { | |
if (e instanceof L10nError) { | |
this.getJunkEntry(); | |
if (!this.emit) { | |
throw e; | |
} | |
} else { | |
throw e; | |
} | |
} | |
if (this._index < this._length) { | |
this.getWS(); | |
} | |
} | |
return this.entries; | |
}, | |
getEntry: function () { | |
if (this._source[this._index] === '<') { | |
++this._index; | |
var id = this.getIdentifier(); | |
if (this._source[this._index] === '[') { | |
++this._index; | |
return this.getEntity(id, this.getItemList(this.getExpression, ']')); | |
} | |
return this.getEntity(id); | |
} | |
if (this._source.startsWith('/*', this._index)) { | |
return this.getComment(); | |
} | |
throw this.error('Invalid entry'); | |
}, | |
getEntity: function (id, index) { | |
if (!this.getRequiredWS()) { | |
throw this.error('Expected white space'); | |
} | |
var ch = this._source[this._index]; | |
var hasIndex = index !== undefined; | |
var value = this.getValue(ch, hasIndex, hasIndex); | |
var attrs = undefined; | |
if (value === undefined) { | |
if (ch === '>') { | |
throw this.error('Expected ">"'); | |
} | |
attrs = this.getAttributes(); | |
} else { | |
var ws1 = this.getRequiredWS(); | |
if (this._source[this._index] !== '>') { | |
if (!ws1) { | |
throw this.error('Expected ">"'); | |
} | |
attrs = this.getAttributes(); | |
} | |
} | |
++this._index; | |
if (id in this.entries) { | |
throw this.error('Duplicate entry ID "' + id, 'duplicateerror'); | |
} | |
if (!attrs && !index && typeof value === 'string') { | |
this.entries[id] = value; | |
} else { | |
this.entries[id] = { | |
value: value, | |
attrs: attrs, | |
index: index | |
}; | |
} | |
}, | |
getValue: function () { | |
var ch = arguments.length <= 0 || arguments[0] === undefined ? this._source[this._index] : arguments[0]; | |
var index = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; | |
var required = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2]; | |
switch (ch) { | |
case '\'': | |
case '"': | |
return this.getString(ch, 1); | |
case '{': | |
return this.getHash(index); | |
} | |
if (required) { | |
throw this.error('Unknown value type'); | |
} | |
return undefined; | |
}, | |
getWS: function () { | |
var cc = this._source.charCodeAt(this._index); | |
while (cc === 32 || cc === 10 || cc === 9 || cc === 13) { | |
cc = this._source.charCodeAt(++this._index); | |
} | |
}, | |
getRequiredWS: function () { | |
var pos = this._index; | |
var cc = this._source.charCodeAt(pos); | |
while (cc === 32 || cc === 10 || cc === 9 || cc === 13) { | |
cc = this._source.charCodeAt(++this._index); | |
} | |
return this._index !== pos; | |
}, | |
getIdentifier: function () { | |
var start = this._index; | |
var cc = this._source.charCodeAt(this._index); | |
if (cc >= 97 && cc <= 122 || cc >= 65 && cc <= 90 || cc === 95) { | |
cc = this._source.charCodeAt(++this._index); | |
} else { | |
throw this.error('Identifier has to start with [a-zA-Z_]'); | |
} | |
while (cc >= 97 && cc <= 122 || cc >= 65 && cc <= 90 || cc >= 48 && cc <= 57 || cc === 95) { | |
cc = this._source.charCodeAt(++this._index); | |
} | |
return this._source.slice(start, this._index); | |
}, | |
getUnicodeChar: function () { | |
for (var i = 0; i < 4; i++) { | |
var cc = this._source.charCodeAt(++this._index); | |
if (cc > 96 && cc < 103 || cc > 64 && cc < 71 || cc > 47 && cc < 58) { | |
continue; | |
} | |
throw this.error('Illegal unicode escape sequence'); | |
} | |
this._index++; | |
return String.fromCharCode(parseInt(this._source.slice(this._index - 4, this._index), 16)); | |
}, | |
stringRe: /"|'|{{|\\/g, | |
getString: function (opchar, opcharLen) { | |
var body = []; | |
var placeables = 0; | |
this._index += opcharLen; | |
var start = this._index; | |
var bufStart = start; | |
var buf = ''; | |
while (true) { | |
this.stringRe.lastIndex = this._index; | |
var match = this.stringRe.exec(this._source); | |
if (!match) { | |
throw this.error('Unclosed string literal'); | |
} | |
if (match[0] === '"' || match[0] === '\'') { | |
if (match[0] !== opchar) { | |
this._index += opcharLen; | |
continue; | |
} | |
this._index = match.index + opcharLen; | |
break; | |
} | |
if (match[0] === '{{') { | |
if (placeables > MAX_PLACEABLES$1 - 1) { | |
throw this.error('Too many placeables, maximum allowed is ' + MAX_PLACEABLES$1); | |
} | |
placeables++; | |
if (match.index > bufStart || buf.length > 0) { | |
body.push(buf + this._source.slice(bufStart, match.index)); | |
buf = ''; | |
} | |
this._index = match.index + 2; | |
this.getWS(); | |
body.push(this.getExpression()); | |
this.getWS(); | |
this._index += 2; | |
bufStart = this._index; | |
continue; | |
} | |
if (match[0] === '\\') { | |
this._index = match.index + 1; | |
var ch2 = this._source[this._index]; | |
if (ch2 === 'u') { | |
buf += this._source.slice(bufStart, match.index) + this.getUnicodeChar(); | |
} else if (ch2 === opchar || ch2 === '\\') { | |
buf += this._source.slice(bufStart, match.index) + ch2; | |
this._index++; | |
} else if (this._source.startsWith('{{', this._index)) { | |
buf += this._source.slice(bufStart, match.index) + '{{'; | |
this._index += 2; | |
} else { | |
throw this.error('Illegal escape sequence'); | |
} | |
bufStart = this._index; | |
} | |
} | |
if (body.length === 0) { | |
return buf + this._source.slice(bufStart, this._index - opcharLen); | |
} | |
if (this._index - opcharLen > bufStart || buf.length > 0) { | |
body.push(buf + this._source.slice(bufStart, this._index - opcharLen)); | |
} | |
return body; | |
}, | |
getAttributes: function () { | |
var attrs = Object.create(null); | |
while (true) { | |
this.getAttribute(attrs); | |
var ws1 = this.getRequiredWS(); | |
var ch = this._source.charAt(this._index); | |
if (ch === '>') { | |
break; | |
} else if (!ws1) { | |
throw this.error('Expected ">"'); | |
} | |
} | |
return attrs; | |
}, | |
getAttribute: function (attrs) { | |
var key = this.getIdentifier(); | |
var index = undefined; | |
if (this._source[this._index] === '[') { | |
++this._index; | |
this.getWS(); | |
index = this.getItemList(this.getExpression, ']'); | |
} | |
this.getWS(); | |
if (this._source[this._index] !== ':') { | |
throw this.error('Expected ":"'); | |
} | |
++this._index; | |
this.getWS(); | |
var hasIndex = index !== undefined; | |
var value = this.getValue(undefined, hasIndex); | |
if (key in attrs) { | |
throw this.error('Duplicate attribute "' + key, 'duplicateerror'); | |
} | |
if (!index && typeof value === 'string') { | |
attrs[key] = value; | |
} else { | |
attrs[key] = { | |
value: value, | |
index: index | |
}; | |
} | |
}, | |
getHash: function (index) { | |
var items = Object.create(null); | |
++this._index; | |
this.getWS(); | |
var defKey = undefined; | |
while (true) { | |
var _getHashItem = this.getHashItem(); | |
var key = _getHashItem[0]; | |
var value = _getHashItem[1]; | |
var def = _getHashItem[2]; | |
items[key] = value; | |
if (def) { | |
if (defKey) { | |
throw this.error('Default item redefinition forbidden'); | |
} | |
defKey = key; | |
} | |
this.getWS(); | |
var comma = this._source[this._index] === ','; | |
if (comma) { | |
++this._index; | |
this.getWS(); | |
} | |
if (this._source[this._index] === '}') { | |
++this._index; | |
break; | |
} | |
if (!comma) { | |
throw this.error('Expected "}"'); | |
} | |
} | |
if (defKey) { | |
items.__default = defKey; | |
} else if (!index) { | |
throw this.error('Unresolvable Hash Value'); | |
} | |
return items; | |
}, | |
getHashItem: function () { | |
var defItem = false; | |
if (this._source[this._index] === '*') { | |
++this._index; | |
defItem = true; | |
} | |
var key = this.getIdentifier(); | |
this.getWS(); | |
if (this._source[this._index] !== ':') { | |
throw this.error('Expected ":"'); | |
} | |
++this._index; | |
this.getWS(); | |
return [key, this.getValue(), defItem]; | |
}, | |
getComment: function () { | |
this._index += 2; | |
var start = this._index; | |
var end = this._source.indexOf('*/', start); | |
if (end === -1) { | |
throw this.error('Comment without a closing tag'); | |
} | |
this._index = end + 2; | |
}, | |
getExpression: function () { | |
var exp = this.getPrimaryExpression(); | |
while (true) { | |
var ch = this._source[this._index]; | |
if (ch === '.' || ch === '[') { | |
++this._index; | |
exp = this.getPropertyExpression(exp, ch === '['); | |
} else if (ch === '(') { | |
++this._index; | |
exp = this.getCallExpression(exp); | |
} else { | |
break; | |
} | |
} | |
return exp; | |
}, | |
getPropertyExpression: function (idref, computed) { | |
var exp = undefined; | |
if (computed) { | |
this.getWS(); | |
exp = this.getExpression(); | |
this.getWS(); | |
if (this._source[this._index] !== ']') { | |
throw this.error('Expected "]"'); | |
} | |
++this._index; | |
} else { | |
exp = this.getIdentifier(); | |
} | |
return { | |
type: 'prop', | |
expr: idref, | |
prop: exp, | |
cmpt: computed | |
}; | |
}, | |
getCallExpression: function (callee) { | |
this.getWS(); | |
return { | |
type: 'call', | |
expr: callee, | |
args: this.getItemList(this.getExpression, ')') | |
}; | |
}, | |
getPrimaryExpression: function () { | |
var ch = this._source[this._index]; | |
switch (ch) { | |
case '$': | |
++this._index; | |
return { | |
type: 'var', | |
name: this.getIdentifier() | |
}; | |
case '@': | |
++this._index; | |
return { | |
type: 'glob', | |
name: this.getIdentifier() | |
}; | |
default: | |
return { | |
type: 'id', | |
name: this.getIdentifier() | |
}; | |
} | |
}, | |
getItemList: function (callback, closeChar) { | |
var items = []; | |
var closed = false; | |
this.getWS(); | |
if (this._source[this._index] === closeChar) { | |
++this._index; | |
closed = true; | |
} | |
while (!closed) { | |
items.push(callback.call(this)); | |
this.getWS(); | |
var ch = this._source.charAt(this._index); | |
switch (ch) { | |
case ',': | |
++this._index; | |
this.getWS(); | |
break; | |
case closeChar: | |
++this._index; | |
closed = true; | |
break; | |
default: | |
throw this.error('Expected "," or "' + closeChar + '"'); | |
} | |
} | |
return items; | |
}, | |
getJunkEntry: function () { | |
var pos = this._index; | |
var nextEntity = this._source.indexOf('<', pos); | |
var nextComment = this._source.indexOf('/*', pos); | |
if (nextEntity === -1) { | |
nextEntity = this._length; | |
} | |
if (nextComment === -1) { | |
nextComment = this._length; | |
} | |
var nextEntry = Math.min(nextEntity, nextComment); | |
this._index = nextEntry; | |
}, | |
error: function (message) { | |
var type = arguments.length <= 1 || arguments[1] === undefined ? 'parsererror' : arguments[1]; | |
var pos = this._index; | |
var start = this._source.lastIndexOf('<', pos - 1); | |
var lastClose = this._source.lastIndexOf('>', pos - 1); | |
start = lastClose > start ? lastClose + 1 : start; | |
var context = this._source.slice(start, pos + 10); | |
var msg = message + ' at pos ' + pos + ': `' + context + '`'; | |
var err = new L10nError(msg); | |
if (this.emit) { | |
this.emit(type, err); | |
} | |
return err; | |
} | |
}; | |
function walkEntry(entry, fn) { | |
if (typeof entry === 'string') { | |
return fn(entry); | |
} | |
var newEntry = Object.create(null); | |
if (entry.value) { | |
newEntry.value = walkValue(entry.value, fn); | |
} | |
if (entry.index) { | |
newEntry.index = entry.index; | |
} | |
if (entry.attrs) { | |
newEntry.attrs = Object.create(null); | |
for (var key in entry.attrs) { | |
newEntry.attrs[key] = walkEntry(entry.attrs[key], fn); | |
} | |
} | |
return newEntry; | |
} | |
function walkValue(value, fn) { | |
if (typeof value === 'string') { | |
return fn(value); | |
} | |
if (value.type) { | |
return value; | |
} | |
var newValue = Array.isArray(value) ? [] : Object.create(null); | |
var keys = Object.keys(value); | |
for (var i = 0, key = undefined; key = keys[i]; i++) { | |
newValue[key] = walkValue(value[key], fn); | |
} | |
return newValue; | |
} | |
function createGetter(id, name) { | |
var _pseudo = null; | |
return function getPseudo() { | |
if (_pseudo) { | |
return _pseudo; | |
} | |
var reAlphas = /[a-zA-Z]/g; | |
var reVowels = /[aeiouAEIOU]/g; | |
var reWords = /[^\W0-9_]+/g; | |
var reExcluded = /(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/; | |
var charMaps = { | |
'fr-x-psaccent': 'ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ[\\]^_`ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ', | |
'ar-x-psbidi': '∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅMXʎZ[\\]ᵥ_,ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz' | |
}; | |
var mods = { | |
'fr-x-psaccent': function (val) { | |
return val.replace(reVowels, function (match) { | |
return match + match.toLowerCase(); | |
}); | |
}, | |
'ar-x-psbidi': function (val) { | |
return val.replace(reWords, function (match) { | |
return '' + match + ''; | |
}); | |
} | |
}; | |
var ASCII_LETTER_A = 65; | |
var replaceChars = function (map, val) { | |
return val.replace(reAlphas, function (match) { | |
return map.charAt(match.charCodeAt(0) - ASCII_LETTER_A); | |
}); | |
}; | |
var transform = function (val) { | |
return replaceChars(charMaps[id], mods[id](val)); | |
}; | |
var apply = function (fn, val) { | |
if (!val) { | |
return val; | |
} | |
var parts = val.split(reExcluded); | |
var modified = parts.map(function (part) { | |
if (reExcluded.test(part)) { | |
return part; | |
} | |
return fn(part); | |
}); | |
return modified.join(''); | |
}; | |
return _pseudo = { | |
name: transform(name), | |
process: function (str) { | |
return apply(transform, str); | |
} | |
}; | |
}; | |
} | |
var pseudo = Object.defineProperties(Object.create(null), { | |
'fr-x-psaccent': { | |
enumerable: true, | |
get: createGetter('fr-x-psaccent', 'Runtime Accented') | |
}, | |
'ar-x-psbidi': { | |
enumerable: true, | |
get: createGetter('ar-x-psbidi', 'Runtime Bidi') | |
} | |
}); | |
var Env = (function () { | |
function Env(fetchResource) { | |
_classCallCheck(this, Env); | |
this.fetchResource = fetchResource; | |
this.resCache = new Map(); | |
this.resRefs = new Map(); | |
this.numberFormatters = null; | |
this.parsers = { | |
properties: PropertiesParser, | |
l20n: L20nParser | |
}; | |
var listeners = {}; | |
this.emit = emit.bind(this, listeners); | |
this.addEventListener = addEventListener.bind(this, listeners); | |
this.removeEventListener = removeEventListener.bind(this, listeners); | |
} | |
Env.prototype.createContext = function createContext(langs, resIds) { | |
var _this8 = this; | |
var ctx = new Context(this, langs, resIds); | |
resIds.forEach(function (resId) { | |
var usedBy = _this8.resRefs.get(resId) || 0; | |
_this8.resRefs.set(resId, usedBy + 1); | |
}); | |
return ctx; | |
}; | |
Env.prototype.destroyContext = function destroyContext(ctx) { | |
var _this9 = this; | |
ctx.resIds.forEach(function (resId) { | |
var usedBy = _this9.resRefs.get(resId) || 0; | |
if (usedBy > 1) { | |
return _this9.resRefs.set(resId, usedBy - 1); | |
} | |
_this9.resRefs.delete(resId); | |
_this9.resCache.forEach(function (val, key) { | |
return key.startsWith(resId) ? _this9.resCache.delete(key) : null; | |
}); | |
}); | |
}; | |
Env.prototype._parse = function _parse(syntax, lang, data) { | |
var _this10 = this; | |
var parser = this.parsers[syntax]; | |
if (!parser) { | |
return data; | |
} | |
var emitAndAmend = function (type, err) { | |
return _this10.emit(type, amendError(lang, err)); | |
}; | |
return parser.parse(emitAndAmend, data); | |
}; | |
Env.prototype._create = function _create(lang, entries) { | |
if (lang.src !== 'pseudo') { | |
return entries; | |
} | |
var pseudoentries = Object.create(null); | |
for (var key in entries) { | |
pseudoentries[key] = walkEntry(entries[key], pseudo[lang.code].process); | |
} | |
return pseudoentries; | |
}; | |
Env.prototype._getResource = function _getResource(lang, res) { | |
var _this11 = this; | |
var cache = this.resCache; | |
var id = res + lang.code + lang.src; | |
if (cache.has(id)) { | |
return cache.get(id); | |
} | |
var syntax = res.substr(res.lastIndexOf('.') + 1); | |
var saveEntries = function (data) { | |
var entries = _this11._parse(syntax, lang, data); | |
cache.set(id, _this11._create(lang, entries)); | |
}; | |
var recover = function (err) { | |
err.lang = lang; | |
_this11.emit('fetcherror', err); | |
cache.set(id, err); | |
}; | |
var langToFetch = lang.src === 'pseudo' ? { code: 'en-US', src: 'app', ver: lang.ver } : lang; | |
var resource = this.fetchResource(res, langToFetch).then(saveEntries, recover); | |
cache.set(id, resource); | |
return resource; | |
}; | |
return Env; | |
})(); | |
function amendError(lang, err) { | |
err.lang = lang; | |
return err; | |
} | |
function prioritizeLocales(def, availableLangs, requested) { | |
var supportedLocale = undefined; | |
for (var i = 0; i < requested.length; i++) { | |
var locale = requested[i]; | |
if (availableLangs.indexOf(locale) !== -1) { | |
supportedLocale = locale; | |
break; | |
} | |
} | |
if (!supportedLocale || supportedLocale === def) { | |
return [def]; | |
} | |
return [supportedLocale, def]; | |
} | |
function negotiateLanguages(_ref7, additionalLangs, prevLangs, requestedLangs) { | |
var appVersion = _ref7.appVersion; | |
var defaultLang = _ref7.defaultLang; | |
var availableLangs = _ref7.availableLangs; | |
var allAvailableLangs = Object.keys(availableLangs).concat(Object.keys(additionalLangs)).concat(Object.keys(pseudo)); | |
var newLangs = prioritizeLocales(defaultLang, allAvailableLangs, requestedLangs); | |
var langs = newLangs.map(function (code) { | |
return { | |
code: code, | |
src: getLangSource(appVersion, availableLangs, additionalLangs, code), | |
ver: appVersion | |
}; | |
}); | |
return { langs: langs, haveChanged: !arrEqual(prevLangs, newLangs) }; | |
} | |
function arrEqual(arr1, arr2) { | |
return arr1.length === arr2.length && arr1.every(function (elem, i) { | |
return elem === arr2[i]; | |
}); | |
} | |
function getMatchingLangpack(appVersion, langpacks) { | |
for (var i = 0, langpack = undefined; langpack = langpacks[i]; i++) { | |
if (langpack.target === appVersion) { | |
return langpack; | |
} | |
} | |
return null; | |
} | |
function getLangSource(appVersion, availableLangs, additionalLangs, code) { | |
if (additionalLangs && additionalLangs[code]) { | |
var lp = getMatchingLangpack(appVersion, additionalLangs[code]); | |
if (lp && (!(code in availableLangs) || parseInt(lp.revision) > availableLangs[code])) { | |
return 'extra'; | |
} | |
} | |
if (code in pseudo && !(code in availableLangs)) { | |
return 'pseudo'; | |
} | |
return 'app'; | |
} | |
var Remote = (function () { | |
function Remote(fetchResource, broadcast) { | |
_classCallCheck(this, Remote); | |
this.broadcast = broadcast; | |
this.env = new Env(fetchResource); | |
this.ctxs = new Map(); | |
} | |
Remote.prototype.registerView = function registerView(view, resources, meta, additionalLangs, requestedLangs) { | |
var _negotiateLanguages = negotiateLanguages(meta, additionalLangs, [], requestedLangs); | |
var langs = _negotiateLanguages.langs; | |
this.ctxs.set(view, this.env.createContext(langs, resources)); | |
return langs; | |
}; | |
Remote.prototype.unregisterView = function unregisterView(view) { | |
this.ctxs.delete(view); | |
return true; | |
}; | |
Remote.prototype.formatEntities = function formatEntities(view, keys) { | |
var _ctxs$get; | |
return (_ctxs$get = this.ctxs.get(view)).formatEntities.apply(_ctxs$get, keys); | |
}; | |
Remote.prototype.formatValues = function formatValues(view, keys) { | |
var _ctxs$get2; | |
return (_ctxs$get2 = this.ctxs.get(view)).formatValues.apply(_ctxs$get2, keys); | |
}; | |
Remote.prototype.changeLanguages = function changeLanguages(view, meta, additionalLangs, requestedLangs) { | |
var oldCtx = this.ctxs.get(view); | |
var prevLangs = oldCtx.langs; | |
var newLangs = negotiateLanguages(meta, additionalLangs, prevLangs, requestedLangs); | |
this.ctxs.set(view, this.env.createContext(newLangs.langs, oldCtx.resIds)); | |
return newLangs; | |
}; | |
Remote.prototype.requestLanguages = function requestLanguages(requestedLangs) { | |
this.broadcast('languageschangerequest', requestedLangs); | |
}; | |
Remote.prototype.getName = function getName(code) { | |
return pseudo[code].name; | |
}; | |
Remote.prototype.processString = function processString(code, str) { | |
return pseudo[code].process(str); | |
}; | |
return Remote; | |
})(); | |
var observerConfig = { | |
attributes: true, | |
characterData: false, | |
childList: true, | |
subtree: true, | |
attributeFilter: ['data-l10n-id', 'data-l10n-args'] | |
}; | |
var observers = new WeakMap(); | |
function initMutationObserver(view) { | |
observers.set(view, { | |
roots: new Set(), | |
observer: new MutationObserver(function (mutations) { | |
return translateMutations(view, mutations); | |
}) | |
}); | |
} | |
function translateRoots(view) { | |
var roots = Array.from(observers.get(view).roots); | |
return Promise.all(roots.map(function (root) { | |
return _translateFragment(view, root); | |
})); | |
} | |
function observe(view, root) { | |
var obs = observers.get(view); | |
if (obs) { | |
obs.roots.add(root); | |
obs.observer.observe(root, observerConfig); | |
} | |
} | |
function disconnect(view, root, allRoots) { | |
var obs = observers.get(view); | |
if (obs) { | |
obs.observer.disconnect(); | |
if (allRoots) { | |
return; | |
} | |
obs.roots.delete(root); | |
obs.roots.forEach(function (other) { | |
return obs.observer.observe(other, observerConfig); | |
}); | |
} | |
} | |
function reconnect(view) { | |
var obs = observers.get(view); | |
if (obs) { | |
obs.roots.forEach(function (root) { | |
return obs.observer.observe(root, observerConfig); | |
}); | |
} | |
} | |
var reOverlay = /<|&#?\w+;/; | |
var allowed = { | |
elements: ['a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u', 'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr'], | |
attributes: { | |
global: ['title', 'aria-label', 'aria-valuetext', 'aria-moz-hint'], | |
a: ['download'], | |
area: ['download', 'alt'], | |
input: ['alt', 'placeholder'], | |
menuitem: ['label'], | |
menu: ['label'], | |
optgroup: ['label'], | |
option: ['label'], | |
track: ['label'], | |
img: ['alt'], | |
textarea: ['placeholder'], | |
th: ['abbr'] | |
} | |
}; | |
function overlayElement(element, translation) { | |
var value = translation.value; | |
if (typeof value === 'string') { | |
if (!reOverlay.test(value)) { | |
element.textContent = value; | |
} else { | |
var tmpl = element.ownerDocument.createElement('template'); | |
tmpl.innerHTML = value; | |
overlay(element, tmpl.content); | |
} | |
} | |
for (var key in translation.attrs) { | |
var attrName = camelCaseToDashed(key); | |
if (isAttrAllowed({ name: attrName }, element)) { | |
element.setAttribute(attrName, translation.attrs[key]); | |
} | |
} | |
} | |
function overlay(sourceElement, translationElement) { | |
var result = translationElement.ownerDocument.createDocumentFragment(); | |
var k = undefined, | |
attr = undefined; | |
var childElement = undefined; | |
while (childElement = translationElement.childNodes[0]) { | |
translationElement.removeChild(childElement); | |
if (childElement.nodeType === childElement.TEXT_NODE) { | |
result.appendChild(childElement); | |
continue; | |
} | |
var index = getIndexOfType(childElement); | |
var sourceChild = getNthElementOfType(sourceElement, childElement, index); | |
if (sourceChild) { | |
overlay(sourceChild, childElement); | |
result.appendChild(sourceChild); | |
continue; | |
} | |
if (isElementAllowed(childElement)) { | |
var sanitizedChild = childElement.ownerDocument.createElement(childElement.nodeName); | |
overlay(sanitizedChild, childElement); | |
result.appendChild(sanitizedChild); | |
continue; | |
} | |
result.appendChild(translationElement.ownerDocument.createTextNode(childElement.textContent)); | |
} | |
sourceElement.textContent = ''; | |
sourceElement.appendChild(result); | |
if (translationElement.attributes) { | |
for (k = 0, attr; attr = translationElement.attributes[k]; k++) { | |
if (isAttrAllowed(attr, sourceElement)) { | |
sourceElement.setAttribute(attr.name, attr.value); | |
} | |
} | |
} | |
} | |
function isElementAllowed(element) { | |
return allowed.elements.indexOf(element.tagName.toLowerCase()) !== -1; | |
} | |
function isAttrAllowed(attr, element) { | |
var attrName = attr.name.toLowerCase(); | |
var tagName = element.tagName.toLowerCase(); | |
if (allowed.attributes.global.indexOf(attrName) !== -1) { | |
return true; | |
} | |
if (!allowed.attributes[tagName]) { | |
return false; | |
} | |
if (allowed.attributes[tagName].indexOf(attrName) !== -1) { | |
return true; | |
} | |
if (tagName === 'input' && attrName === 'value') { | |
var type = element.type.toLowerCase(); | |
if (type === 'submit' || type === 'button' || type === 'reset') { | |
return true; | |
} | |
} | |
return false; | |
} | |
function getNthElementOfType(context, element, index) { | |
var nthOfType = 0; | |
for (var i = 0, child = undefined; child = context.children[i]; i++) { | |
if (child.nodeType === child.ELEMENT_NODE && child.tagName === element.tagName) { | |
if (nthOfType === index) { | |
return child; | |
} | |
nthOfType++; | |
} | |
} | |
return null; | |
} | |
function getIndexOfType(element) { | |
var index = 0; | |
var child = undefined; | |
while (child = element.previousElementSibling) { | |
if (child.tagName === element.tagName) { | |
index++; | |
} | |
} | |
return index; | |
} | |
function camelCaseToDashed(string) { | |
if (string === 'ariaValueText') { | |
return 'aria-valuetext'; | |
} | |
return string.replace(/[A-Z]/g, function (match) { | |
return '-' + match.toLowerCase(); | |
}).replace(/^-/, ''); | |
} | |
var reHtml = /[&<>]/g; | |
var htmlEntities = { | |
'&': '&', | |
'<': '<', | |
'>': '>' | |
}; | |
function setAttributes(element, id, args) { | |
element.setAttribute('data-l10n-id', id); | |
if (args) { | |
element.setAttribute('data-l10n-args', JSON.stringify(args)); | |
} | |
} | |
function getAttributes(element) { | |
return { | |
id: element.getAttribute('data-l10n-id'), | |
args: JSON.parse(element.getAttribute('data-l10n-args')) | |
}; | |
} | |
function getTranslatables(element) { | |
var nodes = Array.from(element.querySelectorAll('[data-l10n-id]')); | |
if (typeof element.hasAttribute === 'function' && element.hasAttribute('data-l10n-id')) { | |
nodes.push(element); | |
} | |
return nodes; | |
} | |
function translateMutations(view, mutations) { | |
var targets = new Set(); | |
for (var _iterator = mutations, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { | |
var _ref; | |
if (_isArray) { | |
if (_i >= _iterator.length) break; | |
_ref = _iterator[_i++]; | |
} else { | |
_i = _iterator.next(); | |
if (_i.done) break; | |
_ref = _i.value; | |
} | |
var mutation = _ref; | |
switch (mutation.type) { | |
case 'attributes': | |
targets.add(mutation.target); | |
break; | |
case 'childList': | |
for (var _iterator2 = mutation.addedNodes, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { | |
var _ref2; | |
if (_isArray2) { | |
if (_i2 >= _iterator2.length) break; | |
_ref2 = _iterator2[_i2++]; | |
} else { | |
_i2 = _iterator2.next(); | |
if (_i2.done) break; | |
_ref2 = _i2.value; | |
} | |
var addedNode = _ref2; | |
if (addedNode.nodeType === addedNode.ELEMENT_NODE) { | |
if (addedNode.childElementCount) { | |
getTranslatables(addedNode).forEach(targets.add.bind(targets)); | |
} else { | |
if (addedNode.hasAttribute('data-l10n-id')) { | |
targets.add(addedNode); | |
} | |
} | |
} | |
} | |
break; | |
} | |
} | |
if (targets.size === 0) { | |
return; | |
} | |
translateElements(view, Array.from(targets)); | |
} | |
function _translateFragment(view, frag) { | |
return translateElements(view, getTranslatables(frag)); | |
} | |
function getElementsTranslation(view, elems) { | |
var keys = elems.map(function (elem) { | |
var id = elem.getAttribute('data-l10n-id'); | |
var args = elem.getAttribute('data-l10n-args'); | |
return args ? [id, JSON.parse(args.replace(reHtml, function (match) { | |
return htmlEntities[match]; | |
}))] : id; | |
}); | |
return view.formatEntities.apply(view, keys); | |
} | |
function translateElements(view, elements) { | |
return getElementsTranslation(view, elements).then(function (translations) { | |
return applyTranslations(view, elements, translations); | |
}); | |
} | |
function applyTranslations(view, elems, translations) { | |
disconnect(view, null, true); | |
for (var i = 0; i < elems.length; i++) { | |
overlayElement(elems[i], translations[i]); | |
} | |
reconnect(view); | |
} | |
if (typeof NodeList === 'function' && !NodeList.prototype[Symbol.iterator]) { | |
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; | |
} | |
function documentReady() { | |
if (document.readyState !== 'loading') { | |
return Promise.resolve(); | |
} | |
return new Promise(function (resolve) { | |
document.addEventListener('readystatechange', function onrsc() { | |
document.removeEventListener('readystatechange', onrsc); | |
resolve(); | |
}); | |
}); | |
} | |
function getDirection(code) { | |
var tag = code.split('-')[0]; | |
return ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(tag) >= 0 ? 'rtl' : 'ltr'; | |
} | |
if (navigator.languages === undefined) { | |
navigator.languages = [navigator.language]; | |
} | |
function getResourceLinks(head) { | |
return Array.prototype.map.call(head.querySelectorAll('link[rel="localization"]'), function (el) { | |
return el.getAttribute('href'); | |
}); | |
} | |
function getMeta(head) { | |
var availableLangs = Object.create(null); | |
var defaultLang = null; | |
var appVersion = null; | |
var metas = Array.from(head.querySelectorAll('meta[name="availableLanguages"],' + 'meta[name="defaultLanguage"],' + 'meta[name="appVersion"]')); | |
for (var _iterator3 = metas, _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { | |
var _ref3; | |
if (_isArray3) { | |
if (_i3 >= _iterator3.length) break; | |
_ref3 = _iterator3[_i3++]; | |
} else { | |
_i3 = _iterator3.next(); | |
if (_i3.done) break; | |
_ref3 = _i3.value; | |
} | |
var meta = _ref3; | |
var _name = meta.getAttribute('name'); | |
var content = meta.getAttribute('content').trim(); | |
switch (_name) { | |
case 'availableLanguages': | |
availableLangs = getLangRevisionMap(availableLangs, content); | |
break; | |
case 'defaultLanguage': | |
var _getLangRevisionTuple = getLangRevisionTuple(content), | |
lang = _getLangRevisionTuple[0], | |
rev = _getLangRevisionTuple[1]; | |
defaultLang = lang; | |
if (!(lang in availableLangs)) { | |
availableLangs[lang] = rev; | |
} | |
break; | |
case 'appVersion': | |
appVersion = content; | |
} | |
} | |
return { | |
defaultLang: defaultLang, | |
availableLangs: availableLangs, | |
appVersion: appVersion | |
}; | |
} | |
function getLangRevisionMap(seq, str) { | |
return str.split(',').reduce(function (prevSeq, cur) { | |
var _getLangRevisionTuple2 = getLangRevisionTuple(cur); | |
var lang = _getLangRevisionTuple2[0]; | |
var rev = _getLangRevisionTuple2[1]; | |
prevSeq[lang] = rev; | |
return prevSeq; | |
}, seq); | |
} | |
function getLangRevisionTuple(str) { | |
var _str$trim$split = str.trim().split(':'); | |
var lang = _str$trim$split[0]; | |
var rev = _str$trim$split[1]; | |
return [lang, parseInt(rev)]; | |
} | |
var viewProps = new WeakMap(); | |
var View = (function () { | |
function View(client, doc) { | |
var _this12 = this; | |
_classCallCheck(this, View); | |
this.pseudo = { | |
'fr-x-psaccent': createPseudo(this, 'fr-x-psaccent'), | |
'ar-x-psbidi': createPseudo(this, 'ar-x-psbidi') | |
}; | |
var initialized = documentReady().then(function () { | |
return init(_this12, client); | |
}); | |
this._interactive = initialized.then(function () { | |
return client; | |
}); | |
this.ready = initialized.then(function (langs) { | |
return translateView(_this12, langs); | |
}); | |
initMutationObserver(this); | |
viewProps.set(this, { | |
doc: doc, | |
ready: false | |
}); | |
client.on('languageschangerequest', function (requestedLangs) { | |
return _this12.requestLanguages(requestedLangs); | |
}); | |
} | |
View.prototype.requestLanguages = function requestLanguages(requestedLangs, isGlobal) { | |
var _this13 = this; | |
var method = isGlobal ? function (client) { | |
return client.method('requestLanguages', requestedLangs); | |
} : function (client) { | |
return changeLanguages(_this13, client, requestedLangs); | |
}; | |
return this._interactive.then(method); | |
}; | |
View.prototype.handleEvent = function handleEvent() { | |
return this.requestLanguages(navigator.languages); | |
}; | |
View.prototype.formatEntities = function formatEntities() { | |
for (var _len7 = arguments.length, keys = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { | |
keys[_key7] = arguments[_key7]; | |
} | |
return this._interactive.then(function (client) { | |
return client.method('formatEntities', client.id, keys); | |
}); | |
}; | |
View.prototype.formatValue = function formatValue(id, args) { | |
return this._interactive.then(function (client) { | |
return client.method('formatValues', client.id, [[id, args]]); | |
}).then(function (values) { | |
return values[0]; | |
}); | |
}; | |
View.prototype.formatValues = function formatValues() { | |
for (var _len8 = arguments.length, keys = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) { | |
keys[_key8] = arguments[_key8]; | |
} | |
return this._interactive.then(function (client) { | |
return client.method('formatValues', client.id, keys); | |
}); | |
}; | |
View.prototype.translateFragment = function translateFragment(frag) { | |
return _translateFragment(this, frag); | |
}; | |
View.prototype.observeRoot = function observeRoot(root) { | |
observe(this, root); | |
}; | |
View.prototype.disconnectRoot = function disconnectRoot(root) { | |
disconnect(this, root); | |
}; | |
return View; | |
})(); | |
View.prototype.setAttributes = setAttributes; | |
View.prototype.getAttributes = getAttributes; | |
function createPseudo(view, code) { | |
return { | |
getName: function () { | |
return view._interactive.then(function (client) { | |
return client.method('getName', code); | |
}); | |
}, | |
processString: function (str) { | |
return view._interactive.then(function (client) { | |
return client.method('processString', code, str); | |
}); | |
} | |
}; | |
} | |
function init(view, client) { | |
var doc = viewProps.get(view).doc; | |
var resources = getResourceLinks(doc.head); | |
var meta = getMeta(doc.head); | |
view.observeRoot(doc.documentElement); | |
return getAdditionalLanguages().then(function (additionalLangs) { | |
return client.method('registerView', client.id, resources, meta, additionalLangs, navigator.languages); | |
}); | |
} | |
function changeLanguages(view, client, requestedLangs) { | |
var doc = viewProps.get(view).doc; | |
var meta = getMeta(doc.head); | |
return getAdditionalLanguages().then(function (additionalLangs) { | |
return client.method('changeLanguages', client.id, meta, additionalLangs, requestedLangs); | |
}).then(function (_ref8) { | |
var langs = _ref8.langs; | |
var haveChanged = _ref8.haveChanged; | |
return haveChanged ? translateView(view, langs) : undefined; | |
}); | |
} | |
function getAdditionalLanguages() { | |
if (navigator.mozApps && navigator.mozApps.getAdditionalLanguages) { | |
return navigator.mozApps.getAdditionalLanguages().catch(function () { | |
return Object.create(null); | |
}); | |
} | |
return Promise.resolve(Object.create(null)); | |
} | |
function translateView(view, langs) { | |
var props = viewProps.get(view); | |
var html = props.doc.documentElement; | |
if (props.ready) { | |
return translateRoots(view).then(function () { | |
return setAllAndEmit(html, langs); | |
}); | |
} | |
var translated = langs[0].code === html.getAttribute('lang') ? Promise.resolve() : translateRoots(view).then(function () { | |
return setLangDir(html, langs); | |
}); | |
return translated.then(function () { | |
setLangs(html, langs); | |
props.ready = true; | |
}); | |
} | |
function setLangs(html, langs) { | |
var codes = langs.map(function (lang) { | |
return lang.code; | |
}); | |
html.setAttribute('langs', codes.join(' ')); | |
} | |
function setLangDir(html, langs) { | |
var code = langs[0].code; | |
html.setAttribute('lang', code); | |
html.setAttribute('dir', getDirection(code)); | |
} | |
function setAllAndEmit(html, langs) { | |
setLangDir(html, langs); | |
setLangs(html, langs); | |
html.parentNode.dispatchEvent(new CustomEvent('DOMRetranslated', { | |
bubbles: false, | |
cancelable: false | |
})); | |
} | |
var remote = new Remote(fetchResource, broadcast); | |
var client = new Client(remote); | |
document.l10n = new View(client, document); | |
window.addEventListener('languagechange', document.l10n); | |
document.addEventListener('additionallanguageschange', document.l10n); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment