@xndrjs/i18n 0.6.0 → 0.7.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 (51) hide show
  1. package/README.md +154 -134
  2. package/dist/codegen/index.js.map +1 -1
  3. package/dist/index.d.ts +276 -61
  4. package/dist/index.js +687 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/validation/index.d.ts +23 -7
  7. package/dist/validation/index.js +196 -89
  8. package/dist/validation/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/IcuTranslationProviderMulti.test.ts +115 -215
  11. package/src/IcuTranslationProviderMulti.ts +107 -96
  12. package/src/IcuTranslationProviderSingle.test.ts +36 -294
  13. package/src/IcuTranslationProviderSingle.ts +55 -72
  14. package/src/builder-load-registry.ts +18 -0
  15. package/src/builder-loaders.ts +18 -0
  16. package/src/builder-multi.ts +481 -0
  17. package/src/builder-types.test.ts +80 -0
  18. package/src/builder-types.ts +24 -0
  19. package/src/builder.test.ts +421 -0
  20. package/src/builder.ts +112 -0
  21. package/src/codegen/delivery-artifacts.test.ts +2 -1
  22. package/src/codegen/delivery-artifacts.ts +2 -1
  23. package/src/codegen/emit/dictionary-file.test.ts +0 -7
  24. package/src/codegen/emit/dictionary-schema-file.ts +55 -42
  25. package/src/codegen/emit/instance-file.test.ts +88 -0
  26. package/src/codegen/emit/instance-file.ts +164 -17
  27. package/src/codegen/emit/namespace-loaders-file.test.ts +35 -10
  28. package/src/codegen/emit/namespace-loaders-file.ts +11 -61
  29. package/src/codegen/emit/types-file.test.ts +0 -2
  30. package/src/codegen/generate-i18n-types.test.ts +8 -303
  31. package/src/codegen/generate-i18n-types.ts +16 -1
  32. package/src/codegen/project-locales-set-namespace.test.ts +25 -32
  33. package/src/engine.ts +71 -0
  34. package/src/index.ts +47 -7
  35. package/src/patch-key.test.ts +186 -0
  36. package/src/patch-key.ts +140 -0
  37. package/src/project-locales.test.ts +76 -0
  38. package/src/project-locales.ts +58 -1
  39. package/src/scope-multi.ts +124 -0
  40. package/src/scope-single.ts +85 -0
  41. package/src/scope-types.ts +27 -0
  42. package/src/scope.test.ts +481 -0
  43. package/src/single-builder.ts +153 -0
  44. package/src/types.ts +16 -0
  45. package/src/validation/create-normalized-schema.ts +1 -1
  46. package/src/validation/errors.ts +2 -0
  47. package/src/validation/index.ts +135 -27
  48. package/src/validation/normalize.ts +176 -46
  49. package/src/validation/types.ts +4 -0
  50. package/src/validation/validate-normalized.ts +43 -1
  51. package/src/validation/validation.test.ts +200 -7
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  A **compiler-first, type-safe i18n system** based on the [ICU MessageFormat](https://formatjs.github.io/docs/core-concepts/icu-syntax/) standard, with **runtime dictionary overrides from external sources** (no rebuild required).
4
4
 
5
- The core idea: your ICU strings live in local JSON files that act as **type-safe fallbacks**. A build-time codegen step parses the ICU AST and generates exact TypeScript types for every translation key and its parameters. At runtime, a centralized provider caches compiled messages and lets you replace the whole dictionary (or a single namespace) on the fly from any source (i.e. a CMS).
5
+ The core idea: your ICU strings live in local JSON files that act as **type-safe fallbacks**. A build-time codegen step parses the ICU AST and generates exact TypeScript types for every translation key and its parameters. At runtime, a centralized engine caches compiled messages; the builder loads lazy artifacts, and locale-bound scopes patch individual keys via `scope.set()` (for example from a CMS) without rebuilding.
6
6
 
7
7
  ## Key features
8
8
 
9
- - **Type-safe `.get()`** — the compiler knows exactly which parameters each key requires (`string`, `number`, or none).
9
+ - **Type-safe `t()`** — the compiler knows exactly which parameters each key requires (`string`, `number`, or none).
10
10
  - **ICU MessageFormat** — full support for interpolation, plurals, and select.
11
- - **Runtime override** — hydrate translations from an external source via `setAll()` / `setNamespace()` without rebuilding.
11
+ - **Runtime override** — patch individual translation keys from an external source via `scope.set()` on locale-bound scopes (after `load()`), without rebuilding.
12
12
  - **Single-file or multi-namespace** — one flat dictionary, or multiple JSON files each bound to a namespace.
13
13
  - **Lazy namespace loading** — optional code-splitting via `loadOnInit` and generated `namespaceLoaders` (multi mode).
14
14
  - **Hot compilation cache** — compiled `IntlMessageFormat` instances are cached and invalidated on override.
@@ -87,8 +87,10 @@ export const i18n = createI18n(defaultDictionary);
87
87
  ```ts
88
88
  import { i18n } from "./i18n";
89
89
 
90
- i18n.get("login_button", "it"); // "Accedi"
91
- i18n.get("welcome", "en", { name: "Ada" }); // "Welcome Ada!"
90
+ const { t } = i18n;
91
+
92
+ t("login_button", "it"); // "Accedi"
93
+ t("welcome", "en", { name: "Ada" }); // "Welcome Ada!"
92
94
  ```
93
95
 
94
96
  Run codegen after every change to your JSON files (or wire it into your build).
@@ -259,21 +261,21 @@ export const i18n = createI18n(defaultDictionary);
259
261
 
260
262
  ### Locale-bound provider (`forLocale`)
261
263
 
262
- Bind a locale once and omit it on every `.get()`:
264
+ Bind a locale once and omit it on every `t()`:
263
265
 
264
266
  ```ts
265
267
  const i18n = createI18n(defaultDictionary);
266
268
  const i18nEn = i18n.forLocale("en");
267
269
 
268
- i18nEn.get("login_button"); // single-file
269
- i18nEn.get("welcome", { name: "Ada" });
270
+ i18nEn.t("login_button"); // single-file
271
+ i18nEn.t("welcome", { name: "Ada" });
270
272
 
271
273
  const i18nIt = i18n.forLocale("it");
272
- i18nIt.get("default", "login_button"); // multi-namespace
273
- i18nIt.get("billing", "invoice_summary", { count: 3 });
274
+ i18nIt.t("default", "login_button"); // multi-namespace
275
+ i18nIt.t("billing", "invoice_summary", { count: 3 });
274
276
  ```
275
277
 
276
- The bound view shares the parent dictionary, cache, and fallback rules. It exposes `locale` and a narrower `get()` signature only.
278
+ The bound scope shares the parent dictionary, cache, and fallback rules. It exposes `locale` and a narrower `t()` signature.
277
279
 
278
280
  Because `Params[K]` (or `Params[NS][K]`) is used in a conditional rest parameter, TypeScript enforces the exact argument shape:
279
281
 
@@ -296,7 +298,7 @@ When a translation is missing for the requested locale (`undefined` in the dicti
296
298
 
297
299
  - `null` marks a terminal locale (no further fallback).
298
300
  - Any other value is the next locale to try, recursively.
299
- - If the chain ends without finding a template, `.get()` throws and includes the full chain in the error message.
301
+ - If the chain ends without finding a template, `t()` throws and includes the full chain in the error message.
300
302
 
301
303
  Codegen emits `LOCALE_FALLBACK` and extends `MyProjectLocale` with the fallback locales. The generated factory wires the map into the provider automatically.
302
304
 
@@ -317,20 +319,25 @@ const i18n = new IcuTranslationProviderMulti(schema, { localeFallback });
317
319
 
318
320
  #### `projectDictionaryLocales` / `projectNamespaceLocales`
319
321
 
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.
322
+ Namespaces split the dictionary by domain; locale projection splits it by **locale**. Use with `load()` + `scope.set()` when ingesting external payloads for a single locale (or a small regional group) in memory.
321
323
 
322
324
  ```ts
325
+ import { createI18n } from "./i18n/generated/instance.generated.js";
326
+ import { validateExternalKey } from "./i18n/generated/dictionary-schema.generated.js";
323
327
  import {
324
- createI18n,
325
328
  projectDictionaryLocales,
326
329
  projectNamespaceLocales,
327
330
  } from "./i18n/generated/instance.generated.js";
328
- import { defaultDictionary } from "./i18n/generated/dictionary.generated.js";
329
331
  import billingDictionary from "./i18n/translations/billing.json";
330
332
 
331
- const i18n = createI18n(defaultDictionary);
332
- i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, [activeLocale]));
333
- i18n.setAll(projectDictionaryLocales(fullDictionary, [activeLocale])); // multi: all namespaces
333
+ const { t, set } = await createI18n({}).withNamespaces(["billing"]).withLocale(activeLocale).load();
334
+
335
+ // Optional: validate a projected slice, then patch one key
336
+ const billingEn = projectNamespaceLocales(billingDictionary, [activeLocale]);
337
+ const result = validateExternalKey("billing", "invoice_summary", billingEn.invoice_summary);
338
+ if (result.ok) {
339
+ set("billing", "invoice_summary", result.data.invoice_summary[activeLocale]!);
340
+ }
334
341
  ```
335
342
 
336
343
  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.
@@ -478,12 +485,12 @@ Codegen writes `{deliveryOutput}/translations/{basename}.{locale}.json` (for exa
478
485
 
479
486
  **Generated API changes (opt-in):**
480
487
 
481
- | Canonical | Split-by-locale / custom (all lazy) |
482
- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
483
- | `export const defaultDictionary` | `dictionary.generated.ts` is not generated |
484
- | `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)` or `namespaceLoaders.billing(area)` |
485
- | — | `ensureNamespacesLoadedForLocale(i18n, locale)` or `ensureNamespacesLoadedForArea(i18n, area)` in `namespace-loaders.generated.ts` |
486
- | — | `DELIVERY_ARTIFACTS`, `LOCALE_DELIVERY_AREA` in `i18n-types.generated.ts` (`custom` only) |
488
+ | Canonical | Split-by-locale / custom (all lazy) |
489
+ | -------------------------------- | -------------------------------------------------------------------------------------------------- |
490
+ | `export const defaultDictionary` | `dictionary.generated.ts` is not generated |
491
+ | `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)` or `namespaceLoaders.billing(area)` |
492
+ | — | `createI18n({}).withNamespaces([...]).withLocale(locale).load()` / `withDeliveryArea(area).load()` |
493
+ | — | `DELIVERY_ARTIFACTS`, `LOCALE_DELIVERY_AREA` in `i18n-types.generated.ts` (`custom` only) |
487
494
 
488
495
  When `loadOnInit` lists eager namespaces in **canonical** delivery only, `dictionary.generated.ts` still exports `defaultDictionary` with static imports. In split/custom delivery, eager slices use `defaultDictionaryFor(locale)` or `defaultDictionaryFor(area)` when configured.
489
496
 
@@ -491,20 +498,25 @@ Example init (multi, split-by-locale, all namespaces lazy):
491
498
 
492
499
  ```ts
493
500
  import { createI18n } from "./generated/instance.generated.js";
494
- import { ensureNamespacesLoadedForLocale } from "./generated/namespace-loaders.generated.js";
495
501
 
496
- const i18n = createI18n({});
497
- await ensureNamespacesLoadedForLocale(i18n, activeLocale);
502
+ const { t } = await createI18n({})
503
+ .withNamespaces(["default", "billing"])
504
+ .withLocale(activeLocale)
505
+ .load();
498
506
  ```
499
507
 
500
508
  Example init (multi, custom delivery, all namespaces lazy):
501
509
 
502
510
  ```ts
503
511
  import { createI18n } from "./generated/instance.generated.js";
504
- import { ensureNamespacesLoadedForArea } from "./generated/namespace-loaders.generated.js";
505
512
 
506
- const i18n = createI18n({});
507
- await ensureNamespacesLoadedForArea(i18n, activeArea);
513
+ const { t } = await createI18n({})
514
+ .withNamespaces(["default", "billing"])
515
+ .withDeliveryArea(activeArea)
516
+ .load();
517
+
518
+ // ActiveLocales is narrowed to DELIVERY_ARTIFACTS[activeArea] — forLocale() only accepts those locales.
519
+ t("default", "login_button", "it");
508
520
  ```
509
521
 
510
522
  Custom delivery config and typed artifacts (no need to read `deliveryArtifacts` from JSON at runtime):
@@ -591,28 +603,15 @@ export const namespaceLoaders = {
591
603
  }
592
604
  },
593
605
  };
594
-
595
- export async function ensureNamespacesLoadedForLocale(
596
- i18n: I18nMultiInstance,
597
- locale: MyProjectLocale,
598
- namespaces: readonly LazyNamespace[] = ["billing", "default"] as const
599
- ): Promise<void> {
600
- await Promise.all(
601
- namespaces.map(async (namespace) => {
602
- i18n.setNamespace(namespace, await namespaceLoaders[namespace](locale));
603
- })
604
- );
605
- }
606
+ export const defaultLazyNamespaces = ["billing", "default"] as const;
606
607
  ```
607
608
 
608
609
  Load only the namespaces you need:
609
610
 
610
611
  ```ts
611
612
  import { createI18n } from "./generated/instance.generated.js";
612
- import { ensureNamespacesLoadedForLocale } from "./generated/namespace-loaders.generated.js";
613
613
 
614
- const i18n = createI18n({});
615
- await ensureNamespacesLoadedForLocale(i18n, activeLocale, ["billing"]);
614
+ const { t } = await createI18n({}).withNamespaces(["billing"]).withLocale(activeLocale).load();
616
615
  ```
617
616
 
618
617
  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.
@@ -621,57 +620,68 @@ Use split delivery when you want smaller lazy chunks (one locale per dynamic imp
621
620
 
622
621
  ### Single vs. multi-namespace API
623
622
 
624
- | | Single-file | Multi-namespace |
625
- | ----------- | ------------------------------ | --------------------------------------------- |
626
- | `I18N_MODE` | `'single'` | `'multi'` |
627
- | Provider | `IcuTranslationProviderSingle` | `IcuTranslationProviderMulti` |
628
- | `.get()` | `get(key, locale, params?)` | `get(namespace, key, locale, params?)` |
629
- | Override | `setAll(schema)` | `setAll(schema)` + `setNamespace(ns, values)` |
623
+ | | Single-file | Multi-namespace |
624
+ | ----------- | ------------------------------------------ | ---------------------------------------------- |
625
+ | `I18N_MODE` | `'single'` | `'multi'` |
626
+ | Provider | `IcuTranslationProviderSingle` | `IcuTranslationProviderMulti` |
627
+ | `t()` | `t(key, locale, params?)` | `t(namespace, key, locale, params?)` |
628
+ | Patch | `set(key, template)` on locale-bound scope | `set(ns, key, template)` on locale-bound scope |
630
629
 
631
630
  ### Multi-namespace example
632
631
 
633
632
  ```ts
634
633
  import { i18n } from "./i18n"; // app singleton from i18n.ts
635
- // or: import { createI18n, defaultDictionary } from './i18n'; const i18n = createI18n(defaultDictionary);
634
+ // or: import { createI18n, defaultDictionary } from './i18n'; const { t } = createI18n(defaultDictionary);
635
+
636
+ const { t } = i18n;
636
637
 
637
- i18n.get("default", "login_button", "it"); // "Accedi"
638
- i18n.get("default", "welcome", "en", { name: "Ada" }); // "Welcome Ada!"
639
- i18n.get("default", "dashboard_status", "it", { msgCount: 3, chatCount: 2 });
640
- i18n.get("billing", "invoice_summary", "en", { count: 12 });
638
+ t("default", "login_button", "it"); // "Accedi"
639
+ t("default", "welcome", "en", { name: "Ada" }); // "Welcome Ada!"
640
+ t("default", "dashboard_status", "it", { msgCount: 3, chatCount: 2 });
641
+ t("billing", "invoice_summary", "en", { count: 12 });
641
642
 
642
643
  // Compile-time errors:
643
- i18n.get("default", "welcome", "it"); // ✗ missing { name }
644
- i18n.get("billing", "login_button", "it"); // ✗ key not in namespace
644
+ t("default", "welcome", "it"); // ✗ missing { name }
645
+ t("billing", "login_button", "it"); // ✗ key not in namespace
645
646
  ```
646
647
 
647
648
  ### Single-file example
648
649
 
649
650
  ```ts
650
651
  import { i18n } from "./i18n"; // app singleton from i18n.ts
651
- // or: import { createI18n, defaultDictionary } from './i18n'; const i18n = createI18n(defaultDictionary);
652
+ // or: import { createI18n, defaultDictionary } from './i18n'; const { t } = createI18n(defaultDictionary);
653
+
654
+ const { t } = i18n;
652
655
 
653
- i18n.get("login_button", "it");
654
- i18n.get("welcome", "en", { name: "Ada" });
656
+ t("login_button", "it");
657
+ t("welcome", "en", { name: "Ada" });
655
658
  ```
656
659
 
657
- ### Runtime override
660
+ ### Runtime override (key-level patch)
661
+
662
+ Runtime patches go through **`scope.set()`** on a locale-bound scope returned from `load()`. The key (and locale) must already be preloaded — typically via a prior `load()` call or the canonical eager dictionary.
658
663
 
659
664
  ```ts
660
- // Full override replaces the entire dictionary and clears the cache
661
- i18n.setAll(externalPayload);
665
+ // Multilocale-bound scope from load()
666
+ const { t, set } = await createI18n({}).withNamespaces(["billing"]).withLocale("en").load();
667
+ set("billing", "invoice_summary", "You have {count, plural, one {1 invoice} other {# invoices}}");
668
+ t("billing", "invoice_summary", { count: 2 });
662
669
 
663
- // Partial patch updates a single namespace and invalidates only its cache
664
- i18n.setNamespace("billing", externalBillingPayload);
670
+ // Singlesame pattern
671
+ const { set: setWelcome } = await createI18n(defaultDictionary).withLocale("en").load();
672
+ setWelcome("welcome", "Welcome {name}!");
665
673
  ```
666
674
 
675
+ Unbound scopes (without `withLocale` / `withDeliveryArea` before `load()`) are read-only; call `.forLocale(locale)` before patching.
676
+
667
677
  ### Lazy namespace loading (multi mode)
668
678
 
669
- `.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering. The pattern depends on `delivery`:
679
+ The pattern depends on `delivery`:
670
680
 
671
- | Delivery | Loading pattern |
672
- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
673
- | `canonical` | `loadOnInit` for eager namespaces; manual `namespaceLoaders.ns()` for lazy ones. Use `hasNamespace` to skip already-loaded namespaces. |
674
- | `split-by-locale` / `custom` | Every namespace is lazy. Prefer `ensureNamespacesLoadedForLocale` / `ensureNamespacesLoadedForArea` — they always call `setNamespace()` (safe when switching locale or area on a shared `i18n` instance). |
681
+ | Delivery | Loading pattern |
682
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
683
+ | `canonical` | `loadOnInit` for eager namespaces; manual `namespaceLoaders.ns()` for lazy ones. |
684
+ | `split-by-locale` / `custom` | Every namespace is lazy. Prefer the builder: `createI18n({}).withNamespaces([...]).withLocale(locale).load()` / `withDeliveryArea(area).load()`. |
675
685
 
676
686
  #### Canonical delivery — `loadOnInit` and manual loaders
677
687
 
@@ -691,90 +701,82 @@ Split namespaces across chunks with `loadOnInit` in **canonical** delivery only.
691
701
 
692
702
  Codegen also emits `LoadOnInitNamespace`, `LazyNamespace`, and `InitialSchema` types. The generated factory accepts a partial `InitialSchema` at init time.
693
703
 
694
- Use `hasNamespace` when loading lazily by hand namespaces loaded once stay in memory:
704
+ Canonical delivery: eager namespaces load via `loadOnInit`. For lazy ones, use the builder or call generated `namespaceLoaders` inside a custom loader wired into `createI18n`.
695
705
 
696
706
  ```ts
697
- import { i18n, namespaceLoaders, type LazyNamespace } from "./i18n";
707
+ import { createI18n, namespaceLoaders, type LazyNamespace } from "./i18n";
698
708
 
699
- i18n.get("default", "login_button", "en"); // available immediately
709
+ // Eager namespace available immediately on the canonical singleton scope
710
+ const { t } = createI18n(defaultDictionary).toScope();
711
+ t("default", "login_button", "en");
700
712
 
701
- if (!i18n.hasNamespace("billing")) {
702
- i18n.setNamespace("billing", await namespaceLoaders.billing());
703
- }
704
- i18n.get("billing", "invoice_summary", "en", { count: 12 });
713
+ // Lazy namespace — load via builder
714
+ const { t: tBilling } = await createI18n(defaultDictionary)
715
+ .withNamespaces(["billing"])
716
+ .withLocale("en")
717
+ .load();
718
+ tBilling("billing", "invoice_summary", { count: 12 });
705
719
 
706
- // batch preload
720
+ // batch preload several lazy namespaces for one locale
707
721
  await Promise.all(
708
722
  (["user", "billing"] as const satisfies readonly LazyNamespace[]).map(async (namespace) => {
709
- if (i18n.hasNamespace(namespace)) return;
710
- i18n.setNamespace(namespace, await namespaceLoaders[namespace]());
723
+ await namespaceLoaders[namespace]();
711
724
  })
712
725
  );
713
726
  ```
714
727
 
715
- #### Split-by-locale / custom — `ensureNamespacesLoaded*`
728
+ #### Split-by-locale / custom — builder loading
716
729
 
717
- With `split-by-locale` or `custom`, codegen emits `ensureNamespacesLoadedForLocale` / `ensureNamespacesLoadedForArea`. These helpers **always** reload the requested namespaces — do not guard with `hasNamespace` when switching locale or delivery area on the same instance:
730
+ With `split-by-locale` or `custom`, codegen wires `namespaceLoaders` into `createI18n({})` and returns a builder. Use `load()` to get a ready scope:
718
731
 
719
732
  ```ts
720
733
  import { createI18n } from "./generated/instance.generated.js";
721
- import {
722
- ensureNamespacesLoadedForLocale,
723
- ensureNamespacesLoadedForArea,
724
- } from "./generated/namespace-loaders.generated.js";
725
-
726
- const i18n = createI18n({});
727
-
728
- // split-by-locale
729
- await ensureNamespacesLoadedForLocale(i18n, "it");
730
- i18n.get("billing", "invoice_summary", "it", { count: 3 });
731
734
 
732
- await ensureNamespacesLoadedForLocale(i18n, "en"); // reloads billing for en
733
- i18n.get("billing", "invoice_summary", "en", { count: 3 });
735
+ const { t } = await createI18n({})
736
+ .withNamespaces(["billing"])
737
+ .withLocale(activeLocale) // split-by-locale
738
+ // .withDeliveryArea(activeArea) // custom delivery
739
+ .load();
734
740
 
735
- // custom delivery
736
- await ensureNamespacesLoadedForArea(i18n, "eu", ["billing"]);
741
+ t("billing", "invoice_summary", { count: 3 });
737
742
  ```
738
743
 
739
- Route-scoped subset:
744
+ Different locale or delivery-area partitions accumulate on the shared engine — load `"it"`, then `"en"`, and both remain available. Loading the **same** namespace + partition again is a no-op (preserves runtime `scope.set()` patches):
740
745
 
741
746
  ```ts
742
- await ensureNamespacesLoadedForLocale(i18n, activeLocale, ["billing"]);
743
- ```
744
-
745
- Manual loader calls (without the helper) follow the same rule call `setNamespace` again when the locale or area changes:
746
-
747
- ```ts
748
- const locale = "it" as const;
749
-
750
- i18n.setNamespace("billing", await namespaceLoaders.billing(locale));
747
+ const builder = createI18n({}).withNamespaces(["billing"] as const);
748
+ const { t: tIt, set: setIt } = await builder.withLocale("it").load();
749
+ const { t: tEn } = await builder.withLocale("en").load();
750
+ // tIt and tEn share one engine; both locales are preloaded for billing keys.
751
751
 
752
- // after locale change on the same instance:
753
- i18n.setNamespace("billing", await namespaceLoaders.billing("en"));
752
+ setIt("billing", "invoice_summary", "Custom {count} for {name}");
753
+ await builder.withLocale("it").load(); // skipped — patch preserved
754
+ tIt("billing", "invoice_summary", { count: 1, name: "Ada" }); // "Custom 1 for Ada"
754
755
  ```
755
756
 
756
- Generated loaders throw if locale/area does not match a known artifact (no silent `undefined` into `setNamespace`).
757
+ Generated loaders throw if locale/area does not match a known artifact (no silent empty payloads into `load()`).
757
758
 
758
- Optional locale projection before register:
759
+ Optional locale projection before validating external slices:
759
760
 
760
761
  ```ts
761
762
  import { projectNamespaceLocales } from "./i18n/generated/instance.generated.js";
762
763
 
764
+ const { set } = await createI18n({}).withNamespaces(["billing"]).withLocale(userLocale).load();
763
765
  const billing = await namespaceLoaders.billing();
764
- i18n.setNamespace("billing", projectNamespaceLocales(billing, [userLocale]));
766
+ const billingSlice = projectNamespaceLocales(billing, [userLocale]);
767
+ const result = validateExternalKey("billing", "invoice_summary", billingSlice.invoice_summary);
768
+ if (result.ok) {
769
+ set("billing", "invoice_summary", result.data.invoice_summary[userLocale]!);
770
+ }
765
771
  ```
766
772
 
767
- Calling `.get()` on a namespace that is not loaded throws:
768
-
769
- `[i18n] Namespace not loaded: "billing". Register it with setNamespace() before calling .get().`
770
-
771
- When `loadOnInit` is omitted, behavior is unchanged: all namespaces are statically imported.
773
+ When a namespace key was never loaded, `t()` resolves through `onMissing` (default: throw).
772
774
 
773
- Fetch/CMS hydration is app-owned: fetch → `validateExternalNamespace()` (requires `dictionarySchemaOutput` + `zod`) → `setNamespace()`.
775
+ Fetch/CMS hydration is app-owned: fetch → validate (`validateExternalKey` / `validateExternalNamespacePartial`) `load()` → `scope.set()` per key.
774
776
 
775
777
  ### External dictionary validation
776
778
 
777
- When translations arrive from an external source (i.e. a CMS), validate the `unknown` input before calling `setAll()` or `setNamespace()`.
779
+ When translations arrive from an external source (i.e. a CMS), validate the `unknown` input before calling `scope.set()`. Keys must be preloaded via `load()` first.
778
780
 
779
781
  Enable validation by adding `dictionarySchemaOutput` to `i18n/i18n.codegen.json`:
780
782
 
@@ -786,35 +788,53 @@ Enable validation by adding `dictionarySchemaOutput` to `i18n/i18n.codegen.json`
786
788
 
787
789
  Add `zod` as a dependency in the consumer app (required peer of `@xndrjs/i18n`).
788
790
 
789
- Codegen emits `DICTIONARY_SPEC` and `validateExternalDictionary()`. Validation runs in two phases:
791
+ Codegen emits `DICTIONARY_SPEC`, `validateExternalDictionary()`, `validateExternalDictionaryPartial()`, `validateExternalNamespacePartial()`, and `validateExternalKey()`. Validation runs in two phases:
790
792
 
791
793
  1. **Normalize** — parse ICU templates, extract variables, check required keys (missing keys fail; extra keys are ignored; partial locales are OK).
792
794
  2. **Validate** — compare extracted arguments against the static `Params` schema via Zod.
793
795
 
794
796
  ```ts
795
797
  import { formatIssues } from "@xndrjs/i18n/validation";
796
- import { validateExternalDictionary } from "./i18n/generated/dictionary-schema.generated.js";
797
- import { i18n } from "./i18n";
798
+ import {
799
+ validateExternalDictionaryPartial,
800
+ validateExternalKey,
801
+ } from "./i18n/generated/dictionary-schema.generated.js";
802
+ import { createI18n } from "./i18n";
798
803
 
799
804
  const raw: unknown = await loadTranslations();
800
805
 
801
- const result = validateExternalDictionary(raw);
806
+ const { t, set } = await createI18n({})
807
+ .withNamespaces(["default", "billing"])
808
+ .withLocale("en")
809
+ .load();
810
+
811
+ const result = validateExternalDictionaryPartial(raw);
802
812
  if (!result.ok) {
803
813
  console.error(formatIssues(result.issues));
804
814
  return;
805
815
  }
806
816
 
807
- i18n.setAll(result.data);
817
+ for (const [namespace, nsData] of Object.entries(result.data)) {
818
+ if (!nsData) continue;
819
+ for (const [key, locales] of Object.entries(nsData)) {
820
+ const template = locales?.en;
821
+ if (template !== undefined) {
822
+ set(namespace as "billing", key as "invoice_summary", template);
823
+ }
824
+ }
825
+ }
808
826
  ```
809
827
 
810
- For a namespace patch (multi mode only):
828
+ For a single key patch (multi mode):
811
829
 
812
830
  ```ts
813
- import { validateExternalNamespace } from "./i18n/generated/dictionary-schema.generated.js";
831
+ import { validateExternalKey } from "./i18n/generated/dictionary-schema.generated.js";
832
+
833
+ const { set } = await createI18n({}).withNamespaces(["billing"]).withLocale("it").load();
814
834
 
815
- const result = validateExternalNamespace("billing", rawBilling);
835
+ const result = validateExternalKey("billing", "invoice_summary", rawLocales);
816
836
  if (result.ok) {
817
- i18n.setNamespace("billing", result.data);
837
+ set("billing", "invoice_summary", result.data.invoice_summary.it!);
818
838
  }
819
839
  ```
820
840
 
@@ -824,11 +844,11 @@ Low-level API is also available from `@xndrjs/i18n/validation` for custom wiring
824
844
 
825
845
  Both providers share this behavior:
826
846
 
827
- - **Compilation cache** — compiled `IntlMessageFormat` instances are cached per locale (and per namespace in multi mode).
847
+ - **Compilation cache** — compiled `IntlMessageFormat` instances are cached per locale (and per namespace in multi mode). Invalidated on `scope.set()` for the affected key/locale.
828
848
  - **`getAll()`** — returns a deep-frozen snapshot of the current dictionary (not a live reference).
829
- - **`hasNamespace(ns)`**(multi only) returns whether a namespace has been loaded (eager init, lazy load, or `setNamespace`).
830
- - **`setAll(values)`**replaces the dictionary and clears the entire cache.
831
- - **`setNamespace(ns, values)`** — (multi only) replaces one namespace and invalidates only its cache entries.
849
+ - **Load deduplication** the engine tracks lazy resources loaded via the builder (`namespace` + locale or delivery area). A repeated `load()` for the same resource is skipped so default artifacts do not overwrite runtime `scope.set()` patches.
850
+ - **Destructuring-safe scopes** `t`, `set`, and `forLocale` on scopes can be extracted (`const { t } = scope`) without manual `.bind()`.
851
+ - **No public merge/replace** `setAll`, `mergeAll`, `setNamespace`, and `mergeNamespace` were removed in 0.7.0. The first `load()` per resource deep-merges defaults; further patches go through `scope.set()`.
832
852
  - **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.
833
853
  - **ICU syntax error** — throws `[i18n ICU Syntax Error] ...`.
834
854
  - **Formatting error** (missing/invalid params) — throws `[i18n Formatting Error] ...`.
@@ -902,7 +922,7 @@ pnpm run demo:multi # tsx multi/src/index.ts
902
922
 
903
923
  1. Add the key with its ICU strings to the relevant JSON file under `translations/`.
904
924
  2. Run `pnpm --filter @xndrjs/i18n-demo i18n:codegen:multi` (or `i18n:codegen:single`).
905
- 3. The generated `MyProjectParams` updates; TypeScript will flag any `.get()` call sites that now need (or no longer need) parameters.
925
+ 3. The generated `MyProjectParams` updates; TypeScript will flag any `t()` call sites that now need (or no longer need) parameters.
906
926
 
907
927
  ## Adding a namespace
908
928
 
@@ -915,7 +935,7 @@ pnpm run demo:multi # tsx multi/src/index.ts
915
935
  1. Split the flat JSON into per-domain files.
916
936
  2. Change `dictionary` to `namespaces` in the config.
917
937
  3. Re-run codegen — types and API switch from flat to namespaced.
918
- 4. Update call sites: `get('key', locale)` → `get('namespace', 'key', locale)`.
938
+ 4. Update call sites: `t(key, locale)` → `t(namespace, key, locale)` on scopes (or use locale-bound scopes from `load()`).
919
939
 
920
940
  ## Tech stack
921
941
 
@@ -1 +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\nexport function getDeliveryArtifactsTypeName(deliveryAreaTypeName: string): string {\n return deliveryAreaTypeName.endsWith(\"DeliveryArea\")\n ? deliveryAreaTypeName.replace(/DeliveryArea$/, \"DeliveryArtifacts\")\n : `${deliveryAreaTypeName}Artifacts`;\n}\n\nexport function formatDeliveryArtifactsBlock(\n deliveryArtifacts: DeliveryArtifactsMap,\n constName: string,\n localeTypeName: string,\n deliveryAreaTypeName: string\n): string {\n const deliveryArtifactsTypeName = getDeliveryArtifactsTypeName(deliveryAreaTypeName);\n const areaEntries = getDeliveryAreaNames(deliveryArtifacts)\n .map((area) => {\n const locales = [...deliveryArtifacts[area]!]\n .sort()\n .map((locale) => JSON.stringify(locale))\n .join(\", \");\n return ` ${JSON.stringify(area)}: [${locales}] as const,`;\n })\n .join(\"\\n\");\n\n return (\n `export const ${constName} = {\\n${areaEntries}\\n} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;\\n\\n` +\n `export type ${deliveryArtifactsTypeName} = typeof ${constName};\\n\\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\n .string()\n .min(1)\n .optional()\n .describe(\n 'Output path for dictionary.generated.ts when emitted (canonical, or split/custom with eager namespaces). Omitted in split/custom when every namespace is lazy — defaults to \"{dirname(typesOutput)}/dictionary.generated.ts\" for stale-file cleanup only.'\n ),\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 if (config.loadOnInit !== undefined && config.delivery !== \"canonical\") {\n ctx.addIssue({\n code: \"custom\",\n path: [\"loadOnInit\"],\n message: 'loadOnInit is only allowed when delivery is \"canonical\".',\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\nconst DEFAULT_DICTIONARY_BASENAME = \"dictionary.generated.ts\";\n\n/** Resolved path for dictionary.generated.ts (explicit config or default next to typesOutput). */\nexport function resolveDictionaryOutputPath(\n config: Pick<CodegenConfig, \"typesOutput\" | \"dictionaryOutput\">\n): string {\n return (\n config.dictionaryOutput ??\n path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME)\n );\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"]}
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\nexport function getDeliveryArtifactsTypeName(deliveryAreaTypeName: string): string {\n return deliveryAreaTypeName.endsWith(\"DeliveryArea\")\n ? deliveryAreaTypeName.replace(/DeliveryArea$/, \"DeliveryArtifacts\")\n : `${deliveryAreaTypeName}Artifacts`;\n}\n\nexport function formatDeliveryArtifactsBlock(\n deliveryArtifacts: DeliveryArtifactsMap,\n constName: string,\n localeTypeName: string,\n deliveryAreaTypeName: string\n): string {\n const deliveryArtifactsTypeName = getDeliveryArtifactsTypeName(deliveryAreaTypeName);\n const areaEntries = getDeliveryAreaNames(deliveryArtifacts)\n .map((area) => {\n const locales = [...deliveryArtifacts[area]!]\n .sort()\n .map((locale) => JSON.stringify(locale))\n .join(\", \");\n return ` ${JSON.stringify(area)}: [${locales}] as const,`;\n })\n .join(\"\\n\");\n\n return (\n `export const ${constName} = {\\n${areaEntries}\\n} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;\\n\\n` +\n `export type ${deliveryArtifactsTypeName} = typeof ${constName};\\n\\n` +\n `export type LocalesForDeliveryArea<A extends ${deliveryAreaTypeName}> = ${deliveryArtifactsTypeName}[A][number];\\n\\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\n .string()\n .min(1)\n .optional()\n .describe(\n 'Output path for dictionary.generated.ts when emitted (canonical, or split/custom with eager namespaces). Omitted in split/custom when every namespace is lazy — defaults to \"{dirname(typesOutput)}/dictionary.generated.ts\" for stale-file cleanup only.'\n ),\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 if (config.loadOnInit !== undefined && config.delivery !== \"canonical\") {\n ctx.addIssue({\n code: \"custom\",\n path: [\"loadOnInit\"],\n message: 'loadOnInit is only allowed when delivery is \"canonical\".',\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\nconst DEFAULT_DICTIONARY_BASENAME = \"dictionary.generated.ts\";\n\n/** Resolved path for dictionary.generated.ts (explicit config or default next to typesOutput). */\nexport function resolveDictionaryOutputPath(\n config: Pick<CodegenConfig, \"typesOutput\" | \"dictionaryOutput\">\n): string {\n return (\n config.dictionaryOutput ??\n path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME)\n );\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"]}