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