Commit 0e9aeacde8e18f3bef4eb06e0a831ded13a389e5

Authored by root
1 parent 79ec939f

localization l20n

Showing 36 changed files with 53094 additions and 45 deletions
bower_components/l20n/.bower.json 0 → 100755
  1 +{
  2 + "name": "l20n",
  3 + "description": "A natural-language localization framework",
  4 + "version": "3.4.1",
  5 + "homepage": "http://l20n.org",
  6 + "repository": {
  7 + "type": "git",
  8 + "url": "git://github.com/l20n/l20n.js.git"
  9 + },
  10 + "authors": [
  11 + "Mozilla <l10n-drivers@mozilla.org>",
  12 + "Zbigniew Braniecki",
  13 + "Staś Małolepszy"
  14 + ],
  15 + "license": "Apache 2.0",
  16 + "keywords": [
  17 + "localization",
  18 + "l10n",
  19 + "l20n"
  20 + ],
  21 + "main": [],
  22 + "ignore": [
  23 + "**/*",
  24 + "!dist/**/*"
  25 + ],
  26 + "_release": "3.4.1",
  27 + "_resolution": {
  28 + "type": "version",
  29 + "tag": "v3.4.1",
  30 + "commit": "4b5428be54003e8dc7243665a5f844a546ddd226"
  31 + },
  32 + "_source": "git://github.com/l20n/l20n.js.git",
  33 + "_target": "v3.x",
  34 + "_originalSource": "l20n",
  35 + "_direct": true
  36 +}
0 37 \ No newline at end of file
... ...
bower_components/l20n/bower.json 0 → 100755
  1 +{
  2 + "name": "l20n",
  3 + "description": "A natural-language localization framework",
  4 + "version": "3.4.1",
  5 + "homepage": "http://l20n.org",
  6 + "repository": {
  7 + "type": "git",
  8 + "url": "git://github.com/l20n/l20n.js.git"
  9 + },
  10 + "authors": [
  11 + "Mozilla <l10n-drivers@mozilla.org>",
  12 + "Zbigniew Braniecki",
  13 + "Staś Małolepszy"
  14 + ],
  15 + "license": "Apache 2.0",
  16 + "keywords": [
  17 + "localization",
  18 + "l10n",
  19 + "l20n"
  20 + ],
  21 + "main": [],
  22 + "ignore": [
  23 + "**/*",
  24 + "!dist/**/*"
  25 + ]
  26 +}
... ...
bower_components/l20n/dist/bundle/aisle/l20n.js 0 → 100755
  1 +define(['exports'], function (exports) { 'use strict';
  2 +
  3 + class Node {
  4 + constructor() {
  5 + this.type = this.constructor.name;
  6 + }
  7 + }
  8 +
  9 + class Entry extends Node {
  10 + constructor() {
  11 + super();
  12 + }
  13 + }
  14 +
  15 + class Identifier extends Node {
  16 + constructor(name) {
  17 + super();
  18 + this.name = name;
  19 + }
  20 + }
  21 +
  22 + class Variable extends Node {
  23 + constructor(name) {
  24 + super();
  25 + this.name = name;
  26 + }
  27 + }
  28 +
  29 + class Global extends Node {
  30 + constructor(name) {
  31 + super();
  32 + this.name = name;
  33 + }
  34 + }
  35 +
  36 + class Value extends Node {
  37 + constructor() {
  38 + super();
  39 + }
  40 + }
  41 +
  42 + class String extends Value {
  43 + constructor(source, content) {
  44 + super();
  45 + this.source = source;
  46 + this.content = content;
  47 +
  48 + this._opchar = '"';
  49 + }
  50 + }
  51 +
  52 + class Hash extends Value {
  53 + constructor(items) {
  54 + super();
  55 + this.items = items;
  56 + }
  57 + }
  58 +
  59 +
  60 + class Entity extends Entry {
  61 + constructor(id, value = null, index = null, attrs = []) {
  62 + super();
  63 + this.id = id;
  64 + this.value = value;
  65 + this.index = index;
  66 + this.attrs = attrs;
  67 + }
  68 + }
  69 +
  70 + class Resource extends Node {
  71 + constructor() {
  72 + super();
  73 + this.body = [];
  74 + }
  75 + }
  76 +
  77 + class Attribute extends Node {
  78 + constructor(id, value, index = null) {
  79 + super();
  80 + this.id = id;
  81 + this.value = value;
  82 + this.index = index;
  83 + }
  84 + }
  85 +
  86 + class HashItem extends Node {
  87 + constructor(id, value, defItem) {
  88 + super();
  89 + this.id = id;
  90 + this.value = value;
  91 + this.default = defItem;
  92 + }
  93 + }
  94 +
  95 + class Comment extends Entry {
  96 + constructor(body) {
  97 + super();
  98 + this.body = body;
  99 + }
  100 + }
  101 +
  102 + class Expression extends Node {
  103 + constructor() {
  104 + super();
  105 + }
  106 + }
  107 +
  108 + class PropertyExpression extends Expression {
  109 + constructor(idref, exp, computed = false) {
  110 + super();
  111 + this.idref = idref;
  112 + this.exp = exp;
  113 + this.computed = computed;
  114 + }
  115 + }
  116 +
  117 + class CallExpression extends Expression {
  118 + constructor(callee, args) {
  119 + super();
  120 + this.callee = callee;
  121 + this.args = args;
  122 + }
  123 + }
  124 +
  125 + class JunkEntry extends Entry {
  126 + constructor(content) {
  127 + super();
  128 + this.content = content;
  129 + }
  130 + }
  131 +
  132 + var AST = {
  133 + Node,
  134 + Identifier,
  135 + Value,
  136 + String,
  137 + Hash,
  138 + Entity,
  139 + Resource,
  140 + Attribute,
  141 + HashItem,
  142 + Comment,
  143 + Variable,
  144 + Global,
  145 + Expression,
  146 + PropertyExpression,
  147 + CallExpression,
  148 + JunkEntry,
  149 + };
  150 +
  151 + function L10nError(message, id, lang) {
  152 + this.name = 'L10nError';
  153 + this.message = message;
  154 + this.id = id;
  155 + this.lang = lang;
  156 + }
  157 + L10nError.prototype = Object.create(Error.prototype);
  158 + L10nError.prototype.constructor = L10nError;
  159 +
  160 + const MAX_PLACEABLES = 100;
  161 +
  162 +
  163 + class ParseContext {
  164 + constructor(string, pos) {
  165 + this._config = {
  166 + pos: pos
  167 + };
  168 + this._source = string;
  169 + this._index = 0;
  170 + this._length = string.length;
  171 + this._curEntryStart = 0;
  172 + }
  173 +
  174 + setPosition(node, start, end) {
  175 + if (!this._config.pos) {
  176 + return;
  177 + }
  178 + node._pos = {start, end};
  179 + }
  180 +
  181 + getResource() {
  182 + let resource = new AST.Resource();
  183 + this.setPosition(resource, 0, this._length);
  184 + resource._errors = [];
  185 +
  186 + this.getWS();
  187 + while (this._index < this._length) {
  188 + try {
  189 + resource.body.push(this.getEntry());
  190 + } catch (e) {
  191 + if (e instanceof L10nError) {
  192 + resource._errors.push(e);
  193 + resource.body.push(this.getJunkEntry());
  194 + } else {
  195 + throw e;
  196 + }
  197 + }
  198 + if (this._index < this._length) {
  199 + this.getWS();
  200 + }
  201 + }
  202 +
  203 + return resource;
  204 + }
  205 +
  206 + getEntry() {
  207 + this._curEntryStart = this._index;
  208 +
  209 + if (this._source[this._index] === '<') {
  210 + ++this._index;
  211 + const id = this.getIdentifier();
  212 + if (this._source[this._index] === '[') {
  213 + ++this._index;
  214 + return this.getEntity(id, this.getItemList(this.getExpression, ']'));
  215 + }
  216 + return this.getEntity(id);
  217 + }
  218 +
  219 + if (this._source.startsWith('/*', this._index)) {
  220 + return this.getComment();
  221 + }
  222 +
  223 + throw this.error('Invalid entry');
  224 + }
  225 +
  226 + getEntity(id, index) {
  227 + if (!this.getRequiredWS()) {
  228 + throw this.error('Expected white space');
  229 + }
  230 +
  231 + const ch = this._source.charAt(this._index);
  232 + const value = this.getValue(ch, index === undefined);
  233 + let attrs;
  234 +
  235 + if (value === null) {
  236 + if (ch === '>') {
  237 + throw this.error('Expected ">"');
  238 + }
  239 + attrs = this.getAttributes();
  240 + } else {
  241 + const ws1 = this.getRequiredWS();
  242 + if (this._source[this._index] !== '>') {
  243 + if (!ws1) {
  244 + throw this.error('Expected ">"');
  245 + }
  246 + attrs = this.getAttributes();
  247 + }
  248 + }
  249 +
  250 + // skip '>'
  251 + ++this._index;
  252 +
  253 + const entity = new AST.Entity(id, value, index, attrs);
  254 + this.setPosition(entity, this._curEntryStart, this._index);
  255 + return entity;
  256 + }
  257 +
  258 + getValue(ch = this._source[this._index], optional = false) {
  259 + switch (ch) {
  260 + case '\'':
  261 + case '"':
  262 + return this.getString(ch, 1);
  263 + case '{':
  264 + return this.getHash();
  265 + }
  266 +
  267 + if (!optional) {
  268 + throw this.error('Unknown value type');
  269 + }
  270 + return null;
  271 + }
  272 +
  273 + getWS() {
  274 + let cc = this._source.charCodeAt(this._index);
  275 + // space, \n, \t, \r
  276 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  277 + cc = this._source.charCodeAt(++this._index);
  278 + }
  279 + }
  280 +
  281 + getRequiredWS() {
  282 + const pos = this._index;
  283 + let cc = this._source.charCodeAt(pos);
  284 + // space, \n, \t, \r
  285 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  286 + cc = this._source.charCodeAt(++this._index);
  287 + }
  288 + return this._index !== pos;
  289 + }
  290 +
  291 + getIdentifier() {
  292 + const start = this._index;
  293 + let cc = this._source.charCodeAt(this._index);
  294 +
  295 + if ((cc >= 97 && cc <= 122) || // a-z
  296 + (cc >= 65 && cc <= 90) || // A-Z
  297 + cc === 95) { // _
  298 + cc = this._source.charCodeAt(++this._index);
  299 + } else {
  300 + throw this.error('Identifier has to start with [a-zA-Z_]');
  301 + }
  302 +
  303 + while ((cc >= 97 && cc <= 122) || // a-z
  304 + (cc >= 65 && cc <= 90) || // A-Z
  305 + (cc >= 48 && cc <= 57) || // 0-9
  306 + cc === 95) { // _
  307 + cc = this._source.charCodeAt(++this._index);
  308 + }
  309 +
  310 + const id = new AST.Identifier(this._source.slice(start, this._index));
  311 + this.setPosition(id, start, this._index);
  312 + return id;
  313 + }
  314 +
  315 + getUnicodeChar() {
  316 + for (let i = 0; i < 4; i++) {
  317 + let cc = this._source.charCodeAt(++this._index);
  318 + if ((cc > 96 && cc < 103) || // a-f
  319 + (cc > 64 && cc < 71) || // A-F
  320 + (cc > 47 && cc < 58)) { // 0-9
  321 + continue;
  322 + }
  323 + throw this.error('Illegal unicode escape sequence');
  324 + }
  325 + return '\\u' + this._source.slice(this._index - 3, this._index + 1);
  326 + }
  327 +
  328 + getString(opchar, opcharLen) {
  329 + let body = [];
  330 + let buf = '';
  331 + let placeables = 0;
  332 +
  333 + this._index += opcharLen - 1;
  334 +
  335 + const start = this._index + 1;
  336 +
  337 + let closed = false;
  338 +
  339 + while (!closed) {
  340 + let ch = this._source[++this._index];
  341 +
  342 + switch (ch) {
  343 + case '\\':
  344 + const ch2 = this._source[++this._index];
  345 + if (ch2 === 'u') {
  346 + buf += this.getUnicodeChar();
  347 + } else if (ch2 === opchar || ch2 === '\\') {
  348 + buf += ch2;
  349 + } else if (ch2 === '{' && this._source[this._index + 1] === '{') {
  350 + buf += '{';
  351 + } else {
  352 + throw this.error('Illegal escape sequence');
  353 + }
  354 + break;
  355 + case '{':
  356 + if (this._source[this._index + 1] === '{') {
  357 + if (placeables > MAX_PLACEABLES - 1) {
  358 + throw this.error('Too many placeables, maximum allowed is ' +
  359 + MAX_PLACEABLES);
  360 + }
  361 + if (buf.length) {
  362 + body.push(buf);
  363 + buf = '';
  364 + }
  365 + this._index += 2;
  366 + this.getWS();
  367 + body.push(this.getExpression());
  368 + this.getWS();
  369 + if (!this._source.startsWith('}}', this._index)) {
  370 + throw this.error('Expected "}}"');
  371 + }
  372 + this._index += 1;
  373 + placeables++;
  374 + break;
  375 + }
  376 + /* falls through */
  377 + default:
  378 + if (ch === opchar) {
  379 + this._index++;
  380 + closed = true;
  381 + break;
  382 + }
  383 +
  384 + buf += ch;
  385 + if (this._index + 1 >= this._length) {
  386 + throw this.error('Unclosed string literal');
  387 + }
  388 + }
  389 + }
  390 +
  391 + if (buf.length) {
  392 + body.push(buf);
  393 + }
  394 +
  395 + const string = new AST.String(
  396 + this._source.slice(start, this._index - 1), body);
  397 + this.setPosition(string, start, this._index);
  398 + string._opchar = opchar;
  399 +
  400 + return string;
  401 + }
  402 +
  403 + getAttributes() {
  404 + const attrs = [];
  405 +
  406 + while (true) {
  407 + const attr = this.getAttribute();
  408 + attrs.push(attr);
  409 + const ws1 = this.getRequiredWS();
  410 + const ch = this._source.charAt(this._index);
  411 + if (ch === '>') {
  412 + break;
  413 + } else if (!ws1) {
  414 + throw this.error('Expected ">"');
  415 + }
  416 + }
  417 + return attrs;
  418 + }
  419 +
  420 + getAttribute() {
  421 + const start = this._index;
  422 + const key = this.getIdentifier();
  423 + let index;
  424 +
  425 + if (this._source[this._index]=== '[') {
  426 + ++this._index;
  427 + this.getWS();
  428 + index = this.getItemList(this.getExpression, ']');
  429 + }
  430 + this.getWS();
  431 + if (this._source[this._index] !== ':') {
  432 + throw this.error('Expected ":"');
  433 + }
  434 + ++this._index;
  435 + this.getWS();
  436 + const attr = new AST.Attribute(key, this.getValue(), index);
  437 + this.setPosition(attr, start, this._index);
  438 + return attr;
  439 + }
  440 +
  441 + getHash() {
  442 + const start = this._index;
  443 + let items = [];
  444 +
  445 + ++this._index;
  446 + this.getWS();
  447 +
  448 + while (true) {
  449 + items.push(this.getHashItem());
  450 + this.getWS();
  451 +
  452 + const comma = this._source[this._index] === ',';
  453 + if (comma) {
  454 + ++this._index;
  455 + this.getWS();
  456 + }
  457 + if (this._source[this._index] === '}') {
  458 + ++this._index;
  459 + break;
  460 + }
  461 + if (!comma) {
  462 + throw this.error('Expected "}"');
  463 + }
  464 + }
  465 +
  466 + const hash = new AST.Hash(items);
  467 + this.setPosition(hash, start, this._index);
  468 + return hash;
  469 + }
  470 +
  471 + getHashItem() {
  472 + const start = this._index;
  473 +
  474 + let defItem = false;
  475 + if (this._source[this._index] === '*') {
  476 + ++this._index;
  477 + defItem = true;
  478 + }
  479 +
  480 + const key = this.getIdentifier();
  481 + this.getWS();
  482 + if (this._source[this._index] !== ':') {
  483 + throw this.error('Expected ":"');
  484 + }
  485 + ++this._index;
  486 + this.getWS();
  487 +
  488 + const hashItem = new AST.HashItem(key, this.getValue(), defItem);
  489 + this.setPosition(hashItem, start, this._index);
  490 + return hashItem;
  491 + }
  492 +
  493 + getComment() {
  494 + this._index += 2;
  495 + const start = this._index;
  496 + const end = this._source.indexOf('*/', start);
  497 +
  498 + if (end === -1) {
  499 + throw this.error('Comment without a closing tag');
  500 + }
  501 +
  502 + this._index = end + 2;
  503 + const comment = new AST.Comment(this._source.slice(start, end));
  504 + this.setPosition(comment, start - 2, this._index);
  505 + return comment;
  506 + }
  507 +
  508 + getExpression() {
  509 + const start = this._index;
  510 + let exp = this.getPrimaryExpression();
  511 +
  512 + while (true) {
  513 + let ch = this._source[this._index];
  514 + if (ch === '.' || ch === '[') {
  515 + ++this._index;
  516 + exp = this.getPropertyExpression(exp, ch === '[', start);
  517 + } else if (ch === '(') {
  518 + ++this._index;
  519 + exp = this.getCallExpression(exp, start);
  520 + } else {
  521 + break;
  522 + }
  523 + }
  524 +
  525 + return exp;
  526 + }
  527 +
  528 + getPropertyExpression(idref, computed, start) {
  529 + let exp;
  530 +
  531 + if (computed) {
  532 + this.getWS();
  533 + exp = this.getExpression();
  534 + this.getWS();
  535 + if (this._source[this._index] !== ']') {
  536 + throw this.error('Expected "]"');
  537 + }
  538 + ++this._index;
  539 + } else {
  540 + exp = this.getIdentifier();
  541 + }
  542 +
  543 + const propExpr = new AST.PropertyExpression(idref, exp, computed);
  544 + this.setPosition(propExpr, start, this._index);
  545 + return propExpr;
  546 + }
  547 +
  548 + getCallExpression(callee, start) {
  549 + this.getWS();
  550 +
  551 + const callExpr = new AST.CallExpression(callee,
  552 + this.getItemList(this.getExpression, ')'));
  553 + this.setPosition(callExpr, start, this._index);
  554 + return callExpr;
  555 + }
  556 +
  557 + getPrimaryExpression() {
  558 + const start = this._index;
  559 + const ch = this._source[this._index];
  560 +
  561 + switch (ch) {
  562 + case '$':
  563 + ++this._index;
  564 + const variable = new AST.Variable(this.getIdentifier());
  565 + this.setPosition(variable, start, this._index);
  566 + return variable;
  567 + case '@':
  568 + ++this._index;
  569 + const global = new AST.Global(this.getIdentifier());
  570 + this.setPosition(global, start, this._index);
  571 + return global;
  572 + default:
  573 + return this.getIdentifier();
  574 + }
  575 + }
  576 +
  577 + getItemList(callback, closeChar) {
  578 + let items = [];
  579 + let closed = false;
  580 +
  581 + this.getWS();
  582 +
  583 + if (this._source[this._index] === closeChar) {
  584 + ++this._index;
  585 + closed = true;
  586 + }
  587 +
  588 + while (!closed) {
  589 + items.push(callback.call(this));
  590 + this.getWS();
  591 + let ch = this._source.charAt(this._index);
  592 + switch (ch) {
  593 + case ',':
  594 + ++this._index;
  595 + this.getWS();
  596 + break;
  597 + case closeChar:
  598 + ++this._index;
  599 + closed = true;
  600 + break;
  601 + default:
  602 + throw this.error('Expected "," or "' + closeChar + '"');
  603 + }
  604 + }
  605 +
  606 + return items;
  607 + }
  608 +
  609 + error(message) {
  610 + const pos = this._index;
  611 +
  612 + let start = this._source.lastIndexOf('<', pos - 1);
  613 + let lastClose = this._source.lastIndexOf('>', pos - 1);
  614 + start = lastClose > start ? lastClose + 1 : start;
  615 + let context = this._source.slice(start, pos + 10);
  616 +
  617 + let msg = message + ' at pos ' + pos + ': `' + context + '`';
  618 +
  619 + const err = new L10nError(msg);
  620 + err._pos = {start: pos, end: undefined};
  621 + err.offset = pos - start;
  622 + err.description = message;
  623 + err.context = context;
  624 + return err;
  625 + }
  626 +
  627 + getJunkEntry() {
  628 + const pos = this._index;
  629 + let nextEntity = this._source.indexOf('<', pos);
  630 + let nextComment = this._source.indexOf('/*', pos);
  631 +
  632 + if (nextEntity === -1) {
  633 + nextEntity = this._length;
  634 + }
  635 + if (nextComment === -1) {
  636 + nextComment = this._length;
  637 + }
  638 +
  639 + let nextEntry = Math.min(nextEntity, nextComment);
  640 +
  641 + this._index = nextEntry;
  642 +
  643 + const junk = new AST.JunkEntry(
  644 + this._source.slice(this._curEntryStart, nextEntry));
  645 +
  646 + this.setPosition(junk, this._curEntryStart, nextEntry);
  647 + return junk;
  648 + }
  649 + }
  650 +
  651 + var parser = {
  652 + parseResource: function(string, pos = false) {
  653 + const parseContext = new ParseContext(string, pos);
  654 + return parseContext.getResource();
  655 + },
  656 + };
  657 +
  658 + exports.L20nParser = parser;
  659 +
  660 +});
0 661 \ No newline at end of file
... ...
bower_components/l20n/dist/bundle/bridge/l20n-client.js 0 → 100755
  1 +(function () { 'use strict';
  2 +
  3 + /* global bridge, BroadcastChannel */
  4 +
  5 + const Client = bridge.client;
  6 + const channel = new BroadcastChannel('l20n-channel');
  7 +
  8 + // match the opening angle bracket (<) in HTML tags, and HTML entities like
  9 + // &amp;, &#0038;, &#x0026;.
  10 + const reOverlay = /<|&#?\w+;/;
  11 +
  12 + const allowed = {
  13 + elements: [
  14 + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data',
  15 + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u',
  16 + 'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr'
  17 + ],
  18 + attributes: {
  19 + global: [ 'title', 'aria-label', 'aria-valuetext', 'aria-moz-hint' ],
  20 + a: [ 'download' ],
  21 + area: [ 'download', 'alt' ],
  22 + // value is special-cased in isAttrAllowed
  23 + input: [ 'alt', 'placeholder' ],
  24 + menuitem: [ 'label' ],
  25 + menu: [ 'label' ],
  26 + optgroup: [ 'label' ],
  27 + option: [ 'label' ],
  28 + track: [ 'label' ],
  29 + img: [ 'alt' ],
  30 + textarea: [ 'placeholder' ],
  31 + th: [ 'abbr']
  32 + }
  33 + };
  34 +
  35 + function overlayElement(element, translation) {
  36 + const value = translation.value;
  37 +
  38 + if (typeof value === 'string') {
  39 + if (!reOverlay.test(value)) {
  40 + element.textContent = value;
  41 + } else {
  42 + // start with an inert template element and move its children into
  43 + // `element` but such that `element`'s own children are not replaced
  44 + const tmpl = element.ownerDocument.createElement('template');
  45 + tmpl.innerHTML = value;
  46 + // overlay the node with the DocumentFragment
  47 + overlay(element, tmpl.content);
  48 + }
  49 + }
  50 +
  51 + for (let key in translation.attrs) {
  52 + const attrName = camelCaseToDashed(key);
  53 + if (isAttrAllowed({ name: attrName }, element)) {
  54 + element.setAttribute(attrName, translation.attrs[key]);
  55 + }
  56 + }
  57 + }
  58 +
  59 + // The goal of overlay is to move the children of `translationElement`
  60 + // into `sourceElement` such that `sourceElement`'s own children are not
  61 + // replaced, but onle have their text nodes and their attributes modified.
  62 + //
  63 + // We want to make it possible for localizers to apply text-level semantics to
  64 + // the translations and make use of HTML entities. At the same time, we
  65 + // don't trust translations so we need to filter unsafe elements and
  66 + // attribtues out and we don't want to break the Web by replacing elements to
  67 + // which third-party code might have created references (e.g. two-way
  68 + // bindings in MVC frameworks).
  69 + function overlay(sourceElement, translationElement) {
  70 + const result = translationElement.ownerDocument.createDocumentFragment();
  71 + let k, attr;
  72 +
  73 + // take one node from translationElement at a time and check it against
  74 + // the allowed list or try to match it with a corresponding element
  75 + // in the source
  76 + let childElement;
  77 + while ((childElement = translationElement.childNodes[0])) {
  78 + translationElement.removeChild(childElement);
  79 +
  80 + if (childElement.nodeType === childElement.TEXT_NODE) {
  81 + result.appendChild(childElement);
  82 + continue;
  83 + }
  84 +
  85 + const index = getIndexOfType(childElement);
  86 + const sourceChild = getNthElementOfType(sourceElement, childElement, index);
  87 + if (sourceChild) {
  88 + // there is a corresponding element in the source, let's use it
  89 + overlay(sourceChild, childElement);
  90 + result.appendChild(sourceChild);
  91 + continue;
  92 + }
  93 +
  94 + if (isElementAllowed(childElement)) {
  95 + const sanitizedChild = childElement.ownerDocument.createElement(
  96 + childElement.nodeName);
  97 + overlay(sanitizedChild, childElement);
  98 + result.appendChild(sanitizedChild);
  99 + continue;
  100 + }
  101 +
  102 + // otherwise just take this child's textContent
  103 + result.appendChild(
  104 + translationElement.ownerDocument.createTextNode(
  105 + childElement.textContent));
  106 + }
  107 +
  108 + // clear `sourceElement` and append `result` which by this time contains
  109 + // `sourceElement`'s original children, overlayed with translation
  110 + sourceElement.textContent = '';
  111 + sourceElement.appendChild(result);
  112 +
  113 + // if we're overlaying a nested element, translate the allowed
  114 + // attributes; top-level attributes are handled in `translateElement`
  115 + // XXX attributes previously set here for another language should be
  116 + // cleared if a new language doesn't use them; https://bugzil.la/922577
  117 + if (translationElement.attributes) {
  118 + for (k = 0, attr; (attr = translationElement.attributes[k]); k++) {
  119 + if (isAttrAllowed(attr, sourceElement)) {
  120 + sourceElement.setAttribute(attr.name, attr.value);
  121 + }
  122 + }
  123 + }
  124 + }
  125 +
  126 + // XXX the allowed list should be amendable; https://bugzil.la/922573
  127 + function isElementAllowed(element) {
  128 + return allowed.elements.indexOf(element.tagName.toLowerCase()) !== -1;
  129 + }
  130 +
  131 + function isAttrAllowed(attr, element) {
  132 + const attrName = attr.name.toLowerCase();
  133 + const tagName = element.tagName.toLowerCase();
  134 + // is it a globally safe attribute?
  135 + if (allowed.attributes.global.indexOf(attrName) !== -1) {
  136 + return true;
  137 + }
  138 + // are there no allowed attributes for this element?
  139 + if (!allowed.attributes[tagName]) {
  140 + return false;
  141 + }
  142 + // is it allowed on this element?
  143 + // XXX the allowed list should be amendable; https://bugzil.la/922573
  144 + if (allowed.attributes[tagName].indexOf(attrName) !== -1) {
  145 + return true;
  146 + }
  147 + // special case for value on inputs with type button, reset, submit
  148 + if (tagName === 'input' && attrName === 'value') {
  149 + const type = element.type.toLowerCase();
  150 + if (type === 'submit' || type === 'button' || type === 'reset') {
  151 + return true;
  152 + }
  153 + }
  154 + return false;
  155 + }
  156 +
  157 + // Get n-th immediate child of context that is of the same type as element.
  158 + // XXX Use querySelector(':scope > ELEMENT:nth-of-type(index)'), when:
  159 + // 1) :scope is widely supported in more browsers and 2) it works with
  160 + // DocumentFragments.
  161 + function getNthElementOfType(context, element, index) {
  162 + /* jshint boss:true */
  163 + let nthOfType = 0;
  164 + for (let i = 0, child; child = context.children[i]; i++) {
  165 + if (child.nodeType === child.ELEMENT_NODE &&
  166 + child.tagName === element.tagName) {
  167 + if (nthOfType === index) {
  168 + return child;
  169 + }
  170 + nthOfType++;
  171 + }
  172 + }
  173 + return null;
  174 + }
  175 +
  176 + // Get the index of the element among siblings of the same type.
  177 + function getIndexOfType(element) {
  178 + let index = 0;
  179 + let child;
  180 + while ((child = element.previousElementSibling)) {
  181 + if (child.tagName === element.tagName) {
  182 + index++;
  183 + }
  184 + }
  185 + return index;
  186 + }
  187 +
  188 + function camelCaseToDashed(string) {
  189 + // XXX workaround for https://bugzil.la/1141934
  190 + if (string === 'ariaValueText') {
  191 + return 'aria-valuetext';
  192 + }
  193 +
  194 + return string
  195 + .replace(/[A-Z]/g, function (match) {
  196 + return '-' + match.toLowerCase();
  197 + })
  198 + .replace(/^-/, '');
  199 + }
  200 +
  201 + const reHtml = /[&<>]/g;
  202 + const htmlEntities = {
  203 + '&': '&amp;',
  204 + '<': '&lt;',
  205 + '>': '&gt;',
  206 + };
  207 +
  208 + function getResourceLinks(head) {
  209 + return Array.prototype.map.call(
  210 + head.querySelectorAll('link[rel="localization"]'),
  211 + el => el.getAttribute('href'));
  212 + }
  213 +
  214 + function setAttributes(element, id, args) {
  215 + element.setAttribute('data-l10n-id', id);
  216 + if (args) {
  217 + element.setAttribute('data-l10n-args', JSON.stringify(args));
  218 + }
  219 + }
  220 +
  221 + function getAttributes(element) {
  222 + return {
  223 + id: element.getAttribute('data-l10n-id'),
  224 + args: JSON.parse(element.getAttribute('data-l10n-args'))
  225 + };
  226 + }
  227 +
  228 + function getTranslatables(element) {
  229 + const nodes = Array.from(element.querySelectorAll('[data-l10n-id]'));
  230 +
  231 + if (typeof element.hasAttribute === 'function' &&
  232 + element.hasAttribute('data-l10n-id')) {
  233 + nodes.push(element);
  234 + }
  235 +
  236 + return nodes;
  237 + }
  238 +
  239 + function translateMutations(view, langs, mutations) {
  240 + const targets = new Set();
  241 +
  242 + for (let mutation of mutations) {
  243 + switch (mutation.type) {
  244 + case 'attributes':
  245 + targets.add(mutation.target);
  246 + break;
  247 + case 'childList':
  248 + for (let addedNode of mutation.addedNodes) {
  249 + if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
  250 + if (addedNode.childElementCount) {
  251 + getTranslatables(addedNode).forEach(targets.add.bind(targets));
  252 + } else {
  253 + if (addedNode.hasAttribute('data-l10n-id')) {
  254 + targets.add(addedNode);
  255 + }
  256 + }
  257 + }
  258 + }
  259 + break;
  260 + }
  261 + }
  262 +
  263 + if (targets.size === 0) {
  264 + return;
  265 + }
  266 +
  267 + translateElements(view, langs, Array.from(targets));
  268 + }
  269 +
  270 + function translateFragment(view, langs, frag) {
  271 + return translateElements(view, langs, getTranslatables(frag));
  272 + }
  273 +
  274 + function getElementsTranslation(view, langs, elems) {
  275 + const keys = elems.map(elem => {
  276 + const id = elem.getAttribute('data-l10n-id');
  277 + const args = elem.getAttribute('data-l10n-args');
  278 + return args ? [
  279 + id,
  280 + JSON.parse(args.replace(reHtml, match => htmlEntities[match]))
  281 + ] : id;
  282 + });
  283 +
  284 + return view._resolveEntities(langs, keys);
  285 + }
  286 +
  287 + function translateElements(view, langs, elements) {
  288 + return getElementsTranslation(view, langs, elements).then(
  289 + translations => applyTranslations(view, elements, translations));
  290 + }
  291 +
  292 + function applyTranslations(view, elems, translations) {
  293 + view._disconnect();
  294 + for (let i = 0; i < elems.length; i++) {
  295 + overlayElement(elems[i], translations[i]);
  296 + }
  297 + view._observe();
  298 + }
  299 +
  300 + // Polyfill NodeList.prototype[Symbol.iterator] for Chrome.
  301 + // See https://code.google.com/p/chromium/issues/detail?id=401699
  302 + if (typeof NodeList === 'function' && !NodeList.prototype[Symbol.iterator]) {
  303 + NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
  304 + }
  305 +
  306 + // A document.ready shim
  307 + // https://github.com/whatwg/html/issues/127
  308 + function documentReady() {
  309 + if (document.readyState !== 'loading') {
  310 + return Promise.resolve();
  311 + }
  312 +
  313 + return new Promise(resolve => {
  314 + document.addEventListener('readystatechange', function onrsc() {
  315 + document.removeEventListener('readystatechange', onrsc);
  316 + resolve();
  317 + });
  318 + });
  319 + }
  320 +
  321 + // Intl.Locale
  322 + function getDirection(code) {
  323 + const tag = code.split('-')[0];
  324 + return ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(tag) >= 0 ?
  325 + 'rtl' : 'ltr';
  326 + }
  327 +
  328 + const observerConfig = {
  329 + attributes: true,
  330 + characterData: false,
  331 + childList: true,
  332 + subtree: true,
  333 + attributeFilter: ['data-l10n-id', 'data-l10n-args']
  334 + };
  335 +
  336 + const readiness = new WeakMap();
  337 +
  338 + class View {
  339 + constructor(client, doc) {
  340 + this._doc = doc;
  341 + this.pseudo = {
  342 + 'fr-x-psaccent': createPseudo(this, 'fr-x-psaccent'),
  343 + 'ar-x-psbidi': createPseudo(this, 'ar-x-psbidi')
  344 + };
  345 +
  346 + this._interactive = documentReady().then(
  347 + () => init(this, client));
  348 +
  349 + const observer = new MutationObserver(onMutations.bind(this));
  350 + this._observe = () => observer.observe(doc, observerConfig);
  351 + this._disconnect = () => observer.disconnect();
  352 +
  353 + const translateView = langs => translateDocument(this, langs);
  354 + client.on('translateDocument', translateView);
  355 + this.ready = this._interactive.then(
  356 + client => client.method('resolvedLanguages')).then(
  357 + translateView);
  358 + }
  359 +
  360 + requestLanguages(langs, global) {
  361 + return this._interactive.then(
  362 + client => client.method('requestLanguages', langs, global));
  363 + }
  364 +
  365 + _resolveEntities(langs, keys) {
  366 + return this._interactive.then(
  367 + client => client.method('resolveEntities', client.id, langs, keys));
  368 + }
  369 +
  370 + formatValue(id, args) {
  371 + return this._interactive.then(
  372 + client => client.method('formatValues', client.id, [[id, args]])).then(
  373 + values => values[0]);
  374 + }
  375 +
  376 + formatValues(...keys) {
  377 + return this._interactive.then(
  378 + client => client.method('formatValues', client.id, keys));
  379 + }
  380 +
  381 + translateFragment(frag) {
  382 + return this._interactive.then(
  383 + client => client.method('resolvedLanguages')).then(
  384 + langs => translateFragment(this, langs, frag));
  385 + }
  386 + }
  387 +
  388 + View.prototype.setAttributes = setAttributes;
  389 + View.prototype.getAttributes = getAttributes;
  390 +
  391 + function createPseudo(view, code) {
  392 + return {
  393 + getName: () => view._interactive.then(
  394 + client => client.method('getName', code)),
  395 + processString: str => view._interactive.then(
  396 + client => client.method('processString', code, str)),
  397 + };
  398 + }
  399 +
  400 + function init(view, client) {
  401 + view._observe();
  402 + return client.method(
  403 + 'registerView', client.id, getResourceLinks(view._doc.head)).then(
  404 + () => client);
  405 + }
  406 +
  407 + function onMutations(mutations) {
  408 + return this._interactive.then(
  409 + client => client.method('resolvedLanguages')).then(
  410 + langs => translateMutations(this, langs, mutations));
  411 + }
  412 +
  413 + function translateDocument(view, langs) {
  414 + const html = view._doc.documentElement;
  415 +
  416 + if (readiness.has(html)) {
  417 + return translateFragment(view, langs, html).then(
  418 + () => setAllAndEmit(html, langs));
  419 + }
  420 +
  421 + const translated =
  422 + // has the document been already pre-translated?
  423 + langs[0].code === html.getAttribute('lang') ?
  424 + Promise.resolve() :
  425 + translateFragment(view, langs, html).then(
  426 + () => setLangDir(html, langs));
  427 +
  428 + return translated.then(() => {
  429 + setLangs(html, langs);
  430 + readiness.set(html, true);
  431 + });
  432 + }
  433 +
  434 + function setLangs(html, langs) {
  435 + const codes = langs.map(lang => lang.code);
  436 + html.setAttribute('langs', codes.join(' '));
  437 + }
  438 +
  439 + function setLangDir(html, langs) {
  440 + const code = langs[0].code;
  441 + html.setAttribute('lang', code);
  442 + html.setAttribute('dir', getDirection(code));
  443 + }
  444 +
  445 + function setAllAndEmit(html, langs) {
  446 + setLangDir(html, langs);
  447 + setLangs(html, langs);
  448 + html.parentNode.dispatchEvent(new CustomEvent('DOMRetranslated', {
  449 + bubbles: false,
  450 + cancelable: false,
  451 + }));
  452 + }
  453 +
  454 + const client = new Client({
  455 + service: 'l20n',
  456 + endpoint: channel,
  457 + timeout: false
  458 + });
  459 +
  460 + window.addEventListener('pageshow', () => client.connect());
  461 + window.addEventListener('pagehide', () => client.disconnect());
  462 +
  463 + document.l10n = new View(client, document);
  464 +
  465 + //Bug 1204660 - Temporary proxy for shared code. Will be removed once
  466 + // l10n.js migration is completed.
  467 + navigator.mozL10n = {
  468 + setAttributes: document.l10n.setAttributes,
  469 + getAttributes: document.l10n.getAttributes,
  470 + formatValue: (...args) => document.l10n.formatValue(...args),
  471 + translateFragment: (...args) => document.l10n.translateFragment(...args),
  472 + once: cb => document.l10n.ready.then(cb),
  473 + ready: cb => document.l10n.ready.then(() => {
  474 + document.addEventListener('DOMRetranslated', cb);
  475 + cb();
  476 + }),
  477 + };
  478 +
  479 +})();
