@xndrjs/i18n 0.4.0 → 0.5.0-alpha.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.
Files changed (57) hide show
  1. package/README.md +156 -29
  2. package/dist/codegen/index.d.ts +85 -0
  3. package/dist/codegen/index.js +181 -0
  4. package/dist/codegen/index.js.map +1 -0
  5. package/dist/index.d.ts +34 -5
  6. package/dist/index.js +175 -74
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +4 -0
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +6 -1
  11. package/src/IcuTranslationProviderMulti.test.ts +47 -0
  12. package/src/IcuTranslationProviderMulti.ts +41 -61
  13. package/src/IcuTranslationProviderSingle.test.ts +67 -0
  14. package/src/IcuTranslationProviderSingle.ts +27 -51
  15. package/src/audit/audit-dictionaries.ts +4 -1
  16. package/src/audit/run-audit.test.ts +35 -1
  17. package/src/audit/run-audit.ts +15 -7
  18. package/src/codegen/codegen-config-schema.ts +68 -1
  19. package/src/codegen/config.test.ts +174 -39
  20. package/src/codegen/config.ts +14 -5
  21. package/src/codegen/constants.ts +8 -0
  22. package/src/codegen/delivery-artifacts.test.ts +142 -0
  23. package/src/codegen/delivery-artifacts.ts +124 -0
  24. package/src/codegen/emit/dictionary-file.test.ts +190 -0
  25. package/src/codegen/emit/dictionary-file.ts +163 -0
  26. package/src/codegen/emit/dictionary-schema-file.ts +5 -0
  27. package/src/codegen/emit/instance-file.ts +114 -28
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
  29. package/src/codegen/emit/namespace-loaders-file.ts +114 -6
  30. package/src/codegen/emit/types-file.test.ts +114 -0
  31. package/src/codegen/emit/types-file.ts +48 -11
  32. package/src/codegen/generate-i18n-types.test.ts +754 -16
  33. package/src/codegen/generate-i18n-types.ts +111 -47
  34. package/src/codegen/icu-analysis.ts +9 -2
  35. package/src/codegen/locale-fallback.ts +19 -10
  36. package/src/codegen/locale-policy.ts +4 -0
  37. package/src/codegen/paths.ts +9 -5
  38. package/src/codegen/project-locales-set-namespace.test.ts +5 -5
  39. package/src/codegen/read-dictionary.test.ts +474 -2
  40. package/src/codegen/read-dictionary.ts +164 -15
  41. package/src/codegen/write-file-if-changed.test.ts +42 -0
  42. package/src/codegen/write-file-if-changed.ts +20 -0
  43. package/src/codegen-config/build-config.ts +34 -0
  44. package/src/codegen-config/codegen-config.test.ts +32 -0
  45. package/src/codegen-config/index.ts +21 -0
  46. package/src/codegen-config/write-config.ts +8 -0
  47. package/src/deep-freeze.test.ts +13 -0
  48. package/src/deep-freeze.ts +5 -0
  49. package/src/format-core.ts +91 -0
  50. package/src/index.ts +8 -2
  51. package/src/project-locales.test.ts +129 -10
  52. package/src/project-locales.ts +90 -3
  53. package/src/setup/setup-i18n.test.ts +1 -1
  54. package/src/setup/setup-i18n.ts +6 -30
  55. package/src/types.ts +19 -4
  56. package/src/validation/normalize.ts +4 -0
  57. /package/src/{setup → codegen-config}/type-names.ts +0 -0
package/README.md CHANGED
@@ -196,8 +196,8 @@ Variables found across **all locales** of the same key are merged. If parsing fa
196
196
  ### 3. Generated files
197
197
 
198
198
  - **`i18n-types.generated.ts`** — `I18N_MODE`, `MyProjectParams`, `MyProjectSchema`.
199
- - **`dictionary.generated.ts`** — imports the JSON files and exports `defaultDictionary` (the codegen fallback dictionary).
200
- - **`instance.generated.ts`** — exports `createI18n(dictionary)` (required argument; no default import of the fallback dictionary), typed `projectLocales()` (full schema), and in multi mode `projectNamespaceLocales()` (single namespace); `LOCALE_FALLBACK` wired in when configured.
199
+ - **`dictionary.generated.ts`** — imports the JSON files and exports `defaultDictionary` (canonical delivery) or `defaultDictionaryFor(locale)` (split-by-locale delivery).
200
+ - **`instance.generated.ts`** — exports `createI18n(dictionary)` (required argument; no default import of the fallback dictionary), typed `projectDictionaryLocales()` (full schema), and in multi mode `projectNamespaceLocales()` (single namespace); with `delivery: "custom"`, also `projectDictionaryForDeliveryArea()` and `projectNamespaceForDeliveryArea()`; `LOCALE_FALLBACK` wired in when configured.
201
201
  - **`i18n.ts`** (optional, hand-written) — app-owned singleton if desired.
202
202
 
203
203
  Example generated types (multi-namespace):
@@ -221,9 +221,18 @@ export type MyProjectParams = {
221
221
  };
222
222
 
223
223
  export type MyProjectSchema = {
224
- default: typeof import("./translations/default.json");
225
- user: typeof import("./translations/user.json");
226
- billing: typeof import("./translations/billing.json");
224
+ default: {
225
+ login_button: Partial<Record<MyProjectLocale, string>>;
226
+ welcome: Partial<Record<MyProjectLocale, string>>;
227
+ dashboard_status: Partial<Record<MyProjectLocale, string>>;
228
+ };
229
+ user: {
230
+ profile_title: Partial<Record<MyProjectLocale, string>>;
231
+ greeting: Partial<Record<MyProjectLocale, string>>;
232
+ };
233
+ billing: {
234
+ invoice_summary: Partial<Record<MyProjectLocale, string>>;
235
+ };
227
236
  };
