Commit 420ea6ab22154b7e1bb293b248599106ece52615
1 parent
82f84018
workcicle...
Showing
39 changed files
with
68 additions
and
53186 deletions
alasql-utility/alasql-utility.js
1 | -function alasql_selectData (data, fields, filters) { | |
2 | - if(fields.length == 0) | |
3 | - return []; | |
4 | - | |
5 | - var _fields = _addParenthesis(fields); | |
6 | - | |
7 | - var select = _alasql_SELECT(_fields); | |
8 | - | |
9 | - var where = ""; | |
10 | - if(filters && filters.length) { | |
11 | - var _filters = _copy(filters); | |
12 | - where = _alasql_WHERE(_filters); | |
13 | - } | |
14 | - | |
15 | - var query = select + " FROM ?" + where; | |
16 | - | |
17 | - return alasql(query, [data]); | |
18 | -} | |
19 | - | |
20 | -function alasql_complexSelectData (data, fields, filters, aggregators, orders) { | |
1 | +function alasql_QUERY (data, fields, filters, aggregators, orders) { | |
21 | 2 | if(fields.length == 0) |
22 | 3 | return []; |
23 | 4 | |
... | ... | @@ -134,17 +115,13 @@ function _addParenthesis (fields) { |
134 | 115 | } |
135 | 116 | |
136 | 117 | function _normalizeField (field) { |
137 | - /*DEPRECATED*/return "[" + field.substring(field.lastIndexOf(",") + 1, field.length) + "]"; | |
138 | - //return "[" + field + "]"; | |
118 | + return "[" + field + "]"; | |
139 | 119 | } |
140 | 120 | |
141 | -function transformData (data, fields, truncate) { | |
121 | +function alasql_transformData (data, fields, truncate) { | |
142 | 122 | if(!data || data.length == 0) |
143 | 123 | return []; |
144 | 124 | |
145 | - /*DEPRECATED*/for (var i=0; i < fields.length; i++) | |
146 | - fields[i] = fields[i].substring(fields[i].lastIndexOf(",") + 1, fields[i].length); | |
147 | - | |
148 | 125 | var tData = []; |
149 | 126 | |
150 | 127 | for (var i in fields){ | ... | ... |
bower_components/l20n/.bower.json deleted
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 | -} | |
37 | 0 | \ No newline at end of file |
bower_components/l20n/bower.json deleted
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 deleted
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 | -}); | |
661 | 0 | \ No newline at end of file |
bower_components/l20n/dist/bundle/bridge/l20n-client.js deleted
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 | -})(); | |
480 | 0 | \ No newline at end of file |
bower_components/l20n/dist/bundle/bridge/l20n-service.js deleted
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 | -})(); | |
2124 | 0 | \ No newline at end of file |
bower_components/l20n/dist/bundle/gaia/build/l20n.js deleted
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; | |
2906 | 0 | \ No newline at end of file |
bower_components/l20n/dist/bundle/gaia/l20n.js deleted
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 | -})(); | |
2568 | 0 | \ No newline at end of file |
bower_components/l20n/dist/bundle/jsshell/l20n.js deleted
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(); |