0 480 \ No newline at end of file
... ...
bower_components/l20n/dist/bundle/bridge/l20n-service.js 0 → 100755
  1 +(function () { 'use strict';
  2 +
  3 + const Service = bridge.service;
  4 + const channel = new BroadcastChannel('l20n-channel');
  5 +
  6 + function broadcast(type, data) {
  7 + return this.service.broadcast(type, data);
  8 + }
  9 +
  10 + function L10nError(message, id, lang) {
  11 + this.name = 'L10nError';
  12 + this.message = message;
  13 + this.id = id;
  14 + this.lang = lang;
  15 + }
  16 + L10nError.prototype = Object.create(Error.prototype);
  17 + L10nError.prototype.constructor = L10nError;
  18 +
  19 + function load(type, url) {
  20 + return new Promise(function(resolve, reject) {
  21 + const xhr = new XMLHttpRequest();
  22 +
  23 + if (xhr.overrideMimeType) {
  24 + xhr.overrideMimeType(type);
  25 + }
  26 +
  27 + xhr.open('GET', url, true);
  28 +
  29 + if (type === 'application/json') {
  30 + xhr.responseType = 'json';
  31 + }
  32 +
  33 + xhr.addEventListener('load', function io_onload(e) {
  34 + if (e.target.status === 200 || e.target.status === 0) {
  35 + // Sinon.JS's FakeXHR doesn't have the response property
  36 + resolve(e.target.response || e.target.responseText);
  37 + } else {
  38 + reject(new L10nError('Not found: ' + url));
  39 + }
  40 + });
  41 + xhr.addEventListener('error', reject);
  42 + xhr.addEventListener('timeout', reject);
  43 +
  44 + // the app: protocol throws on 404, see https://bugzil.la/827243
  45 + try {
  46 + xhr.send(null);
  47 + } catch (e) {
  48 + if (e.name === 'NS_ERROR_FILE_NOT_FOUND') {
  49 + // the app: protocol throws on 404, see https://bugzil.la/827243
  50 + reject(new L10nError('Not found: ' + url));
  51 + } else {
  52 + throw e;
  53 + }
  54 + }
  55 + });
  56 + }
  57 +
  58 + const io = {
  59 + extra: function(code, ver, path, type) {
  60 + return navigator.mozApps.getLocalizationResource(
  61 + code, ver, path, type);
  62 + },
  63 + app: function(code, ver, path, type) {
  64 + switch (type) {
  65 + case 'text':
  66 + return load('text/plain', path);
  67 + case 'json':
  68 + return load('application/json', path);
  69 + default:
  70 + throw new L10nError('Unknown file type: ' + type);
  71 + }
  72 + },
  73 + };
  74 +
  75 + function fetchResource(ver, res, lang) {
  76 + const url = res.replace('{locale}', lang.code);
  77 + const type = res.endsWith('.json') ? 'json' : 'text';
  78 + return io[lang.src](lang.code, ver, url, type);
  79 + }
  80 +
  81 + const MAX_PLACEABLES$1 = 100;
  82 +
  83 + var L20nParser = {
  84 + parse: function(emit, string) {
  85 + this._source = string;
  86 + this._index = 0;
  87 + this._length = string.length;
  88 + this.entries = Object.create(null);
  89 + this.emit = emit;
  90 +
  91 + return this.getResource();
  92 + },
  93 +
  94 + getResource: function() {
  95 + this.getWS();
  96 + while (this._index < this._length) {
  97 + try {
  98 + this.getEntry();
  99 + } catch (e) {
  100 + if (e instanceof L10nError) {
  101 + // we want to recover, but we don't need it in entries
  102 + this.getJunkEntry();
  103 + if (!this.emit) {
  104 + throw e;
  105 + }
  106 + } else {
  107 + throw e;
  108 + }
  109 + }
  110 +
  111 + if (this._index < this._length) {
  112 + this.getWS();
  113 + }
  114 + }
  115 +
  116 + return this.entries;
  117 + },
  118 +
  119 + getEntry: function() {
  120 + if (this._source[this._index] === '<') {
  121 + ++this._index;
  122 + const id = this.getIdentifier();
  123 + if (this._source[this._index] === '[') {
  124 + ++this._index;
  125 + return this.getEntity(id, this.getItemList(this.getExpression, ']'));
  126 + }
  127 + return this.getEntity(id);
  128 + }
  129 +
  130 + if (this._source.startsWith('/*', this._index)) {
  131 + return this.getComment();
  132 + }
  133 +
  134 + throw this.error('Invalid entry');
  135 + },
  136 +
  137 + getEntity: function(id, index) {
  138 + if (!this.getRequiredWS()) {
  139 + throw this.error('Expected white space');
  140 + }
  141 +
  142 + const ch = this._source[this._index];
  143 + const value = this.getValue(ch, index === undefined);
  144 + let attrs;
  145 +
  146 + if (value === undefined) {
  147 + if (ch === '>') {
  148 + throw this.error('Expected ">"');
  149 + }
  150 + attrs = this.getAttributes();
  151 + } else {
  152 + const ws1 = this.getRequiredWS();
  153 + if (this._source[this._index] !== '>') {
  154 + if (!ws1) {
  155 + throw this.error('Expected ">"');
  156 + }
  157 + attrs = this.getAttributes();
  158 + }
  159 + }
  160 +
  161 + // skip '>'
  162 + ++this._index;
  163 +
  164 + if (id in this.entries) {
  165 + throw this.error('Duplicate entry ID "' + id, 'duplicateerror');
  166 + }
  167 + if (!attrs && !index && typeof value === 'string') {
  168 + this.entries[id] = value;
  169 + } else {
  170 + this.entries[id] = {
  171 + value,
  172 + attrs,
  173 + index
  174 + };
  175 + }
  176 + },
  177 +
  178 + getValue: function(ch = this._source[this._index], optional = false) {
  179 + switch (ch) {
  180 + case '\'':
  181 + case '"':
  182 + return this.getString(ch, 1);
  183 + case '{':
  184 + return this.getHash();
  185 + }
  186 +
  187 + if (!optional) {
  188 + throw this.error('Unknown value type');
  189 + }
  190 +
  191 + return;
  192 + },
  193 +
  194 + getWS: function() {
  195 + let cc = this._source.charCodeAt(this._index);
  196 + // space, \n, \t, \r
  197 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  198 + cc = this._source.charCodeAt(++this._index);
  199 + }
  200 + },
  201 +
  202 + getRequiredWS: function() {
  203 + const pos = this._index;
  204 + let cc = this._source.charCodeAt(pos);
  205 + // space, \n, \t, \r
  206 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  207 + cc = this._source.charCodeAt(++this._index);
  208 + }
  209 + return this._index !== pos;
  210 + },
  211 +
  212 + getIdentifier: function() {
  213 + const start = this._index;
  214 + let cc = this._source.charCodeAt(this._index);
  215 +
  216 + if ((cc >= 97 && cc <= 122) || // a-z
  217 + (cc >= 65 && cc <= 90) || // A-Z
  218 + cc === 95) { // _
  219 + cc = this._source.charCodeAt(++this._index);
  220 + } else {
  221 + throw this.error('Identifier has to start with [a-zA-Z_]');
  222 + }
  223 +
  224 + while ((cc >= 97 && cc <= 122) || // a-z
  225 + (cc >= 65 && cc <= 90) || // A-Z
  226 + (cc >= 48 && cc <= 57) || // 0-9
  227 + cc === 95) { // _
  228 + cc = this._source.charCodeAt(++this._index);
  229 + }
  230 +
  231 + return this._source.slice(start, this._index);
  232 + },
  233 +
  234 + getUnicodeChar: function() {
  235 + for (let i = 0; i < 4; i++) {
  236 + let cc = this._source.charCodeAt(++this._index);
  237 + if ((cc > 96 && cc < 103) || // a-f
  238 + (cc > 64 && cc < 71) || // A-F
  239 + (cc > 47 && cc < 58)) { // 0-9
  240 + continue;
  241 + }
  242 + throw this.error('Illegal unicode escape sequence');
  243 + }
  244 + this._index++;
  245 + return String.fromCharCode(
  246 + parseInt(this._source.slice(this._index - 4, this._index), 16));
  247 + },
  248 +
  249 + stringRe: /"|'|{{|\\/g,
  250 + getString: function(opchar, opcharLen) {
  251 + const body = [];
  252 + let placeables = 0;
  253 +
  254 + this._index += opcharLen;
  255 + const start = this._index;
  256 +
  257 + let bufStart = start;
  258 + let buf = '';
  259 +
  260 + while (true) {
  261 + this.stringRe.lastIndex = this._index;
  262 + const match = this.stringRe.exec(this._source);
  263 +
  264 + if (!match) {
  265 + throw this.error('Unclosed string literal');
  266 + }
  267 +
  268 + if (match[0] === '"' || match[0] === '\'') {
  269 + if (match[0] !== opchar) {
  270 + this._index += opcharLen;
  271 + continue;
  272 + }
  273 + this._index = match.index + opcharLen;
  274 + break;
  275 + }
  276 +
  277 + if (match[0] === '{{') {
  278 + if (placeables > MAX_PLACEABLES$1 - 1) {
  279 + throw this.error('Too many placeables, maximum allowed is ' +
  280 + MAX_PLACEABLES$1);
  281 + }
  282 + placeables++;
  283 + if (match.index > bufStart || buf.length > 0) {
  284 + body.push(buf + this._source.slice(bufStart, match.index));
  285 + buf = '';
  286 + }
  287 + this._index = match.index + 2;
  288 + this.getWS();
  289 + body.push(this.getExpression());
  290 + this.getWS();
  291 + this._index += 2;
  292 + bufStart = this._index;
  293 + continue;
  294 + }
  295 +
  296 + if (match[0] === '\\') {
  297 + this._index = match.index + 1;
  298 + const ch2 = this._source[this._index];
  299 + if (ch2 === 'u') {
  300 + buf += this._source.slice(bufStart, match.index) +
  301 + this.getUnicodeChar();
  302 + } else if (ch2 === opchar || ch2 === '\\') {
  303 + buf += this._source.slice(bufStart, match.index) + ch2;
  304 + this._index++;
  305 + } else if (this._source.startsWith('{{', this._index)) {
  306 + buf += this._source.slice(bufStart, match.index) + '{{';
  307 + this._index += 2;
  308 + } else {
  309 + throw this.error('Illegal escape sequence');
  310 + }
  311 + bufStart = this._index;
  312 + }
  313 + }
  314 +
  315 + if (body.length === 0) {
  316 + return buf + this._source.slice(bufStart, this._index - opcharLen);
  317 + }
  318 +
  319 + if (this._index - opcharLen > bufStart || buf.length > 0) {
  320 + body.push(buf + this._source.slice(bufStart, this._index - opcharLen));
  321 + }
  322 +
  323 + return body;
  324 + },
  325 +
  326 + getAttributes: function() {
  327 + const attrs = Object.create(null);
  328 +
  329 + while (true) {
  330 + this.getAttribute(attrs);
  331 + const ws1 = this.getRequiredWS();
  332 + const ch = this._source.charAt(this._index);
  333 + if (ch === '>') {
  334 + break;
  335 + } else if (!ws1) {
  336 + throw this.error('Expected ">"');
  337 + }
  338 + }
  339 + return attrs;
  340 + },
  341 +
  342 + getAttribute: function(attrs) {
  343 + const key = this.getIdentifier();
  344 + let index;
  345 +
  346 + if (this._source[this._index]=== '[') {
  347 + ++this._index;
  348 + this.getWS();
  349 + index = this.getItemList(this.getExpression, ']');
  350 + }
  351 + this.getWS();
  352 + if (this._source[this._index] !== ':') {
  353 + throw this.error('Expected ":"');
  354 + }
  355 + ++this._index;
  356 + this.getWS();
  357 + const value = this.getValue();
  358 +
  359 + if (key in attrs) {
  360 + throw this.error('Duplicate attribute "' + key, 'duplicateerror');
  361 + }
  362 +
  363 + if (!index && typeof value === 'string') {
  364 + attrs[key] = value;
  365 + } else {
  366 + attrs[key] = {
  367 + value,
  368 + index
  369 + };
  370 + }
  371 + },
  372 +
  373 + getHash: function() {
  374 + const items = Object.create(null);
  375 +
  376 + ++this._index;
  377 + this.getWS();
  378 +
  379 + let defKey;
  380 +
  381 + while (true) {
  382 + const [key, value, def] = this.getHashItem();
  383 + items[key] = value;
  384 +
  385 + if (def) {
  386 + if (defKey) {
  387 + throw this.error('Default item redefinition forbidden');
  388 + }
  389 + defKey = key;
  390 + }
  391 + this.getWS();
  392 +
  393 + const comma = this._source[this._index] === ',';
  394 + if (comma) {
  395 + ++this._index;
  396 + this.getWS();
  397 + }
  398 + if (this._source[this._index] === '}') {
  399 + ++this._index;
  400 + break;
  401 + }
  402 + if (!comma) {
  403 + throw this.error('Expected "}"');
  404 + }
  405 + }
  406 +
  407 + if (defKey) {
  408 + items.__default = defKey;
  409 + }
  410 +
  411 + return items;
  412 + },
  413 +
  414 + getHashItem: function() {
  415 + let defItem = false;
  416 + if (this._source[this._index] === '*') {
  417 + ++this._index;
  418 + defItem = true;
  419 + }
  420 +
  421 + const key = this.getIdentifier();
  422 + this.getWS();
  423 + if (this._source[this._index] !== ':') {
  424 + throw this.error('Expected ":"');
  425 + }
  426 + ++this._index;
  427 + this.getWS();
  428 +
  429 + return [key, this.getValue(), defItem];
  430 + },
  431 +
  432 + getComment: function() {
  433 + this._index += 2;
  434 + const start = this._index;
  435 + const end = this._source.indexOf('*/', start);
  436 +
  437 + if (end === -1) {
  438 + throw this.error('Comment without a closing tag');
  439 + }
  440 +
  441 + this._index = end + 2;
  442 + },
  443 +
  444 + getExpression: function () {
  445 + let exp = this.getPrimaryExpression();
  446 +
  447 + while (true) {
  448 + let ch = this._source[this._index];
  449 + if (ch === '.' || ch === '[') {
  450 + ++this._index;
  451 + exp = this.getPropertyExpression(exp, ch === '[');
  452 + } else if (ch === '(') {
  453 + ++this._index;
  454 + exp = this.getCallExpression(exp);
  455 + } else {
  456 + break;
  457 + }
  458 + }
  459 +
  460 + return exp;
  461 + },
  462 +
  463 + getPropertyExpression: function(idref, computed) {
  464 + let exp;
  465 +
  466 + if (computed) {
  467 + this.getWS();
  468 + exp = this.getExpression();
  469 + this.getWS();
  470 + if (this._source[this._index] !== ']') {
  471 + throw this.error('Expected "]"');
  472 + }
  473 + ++this._index;
  474 + } else {
  475 + exp = this.getIdentifier();
  476 + }
  477 +
  478 + return {
  479 + type: 'prop',
  480 + expr: idref,
  481 + prop: exp,
  482 + cmpt: computed
  483 + };
  484 + },
  485 +
  486 + getCallExpression: function(callee) {
  487 + this.getWS();
  488 +
  489 + return {
  490 + type: 'call',
  491 + expr: callee,
  492 + args: this.getItemList(this.getExpression, ')')
  493 + };
  494 + },
  495 +
  496 + getPrimaryExpression: function() {
  497 + const ch = this._source[this._index];
  498 +
  499 + switch (ch) {
  500 + case '$':
  501 + ++this._index;
  502 + return {
  503 + type: 'var',
  504 + name: this.getIdentifier()
  505 + };
  506 + case '@':
  507 + ++this._index;
  508 + return {
  509 + type: 'glob',
  510 + name: this.getIdentifier()
  511 + };
  512 + default:
  513 + return {
  514 + type: 'id',
  515 + name: this.getIdentifier()
  516 + };
  517 + }
  518 + },
  519 +
  520 + getItemList: function(callback, closeChar) {
  521 + const items = [];
  522 + let closed = false;
  523 +
  524 + this.getWS();
  525 +
  526 + if (this._source[this._index] === closeChar) {
  527 + ++this._index;
  528 + closed = true;
  529 + }
  530 +
  531 + while (!closed) {
  532 + items.push(callback.call(this));
  533 + this.getWS();
  534 + let ch = this._source.charAt(this._index);
  535 + switch (ch) {
  536 + case ',':
  537 + ++this._index;
  538 + this.getWS();
  539 + break;
  540 + case closeChar:
  541 + ++this._index;
  542 + closed = true;
  543 + break;
  544 + default:
  545 + throw this.error('Expected "," or "' + closeChar + '"');
  546 + }
  547 + }
  548 +
  549 + return items;
  550 + },
  551 +
  552 +
  553 + getJunkEntry: function() {
  554 + const pos = this._index;
  555 + let nextEntity = this._source.indexOf('<', pos);
  556 + let nextComment = this._source.indexOf('/*', pos);
  557 +
  558 + if (nextEntity === -1) {
  559 + nextEntity = this._length;
  560 + }
  561 + if (nextComment === -1) {
  562 + nextComment = this._length;
  563 + }
  564 +
  565 + let nextEntry = Math.min(nextEntity, nextComment);
  566 +
  567 + this._index = nextEntry;
  568 + },
  569 +
  570 + error: function(message, type = 'parsererror') {
  571 + const pos = this._index;
  572 +
  573 + let start = this._source.lastIndexOf('<', pos - 1);
  574 + const lastClose = this._source.lastIndexOf('>', pos - 1);
  575 + start = lastClose > start ? lastClose + 1 : start;
  576 + const context = this._source.slice(start, pos + 10);
  577 +
  578 + const msg = message + ' at pos ' + pos + ': `' + context + '`';
  579 + const err = new L10nError(msg);
  580 + if (this.emit) {
  581 + this.emit(type, err);
  582 + }
  583 + return err;
  584 + },
  585 + };
  586 +
  587 + var MAX_PLACEABLES = 100;
  588 +
  589 + var PropertiesParser = {
  590 + patterns: null,
  591 + entryIds: null,
  592 + emit: null,
  593 +
  594 + init: function() {
  595 + this.patterns = {
  596 + comment: /^\s*#|^\s*$/,
  597 + entity: /^([^=\s]+)\s*=\s*(.*)$/,
  598 + multiline: /[^\\]\\$/,
  599 + index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,
  600 + unicode: /\\u([0-9a-fA-F]{1,4})/g,
  601 + entries: /[^\r\n]+/g,
  602 + controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g,
  603 + placeables: /\{\{\s*([^\s]*?)\s*\}\}/,
  604 + };
  605 + },
  606 +
  607 + parse: function(emit, source) {
  608 + if (!this.patterns) {
  609 + this.init();
  610 + }
  611 + this.emit = emit;
  612 +
  613 + var entries = {};
  614 +
  615 + var lines = source.match(this.patterns.entries);
  616 + if (!lines) {
  617 + return entries;
  618 + }
  619 + for (var i = 0; i < lines.length; i++) {
  620 + var line = lines[i];
  621 +
  622 + if (this.patterns.comment.test(line)) {
  623 + continue;
  624 + }
  625 +
  626 + while (this.patterns.multiline.test(line) && i < lines.length) {
  627 + line = line.slice(0, -1) + lines[++i].trim();
  628 + }
  629 +
  630 + var entityMatch = line.match(this.patterns.entity);
  631 + if (entityMatch) {
  632 + try {
  633 + this.parseEntity(entityMatch[1], entityMatch[2], entries);
  634 + } catch (e) {
  635 + if (!this.emit) {
  636 + throw e;
  637 + }
  638 + }
  639 + }
  640 + }
  641 + return entries;
  642 + },
  643 +
  644 + parseEntity: function(id, value, entries) {
  645 + var name, key;
  646 +
  647 + var pos = id.indexOf('[');
  648 + if (pos !== -1) {
  649 + name = id.substr(0, pos);
  650 + key = id.substring(pos + 1, id.length - 1);
  651 + } else {
  652 + name = id;
  653 + key = null;
  654 + }
  655 +
  656 + var nameElements = name.split('.');
  657 +
  658 + if (nameElements.length > 2) {
  659 + throw this.error('Error in ID: "' + name + '".' +
  660 + ' Nested attributes are not supported.');
  661 + }
  662 +
  663 + var attr;
  664 + if (nameElements.length > 1) {
  665 + name = nameElements[0];
  666 + attr = nameElements[1];
  667 +
  668 + if (attr[0] === '$') {
  669 + throw this.error('Attribute can\'t start with "$"');
  670 + }
  671 + } else {
  672 + attr = null;
  673 + }
  674 +
  675 + this.setEntityValue(name, attr, key, this.unescapeString(value), entries);
  676 + },
  677 +
  678 + setEntityValue: function(id, attr, key, rawValue, entries) {
  679 + var value = rawValue.indexOf('{{') > -1 ?
  680 + this.parseString(rawValue) : rawValue;
  681 +
  682 + var isSimpleValue = typeof value === 'string';
  683 + var root = entries;
  684 +
  685 + var isSimpleNode = typeof entries[id] === 'string';
  686 +
  687 + if (!entries[id] && (attr || key || !isSimpleValue)) {
  688 + entries[id] = Object.create(null);
  689 + isSimpleNode = false;
  690 + }
  691 +
  692 + if (attr) {
  693 + if (isSimpleNode) {
  694 + const val = entries[id];
  695 + entries[id] = Object.create(null);
  696 + entries[id].value = val;
  697 + }
  698 + if (!entries[id].attrs) {
  699 + entries[id].attrs = Object.create(null);
  700 + }
  701 + if (!entries[id].attrs && !isSimpleValue) {
  702 + entries[id].attrs[attr] = Object.create(null);
  703 + }
  704 + root = entries[id].attrs;
  705 + id = attr;
  706 + }
  707 +
  708 + if (key) {
  709 + isSimpleNode = false;
  710 + if (typeof root[id] === 'string') {
  711 + const val = root[id];
  712 + root[id] = Object.create(null);
  713 + root[id].index = this.parseIndex(val);
  714 + root[id].value = Object.create(null);
  715 + }
  716 + root = root[id].value;
  717 + id = key;
  718 + isSimpleValue = true;
  719 + }
  720 +
  721 + if (isSimpleValue && (!entries[id] || isSimpleNode)) {
  722 + if (id in root) {
  723 + throw this.error();
  724 + }
  725 + root[id] = value;
  726 + } else {
  727 + if (!root[id]) {
  728 + root[id] = Object.create(null);
  729 + }
  730 + root[id].value = value;
  731 + }
  732 + },
  733 +
  734 + parseString: function(str) {
  735 + var chunks = str.split(this.patterns.placeables);
  736 + var complexStr = [];
  737 +
  738 + var len = chunks.length;
  739 + var placeablesCount = (len - 1) / 2;
  740 +
  741 + if (placeablesCount >= MAX_PLACEABLES) {
  742 + throw this.error('Too many placeables (' + placeablesCount +
  743 + ', max allowed is ' + MAX_PLACEABLES + ')');
  744 + }
  745 +
  746 + for (var i = 0; i < chunks.length; i++) {
  747 + if (chunks[i].length === 0) {
  748 + continue;
  749 + }
  750 + if (i % 2 === 1) {
  751 + complexStr.push({type: 'idOrVar', name: chunks[i]});
  752 + } else {
  753 + complexStr.push(chunks[i]);
  754 + }
  755 + }
  756 + return complexStr;
  757 + },
  758 +
  759 + unescapeString: function(str) {
  760 + if (str.lastIndexOf('\\') !== -1) {
  761 + str = str.replace(this.patterns.controlChars, '$1');
  762 + }
  763 + return str.replace(this.patterns.unicode, function(match, token) {
  764 + return String.fromCodePoint(parseInt(token, 16));
  765 + });
  766 + },
  767 +
  768 + parseIndex: function(str) {
  769 + var match = str.match(this.patterns.index);
  770 + if (!match) {
  771 + throw new L10nError('Malformed index');
  772 + }
  773 + if (match[2]) {
  774 + return [{
  775 + type: 'call',
  776 + expr: {
  777 + type: 'prop',
  778 + expr: {
  779 + type: 'glob',
  780 + name: 'cldr'
  781 + },
  782 + prop: 'plural',
  783 + cmpt: false
  784 + }, args: [{
  785 + type: 'idOrVar',
  786 + name: match[2]
  787 + }]
  788 + }];
  789 + } else {
  790 + return [{type: 'idOrVar', name: match[1]}];
  791 + }
  792 + },
  793 +
  794 + error: function(msg, type = 'parsererror') {
  795 + const err = new L10nError(msg);
  796 + if (this.emit) {
  797 + this.emit(type, err);
  798 + }
  799 + return err;
  800 + }
  801 + };
  802 +
  803 + const KNOWN_MACROS = ['plural'];
  804 + const MAX_PLACEABLE_LENGTH = 2500;
  805 +
  806 + // Unicode bidi isolation characters
  807 + const FSI = '\u2068';
  808 + const PDI = '\u2069';
  809 +
  810 + const resolutionChain = new WeakSet();
  811 +
  812 + function format(ctx, lang, args, entity) {
  813 + if (typeof entity === 'string') {
  814 + return [{}, entity];
  815 + }
  816 +
  817 + if (resolutionChain.has(entity)) {
  818 + throw new L10nError('Cyclic reference detected');
  819 + }
  820 +
  821 + resolutionChain.add(entity);
  822 +
  823 + let rv;
  824 + // if format fails, we want the exception to bubble up and stop the whole
  825 + // resolving process; however, we still need to remove the entity from the
  826 + // resolution chain
  827 + try {
  828 + rv = resolveValue(
  829 + {}, ctx, lang, args, entity.value, entity.index);
  830 + } finally {
  831 + resolutionChain.delete(entity);
  832 + }
  833 + return rv;
  834 + }
  835 +
  836 + function resolveIdentifier(ctx, lang, args, id) {
  837 + if (KNOWN_MACROS.indexOf(id) > -1) {
  838 + return [{}, ctx._getMacro(lang, id)];
  839 + }
  840 +
  841 + if (args && args.hasOwnProperty(id)) {
  842 + if (typeof args[id] === 'string' || (typeof args[id] === 'number' &&
  843 + !isNaN(args[id]))) {
  844 + return [{}, args[id]];
  845 + } else {
  846 + throw new L10nError('Arg must be a string or a number: ' + id);
  847 + }
  848 + }
  849 +
  850 + // XXX: special case for Node.js where still:
  851 + // '__proto__' in Object.create(null) => true
  852 + if (id === '__proto__') {
  853 + throw new L10nError('Illegal id: ' + id);
  854 + }
  855 +
  856 + const entity = ctx._getEntity(lang, id);
  857 +
  858 + if (entity) {
  859 + return format(ctx, lang, args, entity);
  860 + }
  861 +
  862 + throw new L10nError('Unknown reference: ' + id);
  863 + }
  864 +
  865 + function subPlaceable(locals, ctx, lang, args, id) {
  866 + let newLocals, value;
  867 +
  868 + try {
  869 + [newLocals, value] = resolveIdentifier(ctx, lang, args, id);
  870 + } catch (err) {
  871 + return [{ error: err }, FSI + '{{ ' + id + ' }}' + PDI];
  872 + }
  873 +
  874 + if (typeof value === 'number') {
  875 + const formatter = ctx._getNumberFormatter(lang);
  876 + return [newLocals, formatter.format(value)];
  877 + }
  878 +
  879 + if (typeof value === 'string') {
  880 + // prevent Billion Laughs attacks
  881 + if (value.length >= MAX_PLACEABLE_LENGTH) {
  882 + throw new L10nError('Too many characters in placeable (' +
  883 + value.length + ', max allowed is ' +
  884 + MAX_PLACEABLE_LENGTH + ')');
  885 + }
  886 + return [newLocals, FSI + value + PDI];
  887 + }
  888 +
  889 + return [{}, FSI + '{{ ' + id + ' }}' + PDI];
  890 + }
  891 +
  892 + function interpolate(locals, ctx, lang, args, arr) {
  893 + return arr.reduce(function([localsSeq, valueSeq], cur) {
  894 + if (typeof cur === 'string') {
  895 + return [localsSeq, valueSeq + cur];
  896 + } else {
  897 + const [, value] = subPlaceable(locals, ctx, lang, args, cur.name);
  898 + // wrap the substitution in bidi isolate characters
  899 + return [localsSeq, valueSeq + value];
  900 + }
  901 + }, [locals, '']);
  902 + }
  903 +
  904 + function resolveSelector(ctx, lang, args, expr, index) {
  905 + //XXX: Dehardcode!!!
  906 + let selectorName;
  907 + if (index[0].type === 'call' && index[0].expr.type === 'prop' &&
  908 + index[0].expr.expr.name === 'cldr') {
  909 + selectorName = 'plural';
  910 + } else {
  911 + selectorName = index[0].name;
  912 + }
  913 + const selector = resolveIdentifier(ctx, lang, args, selectorName)[1];
  914 +
  915 + if (typeof selector !== 'function') {
  916 + // selector is a simple reference to an entity or args
  917 + return selector;
  918 + }
  919 +
  920 + const argValue = index[0].args ?
  921 + resolveIdentifier(ctx, lang, args, index[0].args[0].name)[1] : undefined;
  922 +
  923 + if (selectorName === 'plural') {
  924 + // special cases for zero, one, two if they are defined on the hash
  925 + if (argValue === 0 && 'zero' in expr) {
  926 + return 'zero';
  927 + }
  928 + if (argValue === 1 && 'one' in expr) {
  929 + return 'one';
  930 + }
  931 + if (argValue === 2 && 'two' in expr) {
  932 + return 'two';
  933 + }
  934 + }
  935 +
  936 + return selector(argValue);
  937 + }
  938 +
  939 + function resolveValue(locals, ctx, lang, args, expr, index) {
  940 + if (!expr) {
  941 + return [locals, expr];
  942 + }
  943 +
  944 + if (typeof expr === 'string' ||
  945 + typeof expr === 'boolean' ||
  946 + typeof expr === 'number') {
  947 + return [locals, expr];
  948 + }
  949 +
  950 + if (Array.isArray(expr)) {
  951 + return interpolate(locals, ctx, lang, args, expr);
  952 + }
  953 +
  954 + // otherwise, it's a dict
  955 + if (index) {
  956 + // try to use the index in order to select the right dict member
  957 + const selector = resolveSelector(ctx, lang, args, expr, index);
  958 + if (selector in expr) {
  959 + return resolveValue(locals, ctx, lang, args, expr[selector]);
  960 + }
  961 + }
  962 +
  963 + // if there was no index or no selector was found, try the default
  964 + // XXX 'other' is an artifact from Gaia
  965 + const defaultKey = expr.__default || 'other';
  966 + if (defaultKey in expr) {
  967 + return resolveValue(locals, ctx, lang, args, expr[defaultKey]);
  968 + }
  969 +
  970 + throw new L10nError('Unresolvable value');
  971 + }
  972 +
  973 + const locales2rules = {
  974 + 'af': 3,
  975 + 'ak': 4,
  976 + 'am': 4,
  977 + 'ar': 1,
  978 + 'asa': 3,
  979 + 'az': 0,
  980 + 'be': 11,
  981 + 'bem': 3,
  982 + 'bez': 3,
  983 + 'bg': 3,
  984 + 'bh': 4,
  985 + 'bm': 0,
  986 + 'bn': 3,
  987 + 'bo': 0,
  988 + 'br': 20,
  989 + 'brx': 3,
  990 + 'bs': 11,
  991 + 'ca': 3,
  992 + 'cgg': 3,
  993 + 'chr': 3,
  994 + 'cs': 12,
  995 + 'cy': 17,
  996 + 'da': 3,
  997 + 'de': 3,
  998 + 'dv': 3,
  999 + 'dz': 0,
  1000 + 'ee': 3,
  1001 + 'el': 3,
  1002 + 'en': 3,
  1003 + 'eo': 3,
  1004 + 'es': 3,
  1005 + 'et': 3,
  1006 + 'eu': 3,
  1007 + 'fa': 0,
  1008 + 'ff': 5,
  1009 + 'fi': 3,
  1010 + 'fil': 4,
  1011 + 'fo': 3,
  1012 + 'fr': 5,
  1013 + 'fur': 3,
  1014 + 'fy': 3,
  1015 + 'ga': 8,
  1016 + 'gd': 24,
  1017 + 'gl': 3,
  1018 + 'gsw': 3,
  1019 + 'gu': 3,
  1020 + 'guw': 4,
  1021 + 'gv': 23,
  1022 + 'ha': 3,
  1023 + 'haw': 3,
  1024 + 'he': 2,
  1025 + 'hi': 4,
  1026 + 'hr': 11,
  1027 + 'hu': 0,
  1028 + 'id': 0,
  1029 + 'ig': 0,
  1030 + 'ii': 0,
  1031 + 'is': 3,
  1032 + 'it': 3,
  1033 + 'iu': 7,
  1034 + 'ja': 0,
  1035 + 'jmc': 3,
  1036 + 'jv': 0,
  1037 + 'ka': 0,
  1038 + 'kab': 5,
  1039 + 'kaj': 3,
  1040 + 'kcg': 3,
  1041 + 'kde': 0,
  1042 + 'kea': 0,
  1043 + 'kk': 3,
  1044 + 'kl': 3,
  1045 + 'km': 0,
  1046 + 'kn': 0,
  1047 + 'ko': 0,
  1048 + 'ksb': 3,
  1049 + 'ksh': 21,
  1050 + 'ku': 3,
  1051 + 'kw': 7,
  1052 + 'lag': 18,
  1053 + 'lb': 3,
  1054 + 'lg': 3,
  1055 + 'ln': 4,
  1056 + 'lo': 0,
  1057 + 'lt': 10,
  1058 + 'lv': 6,
  1059 + 'mas': 3,
  1060 + 'mg': 4,
  1061 + 'mk': 16,
  1062 + 'ml': 3,
  1063 + 'mn': 3,
  1064 + 'mo': 9,
  1065 + 'mr': 3,
  1066 + 'ms': 0,
  1067 + 'mt': 15,
  1068 + 'my': 0,
  1069 + 'nah': 3,
  1070 + 'naq': 7,
  1071 + 'nb': 3,
  1072 + 'nd': 3,
  1073 + 'ne': 3,
  1074 + 'nl': 3,
  1075 + 'nn': 3,
  1076 + 'no': 3,
  1077 + 'nr': 3,
  1078 + 'nso': 4,
  1079 + 'ny': 3,
  1080 + 'nyn': 3,
  1081 + 'om': 3,
  1082 + 'or': 3,
  1083 + 'pa': 3,
  1084 + 'pap': 3,
  1085 + 'pl': 13,
  1086 + 'ps': 3,
  1087 + 'pt': 3,
  1088 + 'rm': 3,
  1089 + 'ro': 9,
  1090 + 'rof': 3,
  1091 + 'ru': 11,
  1092 + 'rwk': 3,
  1093 + 'sah': 0,
  1094 + 'saq': 3,
  1095 + 'se': 7,
  1096 + 'seh': 3,
  1097 + 'ses': 0,
  1098 + 'sg': 0,
  1099 + 'sh': 11,
  1100 + 'shi': 19,
  1101 + 'sk': 12,
  1102 + 'sl': 14,
  1103 + 'sma': 7,
  1104 + 'smi': 7,
  1105 + 'smj': 7,
  1106 + 'smn': 7,
  1107 + 'sms': 7,
  1108 + 'sn': 3,
  1109 + 'so': 3,
  1110 + 'sq': 3,
  1111 + 'sr': 11,
  1112 + 'ss': 3,
  1113 + 'ssy': 3,
  1114 + 'st': 3,
  1115 + 'sv': 3,
  1116 + 'sw': 3,
  1117 + 'syr': 3,
  1118 + 'ta': 3,
  1119 + 'te': 3,
  1120 + 'teo': 3,
  1121 + 'th': 0,
  1122 + 'ti': 4,
  1123 + 'tig': 3,
  1124 + 'tk': 3,
  1125 + 'tl': 4,
  1126 + 'tn': 3,
  1127 + 'to': 0,
  1128 + 'tr': 0,
  1129 + 'ts': 3,
  1130 + 'tzm': 22,
  1131 + 'uk': 11,
  1132 + 'ur': 3,
  1133 + 've': 3,
  1134 + 'vi': 0,
  1135 + 'vun': 3,
  1136 + 'wa': 4,
  1137 + 'wae': 3,
  1138 + 'wo': 0,
  1139 + 'xh': 3,
  1140 + 'xog': 3,
  1141 + 'yo': 0,
  1142 + 'zh': 0,
  1143 + 'zu': 3
  1144 + };
  1145 +
  1146 + // utility functions for plural rules methods
  1147 + function isIn(n, list) {
  1148 + return list.indexOf(n) !== -1;
  1149 + }
  1150 + function isBetween(n, start, end) {
  1151 + return typeof n === typeof start && start <= n && n <= end;
  1152 + }
  1153 +
  1154 + // list of all plural rules methods:
  1155 + // map an integer to the plural form name to use
  1156 + const pluralRules = {
  1157 + '0': function() {
  1158 + return 'other';
  1159 + },
  1160 + '1': function(n) {
  1161 + if ((isBetween((n % 100), 3, 10))) {
  1162 + return 'few';
  1163 + }
  1164 + if (n === 0) {
  1165 + return 'zero';
  1166 + }
  1167 + if ((isBetween((n % 100), 11, 99))) {
  1168 + return 'many';
  1169 + }
  1170 + if (n === 2) {
  1171 + return 'two';
  1172 + }
  1173 + if (n === 1) {
  1174 + return 'one';
  1175 + }
  1176 + return 'other';
  1177 + },
  1178 + '2': function(n) {
  1179 + if (n !== 0 && (n % 10) === 0) {
  1180 + return 'many';
  1181 + }
  1182 + if (n === 2) {
  1183 + return 'two';
  1184 + }
  1185 + if (n === 1) {
  1186 + return 'one';
  1187 + }
  1188 + return 'other';
  1189 + },
  1190 + '3': function(n) {
  1191 + if (n === 1) {
  1192 + return 'one';
  1193 + }
  1194 + return 'other';
  1195 + },
  1196 + '4': function(n) {
  1197 + if ((isBetween(n, 0, 1))) {
  1198 + return 'one';
  1199 + }
  1200 + return 'other';
  1201 + },
  1202 + '5': function(n) {
  1203 + if ((isBetween(n, 0, 2)) && n !== 2) {
  1204 + return 'one';
  1205 + }
  1206 + return 'other';
  1207 + },
  1208 + '6': function(n) {
  1209 + if (n === 0) {
  1210 + return 'zero';
  1211 + }
  1212 + if ((n % 10) === 1 && (n % 100) !== 11) {
  1213 + return 'one';
  1214 + }
  1215 + return 'other';
  1216 + },
  1217 + '7': function(n) {
  1218 + if (n === 2) {
  1219 + return 'two';
  1220 + }
  1221 + if (n === 1) {
  1222 + return 'one';
  1223 + }
  1224 + return 'other';
  1225 + },
  1226 + '8': function(n) {
  1227 + if ((isBetween(n, 3, 6))) {
  1228 + return 'few';
  1229 + }
  1230 + if ((isBetween(n, 7, 10))) {
  1231 + return 'many';
  1232 + }
  1233 + if (n === 2) {
  1234 + return 'two';
  1235 + }
  1236 + if (n === 1) {
  1237 + return 'one';
  1238 + }
  1239 + return 'other';
  1240 + },
  1241 + '9': function(n) {
  1242 + if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) {
  1243 + return 'few';
  1244 + }
  1245 + if (n === 1) {
  1246 + return 'one';
  1247 + }
  1248 + return 'other';
  1249 + },
  1250 + '10': function(n) {
  1251 + if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) {
  1252 + return 'few';
  1253 + }
  1254 + if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) {
  1255 + return 'one';
  1256 + }
  1257 + return 'other';
  1258 + },
  1259 + '11': function(n) {
  1260 + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
  1261 + return 'few';
  1262 + }
  1263 + if ((n % 10) === 0 ||
  1264 + (isBetween((n % 10), 5, 9)) ||
  1265 + (isBetween((n % 100), 11, 14))) {
  1266 + return 'many';
  1267 + }
  1268 + if ((n % 10) === 1 && (n % 100) !== 11) {
  1269 + return 'one';
  1270 + }
  1271 + return 'other';
  1272 + },
  1273 + '12': function(n) {
  1274 + if ((isBetween(n, 2, 4))) {
  1275 + return 'few';
  1276 + }
  1277 + if (n === 1) {
  1278 + return 'one';
  1279 + }
  1280 + return 'other';
  1281 + },
  1282 + '13': function(n) {
  1283 + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
  1284 + return 'few';
  1285 + }
  1286 + if (n !== 1 && (isBetween((n % 10), 0, 1)) ||
  1287 + (isBetween((n % 10), 5, 9)) ||
  1288 + (isBetween((n % 100), 12, 14))) {
  1289 + return 'many';
  1290 + }
  1291 + if (n === 1) {
  1292 + return 'one';
  1293 + }
  1294 + return 'other';
  1295 + },
  1296 + '14': function(n) {
  1297 + if ((isBetween((n % 100), 3, 4))) {
  1298 + return 'few';
  1299 + }
  1300 + if ((n % 100) === 2) {
  1301 + return 'two';
  1302 + }
  1303 + if ((n % 100) === 1) {
  1304 + return 'one';
  1305 + }
  1306 + return 'other';
  1307 + },
  1308 + '15': function(n) {
  1309 + if (n === 0 || (isBetween((n % 100), 2, 10))) {
  1310 + return 'few';
  1311 + }
  1312 + if ((isBetween((n % 100), 11, 19))) {
  1313 + return 'many';
  1314 + }
  1315 + if (n === 1) {
  1316 + return 'one';
  1317 + }
  1318 + return 'other';
  1319 + },
  1320 + '16': function(n) {
  1321 + if ((n % 10) === 1 && n !== 11) {
  1322 + return 'one';
  1323 + }
  1324 + return 'other';
  1325 + },
  1326 + '17': function(n) {
  1327 + if (n === 3) {
  1328 + return 'few';
  1329 + }
  1330 + if (n === 0) {
  1331 + return 'zero';
  1332 + }
  1333 + if (n === 6) {
  1334 + return 'many';
  1335 + }
  1336 + if (n === 2) {
  1337 + return 'two';
  1338 + }
  1339 + if (n === 1) {
  1340 + return 'one';
  1341 + }
  1342 + return 'other';
  1343 + },
  1344 + '18': function(n) {
  1345 + if (n === 0) {
  1346 + return 'zero';
  1347 + }
  1348 + if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) {
  1349 + return 'one';
  1350 + }
  1351 + return 'other';
  1352 + },
  1353 + '19': function(n) {
  1354 + if ((isBetween(n, 2, 10))) {
  1355 + return 'few';
  1356 + }
  1357 + if ((isBetween(n, 0, 1))) {
  1358 + return 'one';
  1359 + }
  1360 + return 'other';
  1361 + },
  1362 + '20': function(n) {
  1363 + if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !(
  1364 + isBetween((n % 100), 10, 19) ||
  1365 + isBetween((n % 100), 70, 79) ||
  1366 + isBetween((n % 100), 90, 99)
  1367 + )) {
  1368 + return 'few';
  1369 + }
  1370 + if ((n % 1000000) === 0 && n !== 0) {
  1371 + return 'many';
  1372 + }
  1373 + if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) {
  1374 + return 'two';
  1375 + }
  1376 + if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) {
  1377 + return 'one';
  1378 + }
  1379 + return 'other';
  1380 + },
  1381 + '21': function(n) {
  1382 + if (n === 0) {
  1383 + return 'zero';
  1384 + }
  1385 + if (n === 1) {
  1386 + return 'one';
  1387 + }
  1388 + return 'other';
  1389 + },
  1390 + '22': function(n) {
  1391 + if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) {
  1392 + return 'one';
  1393 + }
  1394 + return 'other';
  1395 + },
  1396 + '23': function(n) {
  1397 + if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) {
  1398 + return 'one';
  1399 + }
  1400 + return 'other';
  1401 + },
  1402 + '24': function(n) {
  1403 + if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) {
  1404 + return 'few';
  1405 + }
  1406 + if (isIn(n, [2, 12])) {
  1407 + return 'two';
  1408 + }
  1409 + if (isIn(n, [1, 11])) {
  1410 + return 'one';
  1411 + }
  1412 + return 'other';
  1413 + }
  1414 + };
  1415 +
  1416 + function getPluralRule(code) {
  1417 + // return a function that gives the plural form name for a given integer
  1418 + const index = locales2rules[code.replace(/-.*$/, '')];
  1419 + if (!(index in pluralRules)) {
  1420 + return function() { return 'other'; };
  1421 + }
  1422 + return pluralRules[index];
  1423 + }
  1424 +
  1425 + class Context {
  1426 + constructor(env) {
  1427 + this._env = env;
  1428 + this._numberFormatters = null;
  1429 + }
  1430 +
  1431 + _formatTuple(lang, args, entity, id, key) {
  1432 + try {
  1433 + return format(this, lang, args, entity);
  1434 + } catch (err) {
  1435 + err.id = key ? id + '::' + key : id;
  1436 + err.lang = lang;
  1437 + this._env.emit('resolveerror', err, this);
  1438 + return [{ error: err }, err.id];
  1439 + }
  1440 + }
  1441 +
  1442 + _formatEntity(lang, args, entity, id) {
  1443 + const [, value] = this._formatTuple(lang, args, entity, id);
  1444 +
  1445 + const formatted = {
  1446 + value,
  1447 + attrs: null,
  1448 + };
  1449 +
  1450 + if (entity.attrs) {
  1451 + formatted.attrs = Object.create(null);
  1452 + for (let key in entity.attrs) {
  1453 + /* jshint -W089 */
  1454 + const [, attrValue] = this._formatTuple(
  1455 + lang, args, entity.attrs[key], id, key);
  1456 + formatted.attrs[key] = attrValue;
  1457 + }
  1458 + }
  1459 +
  1460 + return formatted;
  1461 + }
  1462 +
  1463 + _formatValue(lang, args, entity, id) {
  1464 + return this._formatTuple(lang, args, entity, id)[1];
  1465 + }
  1466 +
  1467 + fetch(langs) {
  1468 + if (langs.length === 0) {
  1469 + return Promise.resolve(langs);
  1470 + }
  1471 +
  1472 + const resIds = Array.from(this._env._resLists.get(this));
  1473 +
  1474 + return Promise.all(
  1475 + resIds.map(
  1476 + this._env._getResource.bind(this._env, langs[0]))).then(
  1477 + () => langs);
  1478 + }
  1479 +
  1480 + _resolve(langs, keys, formatter, prevResolved) {
  1481 + const lang = langs[0];
  1482 +
  1483 + if (!lang) {
  1484 + return reportMissing.call(this, keys, formatter, prevResolved);
  1485 + }
  1486 +
  1487 + let hasUnresolved = false;
  1488 +
  1489 + const resolved = keys.map((key, i) => {
  1490 + if (prevResolved && prevResolved[i] !== undefined) {
  1491 + return prevResolved[i];
  1492 + }
  1493 + const [id, args] = Array.isArray(key) ?
  1494 + key : [key, undefined];
  1495 + const entity = this._getEntity(lang, id);
  1496 +
  1497 + if (entity) {
  1498 + return formatter.call(this, lang, args, entity, id);
  1499 + }
  1500 +
  1501 + this._env.emit('notfounderror',
  1502 + new L10nError('"' + id + '"' + ' not found in ' + lang.code,
  1503 + id, lang), this);
  1504 + hasUnresolved = true;
  1505 + });
  1506 +
  1507 + if (!hasUnresolved) {
  1508 + return resolved;
  1509 + }
  1510 +
  1511 + return this.fetch(langs.slice(1)).then(
  1512 + nextLangs => this._resolve(nextLangs, keys, formatter, resolved));
  1513 + }
  1514 +
  1515 + resolveEntities(langs, keys) {
  1516 + return this.fetch(langs).then(
  1517 + langs => this._resolve(langs, keys, this._formatEntity));
  1518 + }
  1519 +
  1520 + resolveValues(langs, keys) {
  1521 + return this.fetch(langs).then(
  1522 + langs => this._resolve(langs, keys, this._formatValue));
  1523 + }
  1524 +
  1525 + _getEntity(lang, id) {
  1526 + const cache = this._env._resCache;
  1527 + const resIds = Array.from(this._env._resLists.get(this));
  1528 +
  1529 + // Look for `id` in every resource in order.
  1530 + for (let i = 0, resId; resId = resIds[i]; i++) {
  1531 + const resource = cache.get(resId + lang.code + lang.src);
  1532 + if (resource instanceof L10nError) {
  1533 + continue;
  1534 + }
  1535 + if (id in resource) {
  1536 + return resource[id];
  1537 + }
  1538 + }
  1539 + return undefined;
  1540 + }
  1541 +
  1542 + _getNumberFormatter(lang) {
  1543 + if (!this._numberFormatters) {
  1544 + this._numberFormatters = new Map();
  1545 + }
  1546 + if (!this._numberFormatters.has(lang)) {
  1547 + const formatter = Intl.NumberFormat(lang, {
  1548 + useGrouping: false,
  1549 + });
  1550 + this._numberFormatters.set(lang, formatter);
  1551 + return formatter;
  1552 + }
  1553 + return this._numberFormatters.get(lang);
  1554 + }
  1555 +
  1556 + // XXX in the future macros will be stored in localization resources together
  1557 + // with regular entities and this method will not be needed anymore
  1558 + _getMacro(lang, id) {
  1559 + switch(id) {
  1560 + case 'plural':
  1561 + return getPluralRule(lang.code);
  1562 + default:
  1563 + return undefined;
  1564 + }
  1565 + }
  1566 +
  1567 + }
  1568 +
  1569 + function reportMissing(keys, formatter, resolved) {
  1570 + const missingIds = new Set();
  1571 +
  1572 + keys.forEach((key, i) => {
  1573 + if (resolved && resolved[i] !== undefined) {
  1574 + return;
  1575 + }
  1576 + const id = Array.isArray(key) ? key[0] : key;
  1577 + missingIds.add(id);
  1578 + resolved[i] = formatter === this._formatValue ?
  1579 + id : {value: id, attrs: null};
  1580 + });
  1581 +
  1582 + this._env.emit('notfounderror', new L10nError(
  1583 + '"' + Array.from(missingIds).join(', ') + '"' +
  1584 + ' not found in any language', missingIds), this);
  1585 +
  1586 + return resolved;
  1587 + }
  1588 +
  1589 + // Walk an entry node searching for content leaves
  1590 + function walkEntry(entry, fn) {
  1591 + if (typeof entry === 'string') {
  1592 + return fn(entry);
  1593 + }
  1594 +
  1595 + const newEntry = Object.create(null);
  1596 +
  1597 + if (entry.value) {
  1598 + newEntry.value = walkValue(entry.value, fn);
  1599 + }
  1600 +
  1601 + if (entry.index) {
  1602 + newEntry.index = entry.index;
  1603 + }
  1604 +
  1605 + if (entry.attrs) {
  1606 + newEntry.attrs = Object.create(null);
  1607 + for (let key in entry.attrs) {
  1608 + newEntry.attrs[key] = walkEntry(entry.attrs[key], fn);
  1609 + }
  1610 + }
  1611 +
  1612 + return newEntry;
  1613 + }
  1614 +
  1615 + function walkValue(value, fn) {
  1616 + if (typeof value === 'string') {
  1617 + return fn(value);
  1618 + }
  1619 +
  1620 + // skip expressions in placeables
  1621 + if (value.type) {
  1622 + return value;
  1623 + }
  1624 +
  1625 + const newValue = Array.isArray(value) ? [] : Object.create(null);
  1626 + const keys = Object.keys(value);
  1627 +
  1628 + for (let i = 0, key; (key = keys[i]); i++) {
  1629 + newValue[key] = walkValue(value[key], fn);
  1630 + }
  1631 +
  1632 + return newValue;
  1633 + }
  1634 +
  1635 + /* Pseudolocalizations
  1636 + *
  1637 + * pseudo is a dict of strategies to be used to modify the English
  1638 + * context in order to create pseudolocalizations. These can be used by
  1639 + * developers to test the localizability of their code without having to
  1640 + * actually speak a foreign language.
  1641 + *
  1642 + * Currently, the following pseudolocales are supported:
  1643 + *
  1644 + * fr-x-psaccent - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ
  1645 + *
  1646 + * In Accented English all English letters are replaced by accented
  1647 + * Unicode counterparts which don't impair the readability of the content.
  1648 + * This allows developers to quickly test if any given string is being
  1649 + * correctly displayed in its 'translated' form. Additionally, simple
  1650 + * heuristics are used to make certain words longer to better simulate the
  1651 + * experience of international users.
  1652 + *
  1653 + * ar-x-psbidi - ɥsıʅƃuƎ ıpıԐ
  1654 + *
  1655 + * Bidi English is a fake RTL locale. All words are surrounded by
  1656 + * Unicode formatting marks forcing the RTL directionality of characters.
  1657 + * In addition, to make the reversed text easier to read, individual
  1658 + * letters are flipped.
  1659 + *
  1660 + * Note: The name above is hardcoded to be RTL in case code editors have
  1661 + * trouble with the RLO and PDF Unicode marks. In reality, it should be
  1662 + * surrounded by those marks as well.
  1663 + *
  1664 + * See https://bugzil.la/900182 for more information.
  1665 + *
  1666 + */
  1667 +
  1668 + function createGetter(id, name) {
  1669 + let _pseudo = null;
  1670 +
  1671 + return function getPseudo() {
  1672 + if (_pseudo) {
  1673 + return _pseudo;
  1674 + }
  1675 +
  1676 + const reAlphas = /[a-zA-Z]/g;
  1677 + const reVowels = /[aeiouAEIOU]/g;
  1678 + const reWords = /[^\W0-9_]+/g;
  1679 + // strftime tokens (%a, %Eb), template {vars}, HTML entities (&#x202a;)
  1680 + // and HTML tags.
  1681 + const reExcluded = /(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;
  1682 +
  1683 + const charMaps = {
  1684 + 'fr-x-psaccent':
  1685 + 'ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ[\\]^_`ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ',
  1686 + 'ar-x-psbidi':
  1687 + // XXX Use pɟפ˥ʎ as replacements for ᗡℲ⅁⅂⅄. https://bugzil.la/1007340
  1688 + '∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅMXʎZ[\\]ᵥ_,ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz',
  1689 + };
  1690 +
  1691 + const mods = {
  1692 + 'fr-x-psaccent': val =>
  1693 + val.replace(reVowels, match => match + match.toLowerCase()),
  1694 +
  1695 + // Surround each word with Unicode formatting codes, RLO and PDF:
  1696 + // U+202E: RIGHT-TO-LEFT OVERRIDE (RLO)
  1697 + // U+202C: POP DIRECTIONAL FORMATTING (PDF)
  1698 + // See http://www.w3.org/International/questions/qa-bidi-controls
  1699 + 'ar-x-psbidi': val =>
  1700 + val.replace(reWords, match => '\u202e' + match + '\u202c'),
  1701 + };
  1702 +
  1703 + // Replace each Latin letter with a Unicode character from map
  1704 + const replaceChars =
  1705 + (map, val) => val.replace(
  1706 + reAlphas, match => map.charAt(match.charCodeAt(0) - 65));
  1707 +
  1708 + const transform =
  1709 + val => replaceChars(charMaps[id], mods[id](val));
  1710 +
  1711 + // apply fn to translatable parts of val
  1712 + const apply = (fn, val) => {
  1713 + if (!val) {
  1714 + return val;
  1715 + }
  1716 +
  1717 + const parts = val.split(reExcluded);
  1718 + const modified = parts.map(function(part) {
  1719 + if (reExcluded.test(part)) {
  1720 + return part;
  1721 + }
  1722 + return fn(part);
  1723 + });
  1724 + return modified.join('');
  1725 + };
  1726 +
  1727 + return _pseudo = {
  1728 + name: transform(name),
  1729 + process: str => apply(transform, str)
  1730 + };
  1731 + };
  1732 + }
  1733 +
  1734 + const pseudo = Object.defineProperties(Object.create(null), {
  1735 + 'fr-x-psaccent': {
  1736 + enumerable: true,
  1737 + get: createGetter('fr-x-psaccent', 'Runtime Accented')
  1738 + },
  1739 + 'ar-x-psbidi': {
  1740 + enumerable: true,
  1741 + get: createGetter('ar-x-psbidi', 'Runtime Bidi')
  1742 + }
  1743 + });
  1744 +
  1745 + function emit(listeners, ...args) {
  1746 + const type = args.shift();
  1747 +
  1748 + if (listeners['*']) {
  1749 + listeners['*'].slice().forEach(
  1750 + listener => listener.apply(this, args));
  1751 + }
  1752 +
  1753 + if (listeners[type]) {
  1754 + listeners[type].slice().forEach(
  1755 + listener => listener.apply(this, args));
  1756 + }
  1757 + }
  1758 +
  1759 + function addEventListener(listeners, type, listener) {
  1760 + if (!(type in listeners)) {
  1761 + listeners[type] = [];
  1762 + }
  1763 + listeners[type].push(listener);
  1764 + }
  1765 +
  1766 + function removeEventListener(listeners, type, listener) {
  1767 + const typeListeners = listeners[type];
  1768 + const pos = typeListeners.indexOf(listener);
  1769 + if (pos === -1) {
  1770 + return;
  1771 + }
  1772 +
  1773 + typeListeners.splice(pos, 1);
  1774 + }
  1775 +
  1776 + const parsers = {
  1777 + properties: PropertiesParser,
  1778 + l20n: L20nParser,
  1779 + };
  1780 +
  1781 + class Env {
  1782 + constructor(defaultLang, fetchResource) {
  1783 + this.defaultLang = defaultLang;
  1784 + this.fetchResource = fetchResource;
  1785 +
  1786 + this._resLists = new Map();
  1787 + this._resCache = new Map();
  1788 +
  1789 + const listeners = {};
  1790 + this.emit = emit.bind(this, listeners);
  1791 + this.addEventListener = addEventListener.bind(this, listeners);
  1792 + this.removeEventListener = removeEventListener.bind(this, listeners);
  1793 + }
  1794 +
  1795 + createContext(resIds) {
  1796 + const ctx = new Context(this);
  1797 + this._resLists.set(ctx, new Set(resIds));
  1798 + return ctx;
  1799 + }
  1800 +
  1801 + destroyContext(ctx) {
  1802 + const lists = this._resLists;
  1803 + const resList = lists.get(ctx);
  1804 +
  1805 + lists.delete(ctx);
  1806 + resList.forEach(
  1807 + resId => deleteIfOrphan(this._resCache, lists, resId));
  1808 + }
  1809 +
  1810 + _parse(syntax, lang, data) {
  1811 + const parser = parsers[syntax];
  1812 + if (!parser) {
  1813 + return data;
  1814 + }
  1815 +
  1816 + const emit = (type, err) => this.emit(type, amendError(lang, err));
  1817 + return parser.parse.call(parser, emit, data);
  1818 + }
  1819 +
  1820 + _create(lang, entries) {
  1821 + if (lang.src !== 'pseudo') {
  1822 + return entries;
  1823 + }
  1824 +
  1825 + const pseudoentries = Object.create(null);
  1826 + for (let key in entries) {
  1827 + pseudoentries[key] = walkEntry(
  1828 + entries[key], pseudo[lang.code].process);
  1829 + }
  1830 + return pseudoentries;
  1831 + }
  1832 +
  1833 + _getResource(lang, res) {
  1834 + const cache = this._resCache;
  1835 + const id = res + lang.code + lang.src;
  1836 +
  1837 + if (cache.has(id)) {
  1838 + return cache.get(id);
  1839 + }
  1840 +
  1841 + const syntax = res.substr(res.lastIndexOf('.') + 1);
  1842 +
  1843 + const saveEntries = data => {
  1844 + const entries = this._parse(syntax, lang, data);
  1845 + cache.set(id, this._create(lang, entries));
  1846 + };
  1847 +
  1848 + const recover = err => {
  1849 + err.lang = lang;
  1850 + this.emit('fetcherror', err);
  1851 + cache.set(id, err);
  1852 + };
  1853 +
  1854 + const langToFetch = lang.src === 'pseudo' ?
  1855 + { code: this.defaultLang, src: 'app' } :
  1856 + lang;
  1857 +
  1858 + const resource = this.fetchResource(res, langToFetch).then(
  1859 + saveEntries, recover);
  1860 +
  1861 + cache.set(id, resource);
  1862 +
  1863 + return resource;
  1864 + }
  1865 + }
  1866 +
  1867 + function deleteIfOrphan(cache, lists, resId) {
  1868 + const isNeeded = Array.from(lists).some(
  1869 + ([ctx, resIds]) => resIds.has(resId));
  1870 +
  1871 + if (!isNeeded) {
  1872 + cache.forEach((val, key) =>
  1873 + key.startsWith(resId) ? cache.delete(key) : null);
  1874 + }
  1875 + }
  1876 +
  1877 + function amendError(lang, err) {
  1878 + err.lang = lang;
  1879 + return err;
  1880 + }
  1881 +
  1882 + // Polyfill NodeList.prototype[Symbol.iterator] for Chrome.
  1883 + // See https://code.google.com/p/chromium/issues/detail?id=401699
  1884 + if (typeof NodeList === 'function' && !NodeList.prototype[Symbol.iterator]) {
  1885 + NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
  1886 + }
  1887 +
  1888 + // A document.ready shim
  1889 + // https://github.com/whatwg/html/issues/127
  1890 + function documentReady() {
  1891 + if (document.readyState !== 'loading') {
  1892 + return Promise.resolve();
  1893 + }
  1894 +
  1895 + return new Promise(resolve => {
  1896 + document.addEventListener('readystatechange', function onrsc() {
  1897 + document.removeEventListener('readystatechange', onrsc);
  1898 + resolve();
  1899 + });
  1900 + });
  1901 + }
  1902 +
  1903 + function prioritizeLocales(def, availableLangs, requested) {
  1904 + let supportedLocale;
  1905 + // Find the first locale in the requested list that is supported.
  1906 + for (let i = 0; i < requested.length; i++) {
  1907 + const locale = requested[i];
  1908 + if (availableLangs.indexOf(locale) !== -1) {
  1909 + supportedLocale = locale;
  1910 + break;
  1911 + }
  1912 + }
  1913 + if (!supportedLocale ||
  1914 + supportedLocale === def) {
  1915 + return [def];
  1916 + }
  1917 +
  1918 + return [supportedLocale, def];
  1919 + }
  1920 +
  1921 + function getMeta(head) {
  1922 + let availableLangs = Object.create(null);
  1923 + let defaultLang = null;
  1924 + let appVersion = null;
  1925 +
  1926 + // XXX take last found instead of first?
  1927 + const metas = head.querySelectorAll(
  1928 + 'meta[name="availableLanguages"],' +
  1929 + 'meta[name="defaultLanguage"],' +
  1930 + 'meta[name="appVersion"]');
  1931 + for (let meta of metas) {
  1932 + const name = meta.getAttribute('name');
  1933 + const content = meta.getAttribute('content').trim();
  1934 + switch (name) {
  1935 + case 'availableLanguages':
  1936 + availableLangs = getLangRevisionMap(
  1937 + availableLangs, content);
  1938 + break;
  1939 + case 'defaultLanguage':
  1940 + const [lang, rev] = getLangRevisionTuple(content);
  1941 + defaultLang = lang;
  1942 + if (!(lang in availableLangs)) {
  1943 + availableLangs[lang] = rev;
  1944 + }
  1945 + break;
  1946 + case 'appVersion':
  1947 + appVersion = content;
  1948 + }
  1949 + }
  1950 +
  1951 + return {
  1952 + defaultLang,
  1953 + availableLangs,
  1954 + appVersion
  1955 + };
  1956 + }
  1957 +
  1958 + function getLangRevisionMap(seq, str) {
  1959 + return str.split(',').reduce((seq, cur) => {
  1960 + const [lang, rev] = getLangRevisionTuple(cur);
  1961 + seq[lang] = rev;
  1962 + return seq;
  1963 + }, seq);
  1964 + }
  1965 +
  1966 + function getLangRevisionTuple(str) {
  1967 + const [lang, rev] = str.trim().split(':');
  1968 + // if revision is missing, use NaN
  1969 + return [lang, parseInt(rev)];
  1970 + }
  1971 +
  1972 + function negotiateLanguages(
  1973 + fn, appVersion, defaultLang, availableLangs, additionalLangs, prevLangs,
  1974 + requestedLangs) {
  1975 +
  1976 + const allAvailableLangs = Object.keys(availableLangs).concat(
  1977 + additionalLangs || []).concat(Object.keys(pseudo));
  1978 + const newLangs = prioritizeLocales(
  1979 + defaultLang, allAvailableLangs, requestedLangs);
  1980 +
  1981 + const langs = newLangs.map(code => ({
  1982 + code: code,
  1983 + src: getLangSource(appVersion, availableLangs, additionalLangs, code),
  1984 + }));
  1985 +
  1986 + if (!arrEqual(prevLangs, newLangs)) {
  1987 + fn(langs);
  1988 + }
  1989 +
  1990 + return langs;
  1991 + }
  1992 +
  1993 + function arrEqual(arr1, arr2) {
  1994 + return arr1.length === arr2.length &&
  1995 + arr1.every((elem, i) => elem === arr2[i]);
  1996 + }
  1997 +
  1998 + function getMatchingLangpack(appVersion, langpacks) {
  1999 + for (let i = 0, langpack; (langpack = langpacks[i]); i++) {
  2000 + if (langpack.target === appVersion) {
  2001 + return langpack;
  2002 + }
  2003 + }
  2004 + return null;
  2005 + }
  2006 +
  2007 + function getLangSource(appVersion, availableLangs, additionalLangs, code) {
  2008 + if (additionalLangs && additionalLangs[code]) {
  2009 + const lp = getMatchingLangpack(appVersion, additionalLangs[code]);
  2010 + if (lp &&
  2011 + (!(code in availableLangs) ||
  2012 + parseInt(lp.revision) > availableLangs[code])) {
  2013 + return 'extra';
  2014 + }
  2015 + }
  2016 +
  2017 + if ((code in pseudo) && !(code in availableLangs)) {
  2018 + return 'pseudo';
  2019 + }
  2020 +
  2021 + return 'app';
  2022 + }
  2023 +
  2024 + class Remote {
  2025 + constructor(fetchResource, broadcast, requestedLangs) {
  2026 + this.fetchResource = fetchResource;
  2027 + this.broadcast = broadcast;
  2028 + this.ctxs = new Map();
  2029 + this.interactive = documentReady().then(
  2030 + () => this.init(requestedLangs));
  2031 + }
  2032 +
  2033 + init(requestedLangs) {
  2034 + const meta = getMeta(document.head);
  2035 + this.defaultLanguage = meta.defaultLang;
  2036 + this.availableLanguages = meta.availableLangs;
  2037 + this.appVersion = meta.appVersion;
  2038 +
  2039 + this.env = new Env(
  2040 + this.defaultLanguage,
  2041 + (...args) => this.fetchResource(this.appVersion, ...args));
  2042 +
  2043 + return this.requestLanguages(requestedLangs);
  2044 + }
  2045 +
  2046 + registerView(view, resources) {
  2047 + return this.interactive.then(() => {
  2048 + this.ctxs.set(view, this.env.createContext(resources));
  2049 + return true;
  2050 + });
  2051 + }
  2052 +
  2053 + unregisterView(view) {
  2054 + return this.ctxs.delete(view);
  2055 + }
  2056 +
  2057 + resolveEntities(view, langs, keys) {
  2058 + return this.ctxs.get(view).resolveEntities(langs, keys);
  2059 + }
  2060 +
  2061 + formatValues(view, keys) {
  2062 + return this.languages.then(
  2063 + langs => this.ctxs.get(view).resolveValues(langs, keys));
  2064 + }
  2065 +
  2066 + resolvedLanguages() {
  2067 + return this.languages;
  2068 + }
  2069 +
  2070 + requestLanguages(requestedLangs) {
  2071 + return changeLanguages.call(
  2072 + this, getAdditionalLanguages(), requestedLangs);
  2073 + }
  2074 +
  2075 + getName(code) {
  2076 + return pseudo[code].name;
  2077 + }
  2078 +
  2079 + processString(code, str) {
  2080 + return pseudo[code].process(str);
  2081 + }
  2082 +
  2083 + handleEvent(evt) {
  2084 + return changeLanguages.call(
  2085 + this, evt.detail || getAdditionalLanguages(), navigator.languages);
  2086 + }
  2087 + }
  2088 +
  2089 + function getAdditionalLanguages() {
  2090 + if (navigator.mozApps && navigator.mozApps.getAdditionalLanguages) {
  2091 + return navigator.mozApps.getAdditionalLanguages().catch(
  2092 + () => []);
  2093 + }
  2094 +
  2095 + return Promise.resolve([]);
  2096 + }
  2097 +
  2098 + function changeLanguages(additionalLangs, requestedLangs) {
  2099 + const prevLangs = this.languages || [];
  2100 + return this.languages = Promise.all([
  2101 + additionalLangs, prevLangs]).then(
  2102 + ([additionalLangs, prevLangs]) => negotiateLanguages(
  2103 + this.broadcast.bind(this, 'translateDocument'),
  2104 + this.appVersion, this.defaultLanguage, this.availableLanguages,
  2105 + additionalLangs, prevLangs, requestedLangs));
  2106 + }
  2107 +
  2108 + const remote = new Remote(fetchResource, broadcast, navigator.languages);
  2109 + window.addEventListener('languagechange', remote);
  2110 + document.addEventListener('additionallanguageschange', remote);
  2111 +
  2112 + remote.service = new Service('l20n')
  2113 + .method('registerView', (...args) => remote.registerView(...args))
  2114 + .method('resolvedLanguages', (...args) => remote.resolvedLanguages(...args))
  2115 + .method('requestLanguages', (...args) => remote.requestLanguages(...args))
  2116 + .method('resolveEntities', (...args) => remote.resolveEntities(...args))
  2117 + .method('formatValues', (...args) => remote.formatValues(...args))
  2118 + .method('getName', (...args) => remote.getName(...args))
  2119 + .method('processString', (...args) => remote.processString(...args))
  2120 + .on('disconnect', clientId => remote.unregisterView(clientId))
  2121 + .listen(channel);
  2122 +
  2123 +})();
