@zerolith/i18n 0.1.0-alpha.1 → 0.1.0-alpha.10
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.
- package/README.md +8 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +66 -8
- package/dist/cli.js.map +1 -1
- package/dist/compile.d.ts +6 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +179 -85
- package/dist/compile.js.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -14
- package/dist/index.js.map +1 -1
- package/dist/production/compile.js +167 -0
- package/dist/production/compile.js.map +1 -0
- package/dist/production/index.js +2 -0
- package/dist/production/index.js.map +1 -0
- package/package.json +9 -7
- package/src/cli.ts +87 -8
- package/src/compile.ts +203 -86
- package/src/index.ts +43 -16
package/src/compile.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface CompileOptions
|
|
|
25
25
|
/** The result of compiling one catalog. */
|
|
26
26
|
export interface CompiledCatalog
|
|
27
27
|
{
|
|
28
|
+
/** The generated module's source, TypeScript or JavaScript as the `language` option chose; the `zerolith-i18n` CLI writes it to `<locale>.ts` or `<locale>.js`. */
|
|
28
29
|
code: string;
|
|
29
30
|
/** The message keys as function names, in source order. */
|
|
30
31
|
names: string[];
|
|
@@ -414,6 +415,40 @@ const NUMBER_OPTIONS: Record<string, string> = {
|
|
|
414
415
|
numberingSystem: "numberingSystem"
|
|
415
416
|
};
|
|
416
417
|
|
|
418
|
+
/** Names a module cannot bind; a message named so is exported under its name from a binding of its own. */
|
|
419
|
+
const RESERVED: ReadonlySet<string> = new Set(["arguments", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", "false", "finally", "for", "function", "if", "implements", "import", "in", "instanceof", "interface", "let", "new", "null", "package", "private", "protected", "public", "return", "static", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield"]);
|
|
420
|
+
|
|
421
|
+
/** Characters an HTML parser reads inside an inline script, and the line separators older engines refused in string literals. */
|
|
422
|
+
const SCRIPT_UNSAFE = new RegExp(`[<>${String.fromCodePoint(0x2028, 0x2029)}]`, "g");
|
|
423
|
+
|
|
424
|
+
/** The line breaks of a message's source, each starting a new line of its doc comment. */
|
|
425
|
+
const SOURCE_LINES = /\r\n|[\n\r\u2028\u2029]/;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A value written as JavaScript source: its JSON, with `<`, `>`, U+2028, and
|
|
429
|
+
* U+2029 escaped, so generated code stays inert even when a page inlines
|
|
430
|
+
* it in a script element.
|
|
431
|
+
*/
|
|
432
|
+
function jsLiteral(value: unknown): string
|
|
433
|
+
{
|
|
434
|
+
return JSON.stringify(value).replace(SCRIPT_UNSAFE, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Text for a `//` comment, kept on one line and with `<` written as `<`, so it can end neither the comment nor an inline script. */
|
|
438
|
+
function commentText(text: string): string
|
|
439
|
+
{
|
|
440
|
+
return text.split(SOURCE_LINES).join(" ").replaceAll("<", "<");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** An argument or local name JavaScript can write as it is; MessageFormat also allows `-` and `.`, as in `$first-name`. */
|
|
444
|
+
const PLAIN_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
445
|
+
|
|
446
|
+
/** A property name as code: as it is when plain, quoted otherwise. */
|
|
447
|
+
function property(name: string): string
|
|
448
|
+
{
|
|
449
|
+
return PLAIN_NAME.test(name) ? name : jsLiteral(name);
|
|
450
|
+
}
|
|
451
|
+
|
|
417
452
|
function identifier(key: string): string
|
|
418
453
|
{
|
|
419
454
|
const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter((word) => word !== "");
|
|
@@ -428,11 +463,29 @@ class Emitter
|
|
|
428
463
|
readonly prelude: string[] = [];
|
|
429
464
|
private readonly key: string;
|
|
430
465
|
private readonly message: Message;
|
|
466
|
+
private readonly formatters: Map<string, string>;
|
|
467
|
+
private readonly used: Set<Helper>;
|
|
468
|
+
private reads = 0;
|
|
431
469
|
|
|
432
|
-
constructor(key: string, message: Message)
|
|
470
|
+
constructor(key: string, message: Message, formatters: Map<string, string>, used: Set<Helper>)
|
|
433
471
|
{
|
|
434
472
|
this.key = key;
|
|
435
473
|
this.message = message;
|
|
474
|
+
this.formatters = formatters;
|
|
475
|
+
this.used = used;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** The module-level binding of a formatter whose options are known at compile time, created on first use and shared by every message that creates it the same way. */
|
|
479
|
+
private shared(create: string): string
|
|
480
|
+
{
|
|
481
|
+
this.used.add("once");
|
|
482
|
+
let binding = this.formatters.get(create);
|
|
483
|
+
if (binding === undefined)
|
|
484
|
+
{
|
|
485
|
+
binding = `$f${this.formatters.size}`;
|
|
486
|
+
this.formatters.set(create, binding);
|
|
487
|
+
}
|
|
488
|
+
return binding;
|
|
436
489
|
}
|
|
437
490
|
|
|
438
491
|
private fail(text: string): never
|
|
@@ -442,6 +495,7 @@ class Emitter
|
|
|
442
495
|
|
|
443
496
|
private argument(name: string, type: ArgumentType): string
|
|
444
497
|
{
|
|
498
|
+
this.reads++;
|
|
445
499
|
const local = this.locals.get(name);
|
|
446
500
|
if (local !== undefined)
|
|
447
501
|
{
|
|
@@ -456,7 +510,7 @@ class Emitter
|
|
|
456
510
|
{
|
|
457
511
|
this.fail(`$${name} is used both as ${existing} and as ${type}`);
|
|
458
512
|
}
|
|
459
|
-
return `args.${name}`;
|
|
513
|
+
return PLAIN_NAME.test(name) ? `args.${name}` : `args[${jsLiteral(name)}]`;
|
|
460
514
|
}
|
|
461
515
|
|
|
462
516
|
private optionValue(value: Literal | Variable | undefined): string | undefined
|
|
@@ -465,7 +519,7 @@ class Emitter
|
|
|
465
519
|
{
|
|
466
520
|
return undefined;
|
|
467
521
|
}
|
|
468
|
-
return value.kind === "literal" ?
|
|
522
|
+
return value.kind === "literal" ? jsLiteral(value.value) : `String(${this.argument(value.name, "string")})`;
|
|
469
523
|
}
|
|
470
524
|
|
|
471
525
|
private numberOptions(expression: Expression): string
|
|
@@ -500,14 +554,14 @@ class Emitter
|
|
|
500
554
|
}
|
|
501
555
|
else if (option === "useGrouping" && value.kind === "literal")
|
|
502
556
|
{
|
|
503
|
-
entries.push(`useGrouping: ${value.value === "never" ? "false" : value.value === "always" ? "true" :
|
|
557
|
+
entries.push(`useGrouping: ${value.value === "never" ? "false" : value.value === "always" ? "true" : jsLiteral(value.value)}`);
|
|
504
558
|
}
|
|
505
559
|
else if (option === "fractionDigits" && value.kind === "literal" && /^[0-9]+$/.test(value.value))
|
|
506
560
|
{
|
|
507
561
|
entries.push(`minimumFractionDigits: ${value.value}`, `maximumFractionDigits: ${value.value}`);
|
|
508
562
|
}
|
|
509
563
|
}
|
|
510
|
-
return `{ ${entries.join(", ")} }`;
|
|
564
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
511
565
|
}
|
|
512
566
|
|
|
513
567
|
private dateOptions(expression: Expression): string
|
|
@@ -522,17 +576,17 @@ class Emitter
|
|
|
522
576
|
const length = literal(fn === "datetime" ? "dateLength" : "length") ?? (fn === "time" ? undefined : "medium");
|
|
523
577
|
if (length !== undefined && fn !== "time")
|
|
524
578
|
{
|
|
525
|
-
entries.push(`dateStyle: ${
|
|
579
|
+
entries.push(`dateStyle: ${jsLiteral(length)}`);
|
|
526
580
|
}
|
|
527
581
|
const precision = literal(fn === "datetime" ? "timePrecision" : "precision") ?? (fn === "date" ? undefined : "minute");
|
|
528
582
|
if (precision !== undefined && fn !== "date")
|
|
529
583
|
{
|
|
530
|
-
entries.push(`timeStyle: ${
|
|
584
|
+
entries.push(`timeStyle: ${jsLiteral(precision === "second" ? "medium" : "short")}`);
|
|
531
585
|
}
|
|
532
586
|
const zone = literal("timeZone");
|
|
533
587
|
if (zone !== undefined && zone !== "input")
|
|
534
588
|
{
|
|
535
|
-
entries.push(`timeZone: ${
|
|
589
|
+
entries.push(`timeZone: ${jsLiteral(zone)}`);
|
|
536
590
|
}
|
|
537
591
|
const hour12 = literal("hour12");
|
|
538
592
|
if (hour12 !== undefined)
|
|
@@ -542,9 +596,9 @@ class Emitter
|
|
|
542
596
|
const style = literal("timeZoneStyle");
|
|
543
597
|
if (style !== undefined)
|
|
544
598
|
{
|
|
545
|
-
entries.push(`timeZoneName: ${
|
|
599
|
+
entries.push(`timeZoneName: ${jsLiteral(style)}`);
|
|
546
600
|
}
|
|
547
|
-
return `{ ${entries.join(", ")} }`;
|
|
601
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
548
602
|
}
|
|
549
603
|
|
|
550
604
|
/** The JavaScript for an expression's numeric value, before formatting. */
|
|
@@ -577,13 +631,13 @@ class Emitter
|
|
|
577
631
|
return value;
|
|
578
632
|
}
|
|
579
633
|
|
|
580
|
-
/** Resolves a plain `{$x}` to its declaration when one exists. */
|
|
581
|
-
private resolve(expression: Expression): Expression
|
|
634
|
+
/** Resolves a plain `{$x}` to its `.input` declaration when one exists, or with `locals` to its `.local` one too, so a selector on a local selects as its expression does. */
|
|
635
|
+
private resolve(expression: Expression, locals = false): Expression
|
|
582
636
|
{
|
|
583
637
|
if (expression.fn === undefined && expression.operand?.kind === "variable")
|
|
584
638
|
{
|
|
585
639
|
const declared = this.message.declarations.get(expression.operand.name);
|
|
586
|
-
if (declared !== undefined && !declared.local)
|
|
640
|
+
if (declared !== undefined && (locals || !declared.local))
|
|
587
641
|
{
|
|
588
642
|
return declared.expression;
|
|
589
643
|
}
|
|
@@ -603,11 +657,19 @@ class Emitter
|
|
|
603
657
|
{
|
|
604
658
|
this.fail(":string needs an operand");
|
|
605
659
|
}
|
|
606
|
-
return operand.kind === "literal" ?
|
|
660
|
+
return operand.kind === "literal" ? jsLiteral(operand.value) : `String(${this.argument(operand.name, "string")})`;
|
|
607
661
|
}
|
|
608
662
|
if (NUMBER_FUNCTIONS.has(fn))
|
|
609
663
|
{
|
|
610
|
-
|
|
664
|
+
const value = this.numeric(expression);
|
|
665
|
+
const reads = this.reads;
|
|
666
|
+
const options = this.numberOptions(expression);
|
|
667
|
+
if (this.reads === reads)
|
|
668
|
+
{
|
|
669
|
+
return `${this.shared(`new Intl.NumberFormat(locale, ${options})`)}().format(${value})`;
|
|
670
|
+
}
|
|
671
|
+
this.used.add("number");
|
|
672
|
+
return `$number(${value}, ${options})`;
|
|
611
673
|
}
|
|
612
674
|
if (DATE_FUNCTIONS.has(fn))
|
|
613
675
|
{
|
|
@@ -616,8 +678,8 @@ class Emitter
|
|
|
616
678
|
{
|
|
617
679
|
this.fail(`:${fn} needs an operand`);
|
|
618
680
|
}
|
|
619
|
-
const value = operand.kind === "literal" ? `new Date(${
|
|
620
|
-
return `
|
|
681
|
+
const value = operand.kind === "literal" ? `new Date(${jsLiteral(operand.value)})` : this.argument(operand.name, "date");
|
|
682
|
+
return `${this.shared(`new Intl.DateTimeFormat(locale, ${this.dateOptions(expression)})`)}().format(${value})`;
|
|
621
683
|
}
|
|
622
684
|
this.fail(`unknown function :${fn}`);
|
|
623
685
|
}
|
|
@@ -625,7 +687,7 @@ class Emitter
|
|
|
625
687
|
/** The JavaScript for a selector: its comparable value and the categories it matches. */
|
|
626
688
|
selector(raw: Expression): string
|
|
627
689
|
{
|
|
628
|
-
const expression = this.resolve(raw);
|
|
690
|
+
const expression = this.resolve(raw, true);
|
|
629
691
|
const fn = expression.fn ?? "string";
|
|
630
692
|
if (NUMBER_FUNCTIONS.has(fn))
|
|
631
693
|
{
|
|
@@ -636,7 +698,15 @@ class Emitter
|
|
|
636
698
|
{
|
|
637
699
|
return `{ value: String(${value}), categories: [] }`;
|
|
638
700
|
}
|
|
639
|
-
|
|
701
|
+
const type = jsLiteral(mode === "ordinal" ? "ordinal" : "cardinal");
|
|
702
|
+
const reads = this.reads;
|
|
703
|
+
const options = this.numberOptions(expression);
|
|
704
|
+
if (this.reads === reads)
|
|
705
|
+
{
|
|
706
|
+
return `{ value: String(${value}), categories: [${this.shared(`new Intl.PluralRules(locale, { ...new Intl.NumberFormat(locale, ${options}).resolvedOptions(), type: ${type} })`)}().select(${value})] }`;
|
|
707
|
+
}
|
|
708
|
+
this.used.add("plural");
|
|
709
|
+
return `{ value: String(${value}), categories: [$plural(${value}, ${type}, ${options})] }`;
|
|
640
710
|
}
|
|
641
711
|
if (fn === "string")
|
|
642
712
|
{
|
|
@@ -645,7 +715,7 @@ class Emitter
|
|
|
645
715
|
{
|
|
646
716
|
this.fail("a selector needs an operand");
|
|
647
717
|
}
|
|
648
|
-
const value = operand.kind === "literal" ?
|
|
718
|
+
const value = operand.kind === "literal" ? jsLiteral(operand.value) : `String(${this.argument(operand.name, "string")})`;
|
|
649
719
|
return `{ value: ${value}, categories: [] }`;
|
|
650
720
|
}
|
|
651
721
|
this.fail(`:${fn} cannot select`);
|
|
@@ -657,7 +727,7 @@ class Emitter
|
|
|
657
727
|
{
|
|
658
728
|
return "\"\"";
|
|
659
729
|
}
|
|
660
|
-
return parts.map((part) => (typeof part === "string" ?
|
|
730
|
+
return parts.map((part) => (typeof part === "string" ? jsLiteral(part) : this.format(part))).join(" + ");
|
|
661
731
|
}
|
|
662
732
|
|
|
663
733
|
body(): string
|
|
@@ -667,8 +737,9 @@ class Emitter
|
|
|
667
737
|
if (declaration.local)
|
|
668
738
|
{
|
|
669
739
|
const value = this.format(declaration.expression);
|
|
670
|
-
|
|
671
|
-
this.
|
|
740
|
+
const binding = PLAIN_NAME.test(name) ? `local_${name}` : `local_${this.locals.size}`;
|
|
741
|
+
this.locals.set(name, binding);
|
|
742
|
+
this.prelude.push(`const ${binding} = ${value};`);
|
|
672
743
|
}
|
|
673
744
|
}
|
|
674
745
|
if (this.message.pattern !== undefined)
|
|
@@ -676,36 +747,77 @@ class Emitter
|
|
|
676
747
|
return this.pattern(this.message.pattern);
|
|
677
748
|
}
|
|
678
749
|
const selectors = this.message.selectors.map((selector) => this.selector(selector));
|
|
679
|
-
const variants = this.message.variants.map((variant) => `{ keys: ${
|
|
680
|
-
|
|
750
|
+
const variants = this.message.variants.map((variant) => `{ keys: ${jsLiteral(variant.keys)}, pattern: () => ${this.pattern(variant.pattern)} }`);
|
|
751
|
+
this.used.add("select");
|
|
752
|
+
return `$select([${selectors.join(", ")}], [${variants.join(", ")}])`;
|
|
681
753
|
}
|
|
682
754
|
}
|
|
683
755
|
|
|
684
|
-
|
|
685
|
-
|
|
756
|
+
/** A group of helpers a compiled module shares; each is written only when one of the module's messages uses it. */
|
|
757
|
+
type Helper = "cache" | "once" | "number" | "plural" | "select";
|
|
758
|
+
|
|
759
|
+
/** The helpers in JavaScript and in TypeScript, in the order a module writes them. */
|
|
760
|
+
const HELPERS: Record<Helper, { js: string; ts: string }> = {
|
|
761
|
+
cache: {
|
|
762
|
+
js: `const $formats = new Map();
|
|
763
|
+
function $cached(kind, options, create)
|
|
686
764
|
{
|
|
687
765
|
const key = kind + JSON.stringify(options);
|
|
688
|
-
let instance = formats.get(key);
|
|
766
|
+
let instance = $formats.get(key);
|
|
689
767
|
if (instance === undefined)
|
|
690
768
|
{
|
|
691
769
|
instance = create();
|
|
692
|
-
formats.set(key, instance);
|
|
770
|
+
$formats.set(key, instance);
|
|
693
771
|
}
|
|
694
772
|
return instance;
|
|
695
|
-
}
|
|
696
|
-
|
|
773
|
+
}`,
|
|
774
|
+
ts: `const $formats: Map<string, unknown> = new Map();
|
|
775
|
+
function $cached<T>(kind: string, options: object, create: () => T): T
|
|
697
776
|
{
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
777
|
+
const key = kind + JSON.stringify(options);
|
|
778
|
+
let instance = $formats.get(key) as T | undefined;
|
|
779
|
+
if (instance === undefined)
|
|
780
|
+
{
|
|
781
|
+
instance = create();
|
|
782
|
+
$formats.set(key, instance);
|
|
783
|
+
}
|
|
784
|
+
return instance;
|
|
785
|
+
}`
|
|
786
|
+
},
|
|
787
|
+
once: {
|
|
788
|
+
js: `function $once(create)
|
|
701
789
|
{
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
790
|
+
let instance;
|
|
791
|
+
return () => (instance ??= create());
|
|
792
|
+
}`,
|
|
793
|
+
ts: `function $once<T>(create: () => T): () => T
|
|
705
794
|
{
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
795
|
+
let instance: T | undefined;
|
|
796
|
+
return () => (instance ??= create());
|
|
797
|
+
}`
|
|
798
|
+
},
|
|
799
|
+
number: {
|
|
800
|
+
js: `function $number(value, options)
|
|
801
|
+
{
|
|
802
|
+
return $cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
|
|
803
|
+
}`,
|
|
804
|
+
ts: `function $number(value: number, options: Intl.NumberFormatOptions): string
|
|
805
|
+
{
|
|
806
|
+
return $cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
|
|
807
|
+
}`
|
|
808
|
+
},
|
|
809
|
+
plural: {
|
|
810
|
+
js: `function $plural(value, type, options)
|
|
811
|
+
{
|
|
812
|
+
return $cached("p" + type, options, () => new Intl.PluralRules(locale, { ...$cached("n", options, () => new Intl.NumberFormat(locale, options)).resolvedOptions(), type })).select(value);
|
|
813
|
+
}`,
|
|
814
|
+
ts: `function $plural(value: number, type: "cardinal" | "ordinal", options: Intl.NumberFormatOptions): string
|
|
815
|
+
{
|
|
816
|
+
return $cached("p" + type, options, () => new Intl.PluralRules(locale, { ...$cached("n", options, () => new Intl.NumberFormat(locale, options)).resolvedOptions(), type })).select(value);
|
|
817
|
+
}`
|
|
818
|
+
},
|
|
819
|
+
select: {
|
|
820
|
+
js: `function $select(selectors, variants)
|
|
709
821
|
{
|
|
710
822
|
let best;
|
|
711
823
|
let bestRank;
|
|
@@ -738,7 +850,7 @@ function select(selectors, variants)
|
|
|
738
850
|
{
|
|
739
851
|
continue;
|
|
740
852
|
}
|
|
741
|
-
if (matches && better(rank, bestRank))
|
|
853
|
+
if (matches && $better(rank, bestRank))
|
|
742
854
|
{
|
|
743
855
|
best = variant;
|
|
744
856
|
bestRank = rank;
|
|
@@ -746,7 +858,7 @@ function select(selectors, variants)
|
|
|
746
858
|
}
|
|
747
859
|
return best === undefined ? "" : best.pattern();
|
|
748
860
|
}
|
|
749
|
-
function better(rank, current)
|
|
861
|
+
function $better(rank, current)
|
|
750
862
|
{
|
|
751
863
|
if (current === undefined)
|
|
752
864
|
{
|
|
@@ -761,9 +873,8 @@ function better(rank, current)
|
|
|
761
873
|
}
|
|
762
874
|
}
|
|
763
875
|
return false;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const HELPERS_TS = `interface Selector
|
|
876
|
+
}`,
|
|
877
|
+
ts: `interface Selector
|
|
767
878
|
{
|
|
768
879
|
value: string;
|
|
769
880
|
categories: string[];
|
|
@@ -773,31 +884,7 @@ interface Variant
|
|
|
773
884
|
keys: string[];
|
|
774
885
|
pattern: () => string;
|
|
775
886
|
}
|
|
776
|
-
|
|
777
|
-
function cached<T>(kind: string, options: object, create: () => T): T
|
|
778
|
-
{
|
|
779
|
-
const key = kind + JSON.stringify(options);
|
|
780
|
-
let instance = formats.get(key) as T | undefined;
|
|
781
|
-
if (instance === undefined)
|
|
782
|
-
{
|
|
783
|
-
instance = create();
|
|
784
|
-
formats.set(key, instance);
|
|
785
|
-
}
|
|
786
|
-
return instance;
|
|
787
|
-
}
|
|
788
|
-
function number(value: number, options: Intl.NumberFormatOptions): string
|
|
789
|
-
{
|
|
790
|
-
return cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
|
|
791
|
-
}
|
|
792
|
-
function date(value: Date | number, options: Intl.DateTimeFormatOptions): string
|
|
793
|
-
{
|
|
794
|
-
return cached("d", options, () => new Intl.DateTimeFormat(locale, options)).format(value);
|
|
795
|
-
}
|
|
796
|
-
function plural(value: number, type: "cardinal" | "ordinal"): string
|
|
797
|
-
{
|
|
798
|
-
return cached("p", { type }, () => new Intl.PluralRules(locale, { type })).select(value);
|
|
799
|
-
}
|
|
800
|
-
function better(rank: number[], current: number[] | undefined): boolean
|
|
887
|
+
function $better(rank: number[], current: number[] | undefined): boolean
|
|
801
888
|
{
|
|
802
889
|
if (current === undefined)
|
|
803
890
|
{
|
|
@@ -813,7 +900,7 @@ function better(rank: number[], current: number[] | undefined): boolean
|
|
|
813
900
|
}
|
|
814
901
|
return false;
|
|
815
902
|
}
|
|
816
|
-
function select(selectors: Selector[], variants: Variant[]): string
|
|
903
|
+
function $select(selectors: Selector[], variants: Variant[]): string
|
|
817
904
|
{
|
|
818
905
|
let best: Variant | undefined;
|
|
819
906
|
let bestRank: number[] | undefined;
|
|
@@ -842,14 +929,27 @@ function select(selectors: Selector[], variants: Variant[]): string
|
|
|
842
929
|
break;
|
|
843
930
|
}
|
|
844
931
|
}
|
|
845
|
-
if (matches && better(rank, bestRank))
|
|
932
|
+
if (matches && $better(rank, bestRank))
|
|
846
933
|
{
|
|
847
934
|
best = variant;
|
|
848
935
|
bestRank = rank;
|
|
849
936
|
}
|
|
850
937
|
}
|
|
851
938
|
return best === undefined ? "" : best.pattern();
|
|
852
|
-
}
|
|
939
|
+
}`
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
/** The helpers a module's messages use, with the cache that `$number` and `$plural` share. */
|
|
944
|
+
function helpersFor(used: ReadonlySet<Helper>, javascript: boolean): string
|
|
945
|
+
{
|
|
946
|
+
const needed = new Set(used);
|
|
947
|
+
if (needed.has("number") || needed.has("plural"))
|
|
948
|
+
{
|
|
949
|
+
needed.add("cache");
|
|
950
|
+
}
|
|
951
|
+
return (Object.keys(HELPERS) as Helper[]).filter((name) => needed.has(name)).map((name) => HELPERS[name][javascript ? "js" : "ts"]).join("\n");
|
|
952
|
+
}
|
|
853
953
|
|
|
854
954
|
function typeOf(type: ArgumentType): string
|
|
855
955
|
{
|
|
@@ -860,21 +960,26 @@ function typeOf(type: ArgumentType): string
|
|
|
860
960
|
* Compiles one locale's catalog into a module: `locale`, one exported
|
|
861
961
|
* function per message named from its key, and the `Intl` helpers they
|
|
862
962
|
* share. Each function takes an `args` object typed from the message's
|
|
863
|
-
* placeholders, or nothing when the message has none.
|
|
963
|
+
* placeholders, or nothing when the message has none. A formatter whose
|
|
964
|
+
* options are all literals is a module-level binding created on first use
|
|
965
|
+
* and shared by every message that formats the same way; options that read
|
|
966
|
+
* an argument find their formatter in a cache keyed by the options. Only
|
|
967
|
+
* the helpers the messages use are written.
|
|
864
968
|
*/
|
|
865
969
|
export function compileMessages(locale: string, catalog: Catalog, options: CompileOptions = {}): CompiledCatalog
|
|
866
970
|
{
|
|
867
971
|
const javascript = options.language === "js";
|
|
868
972
|
const out: string[] = [];
|
|
869
|
-
out.push(`// Generated by @zerolith/i18n from ${options.source ?? `the ${locale} catalog`}. Do not edit; regenerate instead.`);
|
|
973
|
+
out.push(`// Generated by @zerolith/i18n from ${commentText(options.source ?? `the ${locale} catalog`)}. Do not edit; regenerate instead.`);
|
|
870
974
|
if (javascript)
|
|
871
975
|
{
|
|
872
976
|
out.push("// @ts-check");
|
|
873
977
|
}
|
|
874
|
-
out.push(`export const locale${javascript ? "" : ": string"} = ${
|
|
875
|
-
out.push("");
|
|
876
|
-
out.push(javascript ? HELPERS : HELPERS_TS);
|
|
978
|
+
out.push(`export const locale${javascript ? "" : ": string"} = ${jsLiteral(locale)};`);
|
|
877
979
|
out.push("");
|
|
980
|
+
const head = out.length;
|
|
981
|
+
const formatters = new Map<string, string>();
|
|
982
|
+
const used = new Set<Helper>();
|
|
878
983
|
const names: string[] = [];
|
|
879
984
|
const seen = new Set<string>();
|
|
880
985
|
for (const [key, source] of Object.entries(catalog))
|
|
@@ -884,23 +989,28 @@ export function compileMessages(locale: string, catalog: Catalog, options: Compi
|
|
|
884
989
|
{
|
|
885
990
|
throw new Error(`zerolith/i18n: keys "${key}" and another one both become ${name}`);
|
|
886
991
|
}
|
|
992
|
+
if (name === "locale")
|
|
993
|
+
{
|
|
994
|
+
throw new Error(`zerolith/i18n: key "${key}" becomes locale, which every compiled module exports; rename the key`);
|
|
995
|
+
}
|
|
996
|
+
const binding = RESERVED.has(name) ? `message_${name}` : name;
|
|
887
997
|
seen.add(name);
|
|
888
998
|
names.push(name);
|
|
889
999
|
const message = new Parser(source, key).parse();
|
|
890
|
-
const emitter = new Emitter(key, message);
|
|
1000
|
+
const emitter = new Emitter(key, message, formatters, used);
|
|
891
1001
|
const body = emitter.body();
|
|
892
1002
|
const args = [...emitter.arguments.entries()].toSorted(([a], [b]) => a.localeCompare(b));
|
|
893
|
-
const doc = source.replace(/\*\//g, "* /").split(
|
|
894
|
-
const argumentType = `{ ${args.map(([argument, type]) => `${argument}: ${typeOf(type)}`).join("; ")} }`;
|
|
1003
|
+
const doc = source.replace(/\*\//g, "* /").replaceAll("<", "<").split(SOURCE_LINES).map((line) => ` * ${line}`.trimEnd()).join("\n");
|
|
1004
|
+
const argumentType = `{ ${args.map(([argument, type]) => `${property(argument)}: ${typeOf(type)}`).join("; ")} }`;
|
|
895
1005
|
if (javascript)
|
|
896
1006
|
{
|
|
897
1007
|
out.push(`/**\n${doc}${args.length > 0 ? `\n * @param {${argumentType}} args` : ""}\n * @returns {string}\n */`);
|
|
898
|
-
out.push(
|
|
1008
|
+
out.push(`${binding === name ? "export " : ""}function ${binding}(${args.length > 0 ? "args" : ""})`);
|
|
899
1009
|
}
|
|
900
1010
|
else
|
|
901
1011
|
{
|
|
902
1012
|
out.push(`/**\n${doc}\n */`);
|
|
903
|
-
out.push(
|
|
1013
|
+
out.push(`${binding === name ? "export " : ""}function ${binding}(${args.length > 0 ? `args: ${argumentType}` : ""}): string`);
|
|
904
1014
|
}
|
|
905
1015
|
out.push("{");
|
|
906
1016
|
for (const line of emitter.prelude)
|
|
@@ -909,8 +1019,15 @@ export function compileMessages(locale: string, catalog: Catalog, options: Compi
|
|
|
909
1019
|
}
|
|
910
1020
|
out.push(` return ${body};`);
|
|
911
1021
|
out.push("}");
|
|
1022
|
+
if (binding !== name)
|
|
1023
|
+
{
|
|
1024
|
+
out.push(`export { ${binding} as ${name} };`);
|
|
1025
|
+
}
|
|
912
1026
|
out.push("");
|
|
913
1027
|
}
|
|
1028
|
+
const helpers = helpersFor(used, javascript);
|
|
1029
|
+
const shared = Array.from(formatters, ([create, binding]) => `const ${binding} = $once(() => ${create});`);
|
|
1030
|
+
out.splice(head, 0, ...(helpers === "" ? [] : [helpers, ""]), ...(shared.length === 0 ? [] : [...shared, ""]));
|
|
914
1031
|
return { code: out.join("\n"), names };
|
|
915
1032
|
}
|
|
916
1033
|
|
|
@@ -935,14 +1052,14 @@ export function compileIndex(locales: readonly string[], options: CompileOptions
|
|
|
935
1052
|
}
|
|
936
1053
|
if (!javascript)
|
|
937
1054
|
{
|
|
938
|
-
lines.push(`import type * as first from
|
|
1055
|
+
lines.push(`import type * as first from ${jsLiteral(`./${locales[0] ?? "en"}.${extension}`)};`);
|
|
939
1056
|
lines.push("");
|
|
940
1057
|
}
|
|
941
|
-
lines.push(`export const locales${javascript ? "" : ": readonly string[]"} = ${
|
|
1058
|
+
lines.push(`export const locales${javascript ? "" : ": readonly string[]"} = ${jsLiteral(locales)};`);
|
|
942
1059
|
lines.push("");
|
|
943
1060
|
lines.push(`/** Loads one locale's messages; each locale is its own module. */`);
|
|
944
1061
|
lines.push(javascript ? "export const loaders = {" : "export const loaders: Readonly<Record<string, () => Promise<typeof first>>> = {");
|
|
945
|
-
lines.push(locales.map((locale) => ` ${
|
|
1062
|
+
lines.push(locales.map((locale) => ` ${jsLiteral(locale)}: () => import(${jsLiteral(`./${locale}.${extension}`)})`).join(",\n"));
|
|
946
1063
|
lines.push("};");
|
|
947
1064
|
lines.push("");
|
|
948
1065
|
return lines.join("\n");
|