@vaadin-component-factory/vcf-pdf-viewer 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2395 @@
1
+ import './pdf.js';
2
+ import { f as fetchData } from './node_stream2.js';
3
+ import './util.js';
4
+ import './message_handler.js';
5
+
6
+ /**
7
+ * The `FluentType` class is the base of Fluent's type system.
8
+ *
9
+ * Fluent types wrap JavaScript values and store additional configuration for
10
+ * them, which can then be used in the `toString` method together with a proper
11
+ * `Intl` formatter.
12
+ */
13
+ class FluentType {
14
+ /**
15
+ * Create a `FluentType` instance.
16
+ *
17
+ * @param value The JavaScript value to wrap.
18
+ */
19
+ constructor(value) {
20
+ this.value = value;
21
+ }
22
+ /**
23
+ * Unwrap the raw value stored by this `FluentType`.
24
+ */
25
+ valueOf() {
26
+ return this.value;
27
+ }
28
+ }
29
+ /**
30
+ * A `FluentType` representing no correct value.
31
+ */
32
+ class FluentNone extends FluentType {
33
+ /**
34
+ * Create an instance of `FluentNone` with an optional fallback value.
35
+ * @param value The fallback value of this `FluentNone`.
36
+ */
37
+ constructor(value = "???") {
38
+ super(value);
39
+ }
40
+ /**
41
+ * Format this `FluentNone` to the fallback string.
42
+ */
43
+ toString(scope) {
44
+ return `{${this.value}}`;
45
+ }
46
+ }
47
+ /**
48
+ * A `FluentType` representing a number.
49
+ *
50
+ * A `FluentNumber` instance stores the number value of the number it
51
+ * represents. It may also store an option bag of options which will be passed
52
+ * to `Intl.NumerFormat` when the `FluentNumber` is formatted to a string.
53
+ */
54
+ class FluentNumber extends FluentType {
55
+ /**
56
+ * Create an instance of `FluentNumber` with options to the
57
+ * `Intl.NumberFormat` constructor.
58
+ *
59
+ * @param value The number value of this `FluentNumber`.
60
+ * @param opts Options which will be passed to `Intl.NumberFormat`.
61
+ */
62
+ constructor(value, opts = {}) {
63
+ super(value);
64
+ this.opts = opts;
65
+ }
66
+ /**
67
+ * Format this `FluentNumber` to a string.
68
+ */
69
+ toString(scope) {
70
+ try {
71
+ const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts);
72
+ return nf.format(this.value);
73
+ }
74
+ catch (err) {
75
+ scope.reportError(err);
76
+ return this.value.toString(10);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * A `FluentType` representing a date and time.
82
+ *
83
+ * A `FluentDateTime` instance stores the number value of the date it
84
+ * represents, as a numerical timestamp in milliseconds. It may also store an
85
+ * option bag of options which will be passed to `Intl.DateTimeFormat` when the
86
+ * `FluentDateTime` is formatted to a string.
87
+ */
88
+ class FluentDateTime extends FluentType {
89
+ /**
90
+ * Create an instance of `FluentDateTime` with options to the
91
+ * `Intl.DateTimeFormat` constructor.
92
+ *
93
+ * @param value The number value of this `FluentDateTime`, in milliseconds.
94
+ * @param opts Options which will be passed to `Intl.DateTimeFormat`.
95
+ */
96
+ constructor(value, opts = {}) {
97
+ super(value);
98
+ this.opts = opts;
99
+ }
100
+ /**
101
+ * Format this `FluentDateTime` to a string.
102
+ */
103
+ toString(scope) {
104
+ try {
105
+ const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
106
+ return dtf.format(this.value);
107
+ }
108
+ catch (err) {
109
+ scope.reportError(err);
110
+ return new Date(this.value).toISOString();
111
+ }
112
+ }
113
+ }
114
+
115
+ /* global Intl */
116
+ /**
117
+ * The maximum number of placeables which can be expanded in a single call to
118
+ * `formatPattern`. The limit protects against the Billion Laughs and Quadratic
119
+ * Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
120
+ */
121
+ const MAX_PLACEABLES = 100;
122
+ /** Unicode bidi isolation characters. */
123
+ const FSI = "\u2068";
124
+ const PDI = "\u2069";
125
+ /** Helper: match a variant key to the given selector. */
126
+ function match(scope, selector, key) {
127
+ if (key === selector) {
128
+ // Both are strings.
129
+ return true;
130
+ }
131
+ // XXX Consider comparing options too, e.g. minimumFractionDigits.
132
+ if (key instanceof FluentNumber &&
133
+ selector instanceof FluentNumber &&
134
+ key.value === selector.value) {
135
+ return true;
136
+ }
137
+ if (selector instanceof FluentNumber && typeof key === "string") {
138
+ let category = scope
139
+ .memoizeIntlObject(Intl.PluralRules, selector.opts)
140
+ .select(selector.value);
141
+ if (key === category) {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+ /** Helper: resolve the default variant from a list of variants. */
148
+ function getDefault(scope, variants, star) {
149
+ if (variants[star]) {
150
+ return resolvePattern(scope, variants[star].value);
151
+ }
152
+ scope.reportError(new RangeError("No default"));
153
+ return new FluentNone();
154
+ }
155
+ /** Helper: resolve arguments to a call expression. */
156
+ function getArguments(scope, args) {
157
+ const positional = [];
158
+ const named = Object.create(null);
159
+ for (const arg of args) {
160
+ if (arg.type === "narg") {
161
+ named[arg.name] = resolveExpression(scope, arg.value);
162
+ }
163
+ else {
164
+ positional.push(resolveExpression(scope, arg));
165
+ }
166
+ }
167
+ return { positional, named };
168
+ }
169
+ /** Resolve an expression to a Fluent type. */
170
+ function resolveExpression(scope, expr) {
171
+ switch (expr.type) {
172
+ case "str":
173
+ return expr.value;
174
+ case "num":
175
+ return new FluentNumber(expr.value, {
176
+ minimumFractionDigits: expr.precision,
177
+ });
178
+ case "var":
179
+ return resolveVariableReference(scope, expr);
180
+ case "mesg":
181
+ return resolveMessageReference(scope, expr);
182
+ case "term":
183
+ return resolveTermReference(scope, expr);
184
+ case "func":
185
+ return resolveFunctionReference(scope, expr);
186
+ case "select":
187
+ return resolveSelectExpression(scope, expr);
188
+ default:
189
+ return new FluentNone();
190
+ }
191
+ }
192
+ /** Resolve a reference to a variable. */
193
+ function resolveVariableReference(scope, { name }) {
194
+ let arg;
195
+ if (scope.params) {
196
+ // We're inside a TermReference. It's OK to reference undefined parameters.
197
+ if (Object.prototype.hasOwnProperty.call(scope.params, name)) {
198
+ arg = scope.params[name];
199
+ }
200
+ else {
201
+ return new FluentNone(`$${name}`);
202
+ }
203
+ }
204
+ else if (scope.args &&
205
+ Object.prototype.hasOwnProperty.call(scope.args, name)) {
206
+ // We're in the top-level Pattern or inside a MessageReference. Missing
207
+ // variables references produce ReferenceErrors.
208
+ arg = scope.args[name];
209
+ }
210
+ else {
211
+ scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
212
+ return new FluentNone(`$${name}`);
213
+ }
214
+ // Return early if the argument already is an instance of FluentType.
215
+ if (arg instanceof FluentType) {
216
+ return arg;
217
+ }
218
+ // Convert the argument to a Fluent type.
219
+ switch (typeof arg) {
220
+ case "string":
221
+ return arg;
222
+ case "number":
223
+ return new FluentNumber(arg);
224
+ case "object":
225
+ if (arg instanceof Date) {
226
+ return new FluentDateTime(arg.getTime());
227
+ }
228
+ // eslint-disable-next-line no-fallthrough
229
+ default:
230
+ scope.reportError(new TypeError(`Variable type not supported: $${name}, ${typeof arg}`));
231
+ return new FluentNone(`$${name}`);
232
+ }
233
+ }
234
+ /** Resolve a reference to another message. */
235
+ function resolveMessageReference(scope, { name, attr }) {
236
+ const message = scope.bundle._messages.get(name);
237
+ if (!message) {
238
+ scope.reportError(new ReferenceError(`Unknown message: ${name}`));
239
+ return new FluentNone(name);
240
+ }
241
+ if (attr) {
242
+ const attribute = message.attributes[attr];
243
+ if (attribute) {
244
+ return resolvePattern(scope, attribute);
245
+ }
246
+ scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
247
+ return new FluentNone(`${name}.${attr}`);
248
+ }
249
+ if (message.value) {
250
+ return resolvePattern(scope, message.value);
251
+ }
252
+ scope.reportError(new ReferenceError(`No value: ${name}`));
253
+ return new FluentNone(name);
254
+ }
255
+ /** Resolve a call to a Term with key-value arguments. */
256
+ function resolveTermReference(scope, { name, attr, args }) {
257
+ const id = `-${name}`;
258
+ const term = scope.bundle._terms.get(id);
259
+ if (!term) {
260
+ scope.reportError(new ReferenceError(`Unknown term: ${id}`));
261
+ return new FluentNone(id);
262
+ }
263
+ if (attr) {
264
+ const attribute = term.attributes[attr];
265
+ if (attribute) {
266
+ // Every TermReference has its own variables.
267
+ scope.params = getArguments(scope, args).named;
268
+ const resolved = resolvePattern(scope, attribute);
269
+ scope.params = null;
270
+ return resolved;
271
+ }
272
+ scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
273
+ return new FluentNone(`${id}.${attr}`);
274
+ }
275
+ scope.params = getArguments(scope, args).named;
276
+ const resolved = resolvePattern(scope, term.value);
277
+ scope.params = null;
278
+ return resolved;
279
+ }
280
+ /** Resolve a call to a Function with positional and key-value arguments. */
281
+ function resolveFunctionReference(scope, { name, args }) {
282
+ // Some functions are built-in. Others may be provided by the runtime via
283
+ // the `FluentBundle` constructor.
284
+ let func = scope.bundle._functions[name];
285
+ if (!func) {
286
+ scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
287
+ return new FluentNone(`${name}()`);
288
+ }
289
+ if (typeof func !== "function") {
290
+ scope.reportError(new TypeError(`Function ${name}() is not callable`));
291
+ return new FluentNone(`${name}()`);
292
+ }
293
+ try {
294
+ let resolved = getArguments(scope, args);
295
+ return func(resolved.positional, resolved.named);
296
+ }
297
+ catch (err) {
298
+ scope.reportError(err);
299
+ return new FluentNone(`${name}()`);
300
+ }
301
+ }
302
+ /** Resolve a select expression to the member object. */
303
+ function resolveSelectExpression(scope, { selector, variants, star }) {
304
+ let sel = resolveExpression(scope, selector);
305
+ if (sel instanceof FluentNone) {
306
+ return getDefault(scope, variants, star);
307
+ }
308
+ // Match the selector against keys of each variant, in order.
309
+ for (const variant of variants) {
310
+ const key = resolveExpression(scope, variant.key);
311
+ if (match(scope, sel, key)) {
312
+ return resolvePattern(scope, variant.value);
313
+ }
314
+ }
315
+ return getDefault(scope, variants, star);
316
+ }
317
+ /** Resolve a pattern (a complex string with placeables). */
318
+ function resolveComplexPattern(scope, ptn) {
319
+ if (scope.dirty.has(ptn)) {
320
+ scope.reportError(new RangeError("Cyclic reference"));
321
+ return new FluentNone();
322
+ }
323
+ // Tag the pattern as dirty for the purpose of the current resolution.
324
+ scope.dirty.add(ptn);
325
+ const result = [];
326
+ // Wrap interpolations with Directional Isolate Formatting characters
327
+ // only when the pattern has more than one element.
328
+ const useIsolating = scope.bundle._useIsolating && ptn.length > 1;
329
+ for (const elem of ptn) {
330
+ if (typeof elem === "string") {
331
+ result.push(scope.bundle._transform(elem));
332
+ continue;
333
+ }
334
+ scope.placeables++;
335
+ if (scope.placeables > MAX_PLACEABLES) {
336
+ scope.dirty.delete(ptn);
337
+ // This is a fatal error which causes the resolver to instantly bail out
338
+ // on this pattern. The length check protects against excessive memory
339
+ // usage, and throwing protects against eating up the CPU when long
340
+ // placeables are deeply nested.
341
+ throw new RangeError(`Too many placeables expanded: ${scope.placeables}, ` +
342
+ `max allowed is ${MAX_PLACEABLES}`);
343
+ }
344
+ if (useIsolating) {
345
+ result.push(FSI);
346
+ }
347
+ result.push(resolveExpression(scope, elem).toString(scope));
348
+ if (useIsolating) {
349
+ result.push(PDI);
350
+ }
351
+ }
352
+ scope.dirty.delete(ptn);
353
+ return result.join("");
354
+ }
355
+ /**
356
+ * Resolve a simple or a complex Pattern to a FluentString
357
+ * (which is really the string primitive).
358
+ */
359
+ function resolvePattern(scope, value) {
360
+ // Resolve a simple pattern.
361
+ if (typeof value === "string") {
362
+ return scope.bundle._transform(value);
363
+ }
364
+ return resolveComplexPattern(scope, value);
365
+ }
366
+
367
+ class Scope {
368
+ constructor(bundle, errors, args) {
369
+ /**
370
+ * The Set of patterns already encountered during this resolution.
371
+ * Used to detect and prevent cyclic resolutions.
372
+ * @ignore
373
+ */
374
+ this.dirty = new WeakSet();
375
+ /** A dict of parameters passed to a TermReference. */
376
+ this.params = null;
377
+ /**
378
+ * The running count of placeables resolved so far.
379
+ * Used to detect the Billion Laughs and Quadratic Blowup attacks.
380
+ * @ignore
381
+ */
382
+ this.placeables = 0;
383
+ this.bundle = bundle;
384
+ this.errors = errors;
385
+ this.args = args;
386
+ }
387
+ reportError(error) {
388
+ if (!this.errors || !(error instanceof Error)) {
389
+ throw error;
390
+ }
391
+ this.errors.push(error);
392
+ }
393
+ memoizeIntlObject(ctor, opts) {
394
+ let cache = this.bundle._intls.get(ctor);
395
+ if (!cache) {
396
+ cache = {};
397
+ this.bundle._intls.set(ctor, cache);
398
+ }
399
+ let id = JSON.stringify(opts);
400
+ if (!cache[id]) {
401
+ cache[id] = new ctor(this.bundle.locales, opts);
402
+ }
403
+ return cache[id];
404
+ }
405
+ }
406
+
407
+ /**
408
+ * @overview
409
+ *
410
+ * The FTL resolver ships with a number of functions built-in.
411
+ *
412
+ * Each function take two arguments:
413
+ * - args - an array of positional args
414
+ * - opts - an object of key-value args
415
+ *
416
+ * Arguments to functions are guaranteed to already be instances of
417
+ * `FluentValue`. Functions must return `FluentValues` as well.
418
+ */
419
+ function values(opts, allowed) {
420
+ const unwrapped = Object.create(null);
421
+ for (const [name, opt] of Object.entries(opts)) {
422
+ if (allowed.includes(name)) {
423
+ unwrapped[name] = opt.valueOf();
424
+ }
425
+ }
426
+ return unwrapped;
427
+ }
428
+ const NUMBER_ALLOWED = [
429
+ "unitDisplay",
430
+ "currencyDisplay",
431
+ "useGrouping",
432
+ "minimumIntegerDigits",
433
+ "minimumFractionDigits",
434
+ "maximumFractionDigits",
435
+ "minimumSignificantDigits",
436
+ "maximumSignificantDigits",
437
+ ];
438
+ /**
439
+ * The implementation of the `NUMBER()` builtin available to translations.
440
+ *
441
+ * Translations may call the `NUMBER()` builtin in order to specify formatting
442
+ * options of a number. For example:
443
+ *
444
+ * pi = The value of π is {NUMBER($pi, maximumFractionDigits: 2)}.
445
+ *
446
+ * The implementation expects an array of `FluentValues` representing the
447
+ * positional arguments, and an object of named `FluentValues` representing the
448
+ * named parameters.
449
+ *
450
+ * The following options are recognized:
451
+ *
452
+ * unitDisplay
453
+ * currencyDisplay
454
+ * useGrouping
455
+ * minimumIntegerDigits
456
+ * minimumFractionDigits
457
+ * maximumFractionDigits
458
+ * minimumSignificantDigits
459
+ * maximumSignificantDigits
460
+ *
461
+ * Other options are ignored.
462
+ *
463
+ * @param args The positional arguments passed to this `NUMBER()`.
464
+ * @param opts The named argments passed to this `NUMBER()`.
465
+ */
466
+ function NUMBER(args, opts) {
467
+ let arg = args[0];
468
+ if (arg instanceof FluentNone) {
469
+ return new FluentNone(`NUMBER(${arg.valueOf()})`);
470
+ }
471
+ if (arg instanceof FluentNumber) {
472
+ return new FluentNumber(arg.valueOf(), {
473
+ ...arg.opts,
474
+ ...values(opts, NUMBER_ALLOWED),
475
+ });
476
+ }
477
+ if (arg instanceof FluentDateTime) {
478
+ return new FluentNumber(arg.valueOf(), {
479
+ ...values(opts, NUMBER_ALLOWED),
480
+ });
481
+ }
482
+ throw new TypeError("Invalid argument to NUMBER");
483
+ }
484
+ const DATETIME_ALLOWED = [
485
+ "dateStyle",
486
+ "timeStyle",
487
+ "fractionalSecondDigits",
488
+ "dayPeriod",
489
+ "hour12",
490
+ "weekday",
491
+ "era",
492
+ "year",
493
+ "month",
494
+ "day",
495
+ "hour",
496
+ "minute",
497
+ "second",
498
+ "timeZoneName",
499
+ ];
500
+ /**
501
+ * The implementation of the `DATETIME()` builtin available to translations.
502
+ *
503
+ * Translations may call the `DATETIME()` builtin in order to specify
504
+ * formatting options of a number. For example:
505
+ *
506
+ * now = It's {DATETIME($today, month: "long")}.
507
+ *
508
+ * The implementation expects an array of `FluentValues` representing the
509
+ * positional arguments, and an object of named `FluentValues` representing the
510
+ * named parameters.
511
+ *
512
+ * The following options are recognized:
513
+ *
514
+ * dateStyle
515
+ * timeStyle
516
+ * fractionalSecondDigits
517
+ * dayPeriod
518
+ * hour12
519
+ * weekday
520
+ * era
521
+ * year
522
+ * month
523
+ * day
524
+ * hour
525
+ * minute
526
+ * second
527
+ * timeZoneName
528
+ *
529
+ * Other options are ignored.
530
+ *
531
+ * @param args The positional arguments passed to this `DATETIME()`.
532
+ * @param opts The named argments passed to this `DATETIME()`.
533
+ */
534
+ function DATETIME(args, opts) {
535
+ let arg = args[0];
536
+ if (arg instanceof FluentNone) {
537
+ return new FluentNone(`DATETIME(${arg.valueOf()})`);
538
+ }
539
+ if (arg instanceof FluentDateTime) {
540
+ return new FluentDateTime(arg.valueOf(), {
541
+ ...arg.opts,
542
+ ...values(opts, DATETIME_ALLOWED),
543
+ });
544
+ }
545
+ if (arg instanceof FluentNumber) {
546
+ return new FluentDateTime(arg.valueOf(), {
547
+ ...values(opts, DATETIME_ALLOWED),
548
+ });
549
+ }
550
+ throw new TypeError("Invalid argument to DATETIME");
551
+ }
552
+
553
+ const cache = new Map();
554
+ function getMemoizerForLocale(locales) {
555
+ const stringLocale = Array.isArray(locales) ? locales.join(" ") : locales;
556
+ let memoizer = cache.get(stringLocale);
557
+ if (memoizer === undefined) {
558
+ memoizer = new Map();
559
+ cache.set(stringLocale, memoizer);
560
+ }
561
+ return memoizer;
562
+ }
563
+
564
+ /**
565
+ * Message bundles are single-language stores of translation resources. They are
566
+ * responsible for formatting message values and attributes to strings.
567
+ */
568
+ class FluentBundle {
569
+ /**
570
+ * Create an instance of `FluentBundle`.
571
+ *
572
+ * @example
573
+ * ```js
574
+ * let bundle = new FluentBundle(["en-US", "en"]);
575
+ *
576
+ * let bundle = new FluentBundle(locales, {useIsolating: false});
577
+ *
578
+ * let bundle = new FluentBundle(locales, {
579
+ * useIsolating: true,
580
+ * functions: {
581
+ * NODE_ENV: () => process.env.NODE_ENV
582
+ * }
583
+ * });
584
+ * ```
585
+ *
586
+ * @param locales - Used to instantiate `Intl` formatters used by translations.
587
+ * @param options - Optional configuration for the bundle.
588
+ */
589
+ constructor(locales, { functions, useIsolating = true, transform = (v) => v, } = {}) {
590
+ /** @ignore */
591
+ this._terms = new Map();
592
+ /** @ignore */
593
+ this._messages = new Map();
594
+ this.locales = Array.isArray(locales) ? locales : [locales];
595
+ this._functions = {
596
+ NUMBER,
597
+ DATETIME,
598
+ ...functions,
599
+ };
600
+ this._useIsolating = useIsolating;
601
+ this._transform = transform;
602
+ this._intls = getMemoizerForLocale(locales);
603
+ }
604
+ /**
605
+ * Check if a message is present in the bundle.
606
+ *
607
+ * @param id - The identifier of the message to check.
608
+ */
609
+ hasMessage(id) {
610
+ return this._messages.has(id);
611
+ }
612
+ /**
613
+ * Return a raw unformatted message object from the bundle.
614
+ *
615
+ * Raw messages are `{value, attributes}` shapes containing translation units
616
+ * called `Patterns`. `Patterns` are implementation-specific; they should be
617
+ * treated as black boxes and formatted with `FluentBundle.formatPattern`.
618
+ *
619
+ * @param id - The identifier of the message to check.
620
+ */
621
+ getMessage(id) {
622
+ return this._messages.get(id);
623
+ }
624
+ /**
625
+ * Add a translation resource to the bundle.
626
+ *
627
+ * @example
628
+ * ```js
629
+ * let res = new FluentResource("foo = Foo");
630
+ * bundle.addResource(res);
631
+ * bundle.getMessage("foo");
632
+ * // → {value: .., attributes: {..}}
633
+ * ```
634
+ *
635
+ * @param res
636
+ * @param options
637
+ */
638
+ addResource(res, { allowOverrides = false, } = {}) {
639
+ const errors = [];
640
+ for (let i = 0; i < res.body.length; i++) {
641
+ let entry = res.body[i];
642
+ if (entry.id.startsWith("-")) {
643
+ // Identifiers starting with a dash (-) define terms. Terms are private
644
+ // and cannot be retrieved from FluentBundle.
645
+ if (allowOverrides === false && this._terms.has(entry.id)) {
646
+ errors.push(new Error(`Attempt to override an existing term: "${entry.id}"`));
647
+ continue;
648
+ }
649
+ this._terms.set(entry.id, entry);
650
+ }
651
+ else {
652
+ if (allowOverrides === false && this._messages.has(entry.id)) {
653
+ errors.push(new Error(`Attempt to override an existing message: "${entry.id}"`));
654
+ continue;
655
+ }
656
+ this._messages.set(entry.id, entry);
657
+ }
658
+ }
659
+ return errors;
660
+ }
661
+ /**
662
+ * Format a `Pattern` to a string.
663
+ *
664
+ * Format a raw `Pattern` into a string. `args` will be used to resolve
665
+ * references to variables passed as arguments to the translation.
666
+ *
667
+ * In case of errors `formatPattern` will try to salvage as much of the
668
+ * translation as possible and will still return a string. For performance
669
+ * reasons, the encountered errors are not returned but instead are appended
670
+ * to the `errors` array passed as the third argument.
671
+ *
672
+ * If `errors` is omitted, the first encountered error will be thrown.
673
+ *
674
+ * @example
675
+ * ```js
676
+ * let errors = [];
677
+ * bundle.addResource(
678
+ * new FluentResource("hello = Hello, {$name}!"));
679
+ *
680
+ * let hello = bundle.getMessage("hello");
681
+ * if (hello.value) {
682
+ * bundle.formatPattern(hello.value, {name: "Jane"}, errors);
683
+ * // Returns "Hello, Jane!" and `errors` is empty.
684
+ *
685
+ * bundle.formatPattern(hello.value, undefined, errors);
686
+ * // Returns "Hello, {$name}!" and `errors` is now:
687
+ * // [<ReferenceError: Unknown variable: name>]
688
+ * }
689
+ * ```
690
+ */
691
+ formatPattern(pattern, args = null, errors = null) {
692
+ // Resolve a simple pattern without creating a scope. No error handling is
693
+ // required; by definition simple patterns don't have placeables.
694
+ if (typeof pattern === "string") {
695
+ return this._transform(pattern);
696
+ }
697
+ // Resolve a complex pattern.
698
+ let scope = new Scope(this, errors, args);
699
+ try {
700
+ let value = resolveComplexPattern(scope, pattern);
701
+ return value.toString(scope);
702
+ }
703
+ catch (err) {
704
+ if (scope.errors && err instanceof Error) {
705
+ scope.errors.push(err);
706
+ return new FluentNone().toString(scope);
707
+ }
708
+ throw err;
709
+ }
710
+ }
711
+ }
712
+
713
+ // This regex is used to iterate through the beginnings of messages and terms.
714
+ // With the /m flag, the ^ matches at the beginning of every line.
715
+ const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */gm;
716
+ // Both Attributes and Variants are parsed in while loops. These regexes are
717
+ // used to break out of them.
718
+ const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y;
719
+ const RE_VARIANT_START = /\*?\[/y;
720
+ const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y;
721
+ const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y;
722
+ const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y;
723
+ const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/;
724
+ // A "run" is a sequence of text or string literal characters which don't
725
+ // require any special handling. For TextElements such special characters are: {
726
+ // (starts a placeable), and line breaks which require additional logic to check
727
+ // if the next line is indented. For StringLiterals they are: \ (starts an
728
+ // escape sequence), " (ends the literal), and line breaks which are not allowed
729
+ // in StringLiterals. Note that string runs may be empty; text runs may not.
730
+ const RE_TEXT_RUN = /([^{}\n\r]+)/y;
731
+ const RE_STRING_RUN = /([^\\"\n\r]*)/y;
732
+ // Escape sequences.
733
+ const RE_STRING_ESCAPE = /\\([\\"])/y;
734
+ const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y;
735
+ // Used for trimming TextElements and indents.
736
+ const RE_LEADING_NEWLINES = /^\n+/;
737
+ const RE_TRAILING_SPACES = / +$/;
738
+ // Used in makeIndent to strip spaces from blank lines and normalize CRLF to LF.
739
+ const RE_BLANK_LINES = / *\r?\n/g;
740
+ // Used in makeIndent to measure the indentation.
741
+ const RE_INDENT = /( *)$/;
742
+ // Common tokens.
743
+ const TOKEN_BRACE_OPEN = /{\s*/y;
744
+ const TOKEN_BRACE_CLOSE = /\s*}/y;
745
+ const TOKEN_BRACKET_OPEN = /\[\s*/y;
746
+ const TOKEN_BRACKET_CLOSE = /\s*] */y;
747
+ const TOKEN_PAREN_OPEN = /\s*\(\s*/y;
748
+ const TOKEN_ARROW = /\s*->\s*/y;
749
+ const TOKEN_COLON = /\s*:\s*/y;
750
+ // Note the optional comma. As a deviation from the Fluent EBNF, the parser
751
+ // doesn't enforce commas between call arguments.
752
+ const TOKEN_COMMA = /\s*,?\s*/y;
753
+ const TOKEN_BLANK = /\s+/y;
754
+ /**
755
+ * Fluent Resource is a structure storing parsed localization entries.
756
+ */
757
+ class FluentResource {
758
+ constructor(source) {
759
+ this.body = [];
760
+ RE_MESSAGE_START.lastIndex = 0;
761
+ let cursor = 0;
762
+ // Iterate over the beginnings of messages and terms to efficiently skip
763
+ // comments and recover from errors.
764
+ while (true) {
765
+ let next = RE_MESSAGE_START.exec(source);
766
+ if (next === null) {
767
+ break;
768
+ }
769
+ cursor = RE_MESSAGE_START.lastIndex;
770
+ try {
771
+ this.body.push(parseMessage(next[1]));
772
+ }
773
+ catch (err) {
774
+ if (err instanceof SyntaxError) {
775
+ // Don't report any Fluent syntax errors. Skip directly to the
776
+ // beginning of the next message or term.
777
+ continue;
778
+ }
779
+ throw err;
780
+ }
781
+ }
782
+ // The parser implementation is inlined below for performance reasons,
783
+ // as well as for convenience of accessing `source` and `cursor`.
784
+ // The parser focuses on minimizing the number of false negatives at the
785
+ // expense of increasing the risk of false positives. In other words, it
786
+ // aims at parsing valid Fluent messages with a success rate of 100%, but it
787
+ // may also parse a few invalid messages which the reference parser would
788
+ // reject. The parser doesn't perform any validation and may produce entries
789
+ // which wouldn't make sense in the real world. For best results users are
790
+ // advised to validate translations with the fluent-syntax parser
791
+ // pre-runtime.
792
+ // The parser makes an extensive use of sticky regexes which can be anchored
793
+ // to any offset of the source string without slicing it. Errors are thrown
794
+ // to bail out of parsing of ill-formed messages.
795
+ function test(re) {
796
+ re.lastIndex = cursor;
797
+ return re.test(source);
798
+ }
799
+ // Advance the cursor by the char if it matches. May be used as a predicate
800
+ // (was the match found?) or, if errorClass is passed, as an assertion.
801
+ function consumeChar(char, errorClass) {
802
+ if (source[cursor] === char) {
803
+ cursor++;
804
+ return true;
805
+ }
806
+ if (errorClass) {
807
+ throw new errorClass(`Expected ${char}`);
808
+ }
809
+ return false;
810
+ }
811
+ // Advance the cursor by the token if it matches. May be used as a predicate
812
+ // (was the match found?) or, if errorClass is passed, as an assertion.
813
+ function consumeToken(re, errorClass) {
814
+ if (test(re)) {
815
+ cursor = re.lastIndex;
816
+ return true;
817
+ }
818
+ if (errorClass) {
819
+ throw new errorClass(`Expected ${re.toString()}`);
820
+ }
821
+ return false;
822
+ }
823
+ // Execute a regex, advance the cursor, and return all capture groups.
824
+ function match(re) {
825
+ re.lastIndex = cursor;
826
+ let result = re.exec(source);
827
+ if (result === null) {
828
+ throw new SyntaxError(`Expected ${re.toString()}`);
829
+ }
830
+ cursor = re.lastIndex;
831
+ return result;
832
+ }
833
+ // Execute a regex, advance the cursor, and return the capture group.
834
+ function match1(re) {
835
+ return match(re)[1];
836
+ }
837
+ function parseMessage(id) {
838
+ let value = parsePattern();
839
+ let attributes = parseAttributes();
840
+ if (value === null && Object.keys(attributes).length === 0) {
841
+ throw new SyntaxError("Expected message value or attributes");
842
+ }
843
+ return { id, value, attributes };
844
+ }
845
+ function parseAttributes() {
846
+ let attrs = Object.create(null);
847
+ while (test(RE_ATTRIBUTE_START)) {
848
+ let name = match1(RE_ATTRIBUTE_START);
849
+ let value = parsePattern();
850
+ if (value === null) {
851
+ throw new SyntaxError("Expected attribute value");
852
+ }
853
+ attrs[name] = value;
854
+ }
855
+ return attrs;
856
+ }
857
+ function parsePattern() {
858
+ let first;
859
+ // First try to parse any simple text on the same line as the id.
860
+ if (test(RE_TEXT_RUN)) {
861
+ first = match1(RE_TEXT_RUN);
862
+ }
863
+ // If there's a placeable on the first line, parse a complex pattern.
864
+ if (source[cursor] === "{" || source[cursor] === "}") {
865
+ // Re-use the text parsed above, if possible.
866
+ return parsePatternElements(first ? [first] : [], Infinity);
867
+ }
868
+ // RE_TEXT_VALUE stops at newlines. Only continue parsing the pattern if
869
+ // what comes after the newline is indented.
870
+ let indent = parseIndent();
871
+ if (indent) {
872
+ if (first) {
873
+ // If there's text on the first line, the blank block is part of the
874
+ // translation content in its entirety.
875
+ return parsePatternElements([first, indent], indent.length);
876
+ }
877
+ // Otherwise, we're dealing with a block pattern, i.e. a pattern which
878
+ // starts on a new line. Discrad the leading newlines but keep the
879
+ // inline indent; it will be used by the dedentation logic.
880
+ indent.value = trim(indent.value, RE_LEADING_NEWLINES);
881
+ return parsePatternElements([indent], indent.length);
882
+ }
883
+ if (first) {
884
+ // It was just a simple inline text after all.
885
+ return trim(first, RE_TRAILING_SPACES);
886
+ }
887
+ return null;
888
+ }
889
+ // Parse a complex pattern as an array of elements.
890
+ function parsePatternElements(elements = [], commonIndent) {
891
+ while (true) {
892
+ if (test(RE_TEXT_RUN)) {
893
+ elements.push(match1(RE_TEXT_RUN));
894
+ continue;
895
+ }
896
+ if (source[cursor] === "{") {
897
+ elements.push(parsePlaceable());
898
+ continue;
899
+ }
900
+ if (source[cursor] === "}") {
901
+ throw new SyntaxError("Unbalanced closing brace");
902
+ }
903
+ let indent = parseIndent();
904
+ if (indent) {
905
+ elements.push(indent);
906
+ commonIndent = Math.min(commonIndent, indent.length);
907
+ continue;
908
+ }
909
+ break;
910
+ }
911
+ let lastIndex = elements.length - 1;
912
+ let lastElement = elements[lastIndex];
913
+ // Trim the trailing spaces in the last element if it's a TextElement.
914
+ if (typeof lastElement === "string") {
915
+ elements[lastIndex] = trim(lastElement, RE_TRAILING_SPACES);
916
+ }
917
+ let baked = [];
918
+ for (let element of elements) {
919
+ if (element instanceof Indent) {
920
+ // Dedent indented lines by the maximum common indent.
921
+ element = element.value.slice(0, element.value.length - commonIndent);
922
+ }
923
+ if (element) {
924
+ baked.push(element);
925
+ }
926
+ }
927
+ return baked;
928
+ }
929
+ function parsePlaceable() {
930
+ consumeToken(TOKEN_BRACE_OPEN, SyntaxError);
931
+ let selector = parseInlineExpression();
932
+ if (consumeToken(TOKEN_BRACE_CLOSE)) {
933
+ return selector;
934
+ }
935
+ if (consumeToken(TOKEN_ARROW)) {
936
+ let variants = parseVariants();
937
+ consumeToken(TOKEN_BRACE_CLOSE, SyntaxError);
938
+ return {
939
+ type: "select",
940
+ selector,
941
+ ...variants,
942
+ };
943
+ }
944
+ throw new SyntaxError("Unclosed placeable");
945
+ }
946
+ function parseInlineExpression() {
947
+ if (source[cursor] === "{") {
948
+ // It's a nested placeable.
949
+ return parsePlaceable();
950
+ }
951
+ if (test(RE_REFERENCE)) {
952
+ let [, sigil, name, attr = null] = match(RE_REFERENCE);
953
+ if (sigil === "$") {
954
+ return { type: "var", name };
955
+ }
956
+ if (consumeToken(TOKEN_PAREN_OPEN)) {
957
+ let args = parseArguments();
958
+ if (sigil === "-") {
959
+ // A parameterized term: -term(...).
960
+ return { type: "term", name, attr, args };
961
+ }
962
+ if (RE_FUNCTION_NAME.test(name)) {
963
+ return { type: "func", name, args };
964
+ }
965
+ throw new SyntaxError("Function names must be all upper-case");
966
+ }
967
+ if (sigil === "-") {
968
+ // A non-parameterized term: -term.
969
+ return {
970
+ type: "term",
971
+ name,
972
+ attr,
973
+ args: [],
974
+ };
975
+ }
976
+ return { type: "mesg", name, attr };
977
+ }
978
+ return parseLiteral();
979
+ }
980
+ function parseArguments() {
981
+ let args = [];
982
+ while (true) {
983
+ switch (source[cursor]) {
984
+ case ")": // End of the argument list.
985
+ cursor++;
986
+ return args;
987
+ case undefined: // EOF
988
+ throw new SyntaxError("Unclosed argument list");
989
+ }
990
+ args.push(parseArgument());
991
+ // Commas between arguments are treated as whitespace.
992
+ consumeToken(TOKEN_COMMA);
993
+ }
994
+ }
995
+ function parseArgument() {
996
+ let expr = parseInlineExpression();
997
+ if (expr.type !== "mesg") {
998
+ return expr;
999
+ }
1000
+ if (consumeToken(TOKEN_COLON)) {
1001
+ // The reference is the beginning of a named argument.
1002
+ return {
1003
+ type: "narg",
1004
+ name: expr.name,
1005
+ value: parseLiteral(),
1006
+ };
1007
+ }
1008
+ // It's a regular message reference.
1009
+ return expr;
1010
+ }
1011
+ function parseVariants() {
1012
+ let variants = [];
1013
+ let count = 0;
1014
+ let star;
1015
+ while (test(RE_VARIANT_START)) {
1016
+ if (consumeChar("*")) {
1017
+ star = count;
1018
+ }
1019
+ let key = parseVariantKey();
1020
+ let value = parsePattern();
1021
+ if (value === null) {
1022
+ throw new SyntaxError("Expected variant value");
1023
+ }
1024
+ variants[count++] = { key, value };
1025
+ }
1026
+ if (count === 0) {
1027
+ return null;
1028
+ }
1029
+ if (star === undefined) {
1030
+ throw new SyntaxError("Expected default variant");
1031
+ }
1032
+ return { variants, star };
1033
+ }
1034
+ function parseVariantKey() {
1035
+ consumeToken(TOKEN_BRACKET_OPEN, SyntaxError);
1036
+ let key;
1037
+ if (test(RE_NUMBER_LITERAL)) {
1038
+ key = parseNumberLiteral();
1039
+ }
1040
+ else {
1041
+ key = {
1042
+ type: "str",
1043
+ value: match1(RE_IDENTIFIER),
1044
+ };
1045
+ }
1046
+ consumeToken(TOKEN_BRACKET_CLOSE, SyntaxError);
1047
+ return key;
1048
+ }
1049
+ function parseLiteral() {
1050
+ if (test(RE_NUMBER_LITERAL)) {
1051
+ return parseNumberLiteral();
1052
+ }
1053
+ if (source[cursor] === '"') {
1054
+ return parseStringLiteral();
1055
+ }
1056
+ throw new SyntaxError("Invalid expression");
1057
+ }
1058
+ function parseNumberLiteral() {
1059
+ let [, value, fraction = ""] = match(RE_NUMBER_LITERAL);
1060
+ let precision = fraction.length;
1061
+ return {
1062
+ type: "num",
1063
+ value: parseFloat(value),
1064
+ precision,
1065
+ };
1066
+ }
1067
+ function parseStringLiteral() {
1068
+ consumeChar('"', SyntaxError);
1069
+ let value = "";
1070
+ while (true) {
1071
+ value += match1(RE_STRING_RUN);
1072
+ if (source[cursor] === "\\") {
1073
+ value += parseEscapeSequence();
1074
+ continue;
1075
+ }
1076
+ if (consumeChar('"')) {
1077
+ return { type: "str", value };
1078
+ }
1079
+ // We've reached an EOL of EOF.
1080
+ throw new SyntaxError("Unclosed string literal");
1081
+ }
1082
+ }
1083
+ // Unescape known escape sequences.
1084
+ function parseEscapeSequence() {
1085
+ if (test(RE_STRING_ESCAPE)) {
1086
+ return match1(RE_STRING_ESCAPE);
1087
+ }
1088
+ if (test(RE_UNICODE_ESCAPE)) {
1089
+ let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE);
1090
+ let codepoint = parseInt(codepoint4 || codepoint6, 16);
1091
+ return codepoint <= 0xd7ff || 0xe000 <= codepoint
1092
+ ? // It's a Unicode scalar value.
1093
+ String.fromCodePoint(codepoint)
1094
+ : // Lonely surrogates can cause trouble when the parsing result is
1095
+ // saved using UTF-8. Use U+FFFD REPLACEMENT CHARACTER instead.
1096
+ "�";
1097
+ }
1098
+ throw new SyntaxError("Unknown escape sequence");
1099
+ }
1100
+ // Parse blank space. Return it if it looks like indent before a pattern
1101
+ // line. Skip it othwerwise.
1102
+ function parseIndent() {
1103
+ let start = cursor;
1104
+ consumeToken(TOKEN_BLANK);
1105
+ // Check the first non-blank character after the indent.
1106
+ switch (source[cursor]) {
1107
+ case ".":
1108
+ case "[":
1109
+ case "*":
1110
+ case "}":
1111
+ case undefined: // EOF
1112
+ // A special character. End the Pattern.
1113
+ return false;
1114
+ case "{":
1115
+ // Placeables don't require indentation (in EBNF: block-placeable).
1116
+ // Continue the Pattern.
1117
+ return makeIndent(source.slice(start, cursor));
1118
+ }
1119
+ // If the first character on the line is not one of the special characters
1120
+ // listed above, it's a regular text character. Check if there's at least
1121
+ // one space of indent before it.
1122
+ if (source[cursor - 1] === " ") {
1123
+ // It's an indented text character (in EBNF: indented-char). Continue
1124
+ // the Pattern.
1125
+ return makeIndent(source.slice(start, cursor));
1126
+ }
1127
+ // A not-indented text character is likely the identifier of the next
1128
+ // message. End the Pattern.
1129
+ return false;
1130
+ }
1131
+ // Trim blanks in text according to the given regex.
1132
+ function trim(text, re) {
1133
+ return text.replace(re, "");
1134
+ }
1135
+ // Normalize a blank block and extract the indent details.
1136
+ function makeIndent(blank) {
1137
+ let value = blank.replace(RE_BLANK_LINES, "\n");
1138
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1139
+ let length = RE_INDENT.exec(blank)[1].length;
1140
+ return new Indent(value, length);
1141
+ }
1142
+ }
1143
+ }
1144
+ class Indent {
1145
+ constructor(value, length) {
1146
+ this.value = value;
1147
+ this.length = length;
1148
+ }
1149
+ }
1150
+
1151
+ /* eslint no-console: ["error", {allow: ["warn"]}] */
1152
+ /* global console */
1153
+ // Match the opening angle bracket (<) in HTML tags, and HTML entities like
1154
+ // &amp;, &#0038;, &#x0026;.
1155
+ const reOverlay = /<|&#?\w+;/;
1156
+ /**
1157
+ * Elements allowed in translations even if they are not present in the source
1158
+ * HTML. They are text-level elements as defined by the HTML5 spec:
1159
+ * https://www.w3.org/TR/html5/text-level-semantics.html with the exception of:
1160
+ *
1161
+ * - a - because we don't allow href on it anyways,
1162
+ * - ruby, rt, rp - because we don't allow nested elements to be inserted.
1163
+ */
1164
+ const TEXT_LEVEL_ELEMENTS = {
1165
+ "http://www.w3.org/1999/xhtml": [
1166
+ "em",
1167
+ "strong",
1168
+ "small",
1169
+ "s",
1170
+ "cite",
1171
+ "q",
1172
+ "dfn",
1173
+ "abbr",
1174
+ "data",
1175
+ "time",
1176
+ "code",
1177
+ "var",
1178
+ "samp",
1179
+ "kbd",
1180
+ "sub",
1181
+ "sup",
1182
+ "i",
1183
+ "b",
1184
+ "u",
1185
+ "mark",
1186
+ "bdi",
1187
+ "bdo",
1188
+ "span",
1189
+ "br",
1190
+ "wbr",
1191
+ ],
1192
+ };
1193
+ const LOCALIZABLE_ATTRIBUTES = {
1194
+ "http://www.w3.org/1999/xhtml": {
1195
+ global: ["title", "aria-label", "aria-valuetext"],
1196
+ a: ["download"],
1197
+ area: ["download", "alt"],
1198
+ // value is special-cased in isAttrNameLocalizable
1199
+ input: ["alt", "placeholder"],
1200
+ menuitem: ["label"],
1201
+ menu: ["label"],
1202
+ optgroup: ["label"],
1203
+ option: ["label"],
1204
+ track: ["label"],
1205
+ img: ["alt"],
1206
+ textarea: ["placeholder"],
1207
+ th: ["abbr"],
1208
+ },
1209
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": {
1210
+ global: [
1211
+ "accesskey",
1212
+ "aria-label",
1213
+ "aria-valuetext",
1214
+ "label",
1215
+ "title",
1216
+ "tooltiptext",
1217
+ ],
1218
+ description: ["value"],
1219
+ key: ["key", "keycode"],
1220
+ label: ["value"],
1221
+ textbox: ["placeholder", "value"],
1222
+ },
1223
+ };
1224
+ /**
1225
+ * Translate an element.
1226
+ *
1227
+ * Translate the element's text content and attributes. Some HTML markup is
1228
+ * allowed in the translation. The element's children with the data-l10n-name
1229
+ * attribute will be treated as arguments to the translation. If the
1230
+ * translation defines the same children, their attributes and text contents
1231
+ * will be used for translating the matching source child.
1232
+ *
1233
+ * @param {Element} element
1234
+ * @param {Object} translation
1235
+ * @private
1236
+ */
1237
+ function translateElement(element, translation) {
1238
+ const { value } = translation;
1239
+ if (typeof value === "string") {
1240
+ if (element.localName === "title" &&
1241
+ element.namespaceURI === "http://www.w3.org/1999/xhtml") {
1242
+ // A special case for the HTML title element whose content must be text.
1243
+ element.textContent = value;
1244
+ }
1245
+ else if (!reOverlay.test(value)) {
1246
+ // If the translation doesn't contain any markup skip the overlay logic.
1247
+ element.textContent = value;
1248
+ }
1249
+ else {
1250
+ // Else parse the translation's HTML using an inert template element,
1251
+ // sanitize it and replace the element's content.
1252
+ const templateElement = element.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml", "template");
1253
+ templateElement.innerHTML = value;
1254
+ overlayChildNodes(templateElement.content, element);
1255
+ }
1256
+ }
1257
+ // Even if the translation doesn't define any localizable attributes, run
1258
+ // overlayAttributes to remove any localizable attributes set by previous
1259
+ // translations.
1260
+ overlayAttributes(translation, element);
1261
+ }
1262
+ /**
1263
+ * Replace child nodes of an element with child nodes of another element.
1264
+ *
1265
+ * The contents of the target element will be cleared and fully replaced with
1266
+ * sanitized contents of the source element.
1267
+ *
1268
+ * @param {DocumentFragment} fromFragment - The source of children to overlay.
1269
+ * @param {Element} toElement - The target of the overlay.
1270
+ * @private
1271
+ */
1272
+ function overlayChildNodes(fromFragment, toElement) {
1273
+ for (const childNode of fromFragment.childNodes) {
1274
+ if (childNode.nodeType === childNode.TEXT_NODE) {
1275
+ // Keep the translated text node.
1276
+ continue;
1277
+ }
1278
+ if (childNode.hasAttribute("data-l10n-name")) {
1279
+ const sanitized = getNodeForNamedElement(toElement, childNode);
1280
+ fromFragment.replaceChild(sanitized, childNode);
1281
+ continue;
1282
+ }
1283
+ if (isElementAllowed(childNode)) {
1284
+ const sanitized = createSanitizedElement(childNode);
1285
+ fromFragment.replaceChild(sanitized, childNode);
1286
+ continue;
1287
+ }
1288
+ console.warn(`An element of forbidden type "${childNode.localName}" was found in ` +
1289
+ "the translation. Only safe text-level elements and elements with " +
1290
+ "data-l10n-name are allowed.");
1291
+ // If all else fails, replace the element with its text content.
1292
+ fromFragment.replaceChild(createTextNodeFromTextContent(childNode), childNode);
1293
+ }
1294
+ toElement.textContent = "";
1295
+ toElement.appendChild(fromFragment);
1296
+ }
1297
+ function hasAttribute(attributes, name) {
1298
+ if (!attributes) {
1299
+ return false;
1300
+ }
1301
+ for (let attr of attributes) {
1302
+ if (attr.name === name) {
1303
+ return true;
1304
+ }
1305
+ }
1306
+ return false;
1307
+ }
1308
+ /**
1309
+ * Transplant localizable attributes of an element to another element.
1310
+ *
1311
+ * Any localizable attributes already set on the target element will be
1312
+ * cleared.
1313
+ *
1314
+ * @param {Element|Object} fromElement - The source of child nodes to overlay.
1315
+ * @param {Element} toElement - The target of the overlay.
1316
+ * @private
1317
+ */
1318
+ function overlayAttributes(fromElement, toElement) {
1319
+ const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs")
1320
+ ? toElement
1321
+ .getAttribute("data-l10n-attrs")
1322
+ .split(",")
1323
+ .map(i => i.trim())
1324
+ : null;
1325
+ // Remove existing localizable attributes if they
1326
+ // will not be used in the new translation.
1327
+ for (const attr of Array.from(toElement.attributes)) {
1328
+ if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) &&
1329
+ !hasAttribute(fromElement.attributes, attr.name)) {
1330
+ toElement.removeAttribute(attr.name);
1331
+ }
1332
+ }
1333
+ // fromElement might be a {value, attributes} object as returned by
1334
+ // Localization.messageFromBundle. In which case attributes may be null to
1335
+ // save GC cycles.
1336
+ if (!fromElement.attributes) {
1337
+ return;
1338
+ }
1339
+ // Set localizable attributes.
1340
+ for (const attr of Array.from(fromElement.attributes)) {
1341
+ if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) &&
1342
+ toElement.getAttribute(attr.name) !== attr.value) {
1343
+ toElement.setAttribute(attr.name, attr.value);
1344
+ }
1345
+ }
1346
+ }
1347
+ /**
1348
+ * Sanitize a child element created by the translation.
1349
+ *
1350
+ * Try to find a corresponding child in sourceElement and use it as the base
1351
+ * for the sanitization. This will preserve functional attribtues defined on
1352
+ * the child element in the source HTML.
1353
+ *
1354
+ * @param {Element} sourceElement - The source for data-l10n-name lookups.
1355
+ * @param {Element} translatedChild - The translated child to be sanitized.
1356
+ * @returns {Element}
1357
+ * @private
1358
+ */
1359
+ function getNodeForNamedElement(sourceElement, translatedChild) {
1360
+ const childName = translatedChild.getAttribute("data-l10n-name");
1361
+ const sourceChild = sourceElement.querySelector(`[data-l10n-name="${childName}"]`);
1362
+ if (!sourceChild) {
1363
+ console.warn(`An element named "${childName}" wasn't found in the source.`);
1364
+ return createTextNodeFromTextContent(translatedChild);
1365
+ }
1366
+ if (sourceChild.localName !== translatedChild.localName) {
1367
+ console.warn(`An element named "${childName}" was found in the translation ` +
1368
+ `but its type ${translatedChild.localName} didn't match the ` +
1369
+ `element found in the source (${sourceChild.localName}).`);
1370
+ return createTextNodeFromTextContent(translatedChild);
1371
+ }
1372
+ // Remove it from sourceElement so that the translation cannot use
1373
+ // the same reference name again.
1374
+ sourceElement.removeChild(sourceChild);
1375
+ // We can't currently guarantee that a translation won't remove
1376
+ // sourceChild from the element completely, which could break the app if
1377
+ // it relies on an event handler attached to the sourceChild. Let's make
1378
+ // this limitation explicit for now by breaking the identitiy of the
1379
+ // sourceChild by cloning it. This will destroy all event handlers
1380
+ // attached to sourceChild via addEventListener and via on<name>
1381
+ // properties.
1382
+ const clone = sourceChild.cloneNode(false);
1383
+ return shallowPopulateUsing(translatedChild, clone);
1384
+ }
1385
+ /**
1386
+ * Sanitize an allowed element.
1387
+ *
1388
+ * Text-level elements allowed in translations may only use safe attributes
1389
+ * and will have any nested markup stripped to text content.
1390
+ *
1391
+ * @param {Element} element - The element to be sanitized.
1392
+ * @returns {Element}
1393
+ * @private
1394
+ */
1395
+ function createSanitizedElement(element) {
1396
+ // Start with an empty element of the same type to remove nested children
1397
+ // and non-localizable attributes defined by the translation.
1398
+ const clone = element.ownerDocument.createElement(element.localName);
1399
+ return shallowPopulateUsing(element, clone);
1400
+ }
1401
+ /**
1402
+ * Convert an element to a text node.
1403
+ *
1404
+ * @param {Element} element - The element to be sanitized.
1405
+ * @returns {Node}
1406
+ * @private
1407
+ */
1408
+ function createTextNodeFromTextContent(element) {
1409
+ return element.ownerDocument.createTextNode(element.textContent);
1410
+ }
1411
+ /**
1412
+ * Check if element is allowed in the translation.
1413
+ *
1414
+ * This method is used by the sanitizer when the translation markup contains
1415
+ * an element which is not present in the source code.
1416
+ *
1417
+ * @param {Element} element
1418
+ * @returns {boolean}
1419
+ * @private
1420
+ */
1421
+ function isElementAllowed(element) {
1422
+ const allowed = TEXT_LEVEL_ELEMENTS[element.namespaceURI];
1423
+ return allowed && allowed.includes(element.localName);
1424
+ }
1425
+ /**
1426
+ * Check if attribute is allowed for the given element.
1427
+ *
1428
+ * This method is used by the sanitizer when the translation markup contains
1429
+ * DOM attributes, or when the translation has traits which map to DOM
1430
+ * attributes.
1431
+ *
1432
+ * `explicitlyAllowed` can be passed as a list of attributes explicitly
1433
+ * allowed on this element.
1434
+ *
1435
+ * @param {string} name
1436
+ * @param {Element} element
1437
+ * @param {Array} explicitlyAllowed
1438
+ * @returns {boolean}
1439
+ * @private
1440
+ */
1441
+ function isAttrNameLocalizable(name, element, explicitlyAllowed = null) {
1442
+ if (explicitlyAllowed && explicitlyAllowed.includes(name)) {
1443
+ return true;
1444
+ }
1445
+ const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI];
1446
+ if (!allowed) {
1447
+ return false;
1448
+ }
1449
+ const attrName = name.toLowerCase();
1450
+ const elemName = element.localName;
1451
+ // Is it a globally safe attribute?
1452
+ if (allowed.global.includes(attrName)) {
1453
+ return true;
1454
+ }
1455
+ // Are there no allowed attributes for this element?
1456
+ if (!allowed[elemName]) {
1457
+ return false;
1458
+ }
1459
+ // Is it allowed on this element?
1460
+ if (allowed[elemName].includes(attrName)) {
1461
+ return true;
1462
+ }
1463
+ // Special case for value on HTML inputs with type button, reset, submit
1464
+ if (element.namespaceURI === "http://www.w3.org/1999/xhtml" &&
1465
+ elemName === "input" &&
1466
+ attrName === "value") {
1467
+ const type = element.type.toLowerCase();
1468
+ if (type === "submit" || type === "button" || type === "reset") {
1469
+ return true;
1470
+ }
1471
+ }
1472
+ return false;
1473
+ }
1474
+ /**
1475
+ * Helper to set textContent and localizable attributes on an element.
1476
+ *
1477
+ * @param {Element} fromElement
1478
+ * @param {Element} toElement
1479
+ * @returns {Element}
1480
+ * @private
1481
+ */
1482
+ function shallowPopulateUsing(fromElement, toElement) {
1483
+ toElement.textContent = fromElement.textContent;
1484
+ overlayAttributes(fromElement, toElement);
1485
+ return toElement;
1486
+ }
1487
+
1488
+ /*
1489
+ * Base CachedIterable class.
1490
+ */
1491
+ class CachedIterable extends Array {
1492
+ /**
1493
+ * Create a `CachedIterable` instance from an iterable or, if another
1494
+ * instance of `CachedIterable` is passed, return it without any
1495
+ * modifications.
1496
+ *
1497
+ * @param {Iterable} iterable
1498
+ * @returns {CachedIterable}
1499
+ */
1500
+ static from(iterable) {
1501
+ if (iterable instanceof this) {
1502
+ return iterable;
1503
+ }
1504
+
1505
+ return new this(iterable);
1506
+ }
1507
+ }
1508
+
1509
+ /*
1510
+ * CachedAsyncIterable caches the elements yielded by an async iterable.
1511
+ *
1512
+ * It can be used to iterate over an iterable many times without depleting the
1513
+ * iterable.
1514
+ */
1515
+ class CachedAsyncIterable extends CachedIterable {
1516
+ /**
1517
+ * Create an `CachedAsyncIterable` instance.
1518
+ *
1519
+ * @param {Iterable} iterable
1520
+ * @returns {CachedAsyncIterable}
1521
+ */
1522
+ constructor(iterable) {
1523
+ super();
1524
+
1525
+ if (Symbol.asyncIterator in Object(iterable)) {
1526
+ this.iterator = iterable[Symbol.asyncIterator]();
1527
+ } else if (Symbol.iterator in Object(iterable)) {
1528
+ this.iterator = iterable[Symbol.iterator]();
1529
+ } else {
1530
+ throw new TypeError("Argument must implement the iteration protocol.");
1531
+ }
1532
+ }
1533
+
1534
+ /**
1535
+ * Asynchronous iterator caching the yielded elements.
1536
+ *
1537
+ * Elements yielded by the original iterable will be cached and available
1538
+ * synchronously. Returns an async generator object implementing the
1539
+ * iterator protocol over the elements of the original (async or sync)
1540
+ * iterable.
1541
+ */
1542
+ [Symbol.asyncIterator]() {
1543
+ const cached = this;
1544
+ let cur = 0;
1545
+
1546
+ return {
1547
+ async next() {
1548
+ if (cached.length <= cur) {
1549
+ cached.push(cached.iterator.next());
1550
+ }
1551
+ return cached[cur++];
1552
+ }
1553
+ };
1554
+ }
1555
+
1556
+ /**
1557
+ * This method allows user to consume the next element from the iterator
1558
+ * into the cache.
1559
+ *
1560
+ * @param {number} count - number of elements to consume
1561
+ */
1562
+ async touchNext(count = 1) {
1563
+ let idx = 0;
1564
+ while (idx++ < count) {
1565
+ const last = this[this.length - 1];
1566
+ if (last && (await last).done) {
1567
+ break;
1568
+ }
1569
+ this.push(this.iterator.next());
1570
+ }
1571
+ // Return the last cached {value, done} object to allow the calling
1572
+ // code to decide if it needs to call touchNext again.
1573
+ return this[this.length - 1];
1574
+ }
1575
+ }
1576
+
1577
+ /* eslint no-console: ["error", { allow: ["warn", "error"] }] */
1578
+ /**
1579
+ * The `Localization` class is a central high-level API for vanilla
1580
+ * JavaScript use of Fluent.
1581
+ * It combines language negotiation, FluentBundle and I/O to
1582
+ * provide a scriptable API to format translations.
1583
+ */
1584
+ class Localization {
1585
+ /**
1586
+ * @param {Array<String>} resourceIds - List of resource IDs
1587
+ * @param {Function} generateBundles - Function that returns a
1588
+ * generator over FluentBundles
1589
+ *
1590
+ * @returns {Localization}
1591
+ */
1592
+ constructor(resourceIds = [], generateBundles) {
1593
+ this.resourceIds = resourceIds;
1594
+ this.generateBundles = generateBundles;
1595
+ this.onChange(true);
1596
+ }
1597
+ addResourceIds(resourceIds, eager = false) {
1598
+ this.resourceIds.push(...resourceIds);
1599
+ this.onChange(eager);
1600
+ return this.resourceIds.length;
1601
+ }
1602
+ removeResourceIds(resourceIds) {
1603
+ this.resourceIds = this.resourceIds.filter(r => !resourceIds.includes(r));
1604
+ this.onChange();
1605
+ return this.resourceIds.length;
1606
+ }
1607
+ /**
1608
+ * Format translations and handle fallback if needed.
1609
+ *
1610
+ * Format translations for `keys` from `FluentBundle` instances on this
1611
+ * DOMLocalization. In case of errors, fetch the next context in the
1612
+ * fallback chain.
1613
+ *
1614
+ * @param {Array<Object>} keys - Translation keys to format.
1615
+ * @param {Function} method - Formatting function.
1616
+ * @returns {Promise<Array<string|Object>>}
1617
+ * @private
1618
+ */
1619
+ async formatWithFallback(keys, method) {
1620
+ const translations = [];
1621
+ let hasAtLeastOneBundle = false;
1622
+ for await (const bundle of this.bundles) {
1623
+ hasAtLeastOneBundle = true;
1624
+ const missingIds = keysFromBundle(method, bundle, keys, translations);
1625
+ if (missingIds.size === 0) {
1626
+ break;
1627
+ }
1628
+ if (typeof console !== "undefined") {
1629
+ const locale = bundle.locales[0];
1630
+ const ids = Array.from(missingIds).join(", ");
1631
+ console.warn(`[fluent] Missing translations in ${locale}: ${ids}`);
1632
+ }
1633
+ }
1634
+ if (!hasAtLeastOneBundle && typeof console !== "undefined") {
1635
+ // eslint-disable-next-line max-len
1636
+ console.warn(`[fluent] Request for keys failed because no resource bundles got generated.
1637
+ keys: ${JSON.stringify(keys)}.
1638
+ resourceIds: ${JSON.stringify(this.resourceIds)}.`);
1639
+ }
1640
+ return translations;
1641
+ }
1642
+ /**
1643
+ * Format translations into `{value, attributes}` objects.
1644
+ *
1645
+ * The fallback logic is the same as in `formatValues`
1646
+ * but it returns `{value, attributes}` objects
1647
+ * which are suitable for the translation of DOM elements.
1648
+ *
1649
+ * Returns a Promise resolving to an array of the translation strings.
1650
+ *
1651
+ * @example
1652
+ * ```js
1653
+ * docL10n.formatMessages([
1654
+ * {id: 'hello', args: { who: 'Mary' }},
1655
+ * {id: 'welcome'}
1656
+ * ]).then(console.log);
1657
+ *
1658
+ * // [
1659
+ * // { value: 'Hello, Mary!', attributes: null },
1660
+ * // {
1661
+ * // value: 'Welcome!',
1662
+ * // attributes: [ { name: "title", value: 'Hello' } ]
1663
+ * // }
1664
+ * // ]
1665
+ * ```
1666
+ *
1667
+ * @param {Array<Object>} keys
1668
+ * @returns {Promise<Array<{value: string, attributes: Object}>>}
1669
+ * @private
1670
+ */
1671
+ formatMessages(keys) {
1672
+ return this.formatWithFallback(keys, messageFromBundle);
1673
+ }
1674
+ /**
1675
+ * Retrieve translations corresponding to the passed keys.
1676
+ *
1677
+ * A generalized version of `DOMLocalization.formatValue`. Keys must
1678
+ * be `{id, args}` objects.
1679
+ *
1680
+ * Returns a Promise resolving to an array of the translation strings.
1681
+ *
1682
+ * @example
1683
+ * ```js
1684
+ * docL10n.formatValues([
1685
+ * {id: 'hello', args: { who: 'Mary' }},
1686
+ * {id: 'hello', args: { who: 'John' }},
1687
+ * {id: 'welcome'}
1688
+ * ]).then(console.log);
1689
+ *
1690
+ * // ['Hello, Mary!', 'Hello, John!', 'Welcome!']
1691
+ * ```
1692
+ *
1693
+ * @param {Array<Object>} keys
1694
+ * @returns {Promise<Array<string>>}
1695
+ */
1696
+ formatValues(keys) {
1697
+ return this.formatWithFallback(keys, valueFromBundle);
1698
+ }
1699
+ /**
1700
+ * Retrieve the translation corresponding to the `id` identifier.
1701
+ *
1702
+ * If passed, `args` is a simple hash object with a list of variables that
1703
+ * will be interpolated in the value of the translation.
1704
+ *
1705
+ * Returns a Promise resolving to the translation string.
1706
+ *
1707
+ * Use this sparingly for one-off messages which don't need to be
1708
+ * retranslated when the user changes their language preferences, e.g. in
1709
+ * notifications.
1710
+ *
1711
+ * @example
1712
+ * ```js
1713
+ * docL10n.formatValue(
1714
+ * 'hello', { who: 'world' }
1715
+ * ).then(console.log);
1716
+ *
1717
+ * // 'Hello, world!'
1718
+ * ```
1719
+ *
1720
+ * @param {string} id - Identifier of the translation to format
1721
+ * @param {Object} [args] - Optional external arguments
1722
+ * @returns {Promise<string>}
1723
+ */
1724
+ async formatValue(id, args) {
1725
+ const [val] = await this.formatValues([{ id, args }]);
1726
+ return val;
1727
+ }
1728
+ handleEvent() {
1729
+ this.onChange();
1730
+ }
1731
+ /**
1732
+ * This method should be called when there's a reason to believe
1733
+ * that language negotiation or available resources changed.
1734
+ */
1735
+ onChange(eager = false) {
1736
+ this.bundles = CachedAsyncIterable.from(this.generateBundles(this.resourceIds));
1737
+ if (eager) {
1738
+ this.bundles.touchNext(2);
1739
+ }
1740
+ }
1741
+ }
1742
+ /**
1743
+ * Format the value of a message into a string or `null`.
1744
+ *
1745
+ * This function is passed as a method to `keysFromBundle` and resolve
1746
+ * a value of a single L10n Entity using provided `FluentBundle`.
1747
+ *
1748
+ * If the message doesn't have a value, return `null`.
1749
+ *
1750
+ * @param {FluentBundle} bundle
1751
+ * @param {Array<Error>} errors
1752
+ * @param {Object} message
1753
+ * @param {Object} args
1754
+ * @returns {string|null}
1755
+ * @private
1756
+ */
1757
+ function valueFromBundle(bundle, errors, message, args) {
1758
+ if (message.value) {
1759
+ return bundle.formatPattern(message.value, args, errors);
1760
+ }
1761
+ return null;
1762
+ }
1763
+ /**
1764
+ * Format all public values of a message into a {value, attributes} object.
1765
+ *
1766
+ * This function is passed as a method to `keysFromBundle` and resolve
1767
+ * a single L10n Entity using provided `FluentBundle`.
1768
+ *
1769
+ * The function will return an object with a value and attributes of the
1770
+ * entity.
1771
+ *
1772
+ * @param {FluentBundle} bundle
1773
+ * @param {Array<Error>} errors
1774
+ * @param {Object} message
1775
+ * @param {Object} args
1776
+ * @returns {Object}
1777
+ * @private
1778
+ */
1779
+ function messageFromBundle(bundle, errors, message, args) {
1780
+ const formatted = {
1781
+ value: null,
1782
+ attributes: null,
1783
+ };
1784
+ if (message.value) {
1785
+ formatted.value = bundle.formatPattern(message.value, args, errors);
1786
+ }
1787
+ let attrNames = Object.keys(message.attributes);
1788
+ if (attrNames.length > 0) {
1789
+ formatted.attributes = new Array(attrNames.length);
1790
+ for (let [i, name] of attrNames.entries()) {
1791
+ let value = bundle.formatPattern(message.attributes[name], args, errors);
1792
+ formatted.attributes[i] = { name, value };
1793
+ }
1794
+ }
1795
+ return formatted;
1796
+ }
1797
+ /**
1798
+ * This function is an inner function for `Localization.formatWithFallback`.
1799
+ *
1800
+ * It takes a `FluentBundle`, list of l10n-ids and a method to be used for
1801
+ * key resolution (either `valueFromBundle` or `messageFromBundle`) and
1802
+ * optionally a value returned from `keysFromBundle` executed against
1803
+ * another `FluentBundle`.
1804
+ *
1805
+ * The idea here is that if the previous `FluentBundle` did not resolve
1806
+ * all keys, we're calling this function with the next context to resolve
1807
+ * the remaining ones.
1808
+ *
1809
+ * In the function, we loop over `keys` and check if we have the `prev`
1810
+ * passed and if it has an error entry for the position we're in.
1811
+ *
1812
+ * If it doesn't, it means that we have a good translation for this key and
1813
+ * we return it. If it does, we'll try to resolve the key using the passed
1814
+ * `FluentBundle`.
1815
+ *
1816
+ * In the end, we fill the translations array, and return the Set with
1817
+ * missing ids.
1818
+ *
1819
+ * See `Localization.formatWithFallback` for more info on how this is used.
1820
+ *
1821
+ * @param {Function} method
1822
+ * @param {FluentBundle} bundle
1823
+ * @param {Array<string>} keys
1824
+ * @param {{Array<{value: string, attributes: Object}>}} translations
1825
+ *
1826
+ * @returns {Set<string>}
1827
+ * @private
1828
+ */
1829
+ function keysFromBundle(method, bundle, keys, translations) {
1830
+ const messageErrors = [];
1831
+ const missingIds = new Set();
1832
+ keys.forEach(({ id, args }, i) => {
1833
+ if (translations[i] !== undefined) {
1834
+ return;
1835
+ }
1836
+ let message = bundle.getMessage(id);
1837
+ if (message) {
1838
+ messageErrors.length = 0;
1839
+ translations[i] = method(bundle, messageErrors, message, args);
1840
+ if (messageErrors.length > 0 && typeof console !== "undefined") {
1841
+ const locale = bundle.locales[0];
1842
+ const errors = messageErrors.join(", ");
1843
+ // eslint-disable-next-line max-len
1844
+ console.warn(`[fluent][resolver] errors in ${locale}/${id}: ${errors}.`);
1845
+ }
1846
+ }
1847
+ else {
1848
+ missingIds.add(id);
1849
+ }
1850
+ });
1851
+ return missingIds;
1852
+ }
1853
+
1854
+ const L10NID_ATTR_NAME = "data-l10n-id";
1855
+ const L10NARGS_ATTR_NAME = "data-l10n-args";
1856
+ const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`;
1857
+ /**
1858
+ * The `DOMLocalization` class is responsible for fetching resources and
1859
+ * formatting translations.
1860
+ *
1861
+ * It implements the fallback strategy in case of errors encountered during the
1862
+ * formatting of translations and methods for observing DOM
1863
+ * trees with a `MutationObserver`.
1864
+ */
1865
+ class DOMLocalization extends Localization {
1866
+ /**
1867
+ * @param {Array<String>} resourceIds - List of resource IDs
1868
+ * @param {Function} generateBundles - Function that returns a
1869
+ * generator over FluentBundles
1870
+ * @returns {DOMLocalization}
1871
+ */
1872
+ constructor(resourceIds, generateBundles) {
1873
+ super(resourceIds, generateBundles);
1874
+ // A Set of DOM trees observed by the `MutationObserver`.
1875
+ this.roots = new Set();
1876
+ // requestAnimationFrame handler.
1877
+ this.pendingrAF = null;
1878
+ // list of elements pending for translation.
1879
+ this.pendingElements = new Set();
1880
+ this.windowElement = null;
1881
+ this.mutationObserver = null;
1882
+ this.observerConfig = {
1883
+ attributes: true,
1884
+ characterData: false,
1885
+ childList: true,
1886
+ subtree: true,
1887
+ attributeFilter: [L10NID_ATTR_NAME, L10NARGS_ATTR_NAME],
1888
+ };
1889
+ }
1890
+ onChange(eager = false) {
1891
+ super.onChange(eager);
1892
+ if (this.roots) {
1893
+ this.translateRoots();
1894
+ }
1895
+ }
1896
+ /**
1897
+ * Set the `data-l10n-id` and `data-l10n-args` attributes on DOM elements.
1898
+ * FluentDOM makes use of mutation observers to detect changes
1899
+ * to `data-l10n-*` attributes and translate elements asynchronously.
1900
+ * `setAttributes` is a convenience method which allows to translate
1901
+ * DOM elements declaratively.
1902
+ *
1903
+ * You should always prefer to use `data-l10n-id` on elements (statically in
1904
+ * HTML or dynamically via `setAttributes`) over manually retrieving
1905
+ * translations with `format`. The use of attributes ensures that the
1906
+ * elements can be retranslated when the user changes their language
1907
+ * preferences.
1908
+ *
1909
+ * ```javascript
1910
+ * localization.setAttributes(
1911
+ * document.querySelector('#welcome'), 'hello', { who: 'world' }
1912
+ * );
1913
+ * ```
1914
+ *
1915
+ * This will set the following attributes on the `#welcome` element.
1916
+ * The MutationObserver will pick up this change and will localize the element
1917
+ * asynchronously.
1918
+ *
1919
+ * ```html
1920
+ * <p id='welcome'
1921
+ * data-l10n-id='hello'
1922
+ * data-l10n-args='{"who": "world"}'>
1923
+ * </p>
1924
+ * ```
1925
+ *
1926
+ * @param {Element} element - Element to set attributes on
1927
+ * @param {string} id - l10n-id string
1928
+ * @param {Object<string, string>} args - KVP list of l10n arguments
1929
+ * @returns {Element}
1930
+ */
1931
+ setAttributes(element, id, args) {
1932
+ element.setAttribute(L10NID_ATTR_NAME, id);
1933
+ if (args) {
1934
+ element.setAttribute(L10NARGS_ATTR_NAME, JSON.stringify(args));
1935
+ }
1936
+ else {
1937
+ element.removeAttribute(L10NARGS_ATTR_NAME);
1938
+ }
1939
+ return element;
1940
+ }
1941
+ /**
1942
+ * Get the `data-l10n-*` attributes from DOM elements.
1943
+ *
1944
+ * ```javascript
1945
+ * localization.getAttributes(
1946
+ * document.querySelector('#welcome')
1947
+ * );
1948
+ * // -> { id: 'hello', args: { who: 'world' } }
1949
+ * ```
1950
+ *
1951
+ * @param {Element} element - HTML element
1952
+ * @returns {{id: string, args: Object}}
1953
+ */
1954
+ getAttributes(element) {
1955
+ return {
1956
+ id: element.getAttribute(L10NID_ATTR_NAME),
1957
+ args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null),
1958
+ };
1959
+ }
1960
+ /**
1961
+ * Add `newRoot` to the list of roots managed by this `DOMLocalization`.
1962
+ *
1963
+ * Additionally, if this `DOMLocalization` has an observer, start observing
1964
+ * `newRoot` in order to translate mutations in it.
1965
+ *
1966
+ * @param {Element} newRoot - Root to observe.
1967
+ */
1968
+ connectRoot(newRoot) {
1969
+ for (const root of this.roots) {
1970
+ if (root === newRoot ||
1971
+ root.contains(newRoot) ||
1972
+ newRoot.contains(root)) {
1973
+ throw new Error("Cannot add a root that overlaps with existing root.");
1974
+ }
1975
+ }
1976
+ if (this.windowElement) {
1977
+ if (this.windowElement !== newRoot.ownerDocument.defaultView) {
1978
+ throw new Error(`Cannot connect a root:
1979
+ DOMLocalization already has a root from a different window.`);
1980
+ }
1981
+ }
1982
+ else {
1983
+ this.windowElement = newRoot.ownerDocument.defaultView;
1984
+ this.mutationObserver = new this.windowElement.MutationObserver(mutations => this.translateMutations(mutations));
1985
+ }
1986
+ this.roots.add(newRoot);
1987
+ this.mutationObserver.observe(newRoot, this.observerConfig);
1988
+ }
1989
+ /**
1990
+ * Remove `root` from the list of roots managed by this `DOMLocalization`.
1991
+ *
1992
+ * Additionally, if this `DOMLocalization` has an observer, stop observing
1993
+ * `root`.
1994
+ *
1995
+ * Returns `true` if the root was the last one managed by this
1996
+ * `DOMLocalization`.
1997
+ *
1998
+ * @param {Element} root - Root to disconnect.
1999
+ * @returns {boolean}
2000
+ */
2001
+ disconnectRoot(root) {
2002
+ this.roots.delete(root);
2003
+ // Pause the mutation observer to stop observing `root`.
2004
+ this.pauseObserving();
2005
+ if (this.roots.size === 0) {
2006
+ this.mutationObserver = null;
2007
+ this.windowElement = null;
2008
+ this.pendingrAF = null;
2009
+ this.pendingElements.clear();
2010
+ return true;
2011
+ }
2012
+ // Resume observing all other roots.
2013
+ this.resumeObserving();
2014
+ return false;
2015
+ }
2016
+ /**
2017
+ * Translate all roots associated with this `DOMLocalization`.
2018
+ *
2019
+ * @returns {Promise}
2020
+ */
2021
+ translateRoots() {
2022
+ const roots = Array.from(this.roots);
2023
+ return Promise.all(roots.map(root => this.translateFragment(root)));
2024
+ }
2025
+ /**
2026
+ * Pauses the `MutationObserver`.
2027
+ *
2028
+ * @private
2029
+ */
2030
+ pauseObserving() {
2031
+ if (!this.mutationObserver) {
2032
+ return;
2033
+ }
2034
+ this.translateMutations(this.mutationObserver.takeRecords());
2035
+ this.mutationObserver.disconnect();
2036
+ }
2037
+ /**
2038
+ * Resumes the `MutationObserver`.
2039
+ *
2040
+ * @private
2041
+ */
2042
+ resumeObserving() {
2043
+ if (!this.mutationObserver) {
2044
+ return;
2045
+ }
2046
+ for (const root of this.roots) {
2047
+ this.mutationObserver.observe(root, this.observerConfig);
2048
+ }
2049
+ }
2050
+ /**
2051
+ * Translate mutations detected by the `MutationObserver`.
2052
+ *
2053
+ * @private
2054
+ */
2055
+ translateMutations(mutations) {
2056
+ for (const mutation of mutations) {
2057
+ switch (mutation.type) {
2058
+ case "attributes":
2059
+ if (mutation.target.hasAttribute("data-l10n-id")) {
2060
+ this.pendingElements.add(mutation.target);
2061
+ }
2062
+ break;
2063
+ case "childList":
2064
+ for (const addedNode of mutation.addedNodes) {
2065
+ if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
2066
+ if (addedNode.childElementCount) {
2067
+ for (const element of this.getTranslatables(addedNode)) {
2068
+ this.pendingElements.add(element);
2069
+ }
2070
+ }
2071
+ else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) {
2072
+ this.pendingElements.add(addedNode);
2073
+ }
2074
+ }
2075
+ }
2076
+ break;
2077
+ }
2078
+ }
2079
+ // This fragment allows us to coalesce all pending translations
2080
+ // into a single requestAnimationFrame.
2081
+ if (this.pendingElements.size > 0) {
2082
+ if (this.pendingrAF === null) {
2083
+ this.pendingrAF = this.windowElement.requestAnimationFrame(() => {
2084
+ this.translateElements(Array.from(this.pendingElements));
2085
+ this.pendingElements.clear();
2086
+ this.pendingrAF = null;
2087
+ });
2088
+ }
2089
+ }
2090
+ }
2091
+ /**
2092
+ * Translate a DOM element or fragment asynchronously using this
2093
+ * `DOMLocalization` object.
2094
+ *
2095
+ * Manually trigger the translation (or re-translation) of a DOM fragment.
2096
+ * Use the `data-l10n-id` and `data-l10n-args` attributes to mark up the DOM
2097
+ * with information about which translations to use.
2098
+ *
2099
+ * Returns a `Promise` that gets resolved once the translation is complete.
2100
+ *
2101
+ * @param {DOMFragment} frag - Element or DocumentFragment to be translated
2102
+ * @returns {Promise}
2103
+ */
2104
+ translateFragment(frag) {
2105
+ return this.translateElements(this.getTranslatables(frag));
2106
+ }
2107
+ /**
2108
+ * Translate a list of DOM elements asynchronously using this
2109
+ * `DOMLocalization` object.
2110
+ *
2111
+ * Manually trigger the translation (or re-translation) of a list of elements.
2112
+ * Use the `data-l10n-id` and `data-l10n-args` attributes to mark up the DOM
2113
+ * with information about which translations to use.
2114
+ *
2115
+ * Returns a `Promise` that gets resolved once the translation is complete.
2116
+ *
2117
+ * @param {Array<Element>} elements - List of elements to be translated
2118
+ * @returns {Promise}
2119
+ */
2120
+ async translateElements(elements) {
2121
+ if (!elements.length) {
2122
+ return undefined;
2123
+ }
2124
+ const keys = elements.map(this.getKeysForElement);
2125
+ const translations = await this.formatMessages(keys);
2126
+ return this.applyTranslations(elements, translations);
2127
+ }
2128
+ /**
2129
+ * Applies translations onto elements.
2130
+ *
2131
+ * @param {Array<Element>} elements
2132
+ * @param {Array<Object>} translations
2133
+ * @private
2134
+ */
2135
+ applyTranslations(elements, translations) {
2136
+ this.pauseObserving();
2137
+ for (let i = 0; i < elements.length; i++) {
2138
+ if (translations[i] !== undefined) {
2139
+ translateElement(elements[i], translations[i]);
2140
+ }
2141
+ }
2142
+ this.resumeObserving();
2143
+ }
2144
+ /**
2145
+ * Collects all translatable child elements of the element.
2146
+ *
2147
+ * @param {Element} element
2148
+ * @returns {Array<Element>}
2149
+ * @private
2150
+ */
2151
+ getTranslatables(element) {
2152
+ const nodes = Array.from(element.querySelectorAll(L10N_ELEMENT_QUERY));
2153
+ if (typeof element.hasAttribute === "function" &&
2154
+ element.hasAttribute(L10NID_ATTR_NAME)) {
2155
+ nodes.push(element);
2156
+ }
2157
+ return nodes;
2158
+ }
2159
+ /**
2160
+ * Get the `data-l10n-*` attributes from DOM elements as a two-element
2161
+ * array.
2162
+ *
2163
+ * @param {Element} element
2164
+ * @returns {Object}
2165
+ * @private
2166
+ */
2167
+ getKeysForElement(element) {
2168
+ return {
2169
+ id: element.getAttribute(L10NID_ATTR_NAME),
2170
+ args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null),
2171
+ };
2172
+ }
2173
+ }
2174
+
2175
+ /* Copyright 2023 Mozilla Foundation
2176
+ *
2177
+ * Licensed under the Apache License, Version 2.0 (the "License");
2178
+ * you may not use this file except in compliance with the License.
2179
+ * You may obtain a copy of the License at
2180
+ *
2181
+ * http://www.apache.org/licenses/LICENSE-2.0
2182
+ *
2183
+ * Unless required by applicable law or agreed to in writing, software
2184
+ * distributed under the License is distributed on an "AS IS" BASIS,
2185
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2186
+ * See the License for the specific language governing permissions and
2187
+ * limitations under the License.
2188
+ */
2189
+
2190
+ /** @typedef {import("./interfaces").IL10n} IL10n */
2191
+
2192
+ /**
2193
+ * NOTE: The L10n-implementations should use lowercase language-codes
2194
+ * internally.
2195
+ * @implements {IL10n}
2196
+ */
2197
+ class L10n {
2198
+ #dir;
2199
+ #lang;
2200
+ #l10n;
2201
+ constructor({
2202
+ lang,
2203
+ isRTL
2204
+ }, l10n = null) {
2205
+ this.#lang = L10n.#fixupLangCode(lang);
2206
+ this.#l10n = l10n;
2207
+ this.#dir = (isRTL !== null && isRTL !== void 0 ? isRTL : L10n.#isRTL(this.#lang)) ? "rtl" : "ltr";
2208
+ }
2209
+ _setL10n(l10n) {
2210
+ this.#l10n = l10n;
2211
+ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
2212
+ document.l10n = l10n;
2213
+ }
2214
+ }
2215
+
2216
+ /** @inheritdoc */
2217
+ getLanguage() {
2218
+ return this.#lang;
2219
+ }
2220
+
2221
+ /** @inheritdoc */
2222
+ getDirection() {
2223
+ return this.#dir;
2224
+ }
2225
+
2226
+ /** @inheritdoc */
2227
+ async get(ids, args = null, fallback) {
2228
+ if (Array.isArray(ids)) {
2229
+ ids = ids.map(id => ({
2230
+ id
2231
+ }));
2232
+ const messages = await this.#l10n.formatMessages(ids);
2233
+ return messages.map(message => message.value);
2234
+ }
2235
+ const messages = await this.#l10n.formatMessages([{
2236
+ id: ids,
2237
+ args
2238
+ }]);
2239
+ return (messages === null || messages === void 0 ? void 0 : messages[0].value) || fallback;
2240
+ }
2241
+
2242
+ /** @inheritdoc */
2243
+ async translate(element) {
2244
+ try {
2245
+ this.#l10n.connectRoot(element);
2246
+ await this.#l10n.translateRoots();
2247
+ } catch {
2248
+ // Element is under an existing root, so there is no need to add it again.
2249
+ }
2250
+ }
2251
+
2252
+ /** @inheritdoc */
2253
+ pause() {
2254
+ this.#l10n.pauseObserving();
2255
+ }
2256
+
2257
+ /** @inheritdoc */
2258
+ resume() {
2259
+ this.#l10n.resumeObserving();
2260
+ }
2261
+ static #fixupLangCode(langCode) {
2262
+ var _langCode;
2263
+ // Use only lowercase language-codes internally, and fallback to English.
2264
+ langCode = ((_langCode = langCode) === null || _langCode === void 0 ? void 0 : _langCode.toLowerCase()) || "en-us";
2265
+
2266
+ // Try to support "incompletely" specified language codes (see issue 13689).
2267
+ const PARTIAL_LANG_CODES = {
2268
+ en: "en-us",
2269
+ es: "es-es",
2270
+ fy: "fy-nl",
2271
+ ga: "ga-ie",
2272
+ gu: "gu-in",
2273
+ hi: "hi-in",
2274
+ hy: "hy-am",
2275
+ nb: "nb-no",
2276
+ ne: "ne-np",
2277
+ nn: "nn-no",
2278
+ pa: "pa-in",
2279
+ pt: "pt-pt",
2280
+ sv: "sv-se",
2281
+ zh: "zh-cn"
2282
+ };
2283
+ return PARTIAL_LANG_CODES[langCode] || langCode;
2284
+ }
2285
+ static #isRTL(lang) {
2286
+ const shortCode = lang.split("-", 1)[0];
2287
+ return ["ar", "he", "fa", "ps", "ur"].includes(shortCode);
2288
+ }
2289
+ }
2290
+
2291
+ /* Copyright 2017 Mozilla Foundation
2292
+ *
2293
+ * Licensed under the Apache License, Version 2.0 (the "License");
2294
+ * you may not use this file except in compliance with the License.
2295
+ * You may obtain a copy of the License at
2296
+ *
2297
+ * http://www.apache.org/licenses/LICENSE-2.0
2298
+ *
2299
+ * Unless required by applicable law or agreed to in writing, software
2300
+ * distributed under the License is distributed on an "AS IS" BASIS,
2301
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2302
+ * See the License for the specific language governing permissions and
2303
+ * limitations under the License.
2304
+ */
2305
+ function createBundle(lang, text) {
2306
+ const resource = new FluentResource(text);
2307
+ const bundle = new FluentBundle(lang);
2308
+ const errors = bundle.addResource(resource);
2309
+ if (errors.length) {
2310
+ console.error("L10n errors", errors);
2311
+ }
2312
+ return bundle;
2313
+ }
2314
+
2315
+ /**
2316
+ * @implements {IL10n}
2317
+ */
2318
+ class GenericL10n extends L10n {
2319
+ constructor(lang) {
2320
+ super({
2321
+ lang
2322
+ });
2323
+ const generateBundles = !lang ? GenericL10n.#generateBundlesFallback.bind(GenericL10n, this.getLanguage()) : GenericL10n.#generateBundles.bind(GenericL10n, "en-us", this.getLanguage());
2324
+ this._setL10n(new DOMLocalization([], generateBundles));
2325
+ }
2326
+
2327
+ /**
2328
+ * Generate the bundles for Fluent.
2329
+ * @param {String} defaultLang - The fallback language to use for
2330
+ * translations.
2331
+ * @param {String} baseLang - The base language to use for translations.
2332
+ */
2333
+ static async *#generateBundles(defaultLang, baseLang) {
2334
+ const {
2335
+ baseURL,
2336
+ paths
2337
+ } = await this.#getPaths();
2338
+ const langs = [baseLang];
2339
+ if (defaultLang !== baseLang) {
2340
+ // Also fallback to the short-format of the base language
2341
+ // (see issue 17269).
2342
+ const shortLang = baseLang.split("-", 1)[0];
2343
+ if (shortLang !== baseLang) {
2344
+ langs.push(shortLang);
2345
+ }
2346
+ langs.push(defaultLang);
2347
+ }
2348
+ for (const lang of langs) {
2349
+ const bundle = await this.#createBundle(lang, baseURL, paths);
2350
+ if (bundle) {
2351
+ yield bundle;
2352
+ }
2353
+ if (lang === "en-us") {
2354
+ yield this.#createBundleFallback(lang);
2355
+ }
2356
+ }
2357
+ }
2358
+ static async #createBundle(lang, baseURL, paths) {
2359
+ const path = paths[lang];
2360
+ if (!path) {
2361
+ return null;
2362
+ }
2363
+ const url = new URL(path, baseURL);
2364
+ const text = await fetchData(url, /* type = */"text");
2365
+ return createBundle(lang, text);
2366
+ }
2367
+ static async #getPaths() {
2368
+ try {
2369
+ const {
2370
+ href
2371
+ } = document.querySelector(`link[type="application/l10n"]`);
2372
+ const paths = await fetchData(href, /* type = */"json");
2373
+ return {
2374
+ baseURL: href.replace(/[^/]*$/, "") || "./",
2375
+ paths
2376
+ };
2377
+ } catch {}
2378
+ return {
2379
+ baseURL: "./",
2380
+ paths: Object.create(null)
2381
+ };
2382
+ }
2383
+ static async *#generateBundlesFallback(lang) {
2384
+ yield this.#createBundleFallback(lang);
2385
+ }
2386
+ static async #createBundleFallback(lang) {
2387
+ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
2388
+ throw new Error("Not implemented: #createBundleFallback");
2389
+ }
2390
+ const text = typeof PDFJSDev === "undefined" ? "# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\n## Main toolbar buttons (tooltips and alt text for images)\n\npdfjs-previous-button =\n .title = Previous Page\npdfjs-previous-button-label = Previous\npdfjs-next-button =\n .title = Next Page\npdfjs-next-button-label = Next\n\n# .title: Tooltip for the pageNumber input.\npdfjs-page-input =\n .title = Page\n\n# Variables:\n# $pagesCount (Number) - the total number of pages in the document\n# This string follows an input field with the number of the page currently displayed.\npdfjs-of-pages = of { $pagesCount }\n\n# Variables:\n# $pageNumber (Number) - the currently visible page\n# $pagesCount (Number) - the total number of pages in the document\npdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount })\n\npdfjs-zoom-out-button =\n .title = Zoom Out\npdfjs-zoom-out-button-label = Zoom Out\npdfjs-zoom-in-button =\n .title = Zoom In\npdfjs-zoom-in-button-label = Zoom In\npdfjs-zoom-select =\n .title = Zoom\npdfjs-presentation-mode-button =\n .title = Switch to Presentation Mode\npdfjs-presentation-mode-button-label = Presentation Mode\npdfjs-open-file-button =\n .title = Open File\npdfjs-open-file-button-label = Open\npdfjs-print-button =\n .title = Print\npdfjs-print-button-label = Print\npdfjs-save-button =\n .title = Save\npdfjs-save-button-label = Save\n\n# Used in Firefox for Android as a tooltip for the download button (“download” is a verb).\npdfjs-download-button =\n .title = Download\n\n# Used in Firefox for Android as a label for the download button (“download” is a verb).\n# Length of the translation matters since we are in a mobile context, with limited screen estate.\npdfjs-download-button-label = Download\n\npdfjs-bookmark-button =\n .title = Current Page (View URL from Current Page)\npdfjs-bookmark-button-label = Current Page\n\n## Secondary toolbar and context menu\n\npdfjs-tools-button =\n .title = Tools\n\npdfjs-tools-button-label = Tools\npdfjs-first-page-button =\n .title = Go to First Page\npdfjs-first-page-button-label = Go to First Page\npdfjs-last-page-button =\n .title = Go to Last Page\npdfjs-last-page-button-label = Go to Last Page\npdfjs-page-rotate-cw-button =\n .title = Rotate Clockwise\npdfjs-page-rotate-cw-button-label = Rotate Clockwise\npdfjs-page-rotate-ccw-button =\n .title = Rotate Counterclockwise\npdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise\npdfjs-cursor-text-select-tool-button =\n .title = Enable Text Selection Tool\npdfjs-cursor-text-select-tool-button-label = Text Selection Tool\npdfjs-cursor-hand-tool-button =\n .title = Enable Hand Tool\npdfjs-cursor-hand-tool-button-label = Hand Tool\npdfjs-scroll-page-button =\n .title = Use Page Scrolling\npdfjs-scroll-page-button-label = Page Scrolling\npdfjs-scroll-vertical-button =\n .title = Use Vertical Scrolling\npdfjs-scroll-vertical-button-label = Vertical Scrolling\npdfjs-scroll-horizontal-button =\n .title = Use Horizontal Scrolling\npdfjs-scroll-horizontal-button-label = Horizontal Scrolling\npdfjs-scroll-wrapped-button =\n .title = Use Wrapped Scrolling\npdfjs-scroll-wrapped-button-label = Wrapped Scrolling\npdfjs-spread-none-button =\n .title = Do not join page spreads\npdfjs-spread-none-button-label = No Spreads\npdfjs-spread-odd-button =\n .title = Join page spreads starting with odd-numbered pages\npdfjs-spread-odd-button-label = Odd Spreads\npdfjs-spread-even-button =\n .title = Join page spreads starting with even-numbered pages\npdfjs-spread-even-button-label = Even Spreads\n\n## Document properties dialog\n\npdfjs-document-properties-button =\n .title = Document Properties…\npdfjs-document-properties-button-label = Document Properties…\npdfjs-document-properties-file-name = File name:\npdfjs-document-properties-file-size = File size:\n\n# Variables:\n# $size_kb (Number) - the PDF file size in kilobytes\n# $size_b (Number) - the PDF file size in bytes\npdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes)\n\n# Variables:\n# $size_mb (Number) - the PDF file size in megabytes\n# $size_b (Number) - the PDF file size in bytes\npdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes)\n\npdfjs-document-properties-title = Title:\npdfjs-document-properties-author = Author:\npdfjs-document-properties-subject = Subject:\npdfjs-document-properties-keywords = Keywords:\npdfjs-document-properties-creation-date = Creation Date:\npdfjs-document-properties-modification-date = Modification Date:\n\n# Variables:\n# $date (Date) - the creation/modification date of the PDF file\n# $time (Time) - the creation/modification time of the PDF file\npdfjs-document-properties-date-string = { $date }, { $time }\n\npdfjs-document-properties-creator = Creator:\npdfjs-document-properties-producer = PDF Producer:\npdfjs-document-properties-version = PDF Version:\npdfjs-document-properties-page-count = Page Count:\npdfjs-document-properties-page-size = Page Size:\npdfjs-document-properties-page-size-unit-inches = in\npdfjs-document-properties-page-size-unit-millimeters = mm\npdfjs-document-properties-page-size-orientation-portrait = portrait\npdfjs-document-properties-page-size-orientation-landscape = landscape\npdfjs-document-properties-page-size-name-a-three = A3\npdfjs-document-properties-page-size-name-a-four = A4\npdfjs-document-properties-page-size-name-letter = Letter\npdfjs-document-properties-page-size-name-legal = Legal\n\n## Variables:\n## $width (Number) - the width of the (current) page\n## $height (Number) - the height of the (current) page\n## $unit (String) - the unit of measurement of the (current) page\n## $name (String) - the name of the (current) page\n## $orientation (String) - the orientation of the (current) page\n\npdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation })\npdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation })\n\n##\n\n# The linearization status of the document; usually called \"Fast Web View\" in\n# English locales of Adobe software.\npdfjs-document-properties-linearized = Fast Web View:\npdfjs-document-properties-linearized-yes = Yes\npdfjs-document-properties-linearized-no = No\npdfjs-document-properties-close-button = Close\n\n## Print\n\npdfjs-print-progress-message = Preparing document for printing…\n\n# Variables:\n# $progress (Number) - percent value\npdfjs-print-progress-percent = { $progress }%\n\npdfjs-print-progress-close-button = Cancel\npdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser.\npdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing.\n\n## Tooltips and alt text for side panel toolbar buttons\n\npdfjs-toggle-sidebar-button =\n .title = Toggle Sidebar\npdfjs-toggle-sidebar-notification-button =\n .title = Toggle Sidebar (document contains outline/attachments/layers)\npdfjs-toggle-sidebar-button-label = Toggle Sidebar\npdfjs-document-outline-button =\n .title = Show Document Outline (double-click to expand/collapse all items)\npdfjs-document-outline-button-label = Document Outline\npdfjs-attachments-button =\n .title = Show Attachments\npdfjs-attachments-button-label = Attachments\npdfjs-layers-button =\n .title = Show Layers (double-click to reset all layers to the default state)\npdfjs-layers-button-label = Layers\npdfjs-thumbs-button =\n .title = Show Thumbnails\npdfjs-thumbs-button-label = Thumbnails\npdfjs-current-outline-item-button =\n .title = Find Current Outline Item\npdfjs-current-outline-item-button-label = Current Outline Item\npdfjs-findbar-button =\n .title = Find in Document\npdfjs-findbar-button-label = Find\npdfjs-additional-layers = Additional Layers\n\n## Thumbnails panel item (tooltip and alt text for images)\n\n# Variables:\n# $page (Number) - the page number\npdfjs-thumb-page-title =\n .title = Page { $page }\n\n# Variables:\n# $page (Number) - the page number\npdfjs-thumb-page-canvas =\n .aria-label = Thumbnail of Page { $page }\n\n## Find panel button title and messages\n\npdfjs-find-input =\n .title = Find\n .placeholder = Find in document…\npdfjs-find-previous-button =\n .title = Find the previous occurrence of the phrase\npdfjs-find-previous-button-label = Previous\npdfjs-find-next-button =\n .title = Find the next occurrence of the phrase\npdfjs-find-next-button-label = Next\npdfjs-find-highlight-checkbox = Highlight All\npdfjs-find-match-case-checkbox-label = Match Case\npdfjs-find-match-diacritics-checkbox-label = Match Diacritics\npdfjs-find-entire-word-checkbox-label = Whole Words\npdfjs-find-reached-top = Reached top of document, continued from bottom\npdfjs-find-reached-bottom = Reached end of document, continued from top\n\n# Variables:\n# $current (Number) - the index of the currently active find result\n# $total (Number) - the total number of matches in the document\npdfjs-find-match-count =\n { $total ->\n [one] { $current } of { $total } match\n *[other] { $current } of { $total } matches\n }\n\n# Variables:\n# $limit (Number) - the maximum number of matches\npdfjs-find-match-count-limit =\n { $limit ->\n [one] More than { $limit } match\n *[other] More than { $limit } matches\n }\n\npdfjs-find-not-found = Phrase not found\n\n## Predefined zoom values\n\npdfjs-page-scale-width = Page Width\npdfjs-page-scale-fit = Page Fit\npdfjs-page-scale-auto = Automatic Zoom\npdfjs-page-scale-actual = Actual Size\n\n# Variables:\n# $scale (Number) - percent value for page scale\npdfjs-page-scale-percent = { $scale }%\n\n## PDF page\n\n# Variables:\n# $page (Number) - the page number\npdfjs-page-landmark =\n .aria-label = Page { $page }\n\n## Loading indicator messages\n\npdfjs-loading-error = An error occurred while loading the PDF.\npdfjs-invalid-file-error = Invalid or corrupted PDF file.\npdfjs-missing-file-error = Missing PDF file.\npdfjs-unexpected-response-error = Unexpected server response.\npdfjs-rendering-error = An error occurred while rendering the page.\n\n## Annotations\n\n# Variables:\n# $date (Date) - the modification date of the annotation\n# $time (Time) - the modification time of the annotation\npdfjs-annotation-date-string = { $date }, { $time }\n\n# .alt: This is used as a tooltip.\n# Variables:\n# $type (String) - an annotation type from a list defined in the PDF spec\n# (32000-1:2008 Table 169 – Annotation types).\n# Some common types are e.g.: \"Check\", \"Text\", \"Comment\", \"Note\"\npdfjs-text-annotation-type =\n .alt = [{ $type } Annotation]\n\n## Password\n\npdfjs-password-label = Enter the password to open this PDF file.\npdfjs-password-invalid = Invalid password. Please try again.\npdfjs-password-ok-button = OK\npdfjs-password-cancel-button = Cancel\npdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts.\n\n## Editing\n\npdfjs-editor-free-text-button =\n .title = Text\npdfjs-editor-free-text-button-label = Text\npdfjs-editor-ink-button =\n .title = Draw\npdfjs-editor-ink-button-label = Draw\npdfjs-editor-stamp-button =\n .title = Add or edit images\npdfjs-editor-stamp-button-label = Add or edit images\npdfjs-editor-highlight-button =\n .title = Highlight\npdfjs-editor-highlight-button-label = Highlight\npdfjs-highlight-floating-button1 =\n .title = Highlight\n .aria-label = Highlight\npdfjs-highlight-floating-button-label = Highlight\n\n## Remove button for the various kind of editor.\n\npdfjs-editor-remove-ink-button =\n .title = Remove drawing\npdfjs-editor-remove-freetext-button =\n .title = Remove text\npdfjs-editor-remove-stamp-button =\n .title = Remove image\npdfjs-editor-remove-highlight-button =\n .title = Remove highlight\n\n##\n\n# Editor Parameters\npdfjs-editor-free-text-color-input = Color\npdfjs-editor-free-text-size-input = Size\npdfjs-editor-ink-color-input = Color\npdfjs-editor-ink-thickness-input = Thickness\npdfjs-editor-ink-opacity-input = Opacity\npdfjs-editor-stamp-add-image-button =\n .title = Add image\npdfjs-editor-stamp-add-image-button-label = Add image\n# This refers to the thickness of the line used for free highlighting (not bound to text)\npdfjs-editor-free-highlight-thickness-input = Thickness\npdfjs-editor-free-highlight-thickness-title =\n .title = Change thickness when highlighting items other than text\n\npdfjs-free-text =\n .aria-label = Text Editor\npdfjs-free-text-default-content = Start typing…\npdfjs-ink =\n .aria-label = Draw Editor\npdfjs-ink-canvas =\n .aria-label = User-created image\n\n## Alt-text dialog\n\n# Alternative text (alt text) helps when people can't see the image.\npdfjs-editor-alt-text-button-label = Alt text\n\npdfjs-editor-alt-text-edit-button-label = Edit alt text\npdfjs-editor-alt-text-dialog-label = Choose an option\npdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can’t see the image or when it doesn’t load.\npdfjs-editor-alt-text-add-description-label = Add a description\npdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions.\npdfjs-editor-alt-text-mark-decorative-label = Mark as decorative\npdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks.\npdfjs-editor-alt-text-cancel-button = Cancel\npdfjs-editor-alt-text-save-button = Save\npdfjs-editor-alt-text-decorative-tooltip = Marked as decorative\n\n# .placeholder: This is a placeholder for the alt text input area\npdfjs-editor-alt-text-textarea =\n .placeholder = For example, “A young man sits down at a table to eat a meal”\n\n## Editor resizers\n## This is used in an aria label to help to understand the role of the resizer.\n\npdfjs-editor-resizer-label-top-left = Top left corner — resize\npdfjs-editor-resizer-label-top-middle = Top middle — resize\npdfjs-editor-resizer-label-top-right = Top right corner — resize\npdfjs-editor-resizer-label-middle-right = Middle right — resize\npdfjs-editor-resizer-label-bottom-right = Bottom right corner — resize\npdfjs-editor-resizer-label-bottom-middle = Bottom middle — resize\npdfjs-editor-resizer-label-bottom-left = Bottom left corner — resize\npdfjs-editor-resizer-label-middle-left = Middle left — resize\n\n## Color picker\n\n# This means \"Color used to highlight text\"\npdfjs-editor-highlight-colorpicker-label = Highlight color\n\npdfjs-editor-colorpicker-button =\n .title = Change color\npdfjs-editor-colorpicker-dropdown =\n .aria-label = Color choices\npdfjs-editor-colorpicker-yellow =\n .title = Yellow\npdfjs-editor-colorpicker-green =\n .title = Green\npdfjs-editor-colorpicker-blue =\n .title = Blue\npdfjs-editor-colorpicker-pink =\n .title = Pink\npdfjs-editor-colorpicker-red =\n .title = Red\n\n## Show all highlights\n## This is a toggle button to show/hide all the highlights.\n\npdfjs-editor-highlight-show-all-button-label = Show all\npdfjs-editor-highlight-show-all-button =\n .title = Show all\n" : PDFJSDev.eval("DEFAULT_FTL");
2391
+ return createBundle(lang, text);
2392
+ }
2393
+ }
2394
+
2395
+ export { GenericL10n };