228
237
  ```
229
238
 
@@ -306,14 +315,14 @@ const localeFallback = {
306
315
  const i18n = new IcuTranslationProviderMulti(schema, { localeFallback });
307
316
  ```
308
317
 
309
- #### `projectLocales` / `projectNamespaceLocales`
318
+ #### `projectDictionaryLocales` / `projectNamespaceLocales`
310
319
 
311
320
  Namespaces split the dictionary by domain; locale projection splits it by **locale**. Use before `setAll()` / `setNamespace()` when you only need one locale (or a small regional group) in memory.
312
321
 
313
322
  ```ts
314
323
  import {
315
324
  createI18n,
316
- projectLocales,
325
+ projectDictionaryLocales,
317
326
  projectNamespaceLocales,
318
327
  } from "./i18n/generated/instance.generated.js";
319
328
  import { defaultDictionary } from "./i18n/generated/dictionary.generated.js";
@@ -321,10 +330,10 @@ import billingDictionary from "./i18n/translations/billing.json";
321
330
 
322
331
  const i18n = createI18n(defaultDictionary);
323
332
  i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, [activeLocale]));
324
- i18n.setAll(projectLocales(fullDictionary, [activeLocale])); // multi: all namespaces
333
+ i18n.setAll(projectDictionaryLocales(fullDictionary, [activeLocale])); // multi: all namespaces
325
334
  ```
326
335
 
327
- Codegen emits typed wrappers in `instance.generated.ts` (`projectLocales` for the full schema; `projectNamespaceLocales` in multi mode for one namespace). The low-level `@xndrjs/i18n` exports remain for tooling without codegen.
336
+ Codegen emits typed wrappers in `instance.generated.ts` (`projectDictionaryLocales` for the full schema; `projectNamespaceLocales` in multi mode for one namespace). With `delivery: "custom"`, it also emits `projectDictionaryForDeliveryArea` and `projectNamespaceForDeliveryArea`. The low-level `@xndrjs/i18n` exports are `*Core` helpers for tooling without codegen.
328
337
 
329
338
  ### 6. Translation audit (`xndrjs-i18n-audit`)
330
339
 
@@ -398,29 +407,31 @@ Specify **exactly one** of `dictionary` (single-file) or `namespaces` (multi-fil
398
407
  }