0 2124 \ No newline at end of file
... ...
bower_components/l20n/dist/bundle/gaia/build/l20n.js 0 → 100755
  1 +'use strict';
  2 +
  3 +function L10nError(message, id, lang) {
  4 + this.name = 'L10nError';
  5 + this.message = message;
  6 + this.id = id;
  7 + this.lang = lang;
  8 +}
  9 +L10nError.prototype = Object.create(Error.prototype);
  10 +L10nError.prototype.constructor = L10nError;
  11 +
  12 +function fetchResource(htmloptimizer, res, lang) {
  13 + // We need to decode URI because build system DOM reader
  14 + // may replace `{locale}` with `%7Blocale%7D`. See bug 1098188
  15 + const url = decodeURI(res).replace('{locale}', lang.code);
  16 + const {file, content} = htmloptimizer.getFileByRelativePath(url);
  17 + if (!file) {
  18 + return Promise.reject(new L10nError('Not found: ' + url));
  19 + }
  20 +
  21 + const parsed = res.endsWith('.json') ?
  22 + JSON.parse(content) : content;
  23 + return Promise.resolve(parsed);
  24 +}
  25 +
  26 +// Walk an entry node searching for content leaves
  27 +function walkEntry(entry, fn) {
  28 + if (typeof entry === 'string') {
  29 + return fn(entry);
  30 + }
  31 +
  32 + const newEntry = Object.create(null);
  33 +
  34 + if (entry.value) {
  35 + newEntry.value = walkValue$1(entry.value, fn);
  36 + }
  37 +
  38 + if (entry.index) {
  39 + newEntry.index = entry.index;
  40 + }
  41 +
  42 + if (entry.attrs) {
  43 + newEntry.attrs = Object.create(null);
  44 + for (let key in entry.attrs) {
  45 + newEntry.attrs[key] = walkEntry(entry.attrs[key], fn);
  46 + }
  47 + }
  48 +
  49 + return newEntry;
  50 +}
  51 +
  52 +function walkValue$1(value, fn) {
  53 + if (typeof value === 'string') {
  54 + return fn(value);
  55 + }
  56 +
  57 + // skip expressions in placeables
  58 + if (value.type) {
  59 + return value;
  60 + }
  61 +
  62 + const newValue = Array.isArray(value) ? [] : Object.create(null);
  63 + const keys = Object.keys(value);
  64 +
  65 + for (let i = 0, key; (key = keys[i]); i++) {
  66 + newValue[key] = walkValue$1(value[key], fn);
  67 + }
  68 +
  69 + return newValue;
  70 +}
  71 +
  72 +/* Pseudolocalizations
  73 + *
  74 + * pseudo is a dict of strategies to be used to modify the English
  75 + * context in order to create pseudolocalizations. These can be used by
  76 + * developers to test the localizability of their code without having to
  77 + * actually speak a foreign language.
  78 + *
  79 + * Currently, the following pseudolocales are supported:
  80 + *
  81 + * fr-x-psaccent - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ
  82 + *
  83 + * In Accented English all English letters are replaced by accented
  84 + * Unicode counterparts which don't impair the readability of the content.
  85 + * This allows developers to quickly test if any given string is being
  86 + * correctly displayed in its 'translated' form. Additionally, simple
  87 + * heuristics are used to make certain words longer to better simulate the
  88 + * experience of international users.
  89 + *
  90 + * ar-x-psbidi - ɥsıʅƃuƎ ıpıԐ
  91 + *
  92 + * Bidi English is a fake RTL locale. All words are surrounded by
  93 + * Unicode formatting marks forcing the RTL directionality of characters.
  94 + * In addition, to make the reversed text easier to read, individual
  95 + * letters are flipped.
  96 + *
  97 + * Note: The name above is hardcoded to be RTL in case code editors have
  98 + * trouble with the RLO and PDF Unicode marks. In reality, it should be
  99 + * surrounded by those marks as well.
  100 + *
  101 + * See https://bugzil.la/900182 for more information.
  102 + *
  103 + */
  104 +
  105 +function createGetter(id, name) {
  106 + let _pseudo = null;
  107 +
  108 + return function getPseudo() {
  109 + if (_pseudo) {
  110 + return _pseudo;
  111 + }
  112 +
  113 + const reAlphas = /[a-zA-Z]/g;
  114 + const reVowels = /[aeiouAEIOU]/g;
  115 + const reWords = /[^\W0-9_]+/g;
  116 + // strftime tokens (%a, %Eb), template {vars}, HTML entities (&#x202a;)
  117 + // and HTML tags.
  118 + const reExcluded = /(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;
  119 +
  120 + const charMaps = {
  121 + 'fr-x-psaccent':
  122 + 'ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ[\\]^_`ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ',
  123 + 'ar-x-psbidi':
  124 + // XXX Use pɟפ˥ʎ as replacements for ᗡℲ⅁⅂⅄. https://bugzil.la/1007340
  125 + '∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅMXʎZ[\\]ᵥ_,ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz',
  126 + };
  127 +
  128 + const mods = {
  129 + 'fr-x-psaccent': val =>
  130 + val.replace(reVowels, match => match + match.toLowerCase()),
  131 +
  132 + // Surround each word with Unicode formatting codes, RLO and PDF:
  133 + // U+202E: RIGHT-TO-LEFT OVERRIDE (RLO)
  134 + // U+202C: POP DIRECTIONAL FORMATTING (PDF)
  135 + // See http://www.w3.org/International/questions/qa-bidi-controls
  136 + 'ar-x-psbidi': val =>
  137 + val.replace(reWords, match => '\u202e' + match + '\u202c'),
  138 + };
  139 +
  140 + // Replace each Latin letter with a Unicode character from map
  141 + const replaceChars =
  142 + (map, val) => val.replace(
  143 + reAlphas, match => map.charAt(match.charCodeAt(0) - 65));
  144 +
  145 + const transform =
  146 + val => replaceChars(charMaps[id], mods[id](val));
  147 +
  148 + // apply fn to translatable parts of val
  149 + const apply = (fn, val) => {
  150 + if (!val) {
  151 + return val;
  152 + }
  153 +
  154 + const parts = val.split(reExcluded);
  155 + const modified = parts.map(function(part) {
  156 + if (reExcluded.test(part)) {
  157 + return part;
  158 + }
  159 + return fn(part);
  160 + });
  161 + return modified.join('');
  162 + };
  163 +
  164 + return _pseudo = {
  165 + name: transform(name),
  166 + process: str => apply(transform, str)
  167 + };
  168 + };
  169 +}
  170 +
  171 +const pseudo$1 = Object.defineProperties(Object.create(null), {
  172 + 'fr-x-psaccent': {
  173 + enumerable: true,
  174 + get: createGetter('fr-x-psaccent', 'Runtime Accented')
  175 + },
  176 + 'ar-x-psbidi': {
  177 + enumerable: true,
  178 + get: createGetter('ar-x-psbidi', 'Runtime Bidi')
  179 + }
  180 +});
  181 +
  182 +const MAX_PLACEABLES$1 = 100;
  183 +
  184 +var L20nParser = {
  185 + parse: function(emit, string) {
  186 + this._source = string;
  187 + this._index = 0;
  188 + this._length = string.length;
  189 + this.entries = Object.create(null);
  190 + this.emit = emit;
  191 +
  192 + return this.getResource();
  193 + },
  194 +
  195 + getResource: function() {
  196 + this.getWS();
  197 + while (this._index < this._length) {
  198 + try {
  199 + this.getEntry();
  200 + } catch (e) {
  201 + if (e instanceof L10nError) {
  202 + // we want to recover, but we don't need it in entries
  203 + this.getJunkEntry();
  204 + if (!this.emit) {
  205 + throw e;
  206 + }
  207 + } else {
  208 + throw e;
  209 + }
  210 + }
  211 +
  212 + if (this._index < this._length) {
  213 + this.getWS();
  214 + }
  215 + }
  216 +
  217 + return this.entries;
  218 + },
  219 +
  220 + getEntry: function() {
  221 + if (this._source[this._index] === '<') {
  222 + ++this._index;
  223 + const id = this.getIdentifier();
  224 + if (this._source[this._index] === '[') {
  225 + ++this._index;
  226 + return this.getEntity(id, this.getItemList(this.getExpression, ']'));
  227 + }
  228 + return this.getEntity(id);
  229 + }
  230 +
  231 + if (this._source.startsWith('/*', this._index)) {
  232 + return this.getComment();
  233 + }
  234 +
  235 + throw this.error('Invalid entry');
  236 + },
  237 +
  238 + getEntity: function(id, index) {
  239 + if (!this.getRequiredWS()) {
  240 + throw this.error('Expected white space');
  241 + }
  242 +
  243 + const ch = this._source[this._index];
  244 + const value = this.getValue(ch, index === undefined);
  245 + let attrs;
  246 +
  247 + if (value === undefined) {
  248 + if (ch === '>') {
  249 + throw this.error('Expected ">"');
  250 + }
  251 + attrs = this.getAttributes();
  252 + } else {
  253 + const ws1 = this.getRequiredWS();
  254 + if (this._source[this._index] !== '>') {
  255 + if (!ws1) {
  256 + throw this.error('Expected ">"');
  257 + }
  258 + attrs = this.getAttributes();
  259 + }
  260 + }
  261 +
  262 + // skip '>'
  263 + ++this._index;
  264 +
  265 + if (id in this.entries) {
  266 + throw this.error('Duplicate entry ID "' + id, 'duplicateerror');
  267 + }
  268 + if (!attrs && !index && typeof value === 'string') {
  269 + this.entries[id] = value;
  270 + } else {
  271 + this.entries[id] = {
  272 + value,
  273 + attrs,
  274 + index
  275 + };
  276 + }
  277 + },
  278 +
  279 + getValue: function(ch = this._source[this._index], optional = false) {
  280 + switch (ch) {
  281 + case '\'':
  282 + case '"':
  283 + return this.getString(ch, 1);
  284 + case '{':
  285 + return this.getHash();
  286 + }
  287 +
  288 + if (!optional) {
  289 + throw this.error('Unknown value type');
  290 + }
  291 +
  292 + return;
  293 + },
  294 +
  295 + getWS: function() {
  296 + let cc = this._source.charCodeAt(this._index);
  297 + // space, \n, \t, \r
  298 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  299 + cc = this._source.charCodeAt(++this._index);
  300 + }
  301 + },
  302 +
  303 + getRequiredWS: function() {
  304 + const pos = this._index;
  305 + let cc = this._source.charCodeAt(pos);
  306 + // space, \n, \t, \r
  307 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  308 + cc = this._source.charCodeAt(++this._index);
  309 + }
  310 + return this._index !== pos;
  311 + },
  312 +
  313 + getIdentifier: function() {
  314 + const start = this._index;
  315 + let cc = this._source.charCodeAt(this._index);
  316 +
  317 + if ((cc >= 97 && cc <= 122) || // a-z
  318 + (cc >= 65 && cc <= 90) || // A-Z
  319 + cc === 95) { // _
  320 + cc = this._source.charCodeAt(++this._index);
  321 + } else {
  322 + throw this.error('Identifier has to start with [a-zA-Z_]');
  323 + }
  324 +
  325 + while ((cc >= 97 && cc <= 122) || // a-z
  326 + (cc >= 65 && cc <= 90) || // A-Z
  327 + (cc >= 48 && cc <= 57) || // 0-9
  328 + cc === 95) { // _
  329 + cc = this._source.charCodeAt(++this._index);
  330 + }
  331 +
  332 + return this._source.slice(start, this._index);
  333 + },
  334 +
  335 + getUnicodeChar: function() {
  336 + for (let i = 0; i < 4; i++) {
  337 + let cc = this._source.charCodeAt(++this._index);
  338 + if ((cc > 96 && cc < 103) || // a-f
  339 + (cc > 64 && cc < 71) || // A-F
  340 + (cc > 47 && cc < 58)) { // 0-9
  341 + continue;
  342 + }
  343 + throw this.error('Illegal unicode escape sequence');
  344 + }
  345 + this._index++;
  346 + return String.fromCharCode(
  347 + parseInt(this._source.slice(this._index - 4, this._index), 16));
  348 + },
  349 +
  350 + stringRe: /"|'|{{|\\/g,
  351 + getString: function(opchar, opcharLen) {
  352 + const body = [];
  353 + let placeables = 0;
  354 +
  355 + this._index += opcharLen;
  356 + const start = this._index;
  357 +
  358 + let bufStart = start;
  359 + let buf = '';
  360 +
  361 + while (true) {
  362 + this.stringRe.lastIndex = this._index;
  363 + const match = this.stringRe.exec(this._source);
  364 +
  365 + if (!match) {
  366 + throw this.error('Unclosed string literal');
  367 + }
  368 +
  369 + if (match[0] === '"' || match[0] === '\'') {
  370 + if (match[0] !== opchar) {
  371 + this._index += opcharLen;
  372 + continue;
  373 + }
  374 + this._index = match.index + opcharLen;
  375 + break;
  376 + }
  377 +
  378 + if (match[0] === '{{') {
  379 + if (placeables > MAX_PLACEABLES$1 - 1) {
  380 + throw this.error('Too many placeables, maximum allowed is ' +
  381 + MAX_PLACEABLES$1);
  382 + }
  383 + placeables++;
  384 + if (match.index > bufStart || buf.length > 0) {
  385 + body.push(buf + this._source.slice(bufStart, match.index));
  386 + buf = '';
  387 + }
  388 + this._index = match.index + 2;
  389 + this.getWS();
  390 + body.push(this.getExpression());
  391 + this.getWS();
  392 + this._index += 2;
  393 + bufStart = this._index;
  394 + continue;
  395 + }
  396 +
  397 + if (match[0] === '\\') {
  398 + this._index = match.index + 1;
  399 + const ch2 = this._source[this._index];
  400 + if (ch2 === 'u') {
  401 + buf += this._source.slice(bufStart, match.index) +
  402 + this.getUnicodeChar();
  403 + } else if (ch2 === opchar || ch2 === '\\') {
  404 + buf += this._source.slice(bufStart, match.index) + ch2;
  405 + this._index++;
  406 + } else if (this._source.startsWith('{{', this._index)) {
  407 + buf += this._source.slice(bufStart, match.index) + '{{';
  408 + this._index += 2;
  409 + } else {
  410 + throw this.error('Illegal escape sequence');
  411 + }
  412 + bufStart = this._index;
  413 + }
  414 + }
  415 +
  416 + if (body.length === 0) {
  417 + return buf + this._source.slice(bufStart, this._index - opcharLen);
  418 + }
  419 +
  420 + if (this._index - opcharLen > bufStart || buf.length > 0) {
  421 + body.push(buf + this._source.slice(bufStart, this._index - opcharLen));
  422 + }
  423 +
  424 + return body;
  425 + },
  426 +
  427 + getAttributes: function() {
  428 + const attrs = Object.create(null);
  429 +
  430 + while (true) {
  431 + this.getAttribute(attrs);
  432 + const ws1 = this.getRequiredWS();
  433 + const ch = this._source.charAt(this._index);
  434 + if (ch === '>') {
  435 + break;
  436 + } else if (!ws1) {
  437 + throw this.error('Expected ">"');
  438 + }
  439 + }
  440 + return attrs;
  441 + },
  442 +
  443 + getAttribute: function(attrs) {
  444 + const key = this.getIdentifier();
  445 + let index;
  446 +
  447 + if (this._source[this._index]=== '[') {
  448 + ++this._index;
  449 + this.getWS();
  450 + index = this.getItemList(this.getExpression, ']');
  451 + }
  452 + this.getWS();
  453 + if (this._source[this._index] !== ':') {
  454 + throw this.error('Expected ":"');
  455 + }
  456 + ++this._index;
  457 + this.getWS();
  458 + const value = this.getValue();
  459 +
  460 + if (key in attrs) {
  461 + throw this.error('Duplicate attribute "' + key, 'duplicateerror');
  462 + }
  463 +
  464 + if (!index && typeof value === 'string') {
  465 + attrs[key] = value;
  466 + } else {
  467 + attrs[key] = {
  468 + value,
  469 + index
  470 + };
  471 + }
  472 + },
  473 +
  474 + getHash: function() {
  475 + const items = Object.create(null);
  476 +
  477 + ++this._index;
  478 + this.getWS();
  479 +
  480 + let defKey;
  481 +
  482 + while (true) {
  483 + const [key, value, def] = this.getHashItem();
  484 + items[key] = value;
  485 +
  486 + if (def) {
  487 + if (defKey) {
  488 + throw this.error('Default item redefinition forbidden');
  489 + }
  490 + defKey = key;
  491 + }
  492 + this.getWS();
  493 +
  494 + const comma = this._source[this._index] === ',';
  495 + if (comma) {
  496 + ++this._index;
  497 + this.getWS();
  498 + }
  499 + if (this._source[this._index] === '}') {
  500 + ++this._index;
  501 + break;
  502 + }
  503 + if (!comma) {
  504 + throw this.error('Expected "}"');
  505 + }
  506 + }
  507 +
  508 + if (defKey) {
  509 + items.__default = defKey;
  510 + }
  511 +
  512 + return items;
  513 + },
  514 +
  515 + getHashItem: function() {
  516 + let defItem = false;
  517 + if (this._source[this._index] === '*') {
  518 + ++this._index;
  519 + defItem = true;
  520 + }
  521 +
  522 + const key = this.getIdentifier();
  523 + this.getWS();
  524 + if (this._source[this._index] !== ':') {
  525 + throw this.error('Expected ":"');
  526 + }
  527 + ++this._index;
  528 + this.getWS();
  529 +
  530 + return [key, this.getValue(), defItem];
  531 + },
  532 +
  533 + getComment: function() {
  534 + this._index += 2;
  535 + const start = this._index;
  536 + const end = this._source.indexOf('*/', start);
  537 +
  538 + if (end === -1) {
  539 + throw this.error('Comment without a closing tag');
  540 + }
  541 +
  542 + this._index = end + 2;
  543 + },
  544 +
  545 + getExpression: function () {
  546 + let exp = this.getPrimaryExpression();
  547 +
  548 + while (true) {
  549 + let ch = this._source[this._index];
  550 + if (ch === '.' || ch === '[') {
  551 + ++this._index;
  552 + exp = this.getPropertyExpression(exp, ch === '[');
  553 + } else if (ch === '(') {
  554 + ++this._index;
  555 + exp = this.getCallExpression(exp);
  556 + } else {
  557 + break;
  558 + }
  559 + }
  560 +
  561 + return exp;
  562 + },
  563 +
  564 + getPropertyExpression: function(idref, computed) {
  565 + let exp;
  566 +
  567 + if (computed) {
  568 + this.getWS();
  569 + exp = this.getExpression();
  570 + this.getWS();
  571 + if (this._source[this._index] !== ']') {
  572 + throw this.error('Expected "]"');
  573 + }
  574 + ++this._index;
  575 + } else {
  576 + exp = this.getIdentifier();
  577 + }
  578 +
  579 + return {
  580 + type: 'prop',
  581 + expr: idref,
  582 + prop: exp,
  583 + cmpt: computed
  584 + };
  585 + },
  586 +
  587 + getCallExpression: function(callee) {
  588 + this.getWS();
  589 +
  590 + return {
  591 + type: 'call',
  592 + expr: callee,
  593 + args: this.getItemList(this.getExpression, ')')
  594 + };
  595 + },
  596 +
  597 + getPrimaryExpression: function() {
  598 + const ch = this._source[this._index];
  599 +
  600 + switch (ch) {
  601 + case '$':
  602 + ++this._index;
  603 + return {
  604 + type: 'var',
  605 + name: this.getIdentifier()
  606 + };
  607 + case '@':
  608 + ++this._index;
  609 + return {
  610 + type: 'glob',
  611 + name: this.getIdentifier()
  612 + };
  613 + default:
  614 + return {
  615 + type: 'id',
  616 + name: this.getIdentifier()
  617 + };
  618 + }
  619 + },
  620 +
  621 + getItemList: function(callback, closeChar) {
  622 + const items = [];
  623 + let closed = false;
  624 +
  625 + this.getWS();
  626 +
  627 + if (this._source[this._index] === closeChar) {
  628 + ++this._index;
  629 + closed = true;
  630 + }
  631 +
  632 + while (!closed) {
  633 + items.push(callback.call(this));
  634 + this.getWS();
  635 + let ch = this._source.charAt(this._index);
  636 + switch (ch) {
  637 + case ',':
  638 + ++this._index;
  639 + this.getWS();
  640 + break;
  641 + case closeChar:
  642 + ++this._index;
  643 + closed = true;
  644 + break;
  645 + default:
  646 + throw this.error('Expected "," or "' + closeChar + '"');
  647 + }
  648 + }
  649 +
  650 + return items;
  651 + },
  652 +
  653 +
  654 + getJunkEntry: function() {
  655 + const pos = this._index;
  656 + let nextEntity = this._source.indexOf('<', pos);
  657 + let nextComment = this._source.indexOf('/*', pos);
  658 +
  659 + if (nextEntity === -1) {
  660 + nextEntity = this._length;
  661 + }
  662 + if (nextComment === -1) {
  663 + nextComment = this._length;
  664 + }
  665 +
  666 + let nextEntry = Math.min(nextEntity, nextComment);
  667 +
  668 + this._index = nextEntry;
  669 + },
  670 +
  671 + error: function(message, type = 'parsererror') {
  672 + const pos = this._index;
  673 +
  674 + let start = this._source.lastIndexOf('<', pos - 1);
  675 + const lastClose = this._source.lastIndexOf('>', pos - 1);
  676 + start = lastClose > start ? lastClose + 1 : start;
  677 + const context = this._source.slice(start, pos + 10);
  678 +
  679 + const msg = message + ' at pos ' + pos + ': `' + context + '`';
  680 + const err = new L10nError(msg);
  681 + if (this.emit) {
  682 + this.emit(type, err);
  683 + }
  684 + return err;
  685 + },
  686 +};
  687 +
  688 +var MAX_PLACEABLES = 100;
  689 +
  690 +var PropertiesParser = {
  691 + patterns: null,
  692 + entryIds: null,
  693 + emit: null,
  694 +
  695 + init: function() {
  696 + this.patterns = {
  697 + comment: /^\s*#|^\s*$/,
  698 + entity: /^([^=\s]+)\s*=\s*(.*)$/,
  699 + multiline: /[^\\]\\$/,
  700 + index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,
  701 + unicode: /\\u([0-9a-fA-F]{1,4})/g,
  702 + entries: /[^\r\n]+/g,
  703 + controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g,
  704 + placeables: /\{\{\s*([^\s]*?)\s*\}\}/,
  705 + };
  706 + },
  707 +
  708 + parse: function(emit, source) {
  709 + if (!this.patterns) {
  710 + this.init();
  711 + }
  712 + this.emit = emit;
  713 +
  714 + var entries = {};
  715 +
  716 + var lines = source.match(this.patterns.entries);
  717 + if (!lines) {
  718 + return entries;
  719 + }
  720 + for (var i = 0; i < lines.length; i++) {
  721 + var line = lines[i];
  722 +
  723 + if (this.patterns.comment.test(line)) {
  724 + continue;
  725 + }
  726 +
  727 + while (this.patterns.multiline.test(line) && i < lines.length) {
  728 + line = line.slice(0, -1) + lines[++i].trim();
  729 + }
  730 +
  731 + var entityMatch = line.match(this.patterns.entity);
  732 + if (entityMatch) {
  733 + try {
  734 + this.parseEntity(entityMatch[1], entityMatch[2], entries);
  735 + } catch (e) {
  736 + if (!this.emit) {
  737 + throw e;
  738 + }
  739 + }
  740 + }
  741 + }
  742 + return entries;
  743 + },
  744 +
  745 + parseEntity: function(id, value, entries) {
  746 + var name, key;
  747 +
  748 + var pos = id.indexOf('[');
  749 + if (pos !== -1) {
  750 + name = id.substr(0, pos);
  751 + key = id.substring(pos + 1, id.length - 1);
  752 + } else {
  753 + name = id;
  754 + key = null;
  755 + }
  756 +
  757 + var nameElements = name.split('.');
  758 +
  759 + if (nameElements.length > 2) {
  760 + throw this.error('Error in ID: "' + name + '".' +
  761 + ' Nested attributes are not supported.');
  762 + }
  763 +
  764 + var attr;
  765 + if (nameElements.length > 1) {
  766 + name = nameElements[0];
  767 + attr = nameElements[1];
  768 +
  769 + if (attr[0] === '$') {
  770 + throw this.error('Attribute can\'t start with "$"');
  771 + }
  772 + } else {
  773 + attr = null;
  774 + }
  775 +
  776 + this.setEntityValue(name, attr, key, this.unescapeString(value), entries);
  777 + },
  778 +
  779 + setEntityValue: function(id, attr, key, rawValue, entries) {
  780 + var value = rawValue.indexOf('{{') > -1 ?
  781 + this.parseString(rawValue) : rawValue;
  782 +
  783 + var isSimpleValue = typeof value === 'string';
  784 + var root = entries;
  785 +
  786 + var isSimpleNode = typeof entries[id] === 'string';
  787 +
  788 + if (!entries[id] && (attr || key || !isSimpleValue)) {
  789 + entries[id] = Object.create(null);
  790 + isSimpleNode = false;
  791 + }
  792 +
  793 + if (attr) {
  794 + if (isSimpleNode) {
  795 + const val = entries[id];
  796 + entries[id] = Object.create(null);
  797 + entries[id].value = val;
  798 + }
  799 + if (!entries[id].attrs) {
  800 + entries[id].attrs = Object.create(null);
  801 + }
  802 + if (!entries[id].attrs && !isSimpleValue) {
  803 + entries[id].attrs[attr] = Object.create(null);
  804 + }
  805 + root = entries[id].attrs;
  806 + id = attr;
  807 + }
  808 +
  809 + if (key) {
  810 + isSimpleNode = false;
  811 + if (typeof root[id] === 'string') {
  812 + const val = root[id];
  813 + root[id] = Object.create(null);
  814 + root[id].index = this.parseIndex(val);
  815 + root[id].value = Object.create(null);
  816 + }
  817 + root = root[id].value;
  818 + id = key;
  819 + isSimpleValue = true;
  820 + }
  821 +
  822 + if (isSimpleValue && (!entries[id] || isSimpleNode)) {
  823 + if (id in root) {
  824 + throw this.error();
  825 + }
  826 + root[id] = value;
  827 + } else {
  828 + if (!root[id]) {
  829 + root[id] = Object.create(null);
  830 + }
  831 + root[id].value = value;
  832 + }
  833 + },
  834 +
  835 + parseString: function(str) {
  836 + var chunks = str.split(this.patterns.placeables);
  837 + var complexStr = [];
  838 +
  839 + var len = chunks.length;
  840 + var placeablesCount = (len - 1) / 2;
  841 +
  842 + if (placeablesCount >= MAX_PLACEABLES) {
  843 + throw this.error('Too many placeables (' + placeablesCount +
  844 + ', max allowed is ' + MAX_PLACEABLES + ')');
  845 + }
  846 +
  847 + for (var i = 0; i < chunks.length; i++) {
  848 + if (chunks[i].length === 0) {
  849 + continue;
  850 + }
  851 + if (i % 2 === 1) {
  852 + complexStr.push({type: 'idOrVar', name: chunks[i]});
  853 + } else {
  854 + complexStr.push(chunks[i]);
  855 + }
  856 + }
  857 + return complexStr;
  858 + },
  859 +
  860 + unescapeString: function(str) {
  861 + if (str.lastIndexOf('\\') !== -1) {
  862 + str = str.replace(this.patterns.controlChars, '$1');
  863 + }
  864 + return str.replace(this.patterns.unicode, function(match, token) {
  865 + return String.fromCodePoint(parseInt(token, 16));
  866 + });
  867 + },
  868 +
  869 + parseIndex: function(str) {
  870 + var match = str.match(this.patterns.index);
  871 + if (!match) {
  872 + throw new L10nError('Malformed index');
  873 + }
  874 + if (match[2]) {
  875 + return [{
  876 + type: 'call',
  877 + expr: {
  878 + type: 'prop',
  879 + expr: {
  880 + type: 'glob',
  881 + name: 'cldr'
  882 + },
  883 + prop: 'plural',
  884 + cmpt: false
  885 + }, args: [{
  886 + type: 'idOrVar',
  887 + name: match[2]
  888 + }]
  889 + }];
  890 + } else {
  891 + return [{type: 'idOrVar', name: match[1]}];
  892 + }
  893 + },
  894 +
  895 + error: function(msg, type = 'parsererror') {
  896 + const err = new L10nError(msg);
  897 + if (this.emit) {
  898 + this.emit(type, err);
  899 + }
  900 + return err;
  901 + }
  902 +};
  903 +
  904 +const KNOWN_MACROS$1 = ['plural'];
  905 +const MAX_PLACEABLE_LENGTH$1 = 2500;
  906 +
  907 +// Unicode bidi isolation characters
  908 +const FSI$1 = '\u2068';
  909 +const PDI$1 = '\u2069';
  910 +
  911 +const resolutionChain$1 = new WeakSet();
  912 +
  913 +function format$1(ctx, lang, args, entity) {
  914 + if (typeof entity === 'string') {
  915 + return [{}, entity];
  916 + }
  917 +
  918 + if (resolutionChain$1.has(entity)) {
  919 + throw new L10nError('Cyclic reference detected');
  920 + }
  921 +
  922 + resolutionChain$1.add(entity);
  923 +
  924 + let rv;
  925 + // if format fails, we want the exception to bubble up and stop the whole
  926 + // resolving process; however, we still need to remove the entity from the
  927 + // resolution chain
  928 + try {
  929 + rv = resolveValue$1(
  930 + {}, ctx, lang, args, entity.value, entity.index);
  931 + } finally {
  932 + resolutionChain$1.delete(entity);
  933 + }
  934 + return rv;
  935 +}
  936 +
  937 +function resolveIdentifier$1(ctx, lang, args, id) {
  938 + if (KNOWN_MACROS$1.indexOf(id) > -1) {
  939 + return [{}, ctx._getMacro(lang, id)];
  940 + }
  941 +
  942 + if (args && args.hasOwnProperty(id)) {
  943 + if (typeof args[id] === 'string' || (typeof args[id] === 'number' &&
  944 + !isNaN(args[id]))) {
  945 + return [{}, args[id]];
  946 + } else {
  947 + throw new L10nError('Arg must be a string or a number: ' + id);
  948 + }
  949 + }
  950 +
  951 + // XXX: special case for Node.js where still:
  952 + // '__proto__' in Object.create(null) => true
  953 + if (id === '__proto__') {
  954 + throw new L10nError('Illegal id: ' + id);
  955 + }
  956 +
  957 + const entity = ctx._getEntity(lang, id);
  958 +
  959 + if (entity) {
  960 + return format$1(ctx, lang, args, entity);
  961 + }
  962 +
  963 + throw new L10nError('Unknown reference: ' + id);
  964 +}
  965 +
  966 +function subPlaceable$1(locals, ctx, lang, args, id) {
  967 + let newLocals, value;
  968 +
  969 + try {
  970 + [newLocals, value] = resolveIdentifier$1(ctx, lang, args, id);
  971 + } catch (err) {
  972 + return [{ error: err }, FSI$1 + '{{ ' + id + ' }}' + PDI$1];
  973 + }
  974 +
  975 + if (typeof value === 'number') {
  976 + const formatter = ctx._getNumberFormatter(lang);
  977 + return [newLocals, formatter.format(value)];
  978 + }
  979 +
  980 + if (typeof value === 'string') {
  981 + // prevent Billion Laughs attacks
  982 + if (value.length >= MAX_PLACEABLE_LENGTH$1) {
  983 + throw new L10nError('Too many characters in placeable (' +
  984 + value.length + ', max allowed is ' +
  985 + MAX_PLACEABLE_LENGTH$1 + ')');
  986 + }
  987 + return [newLocals, FSI$1 + value + PDI$1];
  988 + }
  989 +
  990 + return [{}, FSI$1 + '{{ ' + id + ' }}' + PDI$1];
  991 +}
  992 +
  993 +function interpolate$1(locals, ctx, lang, args, arr) {
  994 + return arr.reduce(function([localsSeq, valueSeq], cur) {
  995 + if (typeof cur === 'string') {
  996 + return [localsSeq, valueSeq + cur];
  997 + } else {
  998 + const [, value] = subPlaceable$1(locals, ctx, lang, args, cur.name);
  999 + // wrap the substitution in bidi isolate characters
  1000 + return [localsSeq, valueSeq + value];
  1001 + }
  1002 + }, [locals, '']);
  1003 +}
  1004 +
  1005 +function resolveSelector$1(ctx, lang, args, expr, index) {
  1006 + //XXX: Dehardcode!!!
  1007 + let selectorName;
  1008 + if (index[0].type === 'call' && index[0].expr.type === 'prop' &&
  1009 + index[0].expr.expr.name === 'cldr') {
  1010 + selectorName = 'plural';
  1011 + } else {
  1012 + selectorName = index[0].name;
  1013 + }
  1014 + const selector = resolveIdentifier$1(ctx, lang, args, selectorName)[1];
  1015 +
  1016 + if (typeof selector !== 'function') {
  1017 + // selector is a simple reference to an entity or args
  1018 + return selector;
  1019 + }
  1020 +
  1021 + const argValue = index[0].args ?
  1022 + resolveIdentifier$1(ctx, lang, args, index[0].args[0].name)[1] : undefined;
  1023 +
  1024 + if (selectorName === 'plural') {
  1025 + // special cases for zero, one, two if they are defined on the hash
  1026 + if (argValue === 0 && 'zero' in expr) {
  1027 + return 'zero';
  1028 + }
  1029 + if (argValue === 1 && 'one' in expr) {
  1030 + return 'one';
  1031 + }
  1032 + if (argValue === 2 && 'two' in expr) {
  1033 + return 'two';
  1034 + }
  1035 + }
  1036 +
  1037 + return selector(argValue);
  1038 +}
  1039 +
  1040 +function resolveValue$1(locals, ctx, lang, args, expr, index) {
  1041 + if (!expr) {
  1042 + return [locals, expr];
  1043 + }
  1044 +
  1045 + if (typeof expr === 'string' ||
  1046 + typeof expr === 'boolean' ||
  1047 + typeof expr === 'number') {
  1048 + return [locals, expr];
  1049 + }
  1050 +
  1051 + if (Array.isArray(expr)) {
  1052 + return interpolate$1(locals, ctx, lang, args, expr);
  1053 + }
  1054 +
  1055 + // otherwise, it's a dict
  1056 + if (index) {
  1057 + // try to use the index in order to select the right dict member
  1058 + const selector = resolveSelector$1(ctx, lang, args, expr, index);
  1059 + if (selector in expr) {
  1060 + return resolveValue$1(locals, ctx, lang, args, expr[selector]);
  1061 + }
  1062 + }
  1063 +
  1064 + // if there was no index or no selector was found, try the default
  1065 + // XXX 'other' is an artifact from Gaia
  1066 + const defaultKey = expr.__default || 'other';
  1067 + if (defaultKey in expr) {
  1068 + return resolveValue$1(locals, ctx, lang, args, expr[defaultKey]);
  1069 + }
  1070 +
  1071 + throw new L10nError('Unresolvable value');
  1072 +}
  1073 +
  1074 +const locales2rules = {
  1075 + 'af': 3,
  1076 + 'ak': 4,
  1077 + 'am': 4,
  1078 + 'ar': 1,
  1079 + 'asa': 3,
  1080 + 'az': 0,
  1081 + 'be': 11,
  1082 + 'bem': 3,
  1083 + 'bez': 3,
  1084 + 'bg': 3,
  1085 + 'bh': 4,
  1086 + 'bm': 0,
  1087 + 'bn': 3,
  1088 + 'bo': 0,
  1089 + 'br': 20,
  1090 + 'brx': 3,
  1091 + 'bs': 11,
  1092 + 'ca': 3,
  1093 + 'cgg': 3,
  1094 + 'chr': 3,
  1095 + 'cs': 12,
  1096 + 'cy': 17,
  1097 + 'da': 3,
  1098 + 'de': 3,
  1099 + 'dv': 3,
  1100 + 'dz': 0,
  1101 + 'ee': 3,
  1102 + 'el': 3,
  1103 + 'en': 3,
  1104 + 'eo': 3,
  1105 + 'es': 3,
  1106 + 'et': 3,
  1107 + 'eu': 3,
  1108 + 'fa': 0,
  1109 + 'ff': 5,
  1110 + 'fi': 3,
  1111 + 'fil': 4,
  1112 + 'fo': 3,
  1113 + 'fr': 5,
  1114 + 'fur': 3,
  1115 + 'fy': 3,
  1116 + 'ga': 8,
  1117 + 'gd': 24,
  1118 + 'gl': 3,
  1119 + 'gsw': 3,
  1120 + 'gu': 3,
  1121 + 'guw': 4,
  1122 + 'gv': 23,
  1123 + 'ha': 3,
  1124 + 'haw': 3,
  1125 + 'he': 2,
  1126 + 'hi': 4,
  1127 + 'hr': 11,
  1128 + 'hu': 0,
  1129 + 'id': 0,
  1130 + 'ig': 0,
  1131 + 'ii': 0,
  1132 + 'is': 3,
  1133 + 'it': 3,
  1134 + 'iu': 7,
  1135 + 'ja': 0,
  1136 + 'jmc': 3,
  1137 + 'jv': 0,
  1138 + 'ka': 0,
  1139 + 'kab': 5,
  1140 + 'kaj': 3,
  1141 + 'kcg': 3,
  1142 + 'kde': 0,
  1143 + 'kea': 0,
  1144 + 'kk': 3,
  1145 + 'kl': 3,
  1146 + 'km': 0,
  1147 + 'kn': 0,
  1148 + 'ko': 0,
  1149 + 'ksb': 3,
  1150 + 'ksh': 21,
  1151 + 'ku': 3,
  1152 + 'kw': 7,
  1153 + 'lag': 18,
  1154 + 'lb': 3,
  1155 + 'lg': 3,
  1156 + 'ln': 4,
  1157 + 'lo': 0,
  1158 + 'lt': 10,
  1159 + 'lv': 6,
  1160 + 'mas': 3,
  1161 + 'mg': 4,
  1162 + 'mk': 16,
  1163 + 'ml': 3,
  1164 + 'mn': 3,
  1165 + 'mo': 9,
  1166 + 'mr': 3,
  1167 + 'ms': 0,
  1168 + 'mt': 15,
  1169 + 'my': 0,
  1170 + 'nah': 3,
  1171 + 'naq': 7,
  1172 + 'nb': 3,
  1173 + 'nd': 3,
  1174 + 'ne': 3,
  1175 + 'nl': 3,
  1176 + 'nn': 3,
  1177 + 'no': 3,
  1178 + 'nr': 3,
  1179 + 'nso': 4,
  1180 + 'ny': 3,
  1181 + 'nyn': 3,
  1182 + 'om': 3,
  1183 + 'or': 3,
  1184 + 'pa': 3,
  1185 + 'pap': 3,
  1186 + 'pl': 13,
  1187 + 'ps': 3,
  1188 + 'pt': 3,
  1189 + 'rm': 3,
  1190 + 'ro': 9,
  1191 + 'rof': 3,
  1192 + 'ru': 11,
  1193 + 'rwk': 3,
  1194 + 'sah': 0,
  1195 + 'saq': 3,
  1196 + 'se': 7,
  1197 + 'seh': 3,
  1198 + 'ses': 0,
  1199 + 'sg': 0,
  1200 + 'sh': 11,
  1201 + 'shi': 19,
  1202 + 'sk': 12,
  1203 + 'sl': 14,
  1204 + 'sma': 7,
  1205 + 'smi': 7,
  1206 + 'smj': 7,
  1207 + 'smn': 7,
  1208 + 'sms': 7,
  1209 + 'sn': 3,
  1210 + 'so': 3,
  1211 + 'sq': 3,
  1212 + 'sr': 11,
  1213 + 'ss': 3,
  1214 + 'ssy': 3,
  1215 + 'st': 3,
  1216 + 'sv': 3,
  1217 + 'sw': 3,
  1218 + 'syr': 3,
  1219 + 'ta': 3,
  1220 + 'te': 3,
  1221 + 'teo': 3,
  1222 + 'th': 0,
  1223 + 'ti': 4,
  1224 + 'tig': 3,
  1225 + 'tk': 3,
  1226 + 'tl': 4,
  1227 + 'tn': 3,
  1228 + 'to': 0,
  1229 + 'tr': 0,
  1230 + 'ts': 3,
  1231 + 'tzm': 22,
  1232 + 'uk': 11,
  1233 + 'ur': 3,
  1234 + 've': 3,
  1235 + 'vi': 0,
  1236 + 'vun': 3,
  1237 + 'wa': 4,
  1238 + 'wae': 3,
  1239 + 'wo': 0,
  1240 + 'xh': 3,
  1241 + 'xog': 3,
  1242 + 'yo': 0,
  1243 + 'zh': 0,
  1244 + 'zu': 3
  1245 +};
  1246 +
  1247 +// utility functions for plural rules methods
  1248 +function isIn(n, list) {
  1249 + return list.indexOf(n) !== -1;
  1250 +}
  1251 +function isBetween(n, start, end) {
  1252 + return typeof n === typeof start && start <= n && n <= end;
  1253 +}
  1254 +
  1255 +// list of all plural rules methods:
  1256 +// map an integer to the plural form name to use
  1257 +const pluralRules = {
  1258 + '0': function() {
  1259 + return 'other';
  1260 + },
  1261 + '1': function(n) {
  1262 + if ((isBetween((n % 100), 3, 10))) {
  1263 + return 'few';
  1264 + }
  1265 + if (n === 0) {
  1266 + return 'zero';
  1267 + }
  1268 + if ((isBetween((n % 100), 11, 99))) {
  1269 + return 'many';
  1270 + }
  1271 + if (n === 2) {
  1272 + return 'two';
  1273 + }
  1274 + if (n === 1) {
  1275 + return 'one';
  1276 + }
  1277 + return 'other';
  1278 + },
  1279 + '2': function(n) {
  1280 + if (n !== 0 && (n % 10) === 0) {
  1281 + return 'many';
  1282 + }
  1283 + if (n === 2) {
  1284 + return 'two';
  1285 + }
  1286 + if (n === 1) {
  1287 + return 'one';
  1288 + }
  1289 + return 'other';
  1290 + },
  1291 + '3': function(n) {
  1292 + if (n === 1) {
  1293 + return 'one';
  1294 + }
  1295 + return 'other';
  1296 + },
  1297 + '4': function(n) {
  1298 + if ((isBetween(n, 0, 1))) {
  1299 + return 'one';
  1300 + }
  1301 + return 'other';
  1302 + },
  1303 + '5': function(n) {
  1304 + if ((isBetween(n, 0, 2)) && n !== 2) {
  1305 + return 'one';
  1306 + }
  1307 + return 'other';
  1308 + },
  1309 + '6': function(n) {
  1310 + if (n === 0) {
  1311 + return 'zero';
  1312 + }
  1313 + if ((n % 10) === 1 && (n % 100) !== 11) {
  1314 + return 'one';
  1315 + }
  1316 + return 'other';
  1317 + },
  1318 + '7': function(n) {
  1319 + if (n === 2) {
  1320 + return 'two';
  1321 + }
  1322 + if (n === 1) {
  1323 + return 'one';
  1324 + }
  1325 + return 'other';
  1326 + },
  1327 + '8': function(n) {
  1328 + if ((isBetween(n, 3, 6))) {
  1329 + return 'few';
  1330 + }
  1331 + if ((isBetween(n, 7, 10))) {
  1332 + return 'many';
  1333 + }
  1334 + if (n === 2) {
  1335 + return 'two';
  1336 + }
  1337 + if (n === 1) {
  1338 + return 'one';
  1339 + }
  1340 + return 'other';
  1341 + },
  1342 + '9': function(n) {
  1343 + if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) {
  1344 + return 'few';
  1345 + }
  1346 + if (n === 1) {
  1347 + return 'one';
  1348 + }
  1349 + return 'other';
  1350 + },
  1351 + '10': function(n) {
  1352 + if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) {
  1353 + return 'few';
  1354 + }
  1355 + if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) {
  1356 + return 'one';
  1357 + }
  1358 + return 'other';
  1359 + },
  1360 + '11': function(n) {
  1361 + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
  1362 + return 'few';
  1363 + }
  1364 + if ((n % 10) === 0 ||
  1365 + (isBetween((n % 10), 5, 9)) ||
  1366 + (isBetween((n % 100), 11, 14))) {
  1367 + return 'many';
  1368 + }
  1369 + if ((n % 10) === 1 && (n % 100) !== 11) {
  1370 + return 'one';
  1371 + }
  1372 + return 'other';
  1373 + },
  1374 + '12': function(n) {
  1375 + if ((isBetween(n, 2, 4))) {
  1376 + return 'few';
  1377 + }
  1378 + if (n === 1) {
  1379 + return 'one';
  1380 + }
  1381 + return 'other';
  1382 + },
  1383 + '13': function(n) {
  1384 + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
  1385 + return 'few';
  1386 + }
  1387 + if (n !== 1 && (isBetween((n % 10), 0, 1)) ||
  1388 + (isBetween((n % 10), 5, 9)) ||
  1389 + (isBetween((n % 100), 12, 14))) {
  1390 + return 'many';
  1391 + }
  1392 + if (n === 1) {
  1393 + return 'one';
  1394 + }
  1395 + return 'other';
  1396 + },
  1397 + '14': function(n) {
  1398 + if ((isBetween((n % 100), 3, 4))) {
  1399 + return 'few';
  1400 + }
  1401 + if ((n % 100) === 2) {
  1402 + return 'two';
  1403 + }
  1404 + if ((n % 100) === 1) {
  1405 + return 'one';
  1406 + }
  1407 + return 'other';
  1408 + },
  1409 + '15': function(n) {
  1410 + if (n === 0 || (isBetween((n % 100), 2, 10))) {
  1411 + return 'few';
  1412 + }
  1413 + if ((isBetween((n % 100), 11, 19))) {
  1414 + return 'many';
  1415 + }
  1416 + if (n === 1) {
  1417 + return 'one';
  1418 + }
  1419 + return 'other';
  1420 + },
  1421 + '16': function(n) {
  1422 + if ((n % 10) === 1 && n !== 11) {
  1423 + return 'one';
  1424 + }
  1425 + return 'other';
  1426 + },
  1427 + '17': function(n) {
  1428 + if (n === 3) {
  1429 + return 'few';
  1430 + }
  1431 + if (n === 0) {
  1432 + return 'zero';
  1433 + }
  1434 + if (n === 6) {
  1435 + return 'many';
  1436 + }
  1437 + if (n === 2) {
  1438 + return 'two';
  1439 + }
  1440 + if (n === 1) {
  1441 + return 'one';
  1442 + }
  1443 + return 'other';
  1444 + },
  1445 + '18': function(n) {
  1446 + if (n === 0) {
  1447 + return 'zero';
  1448 + }
  1449 + if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) {
  1450 + return 'one';
  1451 + }
  1452 + return 'other';
  1453 + },
  1454 + '19': function(n) {
  1455 + if ((isBetween(n, 2, 10))) {
  1456 + return 'few';
  1457 + }
  1458 + if ((isBetween(n, 0, 1))) {
  1459 + return 'one';
  1460 + }
  1461 + return 'other';
  1462 + },
  1463 + '20': function(n) {
  1464 + if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !(
  1465 + isBetween((n % 100), 10, 19) ||
  1466 + isBetween((n % 100), 70, 79) ||
  1467 + isBetween((n % 100), 90, 99)
  1468 + )) {
  1469 + return 'few';
  1470 + }
  1471 + if ((n % 1000000) === 0 && n !== 0) {
  1472 + return 'many';
  1473 + }
  1474 + if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) {
  1475 + return 'two';
  1476 + }
  1477 + if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) {
  1478 + return 'one';
  1479 + }
  1480 + return 'other';
  1481 + },
  1482 + '21': function(n) {
  1483 + if (n === 0) {
  1484 + return 'zero';
  1485 + }
  1486 + if (n === 1) {
  1487 + return 'one';
  1488 + }
  1489 + return 'other';
  1490 + },
  1491 + '22': function(n) {
  1492 + if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) {
  1493 + return 'one';
  1494 + }
  1495 + return 'other';
  1496 + },
  1497 + '23': function(n) {
  1498 + if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) {
  1499 + return 'one';
  1500 + }
  1501 + return 'other';
  1502 + },
  1503 + '24': function(n) {
  1504 + if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) {
  1505 + return 'few';
  1506 + }
  1507 + if (isIn(n, [2, 12])) {
  1508 + return 'two';
  1509 + }
  1510 + if (isIn(n, [1, 11])) {
  1511 + return 'one';
  1512 + }
  1513 + return 'other';
  1514 + }
  1515 +};
  1516 +
  1517 +function getPluralRule(code) {
  1518 + // return a function that gives the plural form name for a given integer
  1519 + const index = locales2rules[code.replace(/-.*$/, '')];
  1520 + if (!(index in pluralRules)) {
  1521 + return function() { return 'other'; };
  1522 + }
  1523 + return pluralRules[index];
  1524 +}
  1525 +
  1526 +class Context {
  1527 + constructor(env) {
  1528 + this._env = env;
  1529 + this._numberFormatters = null;
  1530 + }
  1531 +
  1532 + _formatTuple(lang, args, entity, id, key) {
  1533 + try {
  1534 + return format$1(this, lang, args, entity);
  1535 + } catch (err) {
  1536 + err.id = key ? id + '::' + key : id;
  1537 + err.lang = lang;
  1538 + this._env.emit('resolveerror', err, this);
  1539 + return [{ error: err }, err.id];
  1540 + }
  1541 + }
  1542 +
  1543 + _formatEntity(lang, args, entity, id) {
  1544 + const [, value] = this._formatTuple(lang, args, entity, id);
  1545 +
  1546 + const formatted = {
  1547 + value,
  1548 + attrs: null,
  1549 + };
  1550 +
  1551 + if (entity.attrs) {
  1552 + formatted.attrs = Object.create(null);
  1553 + for (let key in entity.attrs) {
  1554 + /* jshint -W089 */
  1555 + const [, attrValue] = this._formatTuple(
  1556 + lang, args, entity.attrs[key], id, key);
  1557 + formatted.attrs[key] = attrValue;
  1558 + }
  1559 + }
  1560 +
  1561 + return formatted;
  1562 + }
  1563 +
  1564 + _formatValue(lang, args, entity, id) {
  1565 + return this._formatTuple(lang, args, entity, id)[1];
  1566 + }
  1567 +
  1568 + fetch(langs) {
  1569 + if (langs.length === 0) {
  1570 + return Promise.resolve(langs);
  1571 + }
  1572 +
  1573 + const resIds = Array.from(this._env._resLists.get(this));
  1574 +
  1575 + return Promise.all(
  1576 + resIds.map(
  1577 + this._env._getResource.bind(this._env, langs[0]))).then(
  1578 + () => langs);
  1579 + }
  1580 +
  1581 + _resolve(langs, keys, formatter, prevResolved) {
  1582 + const lang = langs[0];
  1583 +
  1584 + if (!lang) {
  1585 + return reportMissing.call(this, keys, formatter, prevResolved);
  1586 + }
  1587 +
  1588 + let hasUnresolved = false;
  1589 +
  1590 + const resolved = keys.map((key, i) => {
  1591 + if (prevResolved && prevResolved[i] !== undefined) {
  1592 + return prevResolved[i];
  1593 + }
  1594 + const [id, args] = Array.isArray(key) ?
  1595 + key : [key, undefined];
  1596 + const entity = this._getEntity(lang, id);
  1597 +
  1598 + if (entity) {
  1599 + return formatter.call(this, lang, args, entity, id);
  1600 + }
  1601 +
  1602 + this._env.emit('notfounderror',
  1603 + new L10nError('"' + id + '"' + ' not found in ' + lang.code,
  1604 + id, lang), this);
  1605 + hasUnresolved = true;
  1606 + });
  1607 +
  1608 + if (!hasUnresolved) {
  1609 + return resolved;
  1610 + }
  1611 +
  1612 + return this.fetch(langs.slice(1)).then(
  1613 + nextLangs => this._resolve(nextLangs, keys, formatter, resolved));
  1614 + }
  1615 +
  1616 + resolveEntities(langs, keys) {
  1617 + return this.fetch(langs).then(
  1618 + langs => this._resolve(langs, keys, this._formatEntity));
  1619 + }
  1620 +
  1621 + resolveValues(langs, keys) {
  1622 + return this.fetch(langs).then(
  1623 + langs => this._resolve(langs, keys, this._formatValue));
  1624 + }
  1625 +
  1626 + _getEntity(lang, id) {
  1627 + const cache = this._env._resCache;
  1628 + const resIds = Array.from(this._env._resLists.get(this));
  1629 +
  1630 + // Look for `id` in every resource in order.
  1631 + for (let i = 0, resId; resId = resIds[i]; i++) {
  1632 + const resource = cache.get(resId + lang.code + lang.src);
  1633 + if (resource instanceof L10nError) {
  1634 + continue;
  1635 + }
  1636 + if (id in resource) {
  1637 + return resource[id];
  1638 + }
  1639 + }
  1640 + return undefined;
  1641 + }
  1642 +
  1643 + _getNumberFormatter(lang) {
  1644 + if (!this._numberFormatters) {
  1645 + this._numberFormatters = new Map();
  1646 + }
  1647 + if (!this._numberFormatters.has(lang)) {
  1648 + const formatter = Intl.NumberFormat(lang, {
  1649 + useGrouping: false,
  1650 + });
  1651 + this._numberFormatters.set(lang, formatter);
  1652 + return formatter;
  1653 + }
  1654 + return this._numberFormatters.get(lang);
  1655 + }
  1656 +
  1657 + // XXX in the future macros will be stored in localization resources together
  1658 + // with regular entities and this method will not be needed anymore
  1659 + _getMacro(lang, id) {
  1660 + switch(id) {
  1661 + case 'plural':
  1662 + return getPluralRule(lang.code);
  1663 + default:
  1664 + return undefined;
  1665 + }
  1666 + }
  1667 +
  1668 +}
  1669 +
  1670 +function reportMissing(keys, formatter, resolved) {
  1671 + const missingIds = new Set();
  1672 +
  1673 + keys.forEach((key, i) => {
  1674 + if (resolved && resolved[i] !== undefined) {
  1675 + return;
  1676 + }
  1677 + const id = Array.isArray(key) ? key[0] : key;
  1678 + missingIds.add(id);
  1679 + resolved[i] = formatter === this._formatValue ?
  1680 + id : {value: id, attrs: null};
  1681 + });
  1682 +
  1683 + this._env.emit('notfounderror', new L10nError(
  1684 + '"' + Array.from(missingIds).join(', ') + '"' +
  1685 + ' not found in any language', missingIds), this);
  1686 +
  1687 + return resolved;
  1688 +}
  1689 +
  1690 +function emit(listeners, ...args) {
  1691 + const type = args.shift();
  1692 +
  1693 + if (listeners['*']) {
  1694 + listeners['*'].slice().forEach(
  1695 + listener => listener.apply(this, args));
  1696 + }
  1697 +
  1698 + if (listeners[type]) {
  1699 + listeners[type].slice().forEach(
  1700 + listener => listener.apply(this, args));
  1701 + }
  1702 +}
  1703 +
  1704 +function addEventListener(listeners, type, listener) {
  1705 + if (!(type in listeners)) {
  1706 + listeners[type] = [];
  1707 + }
  1708 + listeners[type].push(listener);
  1709 +}
  1710 +
  1711 +function removeEventListener(listeners, type, listener) {
  1712 + const typeListeners = listeners[type];
  1713 + const pos = typeListeners.indexOf(listener);
  1714 + if (pos === -1) {
  1715 + return;
  1716 + }
  1717 +
  1718 + typeListeners.splice(pos, 1);
  1719 +}
  1720 +
  1721 +const parsers = {
  1722 + properties: PropertiesParser,
  1723 + l20n: L20nParser,
  1724 +};
  1725 +
  1726 +class Env {
  1727 + constructor(defaultLang, fetchResource) {
  1728 + this.defaultLang = defaultLang;
  1729 + this.fetchResource = fetchResource;
  1730 +
  1731 + this._resLists = new Map();
  1732 + this._resCache = new Map();
  1733 +
  1734 + const listeners = {};
  1735 + this.emit = emit.bind(this, listeners);
  1736 + this.addEventListener = addEventListener.bind(this, listeners);
  1737 + this.removeEventListener = removeEventListener.bind(this, listeners);
  1738 + }
  1739 +
  1740 + createContext(resIds) {
  1741 + const ctx = new Context(this);
  1742 + this._resLists.set(ctx, new Set(resIds));
  1743 + return ctx;
  1744 + }
  1745 +
  1746 + destroyContext(ctx) {
  1747 + const lists = this._resLists;
  1748 + const resList = lists.get(ctx);
  1749 +
  1750 + lists.delete(ctx);
  1751 + resList.forEach(
  1752 + resId => deleteIfOrphan(this._resCache, lists, resId));
  1753 + }
  1754 +
  1755 + _parse(syntax, lang, data) {
  1756 + const parser = parsers[syntax];
  1757 + if (!parser) {
  1758 + return data;
  1759 + }
  1760 +
  1761 + const emit = (type, err) => this.emit(type, amendError$1(lang, err));
  1762 + return parser.parse.call(parser, emit, data);
  1763 + }
  1764 +
  1765 + _create(lang, entries) {
  1766 + if (lang.src !== 'pseudo') {
  1767 + return entries;
  1768 + }
  1769 +
  1770 + const pseudoentries = Object.create(null);
  1771 + for (let key in entries) {
  1772 + pseudoentries[key] = walkEntry(
  1773 + entries[key], pseudo$1[lang.code].process);
  1774 + }
  1775 + return pseudoentries;
  1776 + }
  1777 +
  1778 + _getResource(lang, res) {
  1779 + const cache = this._resCache;
  1780 + const id = res + lang.code + lang.src;
  1781 +
  1782 + if (cache.has(id)) {
  1783 + return cache.get(id);
  1784 + }
  1785 +
  1786 + const syntax = res.substr(res.lastIndexOf('.') + 1);
  1787 +
  1788 + const saveEntries = data => {
  1789 + const entries = this._parse(syntax, lang, data);
  1790 + cache.set(id, this._create(lang, entries));
  1791 + };
  1792 +
  1793 + const recover = err => {
  1794 + err.lang = lang;
  1795 + this.emit('fetcherror', err);
  1796 + cache.set(id, err);
  1797 + };
  1798 +
  1799 + const langToFetch = lang.src === 'pseudo' ?
  1800 + { code: this.defaultLang, src: 'app' } :
  1801 + lang;
  1802 +
  1803 + const resource = this.fetchResource(res, langToFetch).then(
  1804 + saveEntries, recover);
  1805 +
  1806 + cache.set(id, resource);
  1807 +
  1808 + return resource;
  1809 + }
  1810 +}
  1811 +
  1812 +function deleteIfOrphan(cache, lists, resId) {
  1813 + const isNeeded = Array.from(lists).some(
  1814 + ([ctx, resIds]) => resIds.has(resId));
  1815 +
  1816 + if (!isNeeded) {
  1817 + cache.forEach((val, key) =>
  1818 + key.startsWith(resId) ? cache.delete(key) : null);
  1819 + }
  1820 +}
  1821 +
  1822 +function amendError$1(lang, err) {
  1823 + err.lang = lang;
  1824 + return err;
  1825 +}
  1826 +
  1827 +const KNOWN_MACROS = ['plural'];
  1828 +const MAX_PLACEABLE_LENGTH = 2500;
  1829 +
  1830 +// Matches characters outside of the Latin-1 character set
  1831 +const nonLatin1 = /[^\x01-\xFF]/;
  1832 +
  1833 +// Unicode bidi isolation characters
  1834 +const FSI = '\u2068';
  1835 +const PDI = '\u2069';
  1836 +
  1837 +const resolutionChain = new WeakSet();
  1838 +
  1839 +function createEntry(node) {
  1840 + const keys = Object.keys(node);
  1841 +
  1842 + // the most common scenario: a simple string with no arguments
  1843 + if (typeof node.$v === 'string' && keys.length === 2) {
  1844 + return node.$v;
  1845 + }
  1846 +
  1847 + let attrs;
  1848 +
  1849 + for (let i = 0, key; (key = keys[i]); i++) {
  1850 + // skip $i (id), $v (value), $x (index)
  1851 + if (key[0] === '$') {
  1852 + continue;
  1853 + }
  1854 +
  1855 + if (!attrs) {
  1856 + attrs = Object.create(null);
  1857 + }
  1858 + attrs[key] = createAttribute(node[key]);
  1859 + }
  1860 +
  1861 + return {
  1862 + value: node.$v !== undefined ? node.$v : null,
  1863 + index: node.$x || null,
  1864 + attrs: attrs || null,
  1865 + };
  1866 +}
  1867 +
  1868 +function createAttribute(node) {
  1869 + if (typeof node === 'string') {
  1870 + return node;
  1871 + }
  1872 +
  1873 + return {
  1874 + value: node.$v || (node !== undefined ? node : null),
  1875 + index: node.$x || null,
  1876 + };
  1877 +}
  1878 +
  1879 +
  1880 +function format(ctx, lang, args, entity) {
  1881 + if (typeof entity === 'string') {
  1882 + return [{}, entity];
  1883 + }
  1884 +
  1885 + if (resolutionChain.has(entity)) {
  1886 + throw new L10nError('Cyclic reference detected');
  1887 + }
  1888 +
  1889 + resolutionChain.add(entity);
  1890 +
  1891 + let rv;
  1892 + // if format fails, we want the exception to bubble up and stop the whole
  1893 + // resolving process; however, we still need to remove the entity from the
  1894 + // resolution chain
  1895 + try {
  1896 + rv = resolveValue(
  1897 + {}, ctx, lang, args, entity.value, entity.index);
  1898 + } finally {
  1899 + resolutionChain.delete(entity);
  1900 + }
  1901 + return rv;
  1902 +}
  1903 +
  1904 +function resolveIdentifier(ctx, lang, args, id) {
  1905 + if (KNOWN_MACROS.indexOf(id) > -1) {
  1906 + return [{}, ctx._getMacro(lang, id)];
  1907 + }
  1908 +
  1909 + if (args && args.hasOwnProperty(id)) {
  1910 + if (typeof args[id] === 'string' || (typeof args[id] === 'number' &&
  1911 + !isNaN(args[id]))) {
  1912 + return [{}, args[id]];
  1913 + } else {
  1914 + throw new L10nError('Arg must be a string or a number: ' + id);
  1915 + }
  1916 + }
  1917 +
  1918 + // XXX: special case for Node.js where still:
  1919 + // '__proto__' in Object.create(null) => true
  1920 + if (id === '__proto__') {
  1921 + throw new L10nError('Illegal id: ' + id);
  1922 + }
  1923 +
  1924 + const entity = ctx._getEntity(lang, id);
  1925 +
  1926 + if (entity) {
  1927 + return format(ctx, lang, args, entity);
  1928 + }
  1929 +
  1930 + throw new L10nError('Unknown reference: ' + id);
  1931 +}
  1932 +
  1933 +function subPlaceable(locals, ctx, lang, args, id) {
  1934 + let res;
  1935 +
  1936 + try {
  1937 + res = resolveIdentifier(ctx, lang, args, id);
  1938 + } catch (err) {
  1939 + return [{ error: err }, '{{ ' + id + ' }}'];
  1940 + }
  1941 +
  1942 + const value = res[1];
  1943 +
  1944 + if (typeof value === 'number') {
  1945 + return res;
  1946 + }
  1947 +
  1948 + if (typeof value === 'string') {
  1949 + // prevent Billion Laughs attacks
  1950 + if (value.length >= MAX_PLACEABLE_LENGTH) {
  1951 + throw new L10nError('Too many characters in placeable (' +
  1952 + value.length + ', max allowed is ' +
  1953 + MAX_PLACEABLE_LENGTH + ')');
  1954 + }
  1955 +
  1956 + if (locals.contextIsNonLatin1 || value.match(nonLatin1)) {
  1957 + // When dealing with non-Latin-1 text
  1958 + // we wrap substitutions in bidi isolate characters
  1959 + // to avoid bidi issues.
  1960 + res[1] = FSI + value + PDI;
  1961 + }
  1962 +
  1963 + return res;
  1964 + }
  1965 +
  1966 + return [{}, '{{ ' + id + ' }}'];
  1967 +}
  1968 +
  1969 +function interpolate(locals, ctx, lang, args, arr) {
  1970 + return arr.reduce(function([localsSeq, valueSeq], cur) {
  1971 + if (typeof cur === 'string') {
  1972 + return [localsSeq, valueSeq + cur];
  1973 + } else if (cur.t === 'idOrVar'){
  1974 + const [, value] = subPlaceable(locals, ctx, lang, args, cur.v);
  1975 + return [localsSeq, valueSeq + value];
  1976 + }
  1977 + }, [locals, '']);
  1978 +}
  1979 +
  1980 +function resolveSelector(ctx, lang, args, expr, index) {
  1981 + const selectorName = index[0].v;
  1982 + const selector = resolveIdentifier(ctx, lang, args, selectorName)[1];
  1983 +
  1984 + if (typeof selector !== 'function') {
  1985 + // selector is a simple reference to an entity or args
  1986 + return selector;
  1987 + }
  1988 +
  1989 + const argValue = index[1] ?
  1990 + resolveIdentifier(ctx, lang, args, index[1])[1] : undefined;
  1991 +
  1992 + if (selectorName === 'plural') {
  1993 + // special cases for zero, one, two if they are defined on the hash
  1994 + if (argValue === 0 && 'zero' in expr) {
  1995 + return 'zero';
  1996 + }
  1997 + if (argValue === 1 && 'one' in expr) {
  1998 + return 'one';
  1999 + }
  2000 + if (argValue === 2 && 'two' in expr) {
  2001 + return 'two';
  2002 + }
  2003 + }
  2004 +
  2005 + return selector(argValue);
  2006 +}
  2007 +
  2008 +function resolveValue(locals, ctx, lang, args, expr, index) {
  2009 + if (!expr) {
  2010 + return [locals, expr];
  2011 + }
  2012 +
  2013 + if (typeof expr === 'string' ||
  2014 + typeof expr === 'boolean' ||
  2015 + typeof expr === 'number') {
  2016 + return [locals, expr];
  2017 + }
  2018 +
  2019 + if (Array.isArray(expr)) {
  2020 + locals.contextIsNonLatin1 = expr.some(function($_) {
  2021 + return typeof($_) === 'string' && $_.match(nonLatin1);
  2022 + });
  2023 + return interpolate(locals, ctx, lang, args, expr);
  2024 + }
  2025 +
  2026 + // otherwise, it's a dict
  2027 + if (index) {
  2028 + // try to use the index in order to select the right dict member
  2029 + const selector = resolveSelector(ctx, lang, args, expr, index);
  2030 + if (expr.hasOwnProperty(selector)) {
  2031 + return resolveValue(locals, ctx, lang, args, expr[selector]);
  2032 + }
  2033 + }
  2034 +
  2035 + // if there was no index or no selector was found, try 'other'
  2036 + if ('other' in expr) {
  2037 + return resolveValue(locals, ctx, lang, args, expr.other);
  2038 + }
  2039 +
  2040 + throw new L10nError('Unresolvable value');
  2041 +}
  2042 +
  2043 +function LegacyContext(env) {
  2044 + Context.call(this, env);
  2045 +}
  2046 +
  2047 +LegacyContext.prototype = Object.create(Context.prototype);
  2048 +
  2049 +LegacyContext.prototype._formatTuple = function(lang, args, entity, id, key) {
  2050 + try {
  2051 + return format(this, lang, args, entity);
  2052 + } catch (err) {
  2053 + err.id = key ? id + '::' + key : id;
  2054 + err.lang = lang;
  2055 + this._env.emit('resolveerror', err, this);
  2056 + return [{ error: err }, err.id];
  2057 + }
  2058 +};
  2059 +
  2060 +var MAX_PLACEABLES$2 = 100;
  2061 +
  2062 +var PropertiesParser$1 = {
  2063 + patterns: null,
  2064 + entryIds: null,
  2065 +
  2066 + init: function() {
  2067 + this.patterns = {
  2068 + comment: /^\s*#|^\s*$/,
  2069 + entity: /^([^=\s]+)\s*=\s*(.*)$/,
  2070 + multiline: /[^\\]\\$/,
  2071 + index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,
  2072 + unicode: /\\u([0-9a-fA-F]{1,4})/g,
  2073 + entries: /[^\r\n]+/g,
  2074 + controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g,
  2075 + placeables: /\{\{\s*([^\s]*?)\s*\}\}/,
  2076 + };
  2077 + },
  2078 +
  2079 + parse: function(emit, source) {
  2080 + if (!this.patterns) {
  2081 + this.init();
  2082 + }
  2083 +
  2084 + var ast = [];
  2085 + this.entryIds = Object.create(null);
  2086 +
  2087 + var entries = source.match(this.patterns.entries);
  2088 + if (!entries) {
  2089 + return ast;
  2090 + }
  2091 + for (var i = 0; i < entries.length; i++) {
  2092 + var line = entries[i];
  2093 +
  2094 + if (this.patterns.comment.test(line)) {
  2095 + continue;
  2096 + }
  2097 +
  2098 + while (this.patterns.multiline.test(line) && i < entries.length) {
  2099 + line = line.slice(0, -1) + entries[++i].trim();
  2100 + }
  2101 +
  2102 + var entityMatch = line.match(this.patterns.entity);
  2103 + if (entityMatch) {
  2104 + try {
  2105 + this.parseEntity(entityMatch[1], entityMatch[2], ast);
  2106 + } catch (e) {
  2107 + if (emit) {
  2108 + emit('parseerror', e);
  2109 + } else {
  2110 + throw e;
  2111 + }
  2112 + }
  2113 + }
  2114 + }
  2115 + return ast;
  2116 + },
  2117 +
  2118 + parseEntity: function(id, value, ast) {
  2119 + var name, key;
  2120 +
  2121 + var pos = id.indexOf('[');
  2122 + if (pos !== -1) {
  2123 + name = id.substr(0, pos);
  2124 + key = id.substring(pos + 1, id.length - 1);
  2125 + } else {
  2126 + name = id;
  2127 + key = null;
  2128 + }
  2129 +
  2130 + var nameElements = name.split('.');
  2131 +
  2132 + if (nameElements.length > 2) {
  2133 + throw new L10nError('Error in ID: "' + name + '".' +
  2134 + ' Nested attributes are not supported.');
  2135 + }
  2136 +
  2137 + var attr;
  2138 + if (nameElements.length > 1) {
  2139 + name = nameElements[0];
  2140 + attr = nameElements[1];
  2141 +
  2142 + if (attr[0] === '$') {
  2143 + throw new L10nError('Attribute can\'t start with "$"', id);
  2144 + }
  2145 + } else {
  2146 + attr = null;
  2147 + }
  2148 +
  2149 + this.setEntityValue(name, attr, key, this.unescapeString(value), ast);
  2150 + },
  2151 +
  2152 + setEntityValue: function(id, attr, key, rawValue, ast) {
  2153 + var pos, v;
  2154 +
  2155 + var value = rawValue.indexOf('{{') > -1 ?
  2156 + this.parseString(rawValue) : rawValue;
  2157 +
  2158 + if (attr) {
  2159 + pos = this.entryIds[id];
  2160 + if (pos === undefined) {
  2161 + v = {$i: id};
  2162 + if (key) {
  2163 + v[attr] = {$v: {}};
  2164 + v[attr].$v[key] = value;
  2165 + } else {
  2166 + v[attr] = value;
  2167 + }
  2168 + ast.push(v);
  2169 + this.entryIds[id] = ast.length - 1;
  2170 + return;
  2171 + }
  2172 + if (key) {
  2173 + if (typeof(ast[pos][attr]) === 'string') {
  2174 + ast[pos][attr] = {
  2175 + $x: this.parseIndex(ast[pos][attr]),
  2176 + $v: {}
  2177 + };
  2178 + }
  2179 + ast[pos][attr].$v[key] = value;
  2180 + return;
  2181 + }
  2182 + ast[pos][attr] = value;
  2183 + return;
  2184 + }
  2185 +
  2186 + // Hash value
  2187 + if (key) {
  2188 + pos = this.entryIds[id];
  2189 + if (pos === undefined) {
  2190 + v = {};
  2191 + v[key] = value;
  2192 + ast.push({$i: id, $v: v});
  2193 + this.entryIds[id] = ast.length - 1;
  2194 + return;
  2195 + }
  2196 + if (typeof(ast[pos].$v) === 'string') {
  2197 + ast[pos].$x = this.parseIndex(ast[pos].$v);
  2198 + ast[pos].$v = {};
  2199 + }
  2200 + ast[pos].$v[key] = value;
  2201 + return;
  2202 + }
  2203 +
  2204 + // simple value
  2205 + ast.push({$i: id, $v: value});
  2206 + this.entryIds[id] = ast.length - 1;
  2207 + },
  2208 +
  2209 + parseString: function(str) {
  2210 + var chunks = str.split(this.patterns.placeables);
  2211 + var complexStr = [];
  2212 +
  2213 + var len = chunks.length;
  2214 + var placeablesCount = (len - 1) / 2;
  2215 +
  2216 + if (placeablesCount >= MAX_PLACEABLES$2) {
  2217 + throw new L10nError('Too many placeables (' + placeablesCount +
  2218 + ', max allowed is ' + MAX_PLACEABLES$2 + ')');
  2219 + }
  2220 +
  2221 + for (var i = 0; i < chunks.length; i++) {
  2222 + if (chunks[i].length === 0) {
  2223 + continue;
  2224 + }
  2225 + if (i % 2 === 1) {
  2226 + complexStr.push({t: 'idOrVar', v: chunks[i]});
  2227 + } else {
  2228 + complexStr.push(chunks[i]);
  2229 + }
  2230 + }
  2231 + return complexStr;
  2232 + },
  2233 +
  2234 + unescapeString: function(str) {
  2235 + if (str.lastIndexOf('\\') !== -1) {
  2236 + str = str.replace(this.patterns.controlChars, '$1');
  2237 + }
  2238 + return str.replace(this.patterns.unicode, function(match, token) {
  2239 + return String.fromCodePoint(parseInt(token, 16));
  2240 + });
  2241 + },
  2242 +
  2243 + parseIndex: function(str) {
  2244 + var match = str.match(this.patterns.index);
  2245 + if (!match) {
  2246 + throw new L10nError('Malformed index');
  2247 + }
  2248 + if (match[2]) {
  2249 + return [{t: 'idOrVar', v: match[1]}, match[2]];
  2250 + } else {
  2251 + return [{t: 'idOrVar', v: match[1]}];
  2252 + }
  2253 + }
  2254 +};
  2255 +
  2256 +// Recursively walk an AST node searching for content leaves
  2257 +function walkContent(node, fn) {
  2258 + if (typeof node === 'string') {
  2259 + return fn(node);
  2260 + }
  2261 +
  2262 + if (node.t === 'idOrVar') {
  2263 + return node;
  2264 + }
  2265 +
  2266 + const rv = Array.isArray(node) ? [] : {};
  2267 + const keys = Object.keys(node);
  2268 +
  2269 + for (let i = 0, key; (key = keys[i]); i++) {
  2270 + // don't change identifier ($i) nor indices ($x)
  2271 + if (key === '$i' || key === '$x') {
  2272 + rv[key] = node[key];
  2273 + } else {
  2274 + rv[key] = walkContent(node[key], fn);
  2275 + }
  2276 + }
  2277 + return rv;
  2278 +}
  2279 +
  2280 +// XXX babel's inheritance code triggers JavaScript warnings about modifying
  2281 +// the prototype object so we use regular prototypal inheritance here
  2282 +function LegacyEnv(defaultLang, fetchResource) {
  2283 + Env.call(this, defaultLang, fetchResource);
  2284 +}
  2285 +
  2286 +LegacyEnv.prototype = Object.create(Env.prototype);
  2287 +
  2288 +LegacyEnv.prototype.createContext = function(resIds) {
  2289 + const ctx = new LegacyContext(this);
  2290 + this._resLists.set(ctx, new Set(resIds));
  2291 + return ctx;
  2292 +};
  2293 +
  2294 +LegacyEnv.prototype._parse = function(syntax, lang, data) {
  2295 + const emit = (type, err) => this.emit(type, amendError$1(lang, err));
  2296 + return PropertiesParser$1.parse.call(PropertiesParser$1, emit, data);
  2297 +};
  2298 +
  2299 +LegacyEnv.prototype._create = function(lang, ast) {
  2300 + const entries = Object.create(null);
  2301 + const create = lang.src === 'pseudo' ?
  2302 + createPseudoEntry : createEntry;
  2303 +
  2304 + for (let i = 0, node; node = ast[i]; i++) {
  2305 + const id = node.$i;
  2306 + if (id in entries) {
  2307 + this.emit('duplicateerror', new L10nError(
  2308 + 'Duplicate string "' + id + '" found in ' + lang.code, id, lang));
  2309 + }
  2310 + entries[id] = create(node, lang);
  2311 + }
  2312 +
  2313 + return entries;
  2314 +};
  2315 +
  2316 +function createPseudoEntry(node, lang) {
  2317 + return createEntry(walkContent(node, pseudo$1[lang.code].process));
  2318 +}
  2319 +
  2320 +// match the opening angle bracket (<) in HTML tags, and HTML entities like
  2321 +// &amp;, &#0038;, &#x0026;.
  2322 +const reOverlay = /<|&#?\w+;/;
  2323 +
  2324 +const allowed = {
  2325 + elements: [
  2326 + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data',
  2327 + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u',
  2328 + 'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr'
  2329 + ],
  2330 + attributes: {
  2331 + global: [ 'title', 'aria-label', 'aria-valuetext', 'aria-moz-hint' ],
  2332 + a: [ 'download' ],
  2333 + area: [ 'download', 'alt' ],
  2334 + // value is special-cased in isAttrAllowed
  2335 + input: [ 'alt', 'placeholder' ],
  2336 + menuitem: [ 'label' ],
  2337 + menu: [ 'label' ],
  2338 + optgroup: [ 'label' ],
  2339 + option: [ 'label' ],
  2340 + track: [ 'label' ],
  2341 + img: [ 'alt' ],
  2342 + textarea: [ 'placeholder' ],
  2343 + th: [ 'abbr']
  2344 + }
  2345 +};
  2346 +
  2347 +function overlayElement(element, translation) {
  2348 + const value = translation.value;
  2349 +
  2350 + if (typeof value === 'string') {
  2351 + if (!reOverlay.test(value)) {
  2352 + element.textContent = value;
  2353 + } else {
  2354 + // start with an inert template element and move its children into
  2355 + // `element` but such that `element`'s own children are not replaced
  2356 + const tmpl = element.ownerDocument.createElement('template');
  2357 + tmpl.innerHTML = value;
  2358 + // overlay the node with the DocumentFragment
  2359 + overlay(element, tmpl.content);
  2360 + }
  2361 + }
  2362 +
  2363 + for (let key in translation.attrs) {
  2364 + const attrName = camelCaseToDashed(key);
  2365 + if (isAttrAllowed({ name: attrName }, element)) {
  2366 + element.setAttribute(attrName, translation.attrs[key]);
  2367 + }
  2368 + }
  2369 +}
  2370 +
  2371 +// The goal of overlay is to move the children of `translationElement`
  2372 +// into `sourceElement` such that `sourceElement`'s own children are not
  2373 +// replaced, but onle have their text nodes and their attributes modified.
  2374 +//
  2375 +// We want to make it possible for localizers to apply text-level semantics to
  2376 +// the translations and make use of HTML entities. At the same time, we
  2377 +// don't trust translations so we need to filter unsafe elements and
  2378 +// attribtues out and we don't want to break the Web by replacing elements to
  2379 +// which third-party code might have created references (e.g. two-way
  2380 +// bindings in MVC frameworks).
  2381 +function overlay(sourceElement, translationElement) {
  2382 + const result = translationElement.ownerDocument.createDocumentFragment();
  2383 + let k, attr;
  2384 +
  2385 + // take one node from translationElement at a time and check it against
  2386 + // the allowed list or try to match it with a corresponding element
  2387 + // in the source
  2388 + let childElement;
  2389 + while ((childElement = translationElement.childNodes[0])) {
  2390 + translationElement.removeChild(childElement);
  2391 +
  2392 + if (childElement.nodeType === childElement.TEXT_NODE) {
  2393 + result.appendChild(childElement);
  2394 + continue;
  2395 + }
  2396 +
  2397 + const index = getIndexOfType(childElement);
  2398 + const sourceChild = getNthElementOfType(sourceElement, childElement, index);
  2399 + if (sourceChild) {
  2400 + // there is a corresponding element in the source, let's use it
  2401 + overlay(sourceChild, childElement);
  2402 + result.appendChild(sourceChild);
  2403 + continue;
  2404 + }
  2405 +
  2406 + if (isElementAllowed(childElement)) {
  2407 + const sanitizedChild = childElement.ownerDocument.createElement(
  2408 + childElement.nodeName);
  2409 + overlay(sanitizedChild, childElement);
  2410 + result.appendChild(sanitizedChild);
  2411 + continue;
  2412 + }
  2413 +
  2414 + // otherwise just take this child's textContent
  2415 + result.appendChild(
  2416 + translationElement.ownerDocument.createTextNode(
  2417 + childElement.textContent));
  2418 + }
  2419 +
  2420 + // clear `sourceElement` and append `result` which by this time contains
  2421 + // `sourceElement`'s original children, overlayed with translation
  2422 + sourceElement.textContent = '';
  2423 + sourceElement.appendChild(result);
  2424 +
  2425 + // if we're overlaying a nested element, translate the allowed
  2426 + // attributes; top-level attributes are handled in `translateElement`
  2427 + // XXX attributes previously set here for another language should be
  2428 + // cleared if a new language doesn't use them; https://bugzil.la/922577
  2429 + if (translationElement.attributes) {
  2430 + for (k = 0, attr; (attr = translationElement.attributes[k]); k++) {
  2431 + if (isAttrAllowed(attr, sourceElement)) {
  2432 + sourceElement.setAttribute(attr.name, attr.value);
  2433 + }
  2434 + }
  2435 + }
  2436 +}
  2437 +
  2438 +// XXX the allowed list should be amendable; https://bugzil.la/922573
  2439 +function isElementAllowed(element) {
  2440 + return allowed.elements.indexOf(element.tagName.toLowerCase()) !== -1;
  2441 +}
  2442 +
  2443 +function isAttrAllowed(attr, element) {
  2444 + const attrName = attr.name.toLowerCase();
  2445 + const tagName = element.tagName.toLowerCase();
  2446 + // is it a globally safe attribute?
  2447 + if (allowed.attributes.global.indexOf(attrName) !== -1) {
  2448 + return true;
  2449 + }
  2450 + // are there no allowed attributes for this element?
  2451 + if (!allowed.attributes[tagName]) {
  2452 + return false;
  2453 + }
  2454 + // is it allowed on this element?
  2455 + // XXX the allowed list should be amendable; https://bugzil.la/922573
  2456 + if (allowed.attributes[tagName].indexOf(attrName) !== -1) {
  2457 + return true;
  2458 + }
  2459 + // special case for value on inputs with type button, reset, submit
  2460 + if (tagName === 'input' && attrName === 'value') {
  2461 + const type = element.type.toLowerCase();
  2462 + if (type === 'submit' || type === 'button' || type === 'reset') {
  2463 + return true;
  2464 + }
  2465 + }
  2466 + return false;
  2467 +}
  2468 +
  2469 +// Get n-th immediate child of context that is of the same type as element.
  2470 +// XXX Use querySelector(':scope > ELEMENT:nth-of-type(index)'), when:
  2471 +// 1) :scope is widely supported in more browsers and 2) it works with
  2472 +// DocumentFragments.
  2473 +function getNthElementOfType(context, element, index) {
  2474 + /* jshint boss:true */
  2475 + let nthOfType = 0;
  2476 + for (let i = 0, child; child = context.children[i]; i++) {
  2477 + if (child.nodeType === child.ELEMENT_NODE &&
  2478 + child.tagName === element.tagName) {
  2479 + if (nthOfType === index) {
  2480 + return child;
  2481 + }
  2482 + nthOfType++;
  2483 + }
  2484 + }
  2485 + return null;
  2486 +}
  2487 +
  2488 +// Get the index of the element among siblings of the same type.
  2489 +function getIndexOfType(element) {
  2490 + let index = 0;
  2491 + let child;
  2492 + while ((child = element.previousElementSibling)) {
  2493 + if (child.tagName === element.tagName) {
  2494 + index++;
  2495 + }
  2496 + }
  2497 + return index;
  2498 +}
  2499 +
  2500 +function camelCaseToDashed(string) {
  2501 + // XXX workaround for https://bugzil.la/1141934
  2502 + if (string === 'ariaValueText') {
  2503 + return 'aria-valuetext';
  2504 + }
  2505 +
  2506 + return string
  2507 + .replace(/[A-Z]/g, function (match) {
  2508 + return '-' + match.toLowerCase();
  2509 + })
  2510 + .replace(/^-/, '');
  2511 +}
  2512 +
  2513 +const reHtml = /[&<>]/g;
  2514 +const htmlEntities = {
  2515 + '&': '&amp;',
  2516 + '<': '&lt;',
  2517 + '>': '&gt;',
  2518 +};
  2519 +
  2520 +function getResourceLinks(head) {
  2521 + return Array.prototype.map.call(
  2522 + head.querySelectorAll('link[rel="localization"]'),
  2523 + el => el.getAttribute('href'));
  2524 +}
  2525 +
  2526 +function getTranslatables(element) {
  2527 + const nodes = Array.from(element.querySelectorAll('[data-l10n-id]'));
  2528 +
  2529 + if (typeof element.hasAttribute === 'function' &&
  2530 + element.hasAttribute('data-l10n-id')) {
  2531 + nodes.push(element);
  2532 + }
  2533 +
  2534 + return nodes;
  2535 +}
  2536 +
  2537 +function translateFragment(view, langs, frag) {
  2538 + return translateElements(view, langs, getTranslatables(frag));
  2539 +}
  2540 +
  2541 +function getElementsTranslation(view, langs, elems) {
  2542 + const keys = elems.map(elem => {
  2543 + const id = elem.getAttribute('data-l10n-id');
  2544 + const args = elem.getAttribute('data-l10n-args');
  2545 + return args ? [
  2546 + id,
  2547 + JSON.parse(args.replace(reHtml, match => htmlEntities[match]))
  2548 + ] : id;
  2549 + });
  2550 +
  2551 + return view._resolveEntities(langs, keys);
  2552 +}
  2553 +
  2554 +function translateElements(view, langs, elements) {
  2555 + return getElementsTranslation(view, langs, elements).then(
  2556 + translations => applyTranslations(view, elements, translations));
  2557 +}
  2558 +
  2559 +function applyTranslations(view, elems, translations) {
  2560 + view._disconnect();
  2561 + for (let i = 0; i < elems.length; i++) {
  2562 + overlayElement(elems[i], translations[i]);
  2563 + }
  2564 + view._observe();
  2565 +}
  2566 +
  2567 +// Polyfill NodeList.prototype[Symbol.iterator] for Chrome.
  2568 +// See https://code.google.com/p/chromium/issues/detail?id=401699
  2569 +if (typeof NodeList === 'function' && !NodeList.prototype[Symbol.iterator]) {
  2570 + NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
  2571 +}
  2572 +
  2573 +// Intl.Locale
  2574 +function getDirection(code) {
  2575 + const tag = code.split('-')[0];
  2576 + return ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(tag) >= 0 ?
  2577 + 'rtl' : 'ltr';
  2578 +}
  2579 +
  2580 +function serializeContext(ctx, lang) {
  2581 + const cache = ctx._env._resCache;
  2582 + const resIds = Array.from(ctx._env._resLists.get(ctx));
  2583 + return resIds.reduceRight(([errorsSeq, entriesSeq], cur) => {
  2584 + const sourceRes = cache.get(cur + 'en-USapp');
  2585 + const langRes = cache.get(cur + lang.code + lang.src);
  2586 + const [errors, entries] = serializeEntries(
  2587 + lang,
  2588 + langRes instanceof L10nError ? {} : langRes,
  2589 + sourceRes instanceof L10nError ? {} : sourceRes);
  2590 + return [errorsSeq.concat(errors), Object.assign(entriesSeq, entries)];
  2591 + }, [[], Object.create(null)]);
  2592 +}
  2593 +
  2594 +function serializeEntries(lang, langEntries, sourceEntries) {
  2595 + const errors = [];
  2596 + const entries = Object.create(null);
  2597 +
  2598 + for (let id in sourceEntries) {
  2599 + const sourceEntry = sourceEntries[id];
  2600 + const langEntry = langEntries[id];
  2601 +
  2602 + if (!langEntry) {
  2603 + errors.push(new L10nError(
  2604 + '"' + id + '"' + ' not found in ' + lang.code, id, lang));
  2605 + entries[id] = sourceEntry;
  2606 + continue;
  2607 + }
  2608 +
  2609 + if (!areEntityStructsEqual(sourceEntry, langEntry)) {
  2610 + errors.push(new L10nError(
  2611 + '"' + id + '"' + ' is malformed in ' + lang.code, id, lang));
  2612 + entries[id] = sourceEntry;
  2613 + continue;
  2614 + }
  2615 +
  2616 + entries[id] = langEntry;
  2617 + }
  2618 +
  2619 + return [errors, entries];
  2620 +}
  2621 +
  2622 +function resolvesToString(entity) {
  2623 + return typeof entity === 'string' || // a simple string
  2624 + typeof entity.value === 'string' || // a simple string, entity with attrs
  2625 + Array.isArray(entity.value) || // a complex string
  2626 + typeof entity.value === 'object' && // a dict with an index
  2627 + entity.index !== null;
  2628 +}
  2629 +
  2630 +function areAttrsEqual(attrs1, attrs2) {
  2631 + const keys1 = Object.keys(attrs1 || Object.create(null));
  2632 + const keys2 = Object.keys(attrs2 || Object.create(null));
  2633 +
  2634 + if (keys1.length !== keys2.length) {
  2635 + return false;
  2636 + }
  2637 +
  2638 + for (let i = 0; i < keys1.length; i++) {
  2639 + if (keys2.indexOf(keys1[i]) === -1) {
  2640 + return false;
  2641 + }
  2642 + }
  2643 +
  2644 + return true;
  2645 +}
  2646 +
  2647 +function areEntityStructsEqual(source, translation) {
  2648 + if (resolvesToString(source) && !resolvesToString(translation)) {
  2649 + return false;
  2650 + }
  2651 +
  2652 + if (source.attrs || translation.attrs) {
  2653 + return areAttrsEqual(source.attrs, translation.attrs);
  2654 + }
  2655 +
  2656 + return true;
  2657 +}
  2658 +
  2659 +function serializeLegacyContext(ctx, lang) {
  2660 + const cache = ctx._env._resCache;
  2661 + const resIds = Array.from(ctx._env._resLists.get(ctx));
  2662 + return resIds.reduce(([errorsSeq, entriesSeq], cur) => {
  2663 + const sourceRes = cache.get(cur + 'en-USapp');
  2664 + const langRes = cache.get(cur + lang.code + lang.src);
  2665 + const [errors, entries] = serializeEntries$1(
  2666 + lang,
  2667 + langRes instanceof L10nError ? {} : langRes,
  2668 + sourceRes instanceof L10nError ? {} : sourceRes);
  2669 + return [errorsSeq.concat(errors), entriesSeq.concat(entries)];
  2670 + }, [[], []]);
  2671 +}
  2672 +
  2673 +function serializeEntries$1(lang, langEntries, sourceEntries) {
  2674 + const errors = [];
  2675 + const entries = Object.keys(sourceEntries).map(id => {
  2676 + const sourceEntry = sourceEntries[id];
  2677 + const langEntry = langEntries[id];
  2678 +
  2679 + if (!langEntry) {
  2680 + errors.push(new L10nError(
  2681 + '"' + id + '"' + ' not found in ' + lang.code, id, lang));
  2682 + return serializeEntry(sourceEntry, id);
  2683 + }
  2684 +
  2685 + if (!areEntityStructsEqual$1(sourceEntry, langEntry)) {
  2686 + errors.push(new L10nError(
  2687 + '"' + id + '"' + ' is malformed in ' + lang.code, id, lang));
  2688 + return serializeEntry(sourceEntry, id);
  2689 + }
  2690 +
  2691 + return serializeEntry(langEntry, id);
  2692 + });
  2693 +
  2694 + return [errors, entries];
  2695 +}
  2696 +
  2697 +function serializeEntry(entry, id) {
  2698 + if (typeof entry === 'string') {
  2699 + return { $i: id, $v: entry };
  2700 + }
  2701 +
  2702 + const node = {
  2703 + $i: id,
  2704 + };
  2705 +
  2706 + if (entry.value !== null) {
  2707 + node.$v = entry.value;
  2708 + }
  2709 +
  2710 + if (entry.index !== null) {
  2711 + node.$x = entry.index;
  2712 + }
  2713 +
  2714 + for (let key in entry.attrs) {
  2715 + node[key] = serializeAttribute(entry.attrs[key]);
  2716 + }
  2717 +
  2718 + return node;
  2719 +}
  2720 +
  2721 +function serializeAttribute(attr) {
  2722 + if (typeof attr === 'string') {
  2723 + return attr;
  2724 + }
  2725 +
  2726 + const node = {};
  2727 +
  2728 + if (attr.value !== null) {
  2729 + node.$v = attr.value;
  2730 + }
  2731 +
  2732 + if (attr.index !== null) {
  2733 + node.$x = attr.index;
  2734 + }
  2735 +
  2736 + return node;
  2737 +}
  2738 +
  2739 +function resolvesToString$1(entity) {
  2740 + return typeof entity === 'string' || // a simple string
  2741 + typeof entity.value === 'string' || // a simple string, entity with attrs
  2742 + Array.isArray(entity.value) || // a complex string
  2743 + typeof entity.value === 'object' && // a dict with an index
  2744 + entity.index !== null;
  2745 +}
  2746 +
  2747 +function areAttrsEqual$1(attrs1, attrs2) {
  2748 + const keys1 = Object.keys(attrs1 || Object.create(null));
  2749 + const keys2 = Object.keys(attrs2 || Object.create(null));
  2750 +
  2751 + if (keys1.length !== keys2.length) {
  2752 + return false;
  2753 + }
  2754 +
  2755 + for (let i = 0; i < keys1.length; i++) {
  2756 + if (keys2.indexOf(keys1[i]) === -1) {
  2757 + return false;
  2758 + }
  2759 + }
  2760 +
  2761 + return true;
  2762 +}
  2763 +
  2764 +function areEntityStructsEqual$1(source, translation) {
  2765 + if (resolvesToString$1(source) && !resolvesToString$1(translation)) {
  2766 + return false;
  2767 + }
  2768 +
  2769 + if (source.attrs || translation.attrs) {
  2770 + return areAttrsEqual$1(source.attrs, translation.attrs);
  2771 + }
  2772 +
  2773 + return true;
  2774 +}
  2775 +
  2776 +class View {
  2777 + constructor(htmloptimizer, fetchResource) {
  2778 + this.htmloptimizer = htmloptimizer;
  2779 + this.doc = htmloptimizer.document;
  2780 +
  2781 + this.isEnabled = this.doc.querySelector('link[rel="localization"]');
  2782 + // XXX we should check if the app uses l10n.js instead, but due to lazy
  2783 + // loading we can't rely on querySelector.
  2784 + this.isLegacy = !this.doc.querySelector('script[src*="l20n"]');
  2785 +
  2786 + const EnvClass = this.isLegacy ? LegacyEnv : Env;
  2787 + this.env = new EnvClass(
  2788 + htmloptimizer.config.GAIA_DEFAULT_LOCALE, fetchResource);
  2789 + this.ctx = this.env.createContext(getResourceLinks(this.doc.head));
  2790 +
  2791 + // add the url of the currently processed webapp to all errors
  2792 + this.env.addEventListener('*', amendError.bind(this));
  2793 +
  2794 + this.stopBuildError = null;
  2795 + const log = logError.bind(this);
  2796 + const stop = stopBuild.bind(this);
  2797 +
  2798 + // stop the build if these errors happen for en-US
  2799 + // XXX tv_apps break the build https://bugzil.la/1179833
  2800 + // this.env.addEventListener('fetcherror', stop);
  2801 + this.env.addEventListener('parseerror', stop);
  2802 + this.env.addEventListener('duplicateerror', stop);
  2803 + this.env.addEventListener('notfounderror', stop);
  2804 + // XXX sms breaks the build https://bugzil.la/1178187
  2805 + // this.env.addEventListener('resolveerror', stop);
  2806 +
  2807 + this.env.addEventListener('deprecatewarning', log);
  2808 +
  2809 + // if LOCALE_BASEDIR is set alert about missing strings
  2810 + if (htmloptimizer.config.LOCALE_BASEDIR !== '') {
  2811 + this.env.addEventListener('fetcherror', log);
  2812 + this.env.addEventListener('parseerror', log);
  2813 + this.env.addEventListener('duplicateerror', log);
  2814 + }
  2815 + }
  2816 +
  2817 + _observe() {}
  2818 + _disconnect() {}
  2819 +
  2820 + _resolveEntities(langs, keys) {
  2821 + return this.ctx.resolveEntities(langs, keys);
  2822 + }
  2823 +
  2824 + translateDocument(code) {
  2825 + const dir = getDirection(code);
  2826 + const langs = [{ code, src: 'app' }];
  2827 + const setDocLang = () => {
  2828 + this.doc.documentElement.lang = code;
  2829 + this.doc.documentElement.dir = dir;
  2830 + };
  2831 + return this.ctx.fetch(langs).then(
  2832 + langs => translateFragment(this, langs, this.doc.documentElement)).then(
  2833 + setDocLang);
  2834 + }
  2835 +
  2836 + serializeResources(code) {
  2837 + const lang = {
  2838 + code,
  2839 + src: code in pseudo$1 ? 'pseudo' : 'app'
  2840 + };
  2841 + return fetchContext(this.ctx, lang).then(() => {
  2842 + const [errors, entries] = this.isLegacy ?
  2843 + serializeLegacyContext(this.ctx, lang) :
  2844 + serializeContext(this.ctx, lang);
  2845 +
  2846 + if (errors.length) {
  2847 + const notFoundErrors = errors.filter(
  2848 + err => err.message.indexOf('not found') > -1).map(
  2849 + err => err.id);
  2850 + const malformedErrors = errors.filter(
  2851 + err => err.message.indexOf('malformed') > -1).map(
  2852 + err => err.id);
  2853 +
  2854 + if (notFoundErrors.length) {
  2855 + this.htmloptimizer.dump(
  2856 + '[l10n] [' + lang.code + ']: ' + notFoundErrors.length +
  2857 + ' missing compared to en-US: ' + notFoundErrors.join(', '));
  2858 + }
  2859 + if (malformedErrors.length) {
  2860 + this.htmloptimizer.dump(
  2861 + '[l10n] [' + lang.code + ']: ' + malformedErrors.length +
  2862 + ' malformed compared to en-US: ' + malformedErrors.join(', '));
  2863 + }
  2864 + }
  2865 +
  2866 + return entries;
  2867 + });
  2868 + }
  2869 +
  2870 + checkError() {
  2871 + return {
  2872 + wait: false,
  2873 + error: this.stopBuildError
  2874 + };
  2875 + }
  2876 +}
  2877 +
  2878 +function amendError(err) {
  2879 + err.message = err.message + ' (' + this.htmloptimizer.webapp.url + ')';
  2880 +}
  2881 +
  2882 +function logError(err) {
  2883 + this.htmloptimizer.dump('[l10n] ' + err);
  2884 +}
  2885 +
  2886 +function stopBuild(err) {
  2887 + if (err.lang && err.lang.code === 'en-US' && !this.stopBuildError) {
  2888 + this.stopBuildError = err;
  2889 + }
  2890 +}
  2891 +
  2892 +function fetchContext(ctx, lang) {
  2893 + const sourceLang = { code: 'en-US', src: 'app' };
  2894 + return Promise.all(
  2895 + [sourceLang, lang].map(lang => ctx.fetch([lang])));
  2896 +}
  2897 +
  2898 +function getView(htmloptimizer) {
  2899 + const htmlFetch = (...args) => fetchResource(htmloptimizer, ...args);
  2900 + return new View(htmloptimizer, htmlFetch);
  2901 +}
  2902 +
  2903 +exports.getView = getView;
  2904 +exports.pseudo = pseudo$1;
  2905 +exports.walkValue = walkValue$1;
