@zerolith/i18n 0.1.0-alpha.5 → 0.1.0-alpha.6

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/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6DA,2EAA2E;AAC3E,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAwDnE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAsGA,2EAA2E;AAC3E,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8EnE"}
package/dist/cli.js CHANGED
@@ -9,6 +9,36 @@ import { basename, join } from "node:path";
9
9
  import process from "node:process";
10
10
  import { compileIndex, compileMessages, messageNames } from "./compile.js";
11
11
  const USAGE = "usage: zerolith-i18n <folder> [--out <folder>] [--js] [--strict]\n";
12
+ /** The locale a catalog's file name gives, refusing a name that is not a BCP 47 language tag, such as `en_US.json`. */
13
+ function localeOf(file) {
14
+ const locale = basename(file, ".json");
15
+ try {
16
+ Intl.getCanonicalLocales(locale);
17
+ }
18
+ catch {
19
+ throw new Error(`${locale} is not a language tag; name the file as en.json or pt-BR.json`);
20
+ }
21
+ return locale;
22
+ }
23
+ /** A catalog's messages, refusing a file that is not a JSON object of strings. */
24
+ function catalogOf(text) {
25
+ const parsed = JSON.parse(text);
26
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
27
+ throw new Error("a catalog is a JSON object of messages");
28
+ }
29
+ for (const [key, value] of Object.entries(parsed)) {
30
+ if (typeof value !== "string") {
31
+ throw new Error(`the message "${key}" is not a string`);
32
+ }
33
+ }
34
+ return parsed;
35
+ }
36
+ /** Reports why a catalog cannot be compiled, naming its file, and gives the exit code; nothing has been written. */
37
+ function failure(file, error) {
38
+ const message = error instanceof Error ? error.message.replace(/^zerolith\/i18n: /, "") : String(error);
39
+ process.stderr.write(`zerolith-i18n: ${file}: ${message}\n`);
40
+ return 1;
41
+ }
12
42
  function parse(argv) {
13
43
  const parsed = { folder: undefined, out: undefined, javascript: false, strict: false };
14
44
  for (let index = 0; index < argv.length; index++) {
@@ -59,10 +89,21 @@ export async function main(argv) {
59
89
  return 1;
60
90
  }
61
91
  const out = parsed.out ?? parsed.folder;
62
- await mkdir(out, { recursive: true });
63
92
  const language = parsed.javascript ? "js" : "ts";
64
93
  const folder = parsed.folder;
65
- const catalogs = new Map(await Promise.all(files.map(async (file) => [basename(file, ".json"), JSON.parse(await readFile(join(folder, file), "utf8"))])));
94
+ const read = await Promise.all(files.map(async (file) => {
95
+ try {
96
+ return { file, locale: localeOf(file), catalog: catalogOf(await readFile(join(folder, file), "utf8")) };
97
+ }
98
+ catch (error) {
99
+ return { file, error };
100
+ }
101
+ }));
102
+ const unreadable = read.find((entry) => "error" in entry);
103
+ if (unreadable !== undefined) {
104
+ return failure(unreadable.file, unreadable.error);
105
+ }
106
+ const catalogs = new Map(read.map((entry) => [entry.locale ?? "", entry.catalog ?? {}]));
66
107
  const all = new Set();
67
108
  for (const catalog of catalogs.values()) {
68
109
  for (const name of messageNames(catalog)) {
@@ -70,7 +111,7 @@ export async function main(argv) {
70
111
  }
71
112
  }
72
113
  let missing = 0;
73
- const writes = [];
114
+ const modules = new Map();
74
115
  for (const [locale, catalog] of catalogs) {
75
116
  const present = new Set(messageNames(catalog));
76
117
  for (const name of all) {
@@ -79,11 +120,16 @@ export async function main(argv) {
79
120
  process.stderr.write(`zerolith-i18n: ${locale} lacks ${name}\n`);
80
121
  }
81
122
  }
82
- const compiled = compileMessages(locale, catalog, { language, source: `${locale}.json` });
83
- writes.push(writeFile(join(out, `${locale}.${language}`), compiled.code));
123
+ try {
124
+ modules.set(`${locale}.${language}`, compileMessages(locale, catalog, { language, source: `${locale}.json` }).code);
125
+ }
126
+ catch (error) {
127
+ return failure(`${locale}.json`, error);
128
+ }
84
129
  }
85
- writes.push(writeFile(join(out, `index.${language}`), compileIndex([...catalogs.keys()], { language })));
86
- await Promise.all(writes);
130
+ modules.set(`index.${language}`, compileIndex([...catalogs.keys()], { language }));
131
+ await mkdir(out, { recursive: true });
132
+ await Promise.all([...modules].map(([name, code]) => writeFile(join(out, name), code)));
87
133
  process.stdout.write(`zerolith-i18n: ${catalogs.size} locale(s), ${all.size} message(s), written to ${out}\n`);
88
134
  return missing > 0 && parsed.strict ? 1 : 0;
89
135
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/compile.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface CompileOptions {
20
20
  }
21
21
  /** The result of compiling one catalog. */
22
22
  export interface CompiledCatalog {
23
+ /** 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`. */
23
24
  code: string;
24
25
  /** The message keys as function names, in source order. */
25
26
  names: string[];
@@ -28,7 +29,11 @@ export interface CompiledCatalog {
28
29
  * Compiles one locale's catalog into a module: `locale`, one exported
29
30
  * function per message named from its key, and the `Intl` helpers they
30
31
  * share. Each function takes an `args` object typed from the message's
31
- * placeholders, or nothing when the message has none.
32
+ * placeholders, or nothing when the message has none. A formatter whose
33
+ * options are all literals is a module-level binding created on first use
34
+ * and shared by every message that formats the same way; options that read
35
+ * an argument find their formatter in a cache keyed by the options. Only
36
+ * the helpers the messages use are written.
32
37
  */
33
38
  export declare function compileMessages(locale: string, catalog: Catalog, options?: CompileOptions): CompiledCatalog;
34
39
  /** The keys of a catalog as function names, for comparing locales. */
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../src/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wDAAwD;AACxD,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEvD,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAE3B,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,eAAe;IAE5B,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;CACnB;AA4zBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,eAAe,CAkD/G;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAGvD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CAsB7F"}
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../src/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wDAAwD;AACxD,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEvD,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAE3B,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,eAAe;IAE5B,mKAAmK;IACnK,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;CACnB;AAy4BD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,eAAe,CA+D/G;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAGvD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CAsB7F"}
package/dist/compile.js CHANGED
@@ -279,6 +279,14 @@ const NUMBER_OPTIONS = {
279
279
  currencySign: "currencySign",
280
280
  numberingSystem: "numberingSystem"
281
281
  };
282
+ /** Names a module cannot bind; a message named so is exported under its name from a binding of its own. */
283
+ const RESERVED = 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"]);
284
+ /** An argument or local name JavaScript can write as it is; MessageFormat also allows `-` and `.`, as in `$first-name`. */
285
+ const PLAIN_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
286
+ /** A property name as code: as it is when plain, quoted otherwise. */
287
+ function property(name) {
288
+ return PLAIN_NAME.test(name) ? name : JSON.stringify(name);
289
+ }
282
290
  function identifier(key) {
283
291
  const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter((word) => word !== "");
284
292
  const name = words.map((word, index) => (index === 0 ? word.charAt(0).toLowerCase() + word.slice(1) : word.charAt(0).toUpperCase() + word.slice(1))).join("");
@@ -290,14 +298,30 @@ class Emitter {
290
298
  prelude = [];
291
299
  key;
292
300
  message;
293
- constructor(key, message) {
301
+ formatters;
302
+ used;
303
+ reads = 0;
304
+ constructor(key, message, formatters, used) {
294
305
  this.key = key;
295
306
  this.message = message;
307
+ this.formatters = formatters;
308
+ this.used = used;
309
+ }
310
+ /** 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. */
311
+ shared(create) {
312
+ this.used.add("once");
313
+ let binding = this.formatters.get(create);
314
+ if (binding === undefined) {
315
+ binding = `$f${this.formatters.size}`;
316
+ this.formatters.set(create, binding);
317
+ }
318
+ return binding;
296
319
  }
297
320
  fail(text) {
298
321
  throw new Error(`zerolith/i18n: message "${this.key}": ${text}`);
299
322
  }
300
323
  argument(name, type) {
324
+ this.reads++;
301
325
  const local = this.locals.get(name);
302
326
  if (local !== undefined) {
303
327
  return local;
@@ -309,7 +333,7 @@ class Emitter {
309
333
  else if (existing !== type && type !== "string") {
310
334
  this.fail(`$${name} is used both as ${existing} and as ${type}`);
311
335
  }
312
- return `args.${name}`;
336
+ return PLAIN_NAME.test(name) ? `args.${name}` : `args[${JSON.stringify(name)}]`;
313
337
  }
314
338
  optionValue(value) {
315
339
  if (value === undefined) {
@@ -347,7 +371,7 @@ class Emitter {
347
371
  entries.push(`minimumFractionDigits: ${value.value}`, `maximumFractionDigits: ${value.value}`);
348
372
  }
349
373
  }
350
- return `{ ${entries.join(", ")} }`;
374
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
351
375
  }
352
376
  dateOptions(expression) {
353
377
  const entries = [];
@@ -376,7 +400,7 @@ class Emitter {
376
400
  if (style !== undefined) {
377
401
  entries.push(`timeZoneName: ${JSON.stringify(style)}`);
378
402
  }
379
- return `{ ${entries.join(", ")} }`;
403
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
380
404
  }
381
405
  /** The JavaScript for an expression's numeric value, before formatting. */
382
406
  numeric(expression) {
@@ -401,11 +425,11 @@ class Emitter {
401
425
  }
402
426
  return value;
403
427
  }
404
- /** Resolves a plain `{$x}` to its declaration when one exists. */
405
- resolve(expression) {
428
+ /** 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. */
429
+ resolve(expression, locals = false) {
406
430
  if (expression.fn === undefined && expression.operand?.kind === "variable") {
407
431
  const declared = this.message.declarations.get(expression.operand.name);
408
- if (declared !== undefined && !declared.local) {
432
+ if (declared !== undefined && (locals || !declared.local)) {
409
433
  return declared.expression;
410
434
  }
411
435
  }
@@ -423,7 +447,14 @@ class Emitter {
423
447
  return operand.kind === "literal" ? JSON.stringify(operand.value) : `String(${this.argument(operand.name, "string")})`;
424
448
  }
425
449
  if (NUMBER_FUNCTIONS.has(fn)) {
426
- return `number(${this.numeric(expression)}, ${this.numberOptions(expression)})`;
450
+ const value = this.numeric(expression);
451
+ const reads = this.reads;
452
+ const options = this.numberOptions(expression);
453
+ if (this.reads === reads) {
454
+ return `${this.shared(`new Intl.NumberFormat(locale, ${options})`)}().format(${value})`;
455
+ }
456
+ this.used.add("number");
457
+ return `$number(${value}, ${options})`;
427
458
  }
428
459
  if (DATE_FUNCTIONS.has(fn)) {
429
460
  const operand = expression.operand;
@@ -431,13 +462,13 @@ class Emitter {
431
462
  this.fail(`:${fn} needs an operand`);
432
463
  }
433
464
  const value = operand.kind === "literal" ? `new Date(${JSON.stringify(operand.value)})` : this.argument(operand.name, "date");
434
- return `date(${value}, ${this.dateOptions(expression)})`;
465
+ return `${this.shared(`new Intl.DateTimeFormat(locale, ${this.dateOptions(expression)})`)}().format(${value})`;
435
466
  }
436
467
  this.fail(`unknown function :${fn}`);
437
468
  }
438
469
  /** The JavaScript for a selector: its comparable value and the categories it matches. */
439
470
  selector(raw) {
440
- const expression = this.resolve(raw);
471
+ const expression = this.resolve(raw, true);
441
472
  const fn = expression.fn ?? "string";
442
473
  if (NUMBER_FUNCTIONS.has(fn)) {
443
474
  const select = expression.options["select"];
@@ -446,7 +477,14 @@ class Emitter {
446
477
  if (mode === "exact") {
447
478
  return `{ value: String(${value}), categories: [] }`;
448
479
  }
449
- return `{ value: String(${value}), categories: [plural(${value}, ${JSON.stringify(mode === "ordinal" ? "ordinal" : "cardinal")})] }`;
480
+ const type = JSON.stringify(mode === "ordinal" ? "ordinal" : "cardinal");
481
+ const reads = this.reads;
482
+ const options = this.numberOptions(expression);
483
+ if (this.reads === reads) {
484
+ return `{ value: String(${value}), categories: [${this.shared(`new Intl.PluralRules(locale, { ...new Intl.NumberFormat(locale, ${options}).resolvedOptions(), type: ${type} })`)}().select(${value})] }`;
485
+ }
486
+ this.used.add("plural");
487
+ return `{ value: String(${value}), categories: [$plural(${value}, ${type}, ${options})] }`;
450
488
  }
451
489
  if (fn === "string") {
452
490
  const operand = expression.operand;
@@ -468,8 +506,9 @@ class Emitter {
468
506
  for (const [name, declaration] of this.message.declarations) {
469
507
  if (declaration.local) {
470
508
  const value = this.format(declaration.expression);
471
- this.locals.set(name, `local_${name}`);
472
- this.prelude.push(`const local_${name} = ${value};`);
509
+ const binding = PLAIN_NAME.test(name) ? `local_${name}` : `local_${this.locals.size}`;
510
+ this.locals.set(name, binding);
511
+ this.prelude.push(`const ${binding} = ${value};`);
473
512
  }
474
513
  }
475
514
  if (this.message.pattern !== undefined) {
@@ -477,34 +516,72 @@ class Emitter {
477
516
  }
478
517
  const selectors = this.message.selectors.map((selector) => this.selector(selector));
479
518
  const variants = this.message.variants.map((variant) => `{ keys: ${JSON.stringify(variant.keys)}, pattern: () => ${this.pattern(variant.pattern)} }`);
480
- return `select([${selectors.join(", ")}], [${variants.join(", ")}])`;
519
+ this.used.add("select");
520
+ return `$select([${selectors.join(", ")}], [${variants.join(", ")}])`;
481
521
  }
482
522
  }
483
- const HELPERS = `const formats = new Map();
484
- function cached(kind, options, create)
523
+ /** The helpers in JavaScript and in TypeScript, in the order a module writes them. */
524
+ const HELPERS = {
525
+ cache: {
526
+ js: `const $formats = new Map();
527
+ function $cached(kind, options, create)
485
528
  {
486
529
  const key = kind + JSON.stringify(options);
487
- let instance = formats.get(key);
530
+ let instance = $formats.get(key);
488
531
  if (instance === undefined)
489
532
  {
490
533
  instance = create();
491
- formats.set(key, instance);
534
+ $formats.set(key, instance);
492
535
  }
493
536
  return instance;
494
- }
495
- function number(value, options)
537
+ }`,
538
+ ts: `const $formats: Map<string, unknown> = new Map();
539
+ function $cached<T>(kind: string, options: object, create: () => T): T
496
540
  {
497
- return cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
498
- }
499
- function date(value, options)
541
+ const key = kind + JSON.stringify(options);
542
+ let instance = $formats.get(key) as T | undefined;
543
+ if (instance === undefined)
544
+ {
545
+ instance = create();
546
+ $formats.set(key, instance);
547
+ }
548
+ return instance;
549
+ }`
550
+ },
551
+ once: {
552
+ js: `function $once(create)
500
553
  {
501
- return cached("d", options, () => new Intl.DateTimeFormat(locale, options)).format(value);
502
- }
503
- function plural(value, type)
554
+ let instance;
555
+ return () => (instance ??= create());
556
+ }`,
557
+ ts: `function $once<T>(create: () => T): () => T
504
558
  {
505
- return cached("p", { type }, () => new Intl.PluralRules(locale, { type })).select(value);
506
- }
507
- function select(selectors, variants)
559
+ let instance: T | undefined;
560
+ return () => (instance ??= create());
561
+ }`
562
+ },
563
+ number: {
564
+ js: `function $number(value, options)
565
+ {
566
+ return $cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
567
+ }`,
568
+ ts: `function $number(value: number, options: Intl.NumberFormatOptions): string
569
+ {
570
+ return $cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
571
+ }`
572
+ },
573
+ plural: {
574
+ js: `function $plural(value, type, options)
575
+ {
576
+ return $cached("p" + type, options, () => new Intl.PluralRules(locale, { ...$cached("n", options, () => new Intl.NumberFormat(locale, options)).resolvedOptions(), type })).select(value);
577
+ }`,
578
+ ts: `function $plural(value: number, type: "cardinal" | "ordinal", options: Intl.NumberFormatOptions): string
579
+ {
580
+ return $cached("p" + type, options, () => new Intl.PluralRules(locale, { ...$cached("n", options, () => new Intl.NumberFormat(locale, options)).resolvedOptions(), type })).select(value);
581
+ }`
582
+ },
583
+ select: {
584
+ js: `function $select(selectors, variants)
508
585
  {
509
586
  let best;
510
587
  let bestRank;
@@ -537,7 +614,7 @@ function select(selectors, variants)
537
614
  {
538
615
  continue;
539
616
  }
540
- if (matches && better(rank, bestRank))
617
+ if (matches && $better(rank, bestRank))
541
618
  {
542
619
  best = variant;
543
620
  bestRank = rank;
@@ -545,7 +622,7 @@ function select(selectors, variants)
545
622
  }
546
623
  return best === undefined ? "" : best.pattern();
547
624
  }
548
- function better(rank, current)
625
+ function $better(rank, current)
549
626
  {
550
627
  if (current === undefined)
551
628
  {
@@ -560,8 +637,8 @@ function better(rank, current)
560
637
  }
561
638
  }
562
639
  return false;
563
- }`;
564
- const HELPERS_TS = `interface Selector
640
+ }`,
641
+ ts: `interface Selector
565
642
  {
566
643
  value: string;
567
644
  categories: string[];
@@ -571,31 +648,7 @@ interface Variant
571
648
  keys: string[];
572
649
  pattern: () => string;
573
650
  }
574
- const formats: Map<string, unknown> = new Map();
575
- function cached<T>(kind: string, options: object, create: () => T): T
576
- {
577
- const key = kind + JSON.stringify(options);
578
- let instance = formats.get(key) as T | undefined;
579
- if (instance === undefined)
580
- {
581
- instance = create();
582
- formats.set(key, instance);
583
- }
584
- return instance;
585
- }
586
- function number(value: number, options: Intl.NumberFormatOptions): string
587
- {
588
- return cached("n", options, () => new Intl.NumberFormat(locale, options)).format(value);
589
- }
590
- function date(value: Date | number, options: Intl.DateTimeFormatOptions): string
591
- {
592
- return cached("d", options, () => new Intl.DateTimeFormat(locale, options)).format(value);
593
- }
594
- function plural(value: number, type: "cardinal" | "ordinal"): string
595
- {
596
- return cached("p", { type }, () => new Intl.PluralRules(locale, { type })).select(value);
597
- }
598
- function better(rank: number[], current: number[] | undefined): boolean
651
+ function $better(rank: number[], current: number[] | undefined): boolean
599
652
  {
600
653
  if (current === undefined)
601
654
  {
@@ -611,7 +664,7 @@ function better(rank: number[], current: number[] | undefined): boolean
611
664
  }
612
665
  return false;
613
666
  }
614
- function select(selectors: Selector[], variants: Variant[]): string
667
+ function $select(selectors: Selector[], variants: Variant[]): string
615
668
  {
616
669
  let best: Variant | undefined;
617
670
  let bestRank: number[] | undefined;
@@ -640,14 +693,24 @@ function select(selectors: Selector[], variants: Variant[]): string
640
693
  break;
641
694
  }
642
695
  }
643
- if (matches && better(rank, bestRank))
696
+ if (matches && $better(rank, bestRank))
644
697
  {
645
698
  best = variant;
646
699
  bestRank = rank;
647
700
  }
648
701
  }
649
702
  return best === undefined ? "" : best.pattern();
650
- }`;
703
+ }`
704
+ }
705
+ };
706
+ /** The helpers a module's messages use, with the cache that `$number` and `$plural` share. */
707
+ function helpersFor(used, javascript) {
708
+ const needed = new Set(used);
709
+ if (needed.has("number") || needed.has("plural")) {
710
+ needed.add("cache");
711
+ }
712
+ return Object.keys(HELPERS).filter((name) => needed.has(name)).map((name) => HELPERS[name][javascript ? "js" : "ts"]).join("\n");
713
+ }
651
714
  function typeOf(type) {
652
715
  return type === "number" ? "number" : type === "date" ? "Date | number" : "string | number";
653
716
  }
@@ -655,7 +718,11 @@ function typeOf(type) {
655
718
  * Compiles one locale's catalog into a module: `locale`, one exported
656
719
  * function per message named from its key, and the `Intl` helpers they
657
720
  * share. Each function takes an `args` object typed from the message's
658
- * placeholders, or nothing when the message has none.
721
+ * placeholders, or nothing when the message has none. A formatter whose
722
+ * options are all literals is a module-level binding created on first use
723
+ * and shared by every message that formats the same way; options that read
724
+ * an argument find their formatter in a cache keyed by the options. Only
725
+ * the helpers the messages use are written.
659
726
  */
660
727
  export function compileMessages(locale, catalog, options = {}) {
661
728
  const javascript = options.language === "js";
@@ -666,8 +733,9 @@ export function compileMessages(locale, catalog, options = {}) {
666
733
  }
667
734
  out.push(`export const locale${javascript ? "" : ": string"} = ${JSON.stringify(locale)};`);
668
735
  out.push("");
669
- out.push(javascript ? HELPERS : HELPERS_TS);
670
- out.push("");
736
+ const head = out.length;
737
+ const formatters = new Map();
738
+ const used = new Set();
671
739
  const names = [];
672
740
  const seen = new Set();
673
741
  for (const [key, source] of Object.entries(catalog)) {
@@ -675,21 +743,25 @@ export function compileMessages(locale, catalog, options = {}) {
675
743
  if (seen.has(name)) {
676
744
  throw new Error(`zerolith/i18n: keys "${key}" and another one both become ${name}`);
677
745
  }
746
+ if (name === "locale") {
747
+ throw new Error(`zerolith/i18n: key "${key}" becomes locale, which every compiled module exports; rename the key`);
748
+ }
749
+ const binding = RESERVED.has(name) ? `message_${name}` : name;
678
750
  seen.add(name);
679
751
  names.push(name);
680
752
  const message = new Parser(source, key).parse();
681
- const emitter = new Emitter(key, message);
753
+ const emitter = new Emitter(key, message, formatters, used);
682
754
  const body = emitter.body();
683
755
  const args = [...emitter.arguments.entries()].toSorted(([a], [b]) => a.localeCompare(b));
684
756
  const doc = source.replace(/\*\//g, "* /").split(/\r?\n/).map((line) => ` * ${line}`.trimEnd()).join("\n");
685
- const argumentType = `{ ${args.map(([argument, type]) => `${argument}: ${typeOf(type)}`).join("; ")} }`;
757
+ const argumentType = `{ ${args.map(([argument, type]) => `${property(argument)}: ${typeOf(type)}`).join("; ")} }`;
686
758
  if (javascript) {
687
759
  out.push(`/**\n${doc}${args.length > 0 ? `\n * @param {${argumentType}} args` : ""}\n * @returns {string}\n */`);
688
- out.push(`export function ${name}(${args.length > 0 ? "args" : ""})`);
760
+ out.push(`${binding === name ? "export " : ""}function ${binding}(${args.length > 0 ? "args" : ""})`);
689
761
  }
690
762
  else {
691
763
  out.push(`/**\n${doc}\n */`);
692
- out.push(`export function ${name}(${args.length > 0 ? `args: ${argumentType}` : ""}): string`);
764
+ out.push(`${binding === name ? "export " : ""}function ${binding}(${args.length > 0 ? `args: ${argumentType}` : ""}): string`);
693
765
  }
694
766
  out.push("{");
695
767
  for (const line of emitter.prelude) {
@@ -697,8 +769,14 @@ export function compileMessages(locale, catalog, options = {}) {
697
769
  }
698
770
  out.push(` return ${body};`);
699
771
  out.push("}");
772
+ if (binding !== name) {
773
+ out.push(`export { ${binding} as ${name} };`);
774
+ }
700
775
  out.push("");
701
776
  }
777
+ const helpers = helpersFor(used, javascript);
778
+ const shared = Array.from(formatters, ([create, binding]) => `const ${binding} = $once(() => ${create});`);
779
+ out.splice(head, 0, ...(helpers === "" ? [] : [helpers, ""]), ...(shared.length === 0 ? [] : [...shared, ""]));
702
780
  return { code: out.join("\n"), names };
703
781
  }
704
782
  /** The keys of a catalog as function names, for comparing locales. */