399
408
  ```
400
409
 
401
- | Field | Description |
402
- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
403
- | `dictionary` | Path to a single dictionary file (`.json`, `.yaml`, or `.yml`) for the flat API. Mutually exclusive with `namespaces`. |
404
- | `namespaces` | Map of `namespace -> dictionary path` (`.json`, `.yaml`, or `.yml`) for the namespaced API. Mutually exclusive with `dictionary`. |
405
- | `defaultNamespace` | Optional. Namespace label used internally in single-file mode (default `"default"`). Not exposed in the flat API. |
406
- | `typesOutput` | Output path for the generated types. |
407
- | `dictionaryOutput` | Output path for the generated dictionary manifest. |
408
- | `instanceOutput` | Output path for the generated factory (`createI18n`). |
409
- | `importExtension` | Optional. Relative import suffix between generated `.ts` modules: `"none"` (default, extensionless), `".ts"`, or `".js"`. |
410
- | `factoryName` | Name of the exported factory function (default `createI18n`). |
411
- | `paramsTypeName` / `schemaTypeName` | Names of the exported types (customizable per project). |
412
- | `localeTypeName` | Name of the exported locale union type (default `MyProjectLocale`). |
413
- | `localeFallback` | Optional map of `locale -> next locale | null` for runtime fallback resolution. |
414
- | `localeFallbackConstName` | Name of the generated fallback constant (default `LOCALE_FALLBACK`). |
415
- | `dictionarySchemaOutput` | Optional path for generated external dictionary validation (`dictionary-schema.generated.ts`). Requires `zod` in the consumer app. |
416
- | `loadOnInit` | Multi mode only. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default). |
417
- | `namespaceLoadersOutput` | Output path for generated `namespaceLoaders` (dynamic `import()` per lazy namespace). Defaults to `{dirname(instanceOutput)}/namespace-loaders.generated.ts`. Required when lazy namespaces exist. |
410
+ | Field | Description |
411
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
412
+ | `dictionary` | Path to a single dictionary file (`.json`, `.yaml`, or `.yml`) for the flat API. Mutually exclusive with `namespaces`. |
413
+ | `namespaces` | Map of `namespace -> dictionary path` (`.json`, `.yaml`, or `.yml`) for the namespaced API. Mutually exclusive with `dictionary`. |
414
+ | `defaultNamespace` | Optional. Namespace label used internally in single-file mode (default `"default"`). Not exposed in the flat API. |
415
+ | `typesOutput` | Output path for the generated types. |
416
+ | `dictionaryOutput` | Output path for the generated dictionary manifest. |
417
+ | `instanceOutput` | Output path for the generated factory (`createI18n`). |
418
+ | `importExtension` | Optional. Relative import suffix between generated `.ts` modules: `"none"` (default, extensionless), `".ts"`, or `".js"`. |
419
+ | `factoryName` | Name of the exported factory function (default `createI18n`). |
420
+ | `paramsTypeName` / `schemaTypeName` | Names of the exported types (customizable per project). |
421
+ | `localeTypeName` | Name of the exported locale union type (default `MyProjectLocale`). |
422
+ | `localeFallback` | Optional map of `locale -> next locale | null` for runtime fallback resolution. |
423
+ | `localeFallbackConstName` | Name of the generated fallback constant (default `LOCALE_FALLBACK`). |
424
+ | `dictionarySchemaOutput` | Optional path for generated external dictionary validation (`dictionary-schema.generated.ts`). Requires `zod` in the consumer app. |
425
+ | `loadOnInit` | Multi mode only. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default). |
426
+ | `namespaceLoadersOutput` | Output path for generated `namespaceLoaders` (dynamic `import()` per lazy namespace). Defaults to `{dirname(instanceOutput)}/namespace-loaders.generated.ts`. Required when lazy namespaces exist. |
427
+ | `delivery` | Optional. `"canonical"` (default) — one multilocale JSON per namespace. `"split-by-locale"` — emits `{basename}.{locale}.json` under `{deliveryOutput}/translations/` and per-locale loaders. See [Split-by-locale delivery](#split-by-locale-delivery). |
428
+ | `deliveryOutput` | Optional. Directory for compiled and split delivery JSON (files land in `{deliveryOutput}/translations/`). Defaults to `dirname(typesOutput)`. Use e.g. `public/i18n` to ship per-locale JSON from a static host while keeping generated TypeScript under `generated/`. |
418
429
 
419
430
  > Paths are resolved relative to the directory containing `i18n.codegen.json` (e.g. `i18n/` when using `xndrjs-i18n-setup .`).
420
431
 
421
432
  ### YAML authoring
422
433
 
423
- Dictionary paths may use `.yaml` or `.yml` instead of `.json`. **YAML is an authoring format, not a runtime format** — codegen compiles YAML to JSON under the generated output directory (for example `translations/billing.yaml` → `{dirname(typesOutput)}/translations/billing.json`); generated TypeScript imports the compiled JSON at runtime. Edit only the YAML source — the compiled JSON is overwritten on each codegen run.
434
+ Dictionary paths may use `.yaml` or `.yml` instead of `.json`. **YAML is an authoring format, not a runtime format** — codegen compiles YAML to JSON under the delivery output directory (for example `translations/billing.yaml` → `{deliveryOutput}/translations/billing.json`; default `{deliveryOutput}` is `dirname(typesOutput)`); generated TypeScript imports the compiled JSON at runtime. Edit only the YAML source — the compiled JSON is overwritten on each codegen run.
424
435
 
425
436
  Use YAML when ICU is hard to read on one line: multiple parameters, or plural/select branches. It is not aimed at long legal copy or styled pages — those usually belong in a separate content template with localized fragments inside.
426
437
 
@@ -443,6 +454,82 @@ Workflow:
443
454
 
444
455
  Mixed namespaces are supported: some namespaces can stay `.json` while others use YAML.
445
456
 
457
+ ### Split-by-locale delivery
458
+
459
+ By default (`delivery: "canonical"`), codegen keeps one multilocale JSON per namespace — either your source file in place (`.json`) or a compiled YAML → JSON under `{deliveryOutput}/translations/` (default: `generated/translations/`).
460
+
461
+ Set `"delivery": "split-by-locale"` to emit **one JSON file per locale** instead:
462
+
463
+ ```json
464
+ {
465
+ "delivery": "split-by-locale",
466
+ "namespaces": {
467
+ "default": "translations/default.json",
468
+ "billing": "translations/billing.yaml"
469
+ },
470
+ "loadOnInit": ["default"]
471
+ }
472
+ ```
473
+
474
+ Codegen writes `{deliveryOutput}/translations/{basename}.{locale}.json` (for example `user.it.json`, `billing.en.json`). Each file contains the same shape as `projectNamespaceLocalesCore(dict, [locale])` — keys map to a single locale entry. With `localeFallback`, locales such as `de-CH` are resolved at codegen time (for example from `de-DE`).
475
+
476
+ **Authoring is unchanged** — edit the canonical source JSON/YAML. Types (`MyProjectSchema`) are generated explicitly from ICU analysis (keys + `Partial<Record<MyProjectLocale, string>>` per key), not from `typeof import` of JSON files. Audit still reads the config paths.
477
+
478
+ **Generated API changes (opt-in):**
479
+
480
+ | Canonical | Split-by-locale |
481
+ | -------------------------------- | ---------------------------------------------- |
482
+ | `export const defaultDictionary` | `export function defaultDictionaryFor(locale)` |
483
+ | `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)` |
484
+
485
+ Example `dictionary.generated.ts` (multi, `loadOnInit: ["default"]`):
486
+
487
+ ```ts
488
+ import defaultEn from "./translations/default.en.json";
489
+ import defaultIt from "./translations/default.it.json";
490
+
491
+ const defaultByLocale = {
492
+ en: defaultEn,
493
+ it: defaultIt,
494
+ } as const satisfies Record<MyProjectLocale, MyProjectSchema["default"]>;
495
+
496
+ export function defaultDictionaryFor(locale: MyProjectLocale): InitialSchema {
497
+ return { default: defaultByLocale[locale] };
498
+ }
499
+ ```
500
+
501
+ Example `namespace-loaders.generated.ts`:
502
+
503
+ ```ts
504
+ export const namespaceLoaders = {
505
+ billing: (locale: MyProjectLocale) => {
506
+ switch (locale) {
507
+ case "en":
508
+ return import("./translations/billing.en.json").then((m) => m.default);
509
+ case "it":
510
+ return import("./translations/billing.it.json").then((m) => m.default);
511
+ case "de-CH":
512
+ return import("./translations/billing.de-CH.json").then((m) => m.default);
513
+ }
514
+ },
515
+ };
516
+ ```
517
+
518
+ Init with the active locale:
519
+
520
+ ```ts
521
+ import { createI18n } from "./generated/instance.generated.js";
522
+ import { defaultDictionaryFor } from "./generated/dictionary.generated.js";
523
+
524
+ const i18n = createI18n(defaultDictionaryFor(activeLocale));
525
+
526
+ if (!i18n.hasNamespace("billing")) {
527
+ i18n.setNamespace("billing", await namespaceLoaders.billing(activeLocale));
528
+ }
529
+ ```
530
+
531
+ Use split delivery when you want smaller lazy chunks (one locale per dynamic import) or when serving per-locale JSON from `public/` without runtime `projectNamespaceLocales`. Runtime `projectDictionaryLocales` / `projectNamespaceLocales` remain available for external CMS/API payloads.
532
+
446
533
  ## Usage
447
534
 
448
535
  ### Single vs. multi-namespace API
@@ -492,7 +579,7 @@ i18n.setNamespace("billing", externalBillingPayload);
492
579
 
493
580
  ### Lazy namespace loading (multi mode)
494
581
 
495
- Split namespaces across chunks by listing only the namespaces you need at startup in `loadOnInit`. Codegen emits typed `namespaceLoaders` — one dynamic `import()` per lazy namespace. `.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering.
582
+ Split namespaces across chunks by listing only the namespaces you need at startup in `loadOnInit`. Codegen emits typed `namespaceLoaders` — one dynamic `import()` per lazy namespace (or per lazy namespace **and locale** when `delivery: "split-by-locale"`). `.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering.
496
583
 
497
584
  ```json