0 2906 \ No newline at end of file
... ...
bower_components/l20n/dist/bundle/gaia/l20n.js 0 → 100755
  1 +(function () { 'use strict';
  2 +
  3 + function emit(listeners, ...args) {
  4 + const type = args.shift();
  5 +
  6 + if (listeners['*']) {
  7 + listeners['*'].slice().forEach(
  8 + listener => listener.apply(this, args));
  9 + }
  10 +
  11 + if (listeners[type]) {
  12 + listeners[type].slice().forEach(
  13 + listener => listener.apply(this, args));
  14 + }
  15 + }
  16 +
  17 + function addEventListener(listeners, type, listener) {
  18 + if (!(type in listeners)) {
  19 + listeners[type] = [];
  20 + }
  21 + listeners[type].push(listener);
  22 + }
  23 +
  24 + function removeEventListener(listeners, type, listener) {
  25 + const typeListeners = listeners[type];
  26 + const pos = typeListeners.indexOf(listener);
  27 + if (pos === -1) {
  28 + return;
  29 + }
  30 +
  31 + typeListeners.splice(pos, 1);
  32 + }
  33 +
  34 + class Client {
  35 + constructor(remote) {
  36 + this.id = this;
  37 + this.remote = remote;
  38 +
  39 + const listeners = {};
  40 + this.on = (...args) => addEventListener(listeners, ...args);
  41 + this.emit = (...args) => emit(listeners, ...args);
  42 + }
  43 +
  44 + method(name, ...args) {
  45 + return this.remote[name](...args);
  46 + }
  47 + }
  48 +
  49 + function broadcast(type, data) {
  50 + Array.from(this.ctxs.keys()).forEach(
  51 + client => client.emit(type, data));
  52 + }
  53 +
  54 + function L10nError(message, id, lang) {
  55 + this.name = 'L10nError';
  56 + this.message = message;
  57 + this.id = id;
  58 + this.lang = lang;
  59 + }
  60 + L10nError.prototype = Object.create(Error.prototype);
  61 + L10nError.prototype.constructor = L10nError;
  62 +
  63 + function load(type, url) {
  64 + return new Promise(function(resolve, reject) {
  65 + const xhr = new XMLHttpRequest();
  66 +
  67 + if (xhr.overrideMimeType) {
  68 + xhr.overrideMimeType(type);
  69 + }
  70 +
  71 + xhr.open('GET', url, true);
  72 +
  73 + if (type === 'application/json') {
  74 + xhr.responseType = 'json';
  75 + }
  76 +
  77 + xhr.addEventListener('load', function io_onload(e) {
  78 + if (e.target.status === 200 || e.target.status === 0) {
  79 + // Sinon.JS's FakeXHR doesn't have the response property
  80 + resolve(e.target.response || e.target.responseText);
  81 + } else {
  82 + reject(new L10nError('Not found: ' + url));
  83 + }
  84 + });
  85 + xhr.addEventListener('error', reject);
  86 + xhr.addEventListener('timeout', reject);
  87 +
  88 + // the app: protocol throws on 404, see https://bugzil.la/827243
  89 + try {
  90 + xhr.send(null);
  91 + } catch (e) {
  92 + if (e.name === 'NS_ERROR_FILE_NOT_FOUND') {
  93 + // the app: protocol throws on 404, see https://bugzil.la/827243
  94 + reject(new L10nError('Not found: ' + url));
  95 + } else {
  96 + throw e;
  97 + }
  98 + }
  99 + });
  100 + }
  101 +
  102 + const io = {
  103 + extra: function(code, ver, path, type) {
  104 + return navigator.mozApps.getLocalizationResource(
  105 + code, ver, path, type);
  106 + },
  107 + app: function(code, ver, path, type) {
  108 + switch (type) {
  109 + case 'text':
  110 + return load('text/plain', path);
  111 + case 'json':
  112 + return load('application/json', path);
  113 + default:
  114 + throw new L10nError('Unknown file type: ' + type);
  115 + }
  116 + },
  117 + };
  118 +
  119 + function fetchResource(ver, res, lang) {
  120 + const url = res.replace('{locale}', lang.code);
  121 + const type = res.endsWith('.json') ? 'json' : 'text';
  122 + return io[lang.src](lang.code, ver, url, type);
  123 + }
  124 +
  125 + const MAX_PLACEABLES$1 = 100;
  126 +
  127 + var L20nParser = {
  128 + parse: function(emit, string) {
  129 + this._source = string;
  130 + this._index = 0;
  131 + this._length = string.length;
  132 + this.entries = Object.create(null);
  133 + this.emit = emit;
  134 +
  135 + return this.getResource();
  136 + },
  137 +
  138 + getResource: function() {
  139 + this.getWS();
  140 + while (this._index < this._length) {
  141 + try {
  142 + this.getEntry();
  143 + } catch (e) {
  144 + if (e instanceof L10nError) {
  145 + // we want to recover, but we don't need it in entries
  146 + this.getJunkEntry();
  147 + if (!this.emit) {
  148 + throw e;
  149 + }
  150 + } else {
  151 + throw e;
  152 + }
  153 + }
  154 +
  155 + if (this._index < this._length) {
  156 + this.getWS();
  157 + }
  158 + }
  159 +
  160 + return this.entries;
  161 + },
  162 +
  163 + getEntry: function() {
  164 + if (this._source[this._index] === '<') {
  165 + ++this._index;
  166 + const id = this.getIdentifier();
  167 + if (this._source[this._index] === '[') {
  168 + ++this._index;
  169 + return this.getEntity(id, this.getItemList(this.getExpression, ']'));
  170 + }
  171 + return this.getEntity(id);
  172 + }
  173 +
  174 + if (this._source.startsWith('/*', this._index)) {
  175 + return this.getComment();
  176 + }
  177 +
  178 + throw this.error('Invalid entry');
  179 + },
  180 +
  181 + getEntity: function(id, index) {
  182 + if (!this.getRequiredWS()) {
  183 + throw this.error('Expected white space');
  184 + }
  185 +
  186 + const ch = this._source[this._index];
  187 + const value = this.getValue(ch, index === undefined);
  188 + let attrs;
  189 +
  190 + if (value === undefined) {
  191 + if (ch === '>') {
  192 + throw this.error('Expected ">"');
  193 + }
  194 + attrs = this.getAttributes();
  195 + } else {
  196 + const ws1 = this.getRequiredWS();
  197 + if (this._source[this._index] !== '>') {
  198 + if (!ws1) {
  199 + throw this.error('Expected ">"');
  200 + }
  201 + attrs = this.getAttributes();
  202 + }
  203 + }
  204 +
  205 + // skip '>'
  206 + ++this._index;
  207 +
  208 + if (id in this.entries) {
  209 + throw this.error('Duplicate entry ID "' + id, 'duplicateerror');
  210 + }
  211 + if (!attrs && !index && typeof value === 'string') {
  212 + this.entries[id] = value;
  213 + } else {
  214 + this.entries[id] = {
  215 + value,
  216 + attrs,
  217 + index
  218 + };
  219 + }
  220 + },
  221 +
  222 + getValue: function(ch = this._source[this._index], optional = false) {
  223 + switch (ch) {
  224 + case '\'':
  225 + case '"':
  226 + return this.getString(ch, 1);
  227 + case '{':
  228 + return this.getHash();
  229 + }
  230 +
  231 + if (!optional) {
  232 + throw this.error('Unknown value type');
  233 + }
  234 +
  235 + return;
  236 + },
  237 +
  238 + getWS: function() {
  239 + let cc = this._source.charCodeAt(this._index);
  240 + // space, \n, \t, \r
  241 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  242 + cc = this._source.charCodeAt(++this._index);
  243 + }
  244 + },
  245 +
  246 + getRequiredWS: function() {
  247 + const pos = this._index;
  248 + let cc = this._source.charCodeAt(pos);
  249 + // space, \n, \t, \r
  250 + while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
  251 + cc = this._source.charCodeAt(++this._index);
  252 + }
  253 + return this._index !== pos;
  254 + },
  255 +
  256 + getIdentifier: function() {
  257 + const start = this._index;
  258 + let cc = this._source.charCodeAt(this._index);
  259 +
  260 + if ((cc >= 97 && cc <= 122) || // a-z
  261 + (cc >= 65 && cc <= 90) || // A-Z
  262 + cc === 95) { // _
  263 + cc = this._source.charCodeAt(++this._index);
  264 + } else {
  265 + throw this.error('Identifier has to start with [a-zA-Z_]');
  266 + }
  267 +
  268 + while ((cc >= 97 && cc <= 122) || // a-z
  269 + (cc >= 65 && cc <= 90) || // A-Z
  270 + (cc >= 48 && cc <= 57) || // 0-9
  271 + cc === 95) { // _
  272 + cc = this._source.charCodeAt(++this._index);
  273 + }
  274 +
  275 + return this._source.slice(start, this._index);
  276 + },
  277 +
  278 + getUnicodeChar: function() {
  279 + for (let i = 0; i < 4; i++) {
  280 + let cc = this._source.charCodeAt(++this._index);
  281 + if ((cc > 96 && cc < 103) || // a-f
  282 + (cc > 64 && cc < 71) || // A-F
  283 + (cc > 47 && cc < 58)) { // 0-9
  284 + continue;
  285 + }
  286 + throw this.error('Illegal unicode escape sequence');
  287 + }
  288 + this._index++;
  289 + return String.fromCharCode(
  290 + parseInt(this._source.slice(this._index - 4, this._index), 16));
  291 + },
  292 +
  293 + stringRe: /"|'|{{|\\/g,
  294 + getString: function(opchar, opcharLen) {
  295 + const body = [];
  296 + let placeables = 0;
  297 +
  298 + this._index += opcharLen;
  299 + const start = this._index;
  300 +
  301 + let bufStart = start;
  302 + let buf = '';
  303 +
  304 + while (true) {
  305 + this.stringRe.lastIndex = this._index;
  306 + const match = this.stringRe.exec(this._source);
  307 +
  308 + if (!match) {
  309 + throw this.error('Unclosed string literal');
  310 + }
  311 +
  312 + if (match[0] === '"' || match[0] === '\'') {
  313 + if (match[0] !== opchar) {
  314 + this._index += opcharLen;
  315 + continue;
  316 + }
  317 + this._index = match.index + opcharLen;
  318 + break;
  319 + }
  320 +
  321 + if (match[0] === '{{') {
  322 + if (placeables > MAX_PLACEABLES$1 - 1) {
  323 + throw this.error('Too many placeables, maximum allowed is ' +
  324 + MAX_PLACEABLES$1);
  325 + }
  326 + placeables++;
  327 + if (match.index > bufStart || buf.length > 0) {
  328 + body.push(buf + this._source.slice(bufStart, match.index));
  329 + buf = '';
  330 + }
  331 + this._index = match.index + 2;
  332 + this.getWS();
  333 + body.push(this.getExpression());
  334 + this.getWS();
  335 + this._index += 2;
  336 + bufStart = this._index;
  337 + continue;
  338 + }
  339 +
  340 + if (match[0] === '\\') {
  341 + this._index = match.index + 1;
  342 + const ch2 = this._source[this._index];
  343 + if (ch2 === 'u') {
  344 + buf += this._source.slice(bufStart, match.index) +
  345 + this.getUnicodeChar();
  346 + } else if (ch2 === opchar || ch2 === '\\') {
  347 + buf += this._source.slice(bufStart, match.index) + ch2;
  348 + this._index++;
  349 + } else if (this._source.startsWith('{{', this._index)) {
  350 + buf += this._source.slice(bufStart, match.index) + '{{';
  351 + this._index += 2;
  352 + } else {
  353 + throw this.error('Illegal escape sequence');
  354 + }
  355 + bufStart = this._index;
  356 + }
  357 + }
  358 +
  359 + if (body.length === 0) {
  360 + return buf + this._source.slice(bufStart, this._index - opcharLen);
  361 + }
  362 +
  363 + if (this._index - opcharLen > bufStart || buf.length > 0) {
  364 + body.push(buf + this._source.slice(bufStart, this._index - opcharLen));
  365 + }
  366 +
  367 + return body;
  368 + },
  369 +
  370 + getAttributes: function() {
  371 + const attrs = Object.create(null);
  372 +
  373 + while (true) {
  374 + this.getAttribute(attrs);
  375 + const ws1 = this.getRequiredWS();
  376 + const ch = this._source.charAt(this._index);
  377 + if (ch === '>') {
  378 + break;
  379 + } else if (!ws1) {
  380 + throw this.error('Expected ">"');
  381 + }
  382 + }
  383 + return attrs;
  384 + },
  385 +
  386 + getAttribute: function(attrs) {
  387 + const key = this.getIdentifier();
  388 + let index;
  389 +
  390 + if (this._source[this._index]=== '[') {
  391 + ++this._index;
  392 + this.getWS();
  393 + index = this.getItemList(this.getExpression, ']');
  394 + }
  395 + this.getWS();
  396 + if (this._source[this._index] !== ':') {
  397 + throw this.error('Expected ":"');
  398 + }
  399 + ++this._index;
  400 + this.getWS();
  401 + const value = this.getValue();
  402 +
  403 + if (key in attrs) {
  404 + throw this.error('Duplicate attribute "' + key, 'duplicateerror');
  405 + }
  406 +
  407 + if (!index && typeof value === 'string') {
  408 + attrs[key] = value;
  409 + } else {
  410 + attrs[key] = {
  411 + value,
  412 + index
  413 + };
  414 + }
  415 + },
  416 +
  417 + getHash: function() {
  418 + const items = Object.create(null);
  419 +
  420 + ++this._index;
  421 + this.getWS();
  422 +
  423 + let defKey;
  424 +
  425 + while (true) {
  426 + const [key, value, def] = this.getHashItem();
  427 + items[key] = value;
  428 +
  429 + if (def) {
  430 + if (defKey) {
  431 + throw this.error('Default item redefinition forbidden');
  432 + }
  433 + defKey = key;
  434 + }
  435 + this.getWS();
  436 +
  437 + const comma = this._source[this._index] === ',';
  438 + if (comma) {
  439 + ++this._index;
  440 + this.getWS();
  441 + }
  442 + if (this._source[this._index] === '}') {
  443 + ++this._index;
  444 + break;
  445 + }
  446 + if (!comma) {
  447 + throw this.error('Expected "}"');
  448 + }
  449 + }
  450 +
  451 + if (defKey) {
  452 + items.__default = defKey;
  453 + }
  454 +
  455 + return items;
  456 + },
  457 +
  458 + getHashItem: function() {
  459 + let defItem = false;
  460 + if (this._source[this._index] === '*') {
  461 + ++this._index;
  462 + defItem = true;
  463 + }
  464 +
  465 + const key = this.getIdentifier();
  466 + this.getWS();
  467 + if (this._source[this._index] !== ':') {
  468 + throw this.error('Expected ":"');
  469 + }
  470 + ++this._index;
  471 + this.getWS();
  472 +
  473 + return [key, this.getValue(), defItem];
  474 + },
  475 +
  476 + getComment: function() {
  477 + this._index += 2;
  478 + const start = this._index;
  479 + const end = this._source.indexOf('*/', start);
  480 +
  481 + if (end === -1) {
  482 + throw this.error('Comment without a closing tag');
  483 + }
  484 +
  485 + this._index = end + 2;
  486 + },
  487 +
  488 + getExpression: function () {
  489 + let exp = this.getPrimaryExpression();
  490 +
  491 + while (true) {
  492 + let ch = this._source[this._index];
  493 + if (ch === '.' || ch === '[') {
  494 + ++this._index;
  495 + exp = this.getPropertyExpression(exp, ch === '[');
  496 + } else if (ch === '(') {
  497 + ++this._index;
  498 + exp = this.getCallExpression(exp);
  499 + } else {
  500 + break;
  501 + }
  502 + }
  503 +
  504 + return exp;
  505 + },
  506 +
  507 + getPropertyExpression: function(idref, computed) {
  508 + let exp;
  509 +
  510 + if (computed) {
  511 + this.getWS();
  512 + exp = this.getExpression();
  513 + this.getWS();
  514 + if (this._source[this._index] !== ']') {
  515 + throw this.error('Expected "]"');
  516 + }
  517 + ++this._index;
  518 + } else {
  519 + exp = this.getIdentifier();
  520 + }
  521 +
  522 + return {
  523 + type: 'prop',
  524 + expr: idref,
  525 + prop: exp,
  526 + cmpt: computed
  527 + };
  528 + },
  529 +
  530 + getCallExpression: function(callee) {
  531 + this.getWS();
  532 +
  533 + return {
  534 + type: 'call',
  535 + expr: callee,
  536 + args: this.getItemList(this.getExpression, ')')
  537 + };
  538 + },
  539 +
  540 + getPrimaryExpression: function() {
  541 + const ch = this._source[this._index];
  542 +
  543 + switch (ch) {
  544 + case '$':
  545 + ++this._index;
  546 + return {
  547 + type: 'var',
  548 + name: this.getIdentifier()
  549 + };
  550 + case '@':
  551 + ++this._index;
  552 + return {
  553 + type: 'glob',
  554 + name: this.getIdentifier()
  555 + };
  556 + default:
  557 + return {
  558 + type: 'id',
  559 + name: this.getIdentifier()
  560 + };
  561 + }
  562 + },
  563 +
  564 + getItemList: function(callback, closeChar) {
  565 + const items = [];
  566 + let closed = false;
  567 +
  568 + this.getWS();
  569 +
  570 + if (this._source[this._index] === closeChar) {
  571 + ++this._index;
  572 + closed = true;
  573 + }
  574 +
  575 + while (!closed) {
  576 + items.push(callback.call(this));
  577 + this.getWS();
  578 + let ch = this._source.charAt(this._index);
  579 + switch (ch) {
  580 + case ',':
  581 + ++this._index;
  582 + this.getWS();
  583 + break;
  584 + case closeChar:
  585 + ++this._index;
  586 + closed = true;
  587 + break;
  588 + default:
  589 + throw this.error('Expected "," or "' + closeChar + '"');
  590 + }
  591 + }
  592 +
  593 + return items;
  594 + },
  595 +
  596 +
  597 + getJunkEntry: function() {
  598 + const pos = this._index;
  599 + let nextEntity = this._source.indexOf('<', pos);
  600 + let nextComment = this._source.indexOf('/*', pos);
  601 +
  602 + if (nextEntity === -1) {
  603 + nextEntity = this._length;
  604 + }
  605 + if (nextComment === -1) {
  606 + nextComment = this._length;
  607 + }
  608 +
  609 + let nextEntry = Math.min(nextEntity, nextComment);
  610 +
  611 + this._index = nextEntry;
  612 + },
  613 +
  614 + error: function(message, type = 'parsererror') {
  615 + const pos = this._index;
  616 +
  617 + let start = this._source.lastIndexOf('<', pos - 1);
  618 + const lastClose = this._source.lastIndexOf('>', pos - 1);
  619 + start = lastClose > start ? lastClose + 1 : start;
  620 + const context = this._source.slice(start, pos + 10);
  621 +
  622 + const msg = message + ' at pos ' + pos + ': `' + context + '`';
  623 + const err = new L10nError(msg);
  624 + if (this.emit) {
  625 + this.emit(type, err);
  626 + }
  627 + return err;
  628 + },
  629 + };
  630 +
  631 + var MAX_PLACEABLES = 100;
  632 +
  633 + var PropertiesParser = {
  634 + patterns: null,
  635 + entryIds: null,
  636 + emit: null,
  637 +
  638 + init: function() {
  639 + this.patterns = {
  640 + comment: /^\s*#|^\s*$/,
  641 + entity: /^([^=\s]+)\s*=\s*(.*)$/,
  642 + multiline: /[^\\]\\$/,
  643 + index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,
  644 + unicode: /\\u([0-9a-fA-F]{1,4})/g,
  645 + entries: /[^\r\n]+/g,
  646 + controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g,
  647 + placeables: /\{\{\s*([^\s]*?)\s*\}\}/,
  648 + };
  649 + },
  650 +
  651 + parse: function(emit, source) {
  652 + if (!this.patterns) {
  653 + this.init();
  654 + }
  655 + this.emit = emit;
  656 +
  657 + var entries = {};
  658 +
  659 + var lines = source.match(this.patterns.entries);
  660 + if (!lines) {
  661 + return entries;
  662 + }
  663 + for (var i = 0; i < lines.length; i++) {
  664 + var line = lines[i];
  665 +
  666 + if (this.patterns.comment.test(line)) {
  667 + continue;
  668 + }
  669 +
  670 + while (this.patterns.multiline.test(line) && i < lines.length) {
  671 + line = line.slice(0, -1) + lines[++i].trim();
  672 + }
  673 +
  674 + var entityMatch = line.match(this.patterns.entity);
  675 + if (entityMatch) {
  676 + try {
  677 + this.parseEntity(entityMatch[1], entityMatch[2], entries);
  678 + } catch (e) {
  679 + if (!this.emit) {
  680 + throw e;
  681 + }
  682 + }
  683 + }
  684 + }
  685 + return entries;
  686 + },
  687 +
  688 + parseEntity: function(id, value, entries) {
  689 + var name, key;
  690 +
  691 + var pos = id.indexOf('[');
  692 + if (pos !== -1) {
  693 + name = id.substr(0, pos);
  694 + key = id.substring(pos + 1, id.length - 1);
  695 + } else {
  696 + name = id;
  697 + key = null;
  698 + }
  699 +
  700 + var nameElements = name.split('.');
  701 +
  702 + if (nameElements.length > 2) {
  703 + throw this.error('Error in ID: "' + name + '".' +
  704 + ' Nested attributes are not supported.');
  705 + }
  706 +
  707 + var attr;
  708 + if (nameElements.length > 1) {
  709 + name = nameElements[0];
  710 + attr = nameElements[1];
  711 +
  712 + if (attr[0] === '$') {
  713 + throw this.error('Attribute can\'t start with "$"');
  714 + }
  715 + } else {
  716 + attr = null;
  717 + }
  718 +
  719 + this.setEntityValue(name, attr, key, this.unescapeString(value), entries);
  720 + },
  721 +
  722 + setEntityValue: function(id, attr, key, rawValue, entries) {
  723 + var value = rawValue.indexOf('{{') > -1 ?
  724 + this.parseString(rawValue) : rawValue;
  725 +
  726 + var isSimpleValue = typeof value === 'string';
  727 + var root = entries;
  728 +
  729 + var isSimpleNode = typeof entries[id] === 'string';
  730 +
  731 + if (!entries[id] && (attr || key || !isSimpleValue)) {
  732 + entries[id] = Object.create(null);
  733 + isSimpleNode = false;
  734 + }
  735 +
  736 + if (attr) {
  737 + if (isSimpleNode) {
  738 + const val = entries[id];
  739 + entries[id] = Object.create(null);
  740 + entries[id].value = val;
  741 + }
  742 + if (!entries[id].attrs) {
  743 + entries[id].attrs = Object.create(null);
  744 + }
  745 + if (!entries[id].attrs && !isSimpleValue) {
  746 + entries[id].attrs[attr] = Object.create(null);
  747 + }
  748 + root = entries[id].attrs;
  749 + id = attr;
  750 + }
  751 +
  752 + if (key) {
  753 + isSimpleNode = false;
  754 + if (typeof root[id] === 'string') {
  755 + const val = root[id];
  756 + root[id] = Object.create(null);
  757 + root[id].index = this.parseIndex(val);
  758 + root[id].value = Object.create(null);
  759 + }
  760 + root = root[id].value;
  761 + id = key;
  762 + isSimpleValue = true;
  763 + }
  764 +
  765 + if (isSimpleValue && (!entries[id] || isSimpleNode)) {
  766 + if (id in root) {
  767 + throw this.error();
  768 + }
  769 + root[id] = value;
  770 + } else {
  771 + if (!root[id]) {
  772 + root[id] = Object.create(null);
  773 + }
  774 + root[id].value = value;
  775 + }
  776 + },
  777 +
  778 + parseString: function(str) {
  779 + var chunks = str.split(this.patterns.placeables);
  780 + var complexStr = [];
  781 +
  782 + var len = chunks.length;
  783 + var placeablesCount = (len - 1) / 2;
  784 +
  785 + if (placeablesCount >= MAX_PLACEABLES) {
  786 + throw this.error('Too many placeables (' + placeablesCount +
  787 + ', max allowed is ' + MAX_PLACEABLES + ')');
  788 + }
  789 +
  790 + for (var i = 0; i < chunks.length; i++) {
  791 + if (chunks[i].length === 0) {
  792 + continue;
  793 + }
  794 + if (i % 2 === 1) {
  795 + complexStr.push({type: 'idOrVar', name: chunks[i]});
  796 + } else {
  797 + complexStr.push(chunks[i]);
  798 + }
  799 + }
  800 + return complexStr;
  801 + },
  802 +
  803 + unescapeString: function(str) {
  804 + if (str.lastIndexOf('\\') !== -1) {
  805 + str = str.replace(this.patterns.controlChars, '$1');
  806 + }
  807 + return str.replace(this.patterns.unicode, function(match, token) {
  808 + return String.fromCodePoint(parseInt(token, 16));
  809 + });
  810 + },
  811 +
  812 + parseIndex: function(str) {
  813 + var match = str.match(this.patterns.index);
  814 + if (!match) {
  815 + throw new L10nError('Malformed index');
  816 + }
  817 + if (match[2]) {
  818 + return [{
  819 + type: 'call',
  820 + expr: {
  821 + type: 'prop',
  822 + expr: {
  823 + type: 'glob',
  824 + name: 'cldr'
  825 + },
  826 + prop: 'plural',
  827 + cmpt: false
  828 + }, args: [{
  829 + type: 'idOrVar',
  830 + name: match[2]
  831 + }]
  832 + }];
  833 + } else {
  834 + return [{type: 'idOrVar', name: match[1]}];
  835 + }
  836 + },
  837 +
  838 + error: function(msg, type = 'parsererror') {
  839 + const err = new L10nError(msg);
  840 + if (this.emit) {
  841 + this.emit(type, err);
  842 + }
  843 + return err;
  844 + }
  845 + };
  846 +
  847 + const KNOWN_MACROS = ['plural'];
  848 + const MAX_PLACEABLE_LENGTH = 2500;
  849 +
  850 + // Unicode bidi isolation characters
  851 + const FSI = '\u2068';
  852 + const PDI = '\u2069';
  853 +
  854 + const resolutionChain = new WeakSet();
  855 +
  856 + function format(ctx, lang, args, entity) {
  857 + if (typeof entity === 'string') {
  858 + return [{}, entity];
  859 + }
  860 +
  861 + if (resolutionChain.has(entity)) {
  862 + throw new L10nError('Cyclic reference detected');
  863 + }
  864 +
  865 + resolutionChain.add(entity);
  866 +
  867 + let rv;
  868 + // if format fails, we want the exception to bubble up and stop the whole
  869 + // resolving process; however, we still need to remove the entity from the
  870 + // resolution chain
  871 + try {
  872 + rv = resolveValue(
  873 + {}, ctx, lang, args, entity.value, entity.index);
  874 + } finally {
  875 + resolutionChain.delete(entity);
  876 + }
  877 + return rv;
  878 + }
  879 +
  880 + function resolveIdentifier(ctx, lang, args, id) {
  881 + if (KNOWN_MACROS.indexOf(id) > -1) {
  882 + return [{}, ctx._getMacro(lang, id)];
  883 + }
  884 +
  885 + if (args && args.hasOwnProperty(id)) {
  886 + if (typeof args[id] === 'string' || (typeof args[id] === 'number' &&
  887 + !isNaN(args[id]))) {
  888 + return [{}, args[id]];
  889 + } else {
  890 + throw new L10nError('Arg must be a string or a number: ' + id);
  891 + }
  892 + }
  893 +
  894 + // XXX: special case for Node.js where still:
  895 + // '__proto__' in Object.create(null) => true
  896 + if (id === '__proto__') {
  897 + throw new L10nError('Illegal id: ' + id);
  898 + }
  899 +
  900 + const entity = ctx._getEntity(lang, id);
  901 +
  902 + if (entity) {
  903 + return format(ctx, lang, args, entity);
  904 + }
  905 +
  906 + throw new L10nError('Unknown reference: ' + id);
  907 + }
  908 +
  909 + function subPlaceable(locals, ctx, lang, args, id) {
  910 + let newLocals, value;
  911 +
  912 + try {
  913 + [newLocals, value] = resolveIdentifier(ctx, lang, args, id);
  914 + } catch (err) {
  915 + return [{ error: err }, FSI + '{{ ' + id + ' }}' + PDI];
  916 + }
  917 +
  918 + if (typeof value === 'number') {
  919 + const formatter = ctx._getNumberFormatter(lang);
  920 + return [newLocals, formatter.format(value)];
  921 + }
  922 +
  923 + if (typeof value === 'string') {
  924 + // prevent Billion Laughs attacks
  925 + if (value.length >= MAX_PLACEABLE_LENGTH) {
  926 + throw new L10nError('Too many characters in placeable (' +
  927 + value.length + ', max allowed is ' +
  928 + MAX_PLACEABLE_LENGTH + ')');
  929 + }
  930 + return [newLocals, FSI + value + PDI];
  931 + }
  932 +
  933 + return [{}, FSI + '{{ ' + id + ' }}' + PDI];
  934 + }
  935 +
  936 + function interpolate(locals, ctx, lang, args, arr) {
  937 + return arr.reduce(function([localsSeq, valueSeq], cur) {
  938 + if (typeof cur === 'string') {
  939 + return [localsSeq, valueSeq + cur];
  940 + } else {
  941 + const [, value] = subPlaceable(locals, ctx, lang, args, cur.name);
  942 + // wrap the substitution in bidi isolate characters
  943 + return [localsSeq, valueSeq + value];
  944 + }
  945 + }, [locals, '']);
  946 + }
  947 +
  948 + function resolveSelector(ctx, lang, args, expr, index) {
  949 + //XXX: Dehardcode!!!
  950 + let selectorName;
  951 + if (index[0].type === 'call' && index[0].expr.type === 'prop' &&
  952 + index[0].expr.expr.name === 'cldr') {
  953 + selectorName = 'plural';
  954 + } else {
  955 + selectorName = index[0].name;
  956 + }
  957 + const selector = resolveIdentifier(ctx, lang, args, selectorName)[1];
  958 +
  959 + if (typeof selector !== 'function') {
  960 + // selector is a simple reference to an entity or args
  961 + return selector;
  962 + }
  963 +
  964 + const argValue = index[0].args ?
  965 + resolveIdentifier(ctx, lang, args, index[0].args[0].name)[1] : undefined;
  966 +
  967 + if (selectorName === 'plural') {
  968 + // special cases for zero, one, two if they are defined on the hash
  969 + if (argValue === 0 && 'zero' in expr) {
  970 + return 'zero';
  971 + }
  972 + if (argValue === 1 && 'one' in expr) {
  973 + return 'one';
  974 + }
  975 + if (argValue === 2 && 'two' in expr) {
  976 + return 'two';
  977 + }
  978 + }
  979 +
  980 + return selector(argValue);
  981 + }
  982 +
  983 + function resolveValue(locals, ctx, lang, args, expr, index) {
  984 + if (!expr) {
  985 + return [locals, expr];
  986 + }
  987 +
  988 + if (typeof expr === 'string' ||
  989 + typeof expr === 'boolean' ||
  990 + typeof expr === 'number') {
  991 + return [locals, expr];
  992 + }
  993 +
  994 + if (Array.isArray(expr)) {
  995 + return interpolate(locals, ctx, lang, args, expr);
  996 + }
  997 +
  998 + // otherwise, it's a dict
  999 + if (index) {
  1000 + // try to use the index in order to select the right dict member
  1001 + const selector = resolveSelector(ctx, lang, args, expr, index);
  1002 + if (selector in expr) {
  1003 + return resolveValue(locals, ctx, lang, args, expr[selector]);
  1004 + }
  1005 + }
  1006 +
  1007 + // if there was no index or no selector was found, try the default
  1008 + // XXX 'other' is an artifact from Gaia
  1009 + const defaultKey = expr.__default || 'other';
  1010 + if (defaultKey in expr) {
  1011 + return resolveValue(locals, ctx, lang, args, expr[defaultKey]);
  1012 + }
  1013 +
  1014 + throw new L10nError('Unresolvable value');
  1015 + }
  1016 +
  1017 + const locales2rules = {
  1018 + 'af': 3,
  1019 + 'ak': 4,
  1020 + 'am': 4,
  1021 + 'ar': 1,
  1022 + 'asa': 3,
  1023 + 'az': 0,
  1024 + 'be': 11,
  1025 + 'bem': 3,
  1026 + 'bez': 3,
  1027 + 'bg': 3,
  1028 + 'bh': 4,
  1029 + 'bm': 0,
  1030 + 'bn': 3,
  1031 + 'bo': 0,
  1032 + 'br': 20,
  1033 + 'brx': 3,
  1034 + 'bs': 11,
  1035 + 'ca': 3,
  1036 + 'cgg': 3,
  1037 + 'chr': 3,
  1038 + 'cs': 12,
  1039 + 'cy': 17,
  1040 + 'da': 3,
  1041 + 'de': 3,
  1042 + 'dv': 3,
  1043 + 'dz': 0,
  1044 + 'ee': 3,
  1045 + 'el': 3,
  1046 + 'en': 3,
  1047 + 'eo': 3,
  1048 + 'es': 3,
  1049 + 'et': 3,
  1050 + 'eu': 3,
  1051 + 'fa': 0,
  1052 + 'ff': 5,
  1053 + 'fi': 3,
  1054 + 'fil': 4,
  1055 + 'fo': 3,
  1056 + 'fr': 5,
  1057 + 'fur': 3,
  1058 + 'fy': 3,
  1059 + 'ga': 8,
  1060 + 'gd': 24,
  1061 + 'gl': 3,
  1062 + 'gsw': 3,
  1063 + 'gu': 3,
  1064 + 'guw': 4,
  1065 + 'gv': 23,
  1066 + 'ha': 3,
  1067 + 'haw': 3,
  1068 + 'he': 2,
  1069 + 'hi': 4,
  1070 + 'hr': 11,
  1071 + 'hu': 0,
  1072 + 'id': 0,
  1073 + 'ig': 0,
  1074 + 'ii': 0,
  1075 + 'is': 3,
  1076 + 'it': 3,
  1077 + 'iu': 7,
  1078 + 'ja': 0,
  1079 + 'jmc': 3,
  1080 + 'jv': 0,
  1081 + 'ka': 0,
  1082 + 'kab': 5,
  1083 + 'kaj': 3,
  1084 + 'kcg': 3,
  1085 + 'kde': 0,
  1086 + 'kea': 0,
  1087 + 'kk': 3,
  1088 + 'kl': 3,
  1089 + 'km': 0,
  1090 + 'kn': 0,
  1091 + 'ko': 0,
  1092 + 'ksb': 3,
  1093 + 'ksh': 21,
  1094 + 'ku': 3,
  1095 + 'kw': 7,
  1096 + 'lag': 18,
  1097 + 'lb': 3,
  1098 + 'lg': 3,
  1099 + 'ln': 4,
  1100 + 'lo': 0,
  1101 + 'lt': 10,
  1102 + 'lv': 6,
  1103 + 'mas': 3,
  1104 + 'mg': 4,
  1105 + 'mk': 16,
  1106 + 'ml': 3,
  1107 + 'mn': 3,
  1108 + 'mo': 9,
  1109 + 'mr': 3,
  1110 + 'ms': 0,
  1111 + 'mt': 15,
  1112 + 'my': 0,
  1113 + 'nah': 3,
  1114 + 'naq': 7,
  1115 + 'nb': 3,
  1116 + 'nd': 3,
  1117 + 'ne': 3,
  1118 + 'nl': 3,
  1119 + 'nn': 3,
  1120 + 'no': 3,
  1121 + 'nr': 3,
  1122 + 'nso': 4,
  1123 + 'ny': 3,
  1124 + 'nyn': 3,
  1125 + 'om': 3,
  1126 + 'or': 3,
  1127 + 'pa': 3,
  1128 + 'pap': 3,
  1129 + 'pl': 13,
  1130 + 'ps': 3,
  1131 + 'pt': 3,
  1132 + 'rm': 3,
  1133 + 'ro': 9,
  1134 + 'rof': 3,
  1135 + 'ru': 11,
  1136 + 'rwk': 3,
  1137 + 'sah': 0,
  1138 + 'saq': 3,
  1139 + 'se': 7,
  1140 + 'seh': 3,
  1141 + 'ses': 0,
  1142 + 'sg': 0,
  1143 + 'sh': 11,
  1144 + 'shi': 19,
  1145 + 'sk': 12,
  1146 + 'sl': 14,
  1147 + 'sma': 7,
  1148 + 'smi': 7,
  1149 + 'smj': 7,
  1150 + 'smn': 7,
  1151 + 'sms': 7,
  1152 + 'sn': 3,
  1153 + 'so': 3,
  1154 + 'sq': 3,
  1155 + 'sr': 11,
  1156 + 'ss': 3,
  1157 + 'ssy': 3,
  1158 + 'st': 3,
  1159 + 'sv': 3,
  1160 + 'sw': 3,
  1161 + 'syr': 3,
  1162 + 'ta': 3,
  1163 + 'te': 3,
  1164 + 'teo': 3,
  1165 + 'th': 0,
  1166 + 'ti': 4,
  1167 + 'tig': 3,
  1168 + 'tk': 3,
  1169 + 'tl': 4,
  1170 + 'tn': 3,
  1171 + 'to': 0,
  1172 + 'tr': 0,
  1173 + 'ts': 3,
  1174 + 'tzm': 22,
  1175 + 'uk': 11,
  1176 + 'ur': 3,
  1177 + 've': 3,
  1178 + 'vi': 0,
  1179 + 'vun': 3,
  1180 + 'wa': 4,
  1181 + 'wae': 3,
  1182 + 'wo': 0,
  1183 + 'xh': 3,
  1184 + 'xog': 3,
  1185 + 'yo': 0,
  1186 + 'zh': 0,
  1187 + 'zu': 3
  1188 + };
  1189 +
  1190 + // utility functions for plural rules methods
  1191 + function isIn(n, list) {
  1192 + return list.indexOf(n) !== -1;
  1193 + }
  1194 + function isBetween(n, start, end) {
  1195 + return typeof n === typeof start && start <= n && n <= end;
  1196 + }
  1197 +
  1198 + // list of all plural rules methods:
  1199 + // map an integer to the plural form name to use
  1200 + const pluralRules = {
  1201 + '0': function() {
  1202 + return 'other';
  1203 + },
  1204 + '1': function(n) {
  1205 + if ((isBetween((n % 100), 3, 10))) {
  1206 + return 'few';
  1207 + }
  1208 + if (n === 0) {
  1209 + return 'zero';
  1210 + }
  1211 + if ((isBetween((n % 100), 11, 99))) {
  1212 + return 'many';
  1213 + }
  1214 + if (n === 2) {
  1215 + return 'two';
  1216 + }
  1217 + if (n === 1) {
  1218 + return 'one';
  1219 + }
  1220 + return 'other';
  1221 + },
  1222 + '2': function(n) {
  1223 + if (n !== 0 && (n % 10) === 0) {
  1224 + return 'many';
  1225 + }
  1226 + if (n === 2) {
  1227 + return 'two';
  1228 + }
  1229 + if (n === 1) {
  1230 + return 'one';
  1231 + }
  1232 + return 'other';
  1233 + },
  1234 + '3': function(n) {
  1235 + if (n === 1) {
  1236 + return 'one';
  1237 + }
  1238 + return 'other';
  1239 + },
  1240 + '4': function(n) {
  1241 + if ((isBetween(n, 0, 1))) {
  1242 + return 'one';
  1243 + }
  1244 + return 'other';
  1245 + },
  1246 + '5': function(n) {
  1247 + if ((isBetween(n, 0, 2)) && n !== 2) {
  1248 + return 'one';
  1249 + }
  1250 + return 'other';
  1251 + },
  1252 + '6': function(n) {
  1253 + if (n === 0) {
  1254 + return 'zero';
  1255 + }
  1256 + if ((n % 10) === 1 && (n % 100) !== 11) {
  1257 + return 'one';
  1258 + }
  1259 + return 'other';
  1260 + },
  1261 + '7': function(n) {
  1262 + if (n === 2) {
  1263 + return 'two';
  1264 + }
  1265 + if (n === 1) {
  1266 + return 'one';
  1267 + }
  1268 + return 'other';
  1269 + },
  1270 + '8': function(n) {
  1271 + if ((isBetween(n, 3, 6))) {
  1272 + return 'few';
  1273 + }
  1274 + if ((isBetween(n, 7, 10))) {
  1275 + return 'many';
  1276 + }
  1277 + if (n === 2) {
  1278 + return 'two';
  1279 + }
  1280 + if (n === 1) {
  1281 + return 'one';
  1282 + }
  1283 + return 'other';
  1284 + },
  1285 + '9': function(n) {
  1286 + if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) {
  1287 + return 'few';
  1288 + }
  1289 + if (n === 1) {
  1290 + return 'one';
  1291 + }
  1292 + return 'other';
  1293 + },
  1294 + '10': function(n) {
  1295 + if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) {
  1296 + return 'few';
  1297 + }
  1298 + if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) {
  1299 + return 'one';
  1300 + }
  1301 + return 'other';
  1302 + },
  1303 + '11': function(n) {
  1304 + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
  1305 + return 'few';
  1306 + }
  1307 + if ((n % 10) === 0 ||
  1308 + (isBetween((n % 10), 5, 9)) ||
  1309 + (isBetween((n % 100), 11, 14))) {
  1310 + return 'many';
  1311 + }
  1312 + if ((n % 10) === 1 && (n % 100) !== 11) {
  1313 + return 'one';
  1314 + }
  1315 + return 'other';
  1316 + },
  1317 + '12': function(n) {
  1318 + if ((isBetween(n, 2, 4))) {
  1319 + return 'few';
  1320 + }
  1321 + if (n === 1) {
  1322 + return 'one';
  1323 + }
  1324 + return 'other';
  1325 + },
  1326 + '13': function(n) {
  1327 + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
  1328 + return 'few';
  1329 + }
  1330 + if (n !== 1 && (isBetween((n % 10), 0, 1)) ||
  1331 + (isBetween((n % 10), 5, 9)) ||
  1332 + (isBetween((n % 100), 12, 14))) {
  1333 + return 'many';
  1334 + }
  1335 + if (n === 1) {
  1336 + return 'one';
  1337 + }
  1338 + return 'other';
  1339 + },
  1340 + '14': function(n) {
  1341 + if ((isBetween((n % 100), 3, 4))) {
  1342 + return 'few';
  1343 + }
  1344 + if ((n % 100) === 2) {
  1345 + return 'two';
  1346 + }
  1347 + if ((n % 100) === 1) {
  1348 + return 'one';
  1349 + }
  1350 + return 'other';
  1351 + },
  1352 + '15': function(n) {
  1353 + if (n === 0 || (isBetween((n % 100), 2, 10))) {
  1354 + return 'few';
  1355 + }
  1356 + if ((isBetween((n % 100), 11, 19))) {
  1357 + return 'many';
  1358 + }
  1359 + if (n === 1) {
  1360 + return 'one';
  1361 + }
  1362 + return 'other';
  1363 + },
  1364 + '16': function(n) {
  1365 + if ((n % 10) === 1 && n !== 11) {
  1366 + return 'one';
  1367 + }
  1368 + return 'other';
  1369 + },
  1370 + '17': function(n) {
  1371 + if (n === 3) {
  1372 + return 'few';
  1373 + }
  1374 + if (n === 0) {
  1375 + return 'zero';
  1376 + }
  1377 + if (n === 6) {
  1378 + return 'many';
  1379 + }
  1380 + if (n === 2) {
  1381 + return 'two';
  1382 + }
  1383 + if (n === 1) {
  1384 + return 'one';
  1385 + }
  1386 + return 'other';
  1387 + },
  1388 + '18': function(n) {
  1389 + if (n === 0) {
  1390 + return 'zero';
  1391 + }
  1392 + if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) {
  1393 + return 'one';
  1394 + }
  1395 + return 'other';
  1396 + },
  1397 + '19': function(n) {
  1398 + if ((isBetween(n, 2, 10))) {
  1399 + return 'few';
  1400 + }
  1401 + if ((isBetween(n, 0, 1))) {
  1402 + return 'one';
  1403 + }
  1404 + return 'other';
  1405 + },
  1406 + '20': function(n) {
  1407 + if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !(
  1408 + isBetween((n % 100), 10, 19) ||
  1409 + isBetween((n % 100), 70, 79) ||
  1410 + isBetween((n % 100), 90, 99)
  1411 + )) {
  1412 + return 'few';
  1413 + }
  1414 + if ((n % 1000000) === 0 && n !== 0) {
  1415 + return 'many';
  1416 + }
  1417 + if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) {
  1418 + return 'two';
  1419 + }
  1420 + if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) {
  1421 + return 'one';
  1422 + }
  1423 + return 'other';
  1424 + },
  1425 + '21': function(n) {
  1426 + if (n === 0) {
  1427 + return 'zero';
  1428 + }
  1429 + if (n === 1) {
  1430 + return 'one';
  1431 + }
  1432 + return 'other';
  1433 + },
  1434 + '22': function(n) {
  1435 + if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) {
  1436 + return 'one';
  1437 + }
  1438 + return 'other';
  1439 + },
  1440 + '23': function(n) {
  1441 + if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) {
  1442 + return 'one';
  1443 + }
  1444 + return 'other';
  1445 + },
  1446 + '24': function(n) {
  1447 + if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) {
  1448 + return 'few';
  1449 + }
  1450 + if (isIn(n, [2, 12])) {
  1451 + return 'two';
  1452 + }
  1453 + if (isIn(n, [1, 11])) {
  1454 + return 'one';
  1455 + }
  1456 + return 'other';
  1457 + }
  1458 + };
  1459 +
  1460 + function getPluralRule(code) {
  1461 + // return a function that gives the plural form name for a given integer
  1462 + const index = locales2rules[code.replace(/-.*$/, '')];
  1463 + if (!(index in pluralRules)) {
  1464 + return function() { return 'other'; };
  1465 + }
  1466 + return pluralRules[index];
  1467 + }
  1468 +
  1469 + class Context {
  1470 + constructor(env) {
  1471 + this._env = env;
  1472 + this._numberFormatters = null;
  1473 + }
  1474 +
  1475 + _formatTuple(lang, args, entity, id, key) {
  1476 + try {
  1477 + return format(this, lang, args, entity);
  1478 + } catch (err) {
  1479 + err.id = key ? id + '::' + key : id;
  1480 + err.lang = lang;
  1481 + this._env.emit('resolveerror', err, this);
  1482 + return [{ error: err }, err.id];
  1483 + }
  1484 + }
  1485 +
  1486 + _formatEntity(lang, args, entity, id) {
  1487 + const [, value] = this._formatTuple(lang, args, entity, id);
  1488 +
  1489 + const formatted = {
  1490 + value,
  1491 + attrs: null,
  1492 + };
  1493 +
  1494 + if (entity.attrs) {
  1495 + formatted.attrs = Object.create(null);
  1496 + for (let key in entity.attrs) {
  1497 + /* jshint -W089 */
  1498 + const [, attrValue] = this._formatTuple(
  1499 + lang, args, entity.attrs[key], id, key);
  1500 + formatted.attrs[key] = attrValue;
  1501 + }
  1502 + }
  1503 +
  1504 + return formatted;
  1505 + }
  1506 +
  1507 + _formatValue(lang, args, entity, id) {
  1508 + return this._formatTuple(lang, args, entity, id)[1];
  1509 + }
  1510 +
  1511 + fetch(langs) {
  1512 + if (langs.length === 0) {
  1513 + return Promise.resolve(langs);
  1514 + }
  1515 +
  1516 + const resIds = Array.from(this._env._resLists.get(this));
  1517 +
  1518 + return Promise.all(
  1519 + resIds.map(
  1520 + this._env._getResource.bind(this._env, langs[0]))).then(
  1521 + () => langs);
  1522 + }
  1523 +
  1524 + _resolve(langs, keys, formatter, prevResolved) {
  1525 + const lang = langs[0];
  1526 +
  1527 + if (!lang) {
  1528 + return reportMissing.call(this, keys, formatter, prevResolved);
  1529 + }
  1530 +
  1531 + let hasUnresolved = false;
  1532 +
  1533 + const resolved = keys.map((key, i) => {
  1534 + if (prevResolved && prevResolved[i] !== undefined) {
  1535 + return prevResolved[i];
  1536 + }
  1537 + const [id, args] = Array.isArray(key) ?
  1538 + key : [key, undefined];
  1539 + const entity = this._getEntity(lang, id);
  1540 +
  1541 + if (entity) {
  1542 + return formatter.call(this, lang, args, entity, id);
  1543 + }
  1544 +
  1545 + this._env.emit('notfounderror',
  1546 + new L10nError('"' + id + '"' + ' not found in ' + lang.code,
  1547 + id, lang), this);
  1548 + hasUnresolved = true;
  1549 + });
  1550 +
  1551 + if (!hasUnresolved) {
  1552 + return resolved;
  1553 + }
  1554 +
  1555 + return this.fetch(langs.slice(1)).then(
  1556 + nextLangs => this._resolve(nextLangs, keys, formatter, resolved));
  1557 + }
  1558 +
  1559 + resolveEntities(langs, keys) {
  1560 + return this.fetch(langs).then(
  1561 + langs => this._resolve(langs, keys, this._formatEntity));
  1562 + }
  1563 +
  1564 + resolveValues(langs, keys) {
  1565 + return this.fetch(langs).then(
  1566 + langs => this._resolve(langs, keys, this._formatValue));
  1567 + }
  1568 +
  1569 + _getEntity(lang, id) {
  1570 + const cache = this._env._resCache;
  1571 + const resIds = Array.from(this._env._resLists.get(this));
  1572 +
  1573 + // Look for `id` in every resource in order.
  1574 + for (let i = 0, resId; resId = resIds[i]; i++) {
  1575 + const resource = cache.get(resId + lang.code + lang.src);
  1576 + if (resource instanceof L10nError) {
  1577 + continue;
  1578 + }
  1579 + if (id in resource) {
  1580 + return resource[id];
  1581 + }
  1582 + }
  1583 + return undefined;
  1584 + }
  1585 +
  1586 + _getNumberFormatter(lang) {
  1587 + if (!this._numberFormatters) {
  1588 + this._numberFormatters = new Map();
  1589 + }
  1590 + if (!this._numberFormatters.has(lang)) {
  1591 + const formatter = Intl.NumberFormat(lang, {
  1592 + useGrouping: false,
  1593 + });
  1594 + this._numberFormatters.set(lang, formatter);
  1595 + return formatter;
  1596 + }
  1597 + return this._numberFormatters.get(lang);
  1598 + }
  1599 +
  1600 + // XXX in the future macros will be stored in localization resources together
  1601 + // with regular entities and this method will not be needed anymore
  1602 + _getMacro(lang, id) {
  1603 + switch(id) {
  1604 + case 'plural':
  1605 + return getPluralRule(lang.code);
  1606 + default:
  1607 + return undefined;
  1608 + }
  1609 + }
  1610 +
  1611 + }
  1612 +
  1613 + function reportMissing(keys, formatter, resolved) {
  1614 + const missingIds = new Set();
  1615 +
  1616 + keys.forEach((key, i) => {
  1617 + if (resolved && resolved[i] !== undefined) {
  1618 + return;
  1619 + }
  1620 + const id = Array.isArray(key) ? key[0] : key;
  1621 + missingIds.add(id);
  1622 + resolved[i] = formatter === this._formatValue ?
  1623 + id : {value: id, attrs: null};
  1624 + });
  1625 +
  1626 + this._env.emit('notfounderror', new L10nError(
  1627 + '"' + Array.from(missingIds).join(', ') + '"' +
  1628 + ' not found in any language', missingIds), this);
  1629 +
  1630 + return resolved;
  1631 + }
  1632 +
  1633 + // Walk an entry node searching for content leaves
  1634 + function walkEntry(entry, fn) {
  1635 + if (typeof entry === 'string') {
  1636 + return fn(entry);
  1637 + }
  1638 +
  1639 + const newEntry = Object.create(null);
  1640 +
  1641 + if (entry.value) {
  1642 + newEntry.value = walkValue(entry.value, fn);
  1643 + }
  1644 +
  1645 + if (entry.index) {
  1646 + newEntry.index = entry.index;
  1647 + }
  1648 +
  1649 + if (entry.attrs) {
  1650 + newEntry.attrs = Object.create(null);
  1651 + for (let key in entry.attrs) {
  1652 + newEntry.attrs[key] = walkEntry(entry.attrs[key], fn);
  1653 + }
  1654 + }
  1655 +
  1656 + return newEntry;
  1657 + }
  1658 +
  1659 + function walkValue(value, fn) {
  1660 + if (typeof value === 'string') {
  1661 + return fn(value);
  1662 + }
  1663 +
  1664 + // skip expressions in placeables
  1665 + if (value.type) {
  1666 + return value;
  1667 + }
  1668 +
  1669 + const newValue = Array.isArray(value) ? [] : Object.create(null);
  1670 + const keys = Object.keys(value);
  1671 +
  1672 + for (let i = 0, key; (key = keys[i]); i++) {
  1673 + newValue[key] = walkValue(value[key], fn);
  1674 + }
  1675 +
  1676 + return newValue;
  1677 + }
  1678 +
  1679 + /* Pseudolocalizations
  1680 + *
  1681 + * pseudo is a dict of strategies to be used to modify the English
  1682 + * context in order to create pseudolocalizations. These can be used by
  1683 + * developers to test the localizability of their code without having to
  1684 + * actually speak a foreign language.
  1685 + *
  1686 + * Currently, the following pseudolocales are supported:
  1687 + *
  1688 + * fr-x-psaccent - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ
  1689 + *
  1690 + * In Accented English all English letters are replaced by accented
  1691 + * Unicode counterparts which don't impair the readability of the content.
  1692 + * This allows developers to quickly test if any given string is being
  1693 + * correctly displayed in its 'translated' form. Additionally, simple
  1694 + * heuristics are used to make certain words longer to better simulate the
  1695 + * experience of international users.
  1696 + *
  1697 + * ar-x-psbidi - ɥsıʅƃuƎ ıpıԐ
  1698 + *
  1699 + * Bidi English is a fake RTL locale. All words are surrounded by
  1700 + * Unicode formatting marks forcing the RTL directionality of characters.
  1701 + * In addition, to make the reversed text easier to read, individual
  1702 + * letters are flipped.
  1703 + *
  1704 + * Note: The name above is hardcoded to be RTL in case code editors have
  1705 + * trouble with the RLO and PDF Unicode marks. In reality, it should be
  1706 + * surrounded by those marks as well.
  1707 + *
  1708 + * See https://bugzil.la/900182 for more information.
  1709 + *
  1710 + */
  1711 +
  1712 + function createGetter(id, name) {
  1713 + let _pseudo = null;
  1714 +
  1715 + return function getPseudo() {
  1716 + if (_pseudo) {
  1717 + return _pseudo;
  1718 + }
  1719 +
  1720 + const reAlphas = /[a-zA-Z]/g;
  1721 + const reVowels = /[aeiouAEIOU]/g;
  1722 + const reWords = /[^\W0-9_]+/g;
  1723 + // strftime tokens (%a, %Eb), template {vars}, HTML entities (&#x202a;)
  1724 + // and HTML tags.
  1725 + const reExcluded = /(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;
  1726 +
  1727 + const charMaps = {
  1728 + 'fr-x-psaccent':
  1729 + 'ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ[\\]^_`ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ',
  1730 + 'ar-x-psbidi':
  1731 + // XXX Use pɟפ˥ʎ as replacements for ᗡℲ⅁⅂⅄. https://bugzil.la/1007340
  1732 + '∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅMXʎZ[\\]ᵥ_,ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz',
  1733 + };
  1734 +
  1735 + const mods = {
  1736 + 'fr-x-psaccent': val =>
  1737 + val.replace(reVowels, match => match + match.toLowerCase()),
  1738 +
  1739 + // Surround each word with Unicode formatting codes, RLO and PDF:
  1740 + // U+202E: RIGHT-TO-LEFT OVERRIDE (RLO)
  1741 + // U+202C: POP DIRECTIONAL FORMATTING (PDF)
  1742 + // See http://www.w3.org/International/questions/qa-bidi-controls
  1743 + 'ar-x-psbidi': val =>
  1744 + val.replace(reWords, match => '\u202e' + match + '\u202c'),
  1745 + };
  1746 +
  1747 + // Replace each Latin letter with a Unicode character from map
  1748 + const replaceChars =
  1749 + (map, val) => val.replace(
  1750 + reAlphas, match => map.charAt(match.charCodeAt(0) - 65));
  1751 +
  1752 + const transform =
  1753 + val => replaceChars(charMaps[id], mods[id](val));
  1754 +
  1755 + // apply fn to translatable parts of val
  1756 + const apply = (fn, val) => {
  1757 + if (!val) {
  1758 + return val;
  1759 + }
  1760 +
  1761 + const parts = val.split(reExcluded);
  1762 + const modified = parts.map(function(part) {
  1763 + if (reExcluded.test(part)) {
  1764 + return part;
  1765 + }
  1766 + return fn(part);
  1767 + });
  1768 + return modified.join('');
  1769 + };
  1770 +
  1771 + return _pseudo = {
  1772 + name: transform(name),
  1773 + process: str => apply(transform, str)
  1774 + };
  1775 + };
  1776 + }
  1777 +
  1778 + const pseudo = Object.defineProperties(Object.create(null), {
  1779 + 'fr-x-psaccent': {
  1780 + enumerable: true,
  1781 + get: createGetter('fr-x-psaccent', 'Runtime Accented')
  1782 + },
  1783 + 'ar-x-psbidi': {
  1784 + enumerable: true,
  1785 + get: createGetter('ar-x-psbidi', 'Runtime Bidi')
  1786 + }
  1787 + });
  1788 +
  1789 + const parsers = {
  1790 + properties: PropertiesParser,
  1791 + l20n: L20nParser,
  1792 + };
  1793 +
  1794 + class Env {
  1795 + constructor(defaultLang, fetchResource) {
  1796 + this.defaultLang = defaultLang;
  1797 + this.fetchResource = fetchResource;
  1798 +
  1799 + this._resLists = new Map();
  1800 + this._resCache = new Map();
  1801 +
  1802 + const listeners = {};
  1803 + this.emit = emit.bind(this, listeners);
  1804 + this.addEventListener = addEventListener.bind(this, listeners);
  1805 + this.removeEventListener = removeEventListener.bind(this, listeners);
  1806 + }
  1807 +
  1808 + createContext(resIds) {
  1809 + const ctx = new Context(this);
  1810 + this._resLists.set(ctx, new Set(resIds));
  1811 + return ctx;
  1812 + }
  1813 +
  1814 + destroyContext(ctx) {
  1815 + const lists = this._resLists;
  1816 + const resList = lists.get(ctx);
  1817 +
  1818 + lists.delete(ctx);
  1819 + resList.forEach(
  1820 + resId => deleteIfOrphan(this._resCache, lists, resId));
  1821 + }
  1822 +
  1823 + _parse(syntax, lang, data) {
  1824 + const parser = parsers[syntax];
  1825 + if (!parser) {
  1826 + return data;
  1827 + }
  1828 +
  1829 + const emit = (type, err) => this.emit(type, amendError(lang, err));
  1830 + return parser.parse.call(parser, emit, data);
  1831 + }
  1832 +
  1833 + _create(lang, entries) {
  1834 + if (lang.src !== 'pseudo') {
  1835 + return entries;
  1836 + }
  1837 +
  1838 + const pseudoentries = Object.create(null);
  1839 + for (let key in entries) {
  1840 + pseudoentries[key] = walkEntry(
  1841 + entries[key], pseudo[lang.code].process);
  1842 + }
  1843 + return pseudoentries;
  1844 + }
  1845 +
  1846 + _getResource(lang, res) {
  1847 + const cache = this._resCache;
  1848 + const id = res + lang.code + lang.src;
  1849 +
  1850 + if (cache.has(id)) {
  1851 + return cache.get(id);
  1852 + }
  1853 +
  1854 + const syntax = res.substr(res.lastIndexOf('.') + 1);
  1855 +
  1856 + const saveEntries = data => {
  1857 + const entries = this._parse(syntax, lang, data);
  1858 + cache.set(id, this._create(lang, entries));
  1859 + };
  1860 +
  1861 + const recover = err => {
  1862 + err.lang = lang;
  1863 + this.emit('fetcherror', err);
  1864 + cache.set(id, err);
  1865 + };
  1866 +
  1867 + const langToFetch = lang.src === 'pseudo' ?
  1868 + { code: this.defaultLang, src: 'app' } :
  1869 + lang;
  1870 +
  1871 + const resource = this.fetchResource(res, langToFetch).then(
  1872 + saveEntries, recover);
  1873 +
  1874 + cache.set(id, resource);
  1875 +
  1876 + return resource;
  1877 + }
  1878 + }
  1879 +
  1880 + function deleteIfOrphan(cache, lists, resId) {
  1881 + const isNeeded = Array.from(lists).some(
  1882 + ([ctx, resIds]) => resIds.has(resId));
  1883 +
  1884 + if (!isNeeded) {
  1885 + cache.forEach((val, key) =>
  1886 + key.startsWith(resId) ? cache.delete(key) : null);
  1887 + }
  1888 + }
  1889 +
  1890 + function amendError(lang, err) {
  1891 + err.lang = lang;
  1892 + return err;
  1893 + }
  1894 +
  1895 + // Polyfill NodeList.prototype[Symbol.iterator] for Chrome.
  1896 + // See https://code.google.com/p/chromium/issues/detail?id=401699
  1897 + if (typeof NodeList === 'function' && !NodeList.prototype[Symbol.iterator]) {
  1898 + NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
  1899 + }
  1900 +
  1901 + // A document.ready shim
  1902 + // https://github.com/whatwg/html/issues/127
  1903 + function documentReady() {
  1904 + if (document.readyState !== 'loading') {
  1905 + return Promise.resolve();
  1906 + }
  1907 +
  1908 + return new Promise(resolve => {
  1909 + document.addEventListener('readystatechange', function onrsc() {
  1910 + document.removeEventListener('readystatechange', onrsc);
  1911 + resolve();
  1912 + });
  1913 + });
  1914 + }
  1915 +
  1916 + // Intl.Locale
  1917 + function getDirection(code) {
  1918 + const tag = code.split('-')[0];
  1919 + return ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(tag) >= 0 ?
  1920 + 'rtl' : 'ltr';
  1921 + }
  1922 +
  1923 + function prioritizeLocales(def, availableLangs, requested) {
  1924 + let supportedLocale;
  1925 + // Find the first locale in the requested list that is supported.
  1926 + for (let i = 0; i < requested.length; i++) {
  1927 + const locale = requested[i];
  1928 + if (availableLangs.indexOf(locale) !== -1) {
  1929 + supportedLocale = locale;
  1930 + break;
  1931 + }
  1932 + }
  1933 + if (!supportedLocale ||
  1934 + supportedLocale === def) {
  1935 + return [def];
  1936 + }
  1937 +
  1938 + return [supportedLocale, def];
  1939 + }
  1940 +
  1941 + function getMeta(head) {
  1942 + let availableLangs = Object.create(null);
  1943 + let defaultLang = null;
  1944 + let appVersion = null;
  1945 +
  1946 + // XXX take last found instead of first?
  1947 + const metas = head.querySelectorAll(
  1948 + 'meta[name="availableLanguages"],' +
  1949 + 'meta[name="defaultLanguage"],' +
  1950 + 'meta[name="appVersion"]');
  1951 + for (let meta of metas) {
  1952 + const name = meta.getAttribute('name');
  1953 + const content = meta.getAttribute('content').trim();
  1954 + switch (name) {
  1955 + case 'availableLanguages':
  1956 + availableLangs = getLangRevisionMap(
  1957 + availableLangs, content);
  1958 + break;
  1959 + case 'defaultLanguage':
  1960 + const [lang, rev] = getLangRevisionTuple(content);
  1961 + defaultLang = lang;
  1962 + if (!(lang in availableLangs)) {
  1963 + availableLangs[lang] = rev;
  1964 + }
  1965 + break;
  1966 + case 'appVersion':
  1967 + appVersion = content;
  1968 + }
  1969 + }
  1970 +
  1971 + return {
  1972 + defaultLang,
  1973 + availableLangs,
  1974 + appVersion
  1975 + };
  1976 + }
  1977 +
  1978 + function getLangRevisionMap(seq, str) {
  1979 + return str.split(',').reduce((seq, cur) => {
  1980 + const [lang, rev] = getLangRevisionTuple(cur);
  1981 + seq[lang] = rev;
  1982 + return seq;
  1983 + }, seq);
  1984 + }
  1985 +
  1986 + function getLangRevisionTuple(str) {
  1987 + const [lang, rev] = str.trim().split(':');
  1988 + // if revision is missing, use NaN
  1989 + return [lang, parseInt(rev)];
  1990 + }
  1991 +
  1992 + function negotiateLanguages(
  1993 + fn, appVersion, defaultLang, availableLangs, additionalLangs, prevLangs,
  1994 + requestedLangs) {
  1995 +
  1996 + const allAvailableLangs = Object.keys(availableLangs).concat(
  1997 + additionalLangs || []).concat(Object.keys(pseudo));
  1998 + const newLangs = prioritizeLocales(
  1999 + defaultLang, allAvailableLangs, requestedLangs);
  2000 +
  2001 + const langs = newLangs.map(code => ({
  2002 + code: code,
  2003 + src: getLangSource(appVersion, availableLangs, additionalLangs, code),
  2004 + }));
  2005 +
  2006 + if (!arrEqual(prevLangs, newLangs)) {
  2007 + fn(langs);
  2008 + }
  2009 +
  2010 + return langs;
  2011 + }
  2012 +
  2013 + function arrEqual(arr1, arr2) {
  2014 + return arr1.length === arr2.length &&
  2015 + arr1.every((elem, i) => elem === arr2[i]);
  2016 + }
  2017 +
  2018 + function getMatchingLangpack(appVersion, langpacks) {
  2019 + for (let i = 0, langpack; (langpack = langpacks[i]); i++) {
  2020 + if (langpack.target === appVersion) {
  2021 + return langpack;
  2022 + }
  2023 + }
  2024 + return null;
  2025 + }
  2026 +
  2027 + function getLangSource(appVersion, availableLangs, additionalLangs, code) {
  2028 + if (additionalLangs && additionalLangs[code]) {
  2029 + const lp = getMatchingLangpack(appVersion, additionalLangs[code]);
  2030 + if (lp &&
  2031 + (!(code in availableLangs) ||
  2032 + parseInt(lp.revision) > availableLangs[code])) {
  2033 + return 'extra';
  2034 + }
  2035 + }
  2036 +
  2037 + if ((code in pseudo) && !(code in availableLangs)) {
  2038 + return 'pseudo';
  2039 + }
  2040 +
  2041 + return 'app';
  2042 + }
  2043 +
  2044 + class Remote {
  2045 + constructor(fetchResource, broadcast, requestedLangs) {
  2046 + this.fetchResource = fetchResource;
  2047 + this.broadcast = broadcast;
  2048 + this.ctxs = new Map();
  2049 + this.interactive = documentReady().then(
  2050 + () => this.init(requestedLangs));
  2051 + }
  2052 +
  2053 + init(requestedLangs) {
  2054 + const meta = getMeta(document.head);
  2055 + this.defaultLanguage = meta.defaultLang;
  2056 + this.availableLanguages = meta.availableLangs;
  2057 + this.appVersion = meta.appVersion;
  2058 +
  2059 + this.env = new Env(
  2060 + this.defaultLanguage,
  2061 + (...args) => this.fetchResource(this.appVersion, ...args));
  2062 +
  2063 + return this.requestLanguages(requestedLangs);
  2064 + }
  2065 +
  2066 + registerView(view, resources) {
  2067 + return this.interactive.then(() => {
  2068 + this.ctxs.set(view, this.env.createContext(resources));
  2069 + return true;
  2070 + });
  2071 + }
  2072 +
  2073 + unregisterView(view) {
  2074 + return this.ctxs.delete(view);
  2075 + }
  2076 +
  2077 + resolveEntities(view, langs, keys) {
  2078 + return this.ctxs.get(view).resolveEntities(langs, keys);
  2079 + }
  2080 +
  2081 + formatValues(view, keys) {
  2082 + return this.languages.then(
  2083 + langs => this.ctxs.get(view).resolveValues(langs, keys));
  2084 + }
  2085 +
  2086 + resolvedLanguages() {
  2087 + return this.languages;
  2088 + }
  2089 +
  2090 + requestLanguages(requestedLangs) {
  2091 + return changeLanguages.call(
  2092 + this, getAdditionalLanguages(), requestedLangs);
  2093 + }
  2094 +
  2095 + getName(code) {
  2096 + return pseudo[code].name;
  2097 + }
  2098 +
  2099 + processString(code, str) {
  2100 + return pseudo[code].process(str);
  2101 + }
  2102 +
  2103 + handleEvent(evt) {
  2104 + return changeLanguages.call(
  2105 + this, evt.detail || getAdditionalLanguages(), navigator.languages);
  2106 + }
  2107 + }
  2108 +
  2109 + function getAdditionalLanguages() {
  2110 + if (navigator.mozApps && navigator.mozApps.getAdditionalLanguages) {
  2111 + return navigator.mozApps.getAdditionalLanguages().catch(
  2112 + () => []);
  2113 + }
  2114 +
  2115 + return Promise.resolve([]);
  2116 + }
  2117 +
  2118 + function changeLanguages(additionalLangs, requestedLangs) {
  2119 + const prevLangs = this.languages || [];
  2120 + return this.languages = Promise.all([
  2121 + additionalLangs, prevLangs]).then(
  2122 + ([additionalLangs, prevLangs]) => negotiateLanguages(
  2123 + this.broadcast.bind(this, 'translateDocument'),
  2124 + this.appVersion, this.defaultLanguage, this.availableLanguages,
  2125 + additionalLangs, prevLangs, requestedLangs));
  2126 + }
  2127 +
  2128 + // match the opening angle bracket (<) in HTML tags, and HTML entities like
  2129 + // &amp;, &#0038;, &#x0026;.
  2130 + const reOverlay = /<|&#?\w+;/;
  2131 +
  2132 + const allowed = {
  2133 + elements: [
  2134 + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data',
  2135 + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u',
  2136 + 'mark', 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr'
  2137 + ],
  2138 + attributes: {
  2139 + global: [ 'title', 'aria-label', 'aria-valuetext', 'aria-moz-hint' ],
  2140 + a: [ 'download' ],
  2141 + area: [ 'download', 'alt' ],
  2142 + // value is special-cased in isAttrAllowed
  2143 + input: [ 'alt', 'placeholder' ],
  2144 + menuitem: [ 'label' ],
  2145 + menu: [ 'label' ],
  2146 + optgroup: [ 'label' ],
  2147 + option: [ 'label' ],
  2148 + track: [ 'label' ],
  2149 + img: [ 'alt' ],
  2150 + textarea: [ 'placeholder' ],
  2151 + th: [ 'abbr']
  2152 + }
  2153 + };
  2154 +
  2155 + function overlayElement(element, translation) {
  2156 + const value = translation.value;
  2157 +
  2158 + if (typeof value === 'string') {
  2159 + if (!reOverlay.test(value)) {
  2160 + element.textContent = value;
  2161 + } else {
  2162 + // start with an inert template element and move its children into
  2163 + // `element` but such that `element`'s own children are not replaced
  2164 + const tmpl = element.ownerDocument.createElement('template');
  2165 + tmpl.innerHTML = value;
  2166 + // overlay the node with the DocumentFragment
  2167 + overlay(element, tmpl.content);
  2168 + }
  2169 + }
  2170 +
  2171 + for (let key in translation.attrs) {
  2172 + const attrName = camelCaseToDashed(key);
  2173 + if (isAttrAllowed({ name: attrName }, element)) {
  2174 + element.setAttribute(attrName, translation.attrs[key]);
  2175 + }
  2176 + }
  2177 + }
  2178 +
  2179 + // The goal of overlay is to move the children of `translationElement`
  2180 + // into `sourceElement` such that `sourceElement`'s own children are not
  2181 + // replaced, but onle have their text nodes and their attributes modified.
  2182 + //
  2183 + // We want to make it possible for localizers to apply text-level semantics to
  2184 + // the translations and make use of HTML entities. At the same time, we
  2185 + // don't trust translations so we need to filter unsafe elements and
  2186 + // attribtues out and we don't want to break the Web by replacing elements to
  2187 + // which third-party code might have created references (e.g. two-way
  2188 + // bindings in MVC frameworks).
  2189 + function overlay(sourceElement, translationElement) {
  2190 + const result = translationElement.ownerDocument.createDocumentFragment();
  2191 + let k, attr;
  2192 +
  2193 + // take one node from translationElement at a time and check it against
  2194 + // the allowed list or try to match it with a corresponding element
  2195 + // in the source
  2196 + let childElement;
  2197 + while ((childElement = translationElement.childNodes[0])) {
  2198 + translationElement.removeChild(childElement);
  2199 +
  2200 + if (childElement.nodeType === childElement.TEXT_NODE) {
  2201 + result.appendChild(childElement);
  2202 + continue;
  2203 + }
  2204 +
  2205 + const index = getIndexOfType(childElement);
  2206 + const sourceChild = getNthElementOfType(sourceElement, childElement, index);
  2207 + if (sourceChild) {
  2208 + // there is a corresponding element in the source, let's use it
  2209 + overlay(sourceChild, childElement);
  2210 + result.appendChild(sourceChild);
  2211 + continue;
  2212 + }
  2213 +
  2214 + if (isElementAllowed(childElement)) {
  2215 + const sanitizedChild = childElement.ownerDocument.createElement(
  2216 + childElement.nodeName);
  2217 + overlay(sanitizedChild, childElement);
  2218 + result.appendChild(sanitizedChild);
  2219 + continue;
  2220 + }
  2221 +
  2222 + // otherwise just take this child's textContent
  2223 + result.appendChild(
  2224 + translationElement.ownerDocument.createTextNode(
  2225 + childElement.textContent));
  2226 + }
  2227 +
  2228 + // clear `sourceElement` and append `result` which by this time contains
  2229 + // `sourceElement`'s original children, overlayed with translation
  2230 + sourceElement.textContent = '';
  2231 + sourceElement.appendChild(result);
  2232 +
  2233 + // if we're overlaying a nested element, translate the allowed
  2234 + // attributes; top-level attributes are handled in `translateElement`
  2235 + // XXX attributes previously set here for another language should be
  2236 + // cleared if a new language doesn't use them; https://bugzil.la/922577
  2237 + if (translationElement.attributes) {
  2238 + for (k = 0, attr; (attr = translationElement.attributes[k]); k++) {
  2239 + if (isAttrAllowed(attr, sourceElement)) {
  2240 + sourceElement.setAttribute(attr.name, attr.value);
  2241 + }
  2242 + }
  2243 + }
  2244 + }
  2245 +
  2246 + // XXX the allowed list should be amendable; https://bugzil.la/922573
  2247 + function isElementAllowed(element) {
  2248 + return allowed.elements.indexOf(element.tagName.toLowerCase()) !== -1;
  2249 + }
  2250 +
  2251 + function isAttrAllowed(attr, element) {
  2252 + const attrName = attr.name.toLowerCase();
  2253 + const tagName = element.tagName.toLowerCase();
  2254 + // is it a globally safe attribute?
  2255 + if (allowed.attributes.global.indexOf(attrName) !== -1) {
  2256 + return true;
  2257 + }
  2258 + // are there no allowed attributes for this element?
  2259 + if (!allowed.attributes[tagName]) {
  2260 + return false;
  2261 + }
  2262 + // is it allowed on this element?
  2263 + // XXX the allowed list should be amendable; https://bugzil.la/922573
  2264 + if (allowed.attributes[tagName].indexOf(attrName) !== -1) {
  2265 + return true;
  2266 + }
  2267 + // special case for value on inputs with type button, reset, submit
  2268 + if (tagName === 'input' && attrName === 'value') {
  2269 + const type = element.type.toLowerCase();
  2270 + if (type === 'submit' || type === 'button' || type === 'reset') {
  2271 + return true;
  2272 + }
  2273 + }
  2274 + return false;
  2275 + }
  2276 +
  2277 + // Get n-th immediate child of context that is of the same type as element.
  2278 + // XXX Use querySelector(':scope > ELEMENT:nth-of-type(index)'), when:
  2279 + // 1) :scope is widely supported in more browsers and 2) it works with
  2280 + // DocumentFragments.
  2281 + function getNthElementOfType(context, element, index) {
  2282 + /* jshint boss:true */
  2283 + let nthOfType = 0;
  2284 + for (let i = 0, child; child = context.children[i]; i++) {
  2285 + if (child.nodeType === child.ELEMENT_NODE &&
  2286 + child.tagName === element.tagName) {
  2287 + if (nthOfType === index) {
  2288 + return child;
  2289 + }
  2290 + nthOfType++;
  2291 + }
  2292 + }
  2293 + return null;
  2294 + }
  2295 +
  2296 + // Get the index of the element among siblings of the same type.
  2297 + function getIndexOfType(element) {
  2298 + let index = 0;
  2299 + let child;
  2300 + while ((child = element.previousElementSibling)) {
  2301 + if (child.tagName === element.tagName) {
  2302 + index++;
  2303 + }
  2304 + }
  2305 + return index;
  2306 + }
  2307 +
  2308 + function camelCaseToDashed(string) {
  2309 + // XXX workaround for https://bugzil.la/1141934
  2310 + if (string === 'ariaValueText') {
  2311 + return 'aria-valuetext';
  2312 + }
  2313 +
  2314 + return string
  2315 + .replace(/[A-Z]/g, function (match) {
  2316 + return '-' + match.toLowerCase();
  2317 + })
  2318 + .replace(/^-/, '');
  2319 + }
  2320 +
  2321 + const reHtml = /[&<>]/g;
  2322 + const htmlEntities = {
  2323 + '&': '&amp;',
  2324 + '<': '&lt;',
  2325 + '>': '&gt;',
  2326 + };
  2327 +
  2328 + function getResourceLinks(head) {
  2329 + return Array.prototype.map.call(
  2330 + head.querySelectorAll('link[rel="localization"]'),
  2331 + el => el.getAttribute('href'));
  2332 + }
  2333 +
  2334 + function setAttributes(element, id, args) {
  2335 + element.setAttribute('data-l10n-id', id);
  2336 + if (args) {
  2337 + element.setAttribute('data-l10n-args', JSON.stringify(args));
  2338 + }
  2339 + }
  2340 +
  2341 + function getAttributes(element) {
  2342 + return {
  2343 + id: element.getAttribute('data-l10n-id'),
  2344 + args: JSON.parse(element.getAttribute('data-l10n-args'))
  2345 + };
  2346 + }
  2347 +
  2348 + function getTranslatables(element) {
  2349 + const nodes = Array.from(element.querySelectorAll('[data-l10n-id]'));
  2350 +
  2351 + if (typeof element.hasAttribute === 'function' &&
  2352 + element.hasAttribute('data-l10n-id')) {
  2353 + nodes.push(element);
  2354 + }
  2355 +
  2356 + return nodes;
  2357 + }
  2358 +
  2359 + function translateMutations(view, langs, mutations) {
  2360 + const targets = new Set();
  2361 +
  2362 + for (let mutation of mutations) {
  2363 + switch (mutation.type) {
  2364 + case 'attributes':
  2365 + targets.add(mutation.target);
  2366 + break;
  2367 + case 'childList':
  2368 + for (let addedNode of mutation.addedNodes) {
  2369 + if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
  2370 + if (addedNode.childElementCount) {
  2371 + getTranslatables(addedNode).forEach(targets.add.bind(targets));
  2372 + } else {
  2373 + if (addedNode.hasAttribute('data-l10n-id')) {
  2374 + targets.add(addedNode);
  2375 + }
  2376 + }
  2377 + }
  2378 + }
  2379 + break;
  2380 + }
  2381 + }
  2382 +
  2383 + if (targets.size === 0) {
  2384 + return;
  2385 + }
  2386 +
  2387 + translateElements(view, langs, Array.from(targets));
  2388 + }
  2389 +
  2390 + function translateFragment(view, langs, frag) {
  2391 + return translateElements(view, langs, getTranslatables(frag));
  2392 + }
  2393 +
  2394 + function getElementsTranslation(view, langs, elems) {
  2395 + const keys = elems.map(elem => {
  2396 + const id = elem.getAttribute('data-l10n-id');
  2397 + const args = elem.getAttribute('data-l10n-args');
  2398 + return args ? [
  2399 + id,
  2400 + JSON.parse(args.replace(reHtml, match => htmlEntities[match]))
  2401 + ] : id;
  2402 + });
  2403 +
  2404 + return view._resolveEntities(langs, keys);
  2405 + }
  2406 +
  2407 + function translateElements(view, langs, elements) {
  2408 + return getElementsTranslation(view, langs, elements).then(
  2409 + translations => applyTranslations(view, elements, translations));
  2410 + }
  2411 +
  2412 + function applyTranslations(view, elems, translations) {
  2413 + view._disconnect();
  2414 + for (let i = 0; i < elems.length; i++) {
  2415 + overlayElement(elems[i], translations[i]);
  2416 + }
  2417 + view._observe();
  2418 + }
  2419 +
  2420 + const observerConfig = {
  2421 + attributes: true,
  2422 + characterData: false,
  2423 + childList: true,
  2424 + subtree: true,
  2425 + attributeFilter: ['data-l10n-id', 'data-l10n-args']
  2426 + };
  2427 +
  2428 + const readiness = new WeakMap();
  2429 +
  2430 + class View {
  2431 + constructor(client, doc) {
  2432 + this._doc = doc;
  2433 + this.pseudo = {
  2434 + 'fr-x-psaccent': createPseudo(this, 'fr-x-psaccent'),
  2435 + 'ar-x-psbidi': createPseudo(this, 'ar-x-psbidi')
  2436 + };
  2437 +
  2438 + this._interactive = documentReady().then(
  2439 + () => init(this, client));
  2440 +
  2441 + const observer = new MutationObserver(onMutations.bind(this));
  2442 + this._observe = () => observer.observe(doc, observerConfig);
  2443 + this._disconnect = () => observer.disconnect();
  2444 +
  2445 + const translateView = langs => translateDocument(this, langs);
  2446 + client.on('translateDocument', translateView);
  2447 + this.ready = this._interactive.then(
  2448 + client => client.method('resolvedLanguages')).then(
  2449 + translateView);
  2450 + }
  2451 +
  2452 + requestLanguages(langs, global) {
  2453 + return this._interactive.then(
  2454 + client => client.method('requestLanguages', langs, global));
  2455 + }
  2456 +
  2457 + _resolveEntities(langs, keys) {
  2458 + return this._interactive.then(
  2459 + client => client.method('resolveEntities', client.id, langs, keys));
  2460 + }
  2461 +
  2462 + formatValue(id, args) {
  2463 + return this._interactive.then(
  2464 + client => client.method('formatValues', client.id, [[id, args]])).then(
  2465 + values => values[0]);
  2466 + }
  2467 +
  2468 + formatValues(...keys) {
  2469 + return this._interactive.then(
  2470 + client => client.method('formatValues', client.id, keys));
  2471 + }
  2472 +
  2473 + translateFragment(frag) {
  2474 + return this._interactive.then(
  2475 + client => client.method('resolvedLanguages')).then(
  2476 + langs => translateFragment(this, langs, frag));
  2477 + }
  2478 + }
  2479 +
  2480 + View.prototype.setAttributes = setAttributes;
  2481 + View.prototype.getAttributes = getAttributes;
  2482 +
  2483 + function createPseudo(view, code) {
  2484 + return {
  2485 + getName: () => view._interactive.then(
  2486 + client => client.method('getName', code)),
  2487 + processString: str => view._interactive.then(
  2488 + client => client.method('processString', code, str)),
  2489 + };
  2490 + }
  2491 +
  2492 + function init(view, client) {
  2493 + view._observe();
  2494 + return client.method(
  2495 + 'registerView', client.id, getResourceLinks(view._doc.head)).then(
  2496 + () => client);
  2497 + }
  2498 +
  2499 + function onMutations(mutations) {
  2500 + return this._interactive.then(
  2501 + client => client.method('resolvedLanguages')).then(
  2502 + langs => translateMutations(this, langs, mutations));
  2503 + }
  2504 +
  2505 + function translateDocument(view, langs) {
  2506 + const html = view._doc.documentElement;
  2507 +
  2508 + if (readiness.has(html)) {
  2509 + return translateFragment(view, langs, html).then(
  2510 + () => setAllAndEmit(html, langs));
  2511 + }
  2512 +
  2513 + const translated =
  2514 + // has the document been already pre-translated?
  2515 + langs[0].code === html.getAttribute('lang') ?
  2516 + Promise.resolve() :
  2517 + translateFragment(view, langs, html).then(
  2518 + () => setLangDir(html, langs));
  2519 +
  2520 + return translated.then(() => {
  2521 + setLangs(html, langs);
  2522 + readiness.set(html, true);
  2523 + });
  2524 + }
  2525 +
  2526 + function setLangs(html, langs) {
  2527 + const codes = langs.map(lang => lang.code);
  2528 + html.setAttribute('langs', codes.join(' '));
  2529 + }
  2530 +
  2531 + function setLangDir(html, langs) {
  2532 + const code = langs[0].code;
  2533 + html.setAttribute('lang', code);
  2534 + html.setAttribute('dir', getDirection(code));
  2535 + }
  2536 +
  2537 + function setAllAndEmit(html, langs) {
  2538 + setLangDir(html, langs);
  2539 + setLangs(html, langs);
  2540 + html.parentNode.dispatchEvent(new CustomEvent('DOMRetranslated', {
  2541 + bubbles: false,
  2542 + cancelable: false,
  2543 + }));
  2544 + }
  2545 +
  2546 + const remote = new Remote(fetchResource, broadcast, navigator.languages);
  2547 + window.addEventListener('languagechange', remote);
  2548 + document.addEventListener('additionallanguageschange', remote);
  2549 +
  2550 + document.l10n = new View(
  2551 + new Client(remote), document);
  2552 +
  2553 + //Bug 1204660 - Temporary proxy for shared code. Will be removed once
  2554 + // l10n.js migration is completed.
  2555 + navigator.mozL10n = {
  2556 + setAttributes: document.l10n.setAttributes,
  2557 + getAttributes: document.l10n.getAttributes,
  2558 + formatValue: (...args) => document.l10n.formatValue(...args),
  2559 + translateFragment: (...args) => document.l10n.translateFragment(...args),
  2560 + once: cb => document.l10n.ready.then(cb),
  2561 + ready: cb => document.l10n.ready.then(() => {
  2562 + document.addEventListener('DOMRetranslated', cb);
  2563 + cb();
  2564 + }),
  2565 + };
  2566 +
  2567 +})();
