Commit 0e9aeacde8e18f3bef4eb06e0a831ded13a389e5
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 | + // &, &, &. | |
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 | + '&': '&', | |
204 | + '<': '<', | |
205 | + '>': '>', | |
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 (‪) | |
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 (‪) | |
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 | +// &, &, &. | |
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 | + '&': '&', | |
2516 | + '<': '<', | |
2517 | + '>': '>', | |
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 (‪) | |
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 | + // &, &, &. | |
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 | + '&': '&', | |
2324 | + '<': '<', | |
2325 | + '>': '>', | |
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 { | |