498
585
  {
@@ -526,6 +613,16 @@ await Promise.all(
526
613
  );
527
614
  ```
528
615
 
616
+ With `delivery: "split-by-locale"`, pick the locale on each loader:
617
+
618
+ ```ts
619
+ const locale = "it" as const;
620
+
621
+ if (!i18n.hasNamespace("billing")) {
622
+ i18n.setNamespace("billing", await namespaceLoaders.billing(locale));
623
+ }
624
+ ```
625
+
529
626
  Optional locale projection before register:
530
627
 
531
628
  ```ts
@@ -600,10 +697,40 @@ Both providers share this behavior:
600
697
  - **`hasNamespace(ns)`** — (multi only) returns whether a namespace has been loaded (eager init, lazy load, or `setNamespace`).
601
698
  - **`setAll(values)`** — replaces the dictionary and clears the entire cache.
602
699
  - **`setNamespace(ns, values)`** — (multi only) replaces one namespace and invalidates only its cache entries.
603
- - **Missing key/locale** — throws an error if the template is `undefined`. An empty string (`""`) is treated as a valid template.
700
+ - **Missing key/locale** — by default throws an error if the template is `undefined` (configurable via `onMissing`, see below). An empty string (`""`) is treated as a valid template.
604
701
  - **ICU syntax error** — throws `[i18n ICU Syntax Error] ...`.
605
702
  - **Formatting error** (missing/invalid params) — throws `[i18n Formatting Error] ...`.
606
703
 
704
+ ### Missing-translation strategy (`onMissing`)
705
+
706
+ Both providers accept an `onMissing` option controlling what happens when no template resolves for a key/locale (after walking the full fallback chain):
707
+
708
+ ```ts
709
+ onMissing?: "throw" | "key" | ((context: MissingTranslationContext) => string);
710
+ // MissingTranslationContext = { namespace?: string; key: string; locale: string; fallbackChain: string }
711
+ ```
712
+
713
+ - **`"throw"`** (default) — throws `[i18n] Missing key or locale: ...` including the fallback chain.
714
+ - **`"key"`** — returns the key itself: `key` in single mode, `namespace.key` in multi mode.
715
+ - **Function** — receives the missing-translation context (`namespace` is only set in multi mode) and returns the string to display.
716
+
717
+ ```ts
718
+ const i18n = new IcuTranslationProviderMulti(schema, {
719
+ localeFallback,
720
+ onMissing: ({ namespace, key }) => `[missing: ${namespace}.${key}]`,
721
+ });
722
+ ```
723
+
724
+ `onMissing` only applies to missing-template resolution. ICU syntax errors, formatting errors, and (in multi mode) unloaded namespaces still throw.
725
+
726
+ The generated factory accepts `onMissing` as a pass-through option, merged with the codegen-wired `localeFallback`:
727
+
728
+ ```ts
729
+ import { createI18n } from "./i18n/generated/instance.generated.js";
730
+
731
+ const i18n = createI18n(dictionary, { onMissing: "key" });
732
+ ```
733
+
607
734
  ## Commands
608
735
 
609
736
  Run from the repo root:
@@ -0,0 +1,85 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const DELIVERY_MODES: readonly ["canonical", "split-by-locale", "custom"];
4
+ type DeliveryMode = (typeof DELIVERY_MODES)[number];
5
+ declare const codegenConfigShape: {
6
+ dictionary: z.ZodOptional<z.ZodString>;
7
+ namespaces: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
8
+ defaultNamespace: z.ZodOptional<z.ZodString>;
9
+ typesOutput: z.ZodString;
10
+ dictionaryOutput: z.ZodString;
11
+ instanceOutput: z.ZodString;
12
+ dictionarySchemaOutput: z.ZodOptional<z.ZodString>;
13
+ loadOnInit: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
+ namespaceLoadersOutput: z.ZodOptional<z.ZodString>;
15
+ importExtension: z.ZodOptional<z.ZodEnum<{
16
+ none: "none";
17
+ ".ts": ".ts";
18
+ ".js": ".js";
19
+ }>>;
20
+ paramsTypeName: z.ZodString;
21
+ schemaTypeName: z.ZodString;
22
+ localeTypeName: z.ZodOptional<z.ZodString>;
23
+ localeFallbackConstName: z.ZodOptional<z.ZodString>;
24
+ localeFallback: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>>;
25
+ factoryName: z.ZodOptional<z.ZodString>;
26
+ delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
27
+ custom: "custom";
28
+ canonical: "canonical";
29
+ "split-by-locale": "split-by-locale";
30
+ }>>>;
31
+ deliveryArtifacts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
32
+ deliveryOutput: z.ZodOptional<z.ZodString>;
33
+ };
34
+ declare const codegenConfigKeys: (keyof typeof codegenConfigShape)[];
35
+ declare const codegenConfigSchema: z.ZodObject<{
36
+ dictionary: z.ZodOptional<z.ZodString>;
37
+ namespaces: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
38
+ defaultNamespace: z.ZodOptional<z.ZodString>;
39
+ typesOutput: z.ZodString;
40
+ dictionaryOutput: z.ZodString;
41
+ instanceOutput: z.ZodString;
42
+ dictionarySchemaOutput: z.ZodOptional<z.ZodString>;
43
+ loadOnInit: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
+ namespaceLoadersOutput: z.ZodOptional<z.ZodString>;
45
+ importExtension: z.ZodOptional<z.ZodEnum<{
46
+ none: "none";
47
+ ".ts": ".ts";
48
+ ".js": ".js";
49
+ }>>;
50
+ paramsTypeName: z.ZodString;
51
+ schemaTypeName: z.ZodString;
52
+ localeTypeName: z.ZodOptional<z.ZodString>;
53
+ localeFallbackConstName: z.ZodOptional<z.ZodString>;
54
+ localeFallback: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>>;
55
+ factoryName: z.ZodOptional<z.ZodString>;
56
+ delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
57
+ custom: "custom";
58
+ canonical: "canonical";
59
+ "split-by-locale": "split-by-locale";
60
+ }>>>;
61
+ deliveryArtifacts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
62
+ deliveryOutput: z.ZodOptional<z.ZodString>;
63
+ }, z.core.$strict>;
64
+ type CodegenConfigInput = z.input<typeof codegenConfigSchema>;
65
+ type CodegenConfig = z.infer<typeof codegenConfigSchema>;
66
+ declare function resolveDeliveryOutputDir(config: Pick<CodegenConfig, "typesOutput" | "deliveryOutput">): string;
67
+
68
+ type DeliveryArtifactsMap = Record<string, readonly string[]>;
69
+
70
+ declare const SUPPORTED_IMPORT_EXTENSIONS: readonly ["none", ".ts", ".js"];
71
+ type SupportedImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];
72
+
73
+ type SetupMode = "single" | "multi";
74
+ declare function buildCodegenConfig(mode: SetupMode, project: string): CodegenConfigInput;
75
+
76
+ declare function inferProjectName(dirName: string): string;
77
+ declare function typeNamesForProject(project: string): {
78
+ paramsTypeName: string;
79
+ schemaTypeName: string;
80
+ localeTypeName: string;
81
+ };
82
+
83
+ declare function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void;
84
+
85
+ export { type CodegenConfig, type CodegenConfigInput, DELIVERY_MODES, type DeliveryArtifactsMap, type DeliveryMode, SUPPORTED_IMPORT_EXTENSIONS, type SetupMode, type SupportedImportExtension, buildCodegenConfig, codegenConfigKeys, inferProjectName, resolveDeliveryOutputDir, typeNamesForProject, writeCodegenConfig };
@@ -0,0 +1,181 @@
1
+ import { z } from 'zod';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+
5
+ // src/codegen/codegen-config-schema.ts
6
+
7
+ // src/codegen/constants.ts
8
+ var SUPPORTED_IMPORT_EXTENSIONS = ["none", ".ts", ".js"];
9
+ var IDENTIFIER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
10
+ var IDENTIFIER_NAME_REQUIREMENT = "allowed: letters, digits, underscore; must not start with a digit";
11
+
12
+ // src/codegen/delivery-artifacts.ts
13
+ var DELIVERY_AREA_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
14
+ function getDeliveryArtifactsStructureIssues(deliveryArtifacts) {
15
+ const issues = [];
16
+ for (const [area, locales] of Object.entries(deliveryArtifacts)) {
17
+ if (!DELIVERY_AREA_NAME_PATTERN.test(area)) {
18
+ issues.push({
19
+ path: ["deliveryArtifacts", area],
20
+ message: `Invalid delivery area name "${area}": must match ${DELIVERY_AREA_NAME_PATTERN.source}`
21
+ });
22
+ }
23
+ if (locales.length === 0) {
24
+ issues.push({
25
+ path: ["deliveryArtifacts", area],
26
+ message: `Delivery area "${area}" must include at least one locale.`
27
+ });
28
+ }
29
+ }
30
+ const localeToArea = /* @__PURE__ */ new Map();
31
+ for (const [area, locales] of Object.entries(deliveryArtifacts)) {
32
+ for (const locale of locales) {
33
+ const previousArea = localeToArea.get(locale);
34
+ if (previousArea) {
35
+ issues.push({
36
+ path: ["deliveryArtifacts", area],
37
+ message: `Locale "${locale}" appears in both "${previousArea}" and "${area}".`
38
+ });
39
+ } else {
40
+ localeToArea.set(locale, area);
41
+ }
42
+ }
43
+ }
44
+ return issues;
45
+ }
46
+
47
+ // src/codegen/codegen-config-schema.ts
48
+ var localeFallbackSchema = z.record(z.string(), z.union([z.string(), z.null()]));
49
+ var deliveryArtifactsSchema = z.record(z.string(), z.array(z.string().min(1)));
50
+ var DELIVERY_MODES = ["canonical", "split-by-locale", "custom"];
51
+ var codegenConfigShape = {
52
+ dictionary: z.string().min(1).optional(),
53
+ namespaces: z.record(z.string(), z.string().min(1)).optional(),
54
+ defaultNamespace: z.string().min(1).optional(),
55
+ typesOutput: z.string().min(1),
56
+ dictionaryOutput: z.string().min(1),
57
+ instanceOutput: z.string().min(1),
58
+ dictionarySchemaOutput: z.string().min(1).optional(),
59
+ loadOnInit: z.array(z.string().min(1)).optional(),
60
+ namespaceLoadersOutput: z.string().min(1).optional(),
61
+ importExtension: z.enum(SUPPORTED_IMPORT_EXTENSIONS).optional(),
62
+ paramsTypeName: z.string().min(1),
63
+ schemaTypeName: z.string().min(1),
64
+ localeTypeName: z.string().min(1).optional(),
65
+ localeFallbackConstName: z.string().min(1).optional(),
66
+ localeFallback: localeFallbackSchema.optional(),
67
+ factoryName: z.string().min(1).optional(),
68
+ delivery: z.enum(DELIVERY_MODES).optional().default("canonical"),
69
+ deliveryArtifacts: deliveryArtifactsSchema.optional(),
70
+ deliveryOutput: z.string().min(1).optional()
71
+ };
72
+ var codegenConfigKeys = Object.keys(
73
+ codegenConfigShape
74
+ );
75
+ z.object(codegenConfigShape).strict().superRefine((config, ctx) => {
76
+ const hasDictionary = config.dictionary !== void 0;
77
+ const hasNamespaces = config.namespaces !== void 0;
78
+ if (hasDictionary === hasNamespaces) {
79
+ ctx.addIssue({
80
+ code: "custom",
81
+ message: 'Specify exactly one of "dictionary" or "namespaces".'
82
+ });
83
+ }
84
+ if (config.namespaces) {
85
+ for (const namespace of Object.keys(config.namespaces)) {
86
+ if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {
87
+ ctx.addIssue({
88
+ code: "custom",
89
+ path: ["namespaces", namespace],
90
+ message: `Invalid namespace name "${namespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`
91
+ });
92
+ }
93
+ }
94
+ }
95
+ if (config.defaultNamespace !== void 0 && !IDENTIFIER_NAME_PATTERN.test(config.defaultNamespace)) {
96
+ ctx.addIssue({
97
+ code: "custom",
98
+ path: ["defaultNamespace"],
99
+ message: `Invalid namespace name "${config.defaultNamespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`
100
+ });
101
+ }
102
+ if (config.delivery === "custom") {
103
+ if (!config.deliveryArtifacts) {
104
+ ctx.addIssue({
105
+ code: "custom",
106
+ path: ["deliveryArtifacts"],
107
+ message: 'deliveryArtifacts is required when delivery is "custom".'
108
+ });
109
+ } else {
110
+ for (const issue of getDeliveryArtifactsStructureIssues(config.deliveryArtifacts)) {
111
+ ctx.addIssue({
112
+ code: "custom",
113
+ path: issue.path,
114
+ message: issue.message
115
+ });
116
+ }
117
+ }
118
+ } else if (config.deliveryArtifacts !== void 0) {
119
+ ctx.addIssue({
120
+ code: "custom",
121
+ path: ["deliveryArtifacts"],
122
+ message: 'deliveryArtifacts is only allowed when delivery is "custom".'
123
+ });
124
+ }
125
+ });
126
+ function resolveDeliveryOutputDir(config) {
127
+ return config.deliveryOutput ?? path.dirname(config.typesOutput);
128
+ }
129
+
130
+ // src/codegen-config/type-names.ts
131
+ function inferProjectName(dirName) {
132
+ const parts = dirName.split(/[-_]+/).filter(Boolean);
133
+ if (parts.length === 0) {
134
+ return "App";
135
+ }
136
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
137
+ }
138
+ function typeNamesForProject(project) {
139
+ return {
140
+ paramsTypeName: `${project}Params`,
141
+ schemaTypeName: `${project}Schema`,
142
+ localeTypeName: `${project}Locale`
143
+ };
144
+ }
145
+
146
+ // src/codegen-config/build-config.ts
147
+ var GENERATED_DIR = "generated";
148
+ var TRANSLATIONS_DIR = "translations";
149
+ function buildCodegenConfig(mode, project) {
150
+ const typeNames = typeNamesForProject(project);
151
+ const base = {
152
+ typesOutput: `${GENERATED_DIR}/i18n-types.generated.ts`,
153
+ dictionaryOutput: `${GENERATED_DIR}/dictionary.generated.ts`,
154
+ instanceOutput: `${GENERATED_DIR}/instance.generated.ts`,
155
+ paramsTypeName: typeNames.paramsTypeName,
156
+ schemaTypeName: typeNames.schemaTypeName,
157
+ localeTypeName: typeNames.localeTypeName,
158
+ factoryName: "createI18n"
159
+ };
160
+ if (mode === "single") {
161
+ return {
162
+ dictionary: `${TRANSLATIONS_DIR}/translations.json`,
163
+ ...base
164
+ };
165
+ }
166
+ return {
167
+ namespaces: {
168
+ default: `${TRANSLATIONS_DIR}/default.json`
169
+ },
170
+ ...base
171
+ };
172
+ }
173
+ function writeCodegenConfig(configPath, config) {
174
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
175
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
176
+ `);
177
+ }
178
+
179
+ export { DELIVERY_MODES, SUPPORTED_IMPORT_EXTENSIONS, buildCodegenConfig, codegenConfigKeys, inferProjectName, resolveDeliveryOutputDir, typeNamesForProject, writeCodegenConfig };
180
+ //# sourceMappingURL=index.js.map
181
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/codegen/constants.ts","../../src/codegen/delivery-artifacts.ts","../../src/codegen/codegen-config-schema.ts","../../src/codegen-config/type-names.ts","../../src/codegen-config/build-config.ts","../../src/codegen-config/write-config.ts"],"names":["path"],"mappings":"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","file":"index.js","sourcesContent":["export const SUPPORTED_IMPORT_EXTENSIONS = [\"none\", \".ts\", \".js\"] as const;\nexport type SupportedImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];\n\n// Translation keys and namespace names become TypeScript identifiers and\n// template-literal type segments in the generated code, so they must be valid\n// identifiers (e.g. \"app.title\" would emit the broken type `\"app.title:...\"`).\nexport const IDENTIFIER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;\nexport const IDENTIFIER_NAME_REQUIREMENT =\n \"allowed: letters, digits, underscore; must not start with a digit\";\n","/** Valid TypeScript identifier / filename segment for a delivery area id. */\nexport const DELIVERY_AREA_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n\nexport type DeliveryArtifactsMap = Record<string, readonly string[]>;\n\nexport type DeliveryArtifactsIssue = {\n path: (string | number)[];\n message: string;\n};\n\nexport function getDeliveryArtifactsStructureIssues(\n deliveryArtifacts: DeliveryArtifactsMap\n): DeliveryArtifactsIssue[] {\n const issues: DeliveryArtifactsIssue[] = [];\n\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n if (!DELIVERY_AREA_NAME_PATTERN.test(area)) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Invalid delivery area name \"${area}\": must match ${DELIVERY_AREA_NAME_PATTERN.source}`,\n });\n }\n\n if (locales.length === 0) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Delivery area \"${area}\" must include at least one locale.`,\n });\n }\n }\n\n const localeToArea = new Map<string, string>();\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n for (const locale of locales) {\n const previousArea = localeToArea.get(locale);\n if (previousArea) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Locale \"${locale}\" appears in both \"${previousArea}\" and \"${area}\".`,\n });\n } else {\n localeToArea.set(locale, area);\n }\n }\n }\n\n return issues;\n}\n\nexport function getDeliveryArtifactsPartitionIssues(\n deliveryArtifacts: DeliveryArtifactsMap,\n requestLocales: ReadonlySet<string>\n): DeliveryArtifactsIssue[] {\n const issues: DeliveryArtifactsIssue[] = [];\n const artifactLocales = new Set<string>();\n\n for (const locales of Object.values(deliveryArtifacts)) {\n for (const locale of locales) {\n artifactLocales.add(locale);\n }\n }\n\n const missing = [...requestLocales].filter((locale) => !artifactLocales.has(locale)).sort();\n if (missing.length > 0) {\n issues.push({\n path: [\"deliveryArtifacts\"],\n message: `deliveryArtifacts is missing locales required by dictionaries and localeFallback: ${missing.join(\", \")}`,\n });\n }\n\n const excess = [...artifactLocales].filter((locale) => !requestLocales.has(locale)).sort();\n if (excess.length > 0) {\n issues.push({\n path: [\"deliveryArtifacts\"],\n message: `deliveryArtifacts includes locales not required by dictionaries and localeFallback: ${excess.join(\", \")}`,\n });\n }\n\n return issues;\n}\n\n/** Pre-emit validation gate for `deliveryArtifacts` in custom delivery mode. */\nexport function getDeliveryArtifactsIssues(\n deliveryArtifacts: DeliveryArtifactsMap,\n requestLocales: ReadonlySet<string>\n): DeliveryArtifactsIssue[] {\n return [\n ...getDeliveryArtifactsStructureIssues(deliveryArtifacts),\n ...getDeliveryArtifactsPartitionIssues(deliveryArtifacts, requestLocales),\n ];\n}\n\nexport function getDeliveryAreaNames(deliveryArtifacts: DeliveryArtifactsMap): string[] {\n return Object.keys(deliveryArtifacts).sort();\n}\n\nexport function getLocaleDeliveryAreaMap(\n deliveryArtifacts: DeliveryArtifactsMap\n): Record<string, string> {\n const localeToArea: Record<string, string> = {};\n\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n for (const locale of locales) {\n localeToArea[locale] = area;\n }\n }\n\n return localeToArea;\n}\n\nexport function formatLocaleDeliveryAreaBlock(\n deliveryArtifacts: DeliveryArtifactsMap,\n constName: string,\n localeTypeName: string,\n deliveryAreaTypeName: string\n): string {\n const localeToArea = getLocaleDeliveryAreaMap(deliveryArtifacts);\n const lines = Object.keys(localeToArea)\n .sort()\n .map((locale) => ` ${JSON.stringify(locale)}: ${JSON.stringify(localeToArea[locale])},`)\n .join(\"\\n\");\n\n return `export const ${constName} = {\\n${lines}\\n} as const satisfies Record<${localeTypeName}, ${deliveryAreaTypeName}>;\\n\\n`;\n}\n","import { z } from \"zod\";\nimport path from \"node:path\";\nimport {\n IDENTIFIER_NAME_PATTERN,\n IDENTIFIER_NAME_REQUIREMENT,\n SUPPORTED_IMPORT_EXTENSIONS,\n} from \"./constants.js\";\nimport { getDeliveryArtifactsStructureIssues } from \"./delivery-artifacts.js\";\n\nconst localeFallbackSchema = z.record(z.string(), z.union([z.string(), z.null()]));\nconst deliveryArtifactsSchema = z.record(z.string(), z.array(z.string().min(1)));\n\nexport const DELIVERY_MODES = [\"canonical\", \"split-by-locale\", \"custom\"] as const;\nexport type DeliveryMode = (typeof DELIVERY_MODES)[number];\n\nconst codegenConfigShape = {\n dictionary: z.string().min(1).optional(),\n namespaces: z.record(z.string(), z.string().min(1)).optional(),\n defaultNamespace: z.string().min(1).optional(),\n typesOutput: z.string().min(1),\n dictionaryOutput: z.string().min(1),\n instanceOutput: z.string().min(1),\n dictionarySchemaOutput: z.string().min(1).optional(),\n loadOnInit: z.array(z.string().min(1)).optional(),\n namespaceLoadersOutput: z.string().min(1).optional(),\n importExtension: z.enum(SUPPORTED_IMPORT_EXTENSIONS).optional(),\n paramsTypeName: z.string().min(1),\n schemaTypeName: z.string().min(1),\n localeTypeName: z.string().min(1).optional(),\n localeFallbackConstName: z.string().min(1).optional(),\n localeFallback: localeFallbackSchema.optional(),\n factoryName: z.string().min(1).optional(),\n delivery: z.enum(DELIVERY_MODES).optional().default(\"canonical\"),\n deliveryArtifacts: deliveryArtifactsSchema.optional(),\n deliveryOutput: z.string().min(1).optional(),\n};\n\nexport const codegenConfigKeys = Object.keys(\n codegenConfigShape\n) as (keyof typeof codegenConfigShape)[];\n\nexport const codegenConfigSchema = z\n .object(codegenConfigShape)\n .strict()\n .superRefine((config, ctx) => {\n const hasDictionary = config.dictionary !== undefined;\n const hasNamespaces = config.namespaces !== undefined;\n\n if (hasDictionary === hasNamespaces) {\n ctx.addIssue({\n code: \"custom\",\n message: 'Specify exactly one of \"dictionary\" or \"namespaces\".',\n });\n }\n\n if (config.namespaces) {\n for (const namespace of Object.keys(config.namespaces)) {\n if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"namespaces\", namespace],\n message: `Invalid namespace name \"${namespace}\" (${IDENTIFIER_NAME_REQUIREMENT}).`,\n });\n }\n }\n }\n\n if (\n config.defaultNamespace !== undefined &&\n !IDENTIFIER_NAME_PATTERN.test(config.defaultNamespace)\n ) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"defaultNamespace\"],\n message: `Invalid namespace name \"${config.defaultNamespace}\" (${IDENTIFIER_NAME_REQUIREMENT}).`,\n });\n }\n\n if (config.delivery === \"custom\") {\n if (!config.deliveryArtifacts) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"deliveryArtifacts\"],\n message: 'deliveryArtifacts is required when delivery is \"custom\".',\n });\n } else {\n for (const issue of getDeliveryArtifactsStructureIssues(config.deliveryArtifacts)) {\n ctx.addIssue({\n code: \"custom\",\n path: issue.path,\n message: issue.message,\n });\n }\n }\n } else if (config.deliveryArtifacts !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"deliveryArtifacts\"],\n message: 'deliveryArtifacts is only allowed when delivery is \"custom\".',\n });\n }\n });\n\nexport type CodegenConfigInput = z.input<typeof codegenConfigSchema>;\nexport type CodegenConfig = z.infer<typeof codegenConfigSchema>;\n\nexport function resolveDeliveryOutputDir(\n config: Pick<CodegenConfig, \"typesOutput\" | \"deliveryOutput\">\n): string {\n return config.deliveryOutput ?? path.dirname(config.typesOutput);\n}\n\nexport function formatCodegenConfigIssues(error: z.ZodError): string {\n const issueLines = error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"(root)\";\n return ` - ${path}: ${issue.message}`;\n });\n\n return [\n \"[Codegen Error] Invalid i18n.codegen.json:\",\n ...issueLines,\n \"\",\n `Allowed keys: ${codegenConfigKeys.join(\", \")}`,\n ].join(\"\\n\");\n}\n","export function inferProjectName(dirName: string): string {\n const parts = dirName.split(/[-_]+/).filter(Boolean);\n if (parts.length === 0) {\n return \"App\";\n }\n\n return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(\"\");\n}\n\nexport function typeNamesForProject(project: string): {\n paramsTypeName: string;\n schemaTypeName: string;\n localeTypeName: string;\n} {\n return {\n paramsTypeName: `${project}Params`,\n schemaTypeName: `${project}Schema`,\n localeTypeName: `${project}Locale`,\n };\n}\n","import type { CodegenConfigInput } from \"../codegen/codegen-config-schema.js\";\nimport { typeNamesForProject } from \"./type-names.js\";\n\nexport type SetupMode = \"single\" | \"multi\";\n\nconst GENERATED_DIR = \"generated\";\nconst TRANSLATIONS_DIR = \"translations\";\n\nexport function buildCodegenConfig(mode: SetupMode, project: string): CodegenConfigInput {\n const typeNames = typeNamesForProject(project);\n const base = {\n typesOutput: `${GENERATED_DIR}/i18n-types.generated.ts`,\n dictionaryOutput: `${GENERATED_DIR}/dictionary.generated.ts`,\n instanceOutput: `${GENERATED_DIR}/instance.generated.ts`,\n paramsTypeName: typeNames.paramsTypeName,\n schemaTypeName: typeNames.schemaTypeName,\n localeTypeName: typeNames.localeTypeName,\n factoryName: \"createI18n\",\n } satisfies CodegenConfigInput;\n\n if (mode === \"single\") {\n return {\n dictionary: `${TRANSLATIONS_DIR}/translations.json`,\n ...base,\n };\n }\n\n return {\n namespaces: {\n default: `${TRANSLATIONS_DIR}/default.json`,\n },\n ...base,\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { CodegenConfigInput } from \"../codegen/codegen-config-schema.js\";\n\nexport function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void {\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`);\n}\n"]}