0 2568 \ No newline at end of file
... ...
bower_components/l20n/dist/bundle/jsshell/l20n.js 0 → 100755
  1 +(function () { 'use strict';
  2 +
  3 + function L10nError(message, id, lang) {
  4 + this.name = 'L10nError';
  5 + this.message = message;
  6 + this.id = id;
  7 + this.lang = lang;
  8 + }
  9 + L10nError.prototype = Object.create(Error.prototype);
  10 + L10nError.prototype.constructor = L10nError;
  11 +
  12 + const MAX_PLACEABLES = 100;
  13 +
  14 + var L20nParser = {
  15 + parse: function(emit, string) {
  16 + this._source = string;
  17 + this._index = 0;
  18 + this._length = string.length;
  19 + this.entries = Object.create(null);
  20 + this.emit = emit;
  21 +
  22 + return this.getResource();
  23 + },
  24 +
  25 + getResource: function() {
  26 + this.getWS();
  27 + while (this._index < this._length) {
  28 + try {
  29 + this.getEntry();
  30 + } catch (e) {
  31 + if (e instanceof L10nError) {
  32 + // we want to recover, but we don't need it in entries
  33 + this.getJunkEntry();
  34 + if (!this.emit) {
  35 + throw e;
  36 + }
  37 + } else {