@xndrjs/i18n 0.6.1 → 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.
- package/README.md +154 -141
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +268 -69
- package/dist/index.js +572 -65
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +23 -7
- package/dist/validation/index.js +196 -89
- package/dist/validation/index.js.map +1 -1
- package/package.json +1 -1
- package/src/IcuTranslationProviderMulti.test.ts +80 -234
- package/src/IcuTranslationProviderMulti.ts +101 -112
- package/src/IcuTranslationProviderSingle.test.ts +30 -309
- package/src/IcuTranslationProviderSingle.ts +42 -75
- package/src/builder-load-registry.ts +18 -0
- package/src/builder-loaders.ts +18 -0
- package/src/builder-multi.ts +481 -0
- package/src/builder-types.test.ts +80 -0
- package/src/builder-types.ts +24 -0
- package/src/builder.test.ts +421 -0
- package/src/builder.ts +112 -0
- package/src/codegen/delivery-artifacts.test.ts +2 -1
- package/src/codegen/delivery-artifacts.ts +2 -1
- package/src/codegen/emit/dictionary-file.test.ts +0 -7
- package/src/codegen/emit/dictionary-schema-file.ts +55 -42
- package/src/codegen/emit/instance-file.test.ts +88 -0
- package/src/codegen/emit/instance-file.ts +164 -17
- package/src/codegen/emit/namespace-loaders-file.test.ts +4 -18
- package/src/codegen/emit/namespace-loaders-file.ts +4 -54
- package/src/codegen/emit/types-file.test.ts +0 -2
- package/src/codegen/generate-i18n-types.test.ts +8 -303
- package/src/codegen/generate-i18n-types.ts +16 -1
- package/src/codegen/project-locales-set-namespace.test.ts +25 -32
- package/src/engine.ts +71 -0
- package/src/index.ts +45 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +16 -6
- package/src/project-locales.ts +17 -6
- package/src/scope-multi.ts +124 -0
- package/src/scope-single.ts +85 -0
- package/src/scope-types.ts +27 -0
- package/src/scope.test.ts +481 -0
- package/src/single-builder.ts +153 -0
- package/src/types.ts +16 -0
- package/src/validation/create-normalized-schema.ts +1 -1
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +135 -27
- package/src/validation/normalize.ts +176 -46
- package/src/validation/types.ts +4 -0
- package/src/validation/validate-normalized.ts +43 -1
- 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
|
|
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
|
|
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** —
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
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.
|
|
269
|
-
i18nEn.
|
|
270
|
+
i18nEn.t("login_button"); // single-file
|
|
271
|
+
i18nEn.t("welcome", { name: "Ada" });
|
|
270
272
|
|
|
271
273
|
const i18nIt = i18n.forLocale("it");
|
|
272
|
-
i18nIt.
|
|
273
|
-
i18nIt.
|
|
274
|
+
i18nIt.t("default", "login_button"); // multi-namespace
|
|
275
|
+
i18nIt.t("billing", "invoice_summary", { count: 3 });
|
|
274
276
|
```
|
|
275
277
|
|
|
276
|
-
The bound
|
|
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,
|
|
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
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
| — | `
|
|
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
|
|
497
|
-
|
|
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
|
|
507
|
-
|
|
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.mergeNamespace(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
|
|
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
|
|
625
|
-
| ----------- |
|
|
626
|
-
| `I18N_MODE` | `'single'`
|
|
627
|
-
| Provider | `IcuTranslationProviderSingle`
|
|
628
|
-
|
|
|
629
|
-
|
|
|
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
|
|
634
|
+
// or: import { createI18n, defaultDictionary } from './i18n'; const { t } = createI18n(defaultDictionary);
|
|
635
|
+
|
|
636
|
+
const { t } = i18n;
|
|
636
637
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
|
652
|
+
// or: import { createI18n, defaultDictionary } from './i18n'; const { t } = createI18n(defaultDictionary);
|
|
653
|
+
|
|
654
|
+
const { t } = i18n;
|
|
652
655
|
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
//
|
|
661
|
-
|
|
665
|
+
// Multi — locale-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
|
-
//
|
|
664
|
-
|
|
670
|
+
// Single — same 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
|
-
|
|
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.
|
|
674
|
-
| `split-by-locale` / `custom` | Every namespace is lazy. Prefer
|
|
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,95 +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
|
-
|
|
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 {
|
|
707
|
+
import { createI18n, namespaceLoaders, type LazyNamespace } from "./i18n";
|
|
698
708
|
|
|
699
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
710
|
-
i18n.setNamespace(namespace, await namespaceLoaders[namespace]());
|
|
723
|
+
await namespaceLoaders[namespace]();
|
|
711
724
|
})
|
|
712
725
|
);
|
|
713
726
|
```
|
|
714
727
|
|
|
715
|
-
#### Split-by-locale / custom —
|
|
728
|
+
#### Split-by-locale / custom — builder loading
|
|
716
729
|
|
|
717
|
-
With `split-by-locale` or `custom`, codegen
|
|
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 — en and it coexist after both calls
|
|
729
|
-
await ensureNamespacesLoadedForLocale(i18n, "it");
|
|
730
|
-
i18n.get("billing", "invoice_summary", "it", { count: 3 });
|
|
731
734
|
|
|
732
|
-
await
|
|
733
|
-
|
|
734
|
-
|
|
735
|
+
const { t } = await createI18n({})
|
|
736
|
+
.withNamespaces(["billing"])
|
|
737
|
+
.withLocale(activeLocale) // split-by-locale
|
|
738
|
+
// .withDeliveryArea(activeArea) // custom delivery
|
|
739
|
+
.load();
|
|
735
740
|
|
|
736
|
-
|
|
737
|
-
await ensureNamespacesLoadedForArea(i18n, "eu", ["billing"]);
|
|
741
|
+
t("billing", "invoice_summary", { count: 3 });
|
|
738
742
|
```
|
|
739
743
|
|
|
740
|
-
|
|
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):
|
|
741
745
|
|
|
742
746
|
```ts
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
```ts
|
|
749
|
-
const locale = "it" as const;
|
|
750
|
-
|
|
751
|
-
// single
|
|
752
|
-
i18n.mergeAll(await namespaceLoaders.default(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.
|
|
753
751
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
// add another locale on the same instance:
|
|
758
|
-
i18n.mergeNamespace("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"
|
|
759
755
|
```
|
|
760
756
|
|
|
761
|
-
Generated loaders throw if locale/area does not match a known artifact (no silent
|
|
757
|
+
Generated loaders throw if locale/area does not match a known artifact (no silent empty payloads into `load()`).
|
|
762
758
|
|
|
763
|
-
Optional locale projection before
|
|
759
|
+
Optional locale projection before validating external slices:
|
|
764
760
|
|
|
765
761
|
```ts
|
|
766
762
|
import { projectNamespaceLocales } from "./i18n/generated/instance.generated.js";
|
|
767
763
|
|
|
764
|
+
const { set } = await createI18n({}).withNamespaces(["billing"]).withLocale(userLocale).load();
|
|
768
765
|
const billing = await namespaceLoaders.billing();
|
|
769
|
-
|
|
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
|
+
}
|
|
770
771
|
```
|
|
771
772
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
`[i18n] Namespace not loaded: "billing". Register it with setNamespace() before calling .get().`
|
|
773
|
+
When a namespace key was never loaded, `t()` resolves through `onMissing` (default: throw).
|
|
775
774
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
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.
|
|
779
776
|
|
|
780
777
|
### External dictionary validation
|
|
781
778
|
|
|
782
|
-
When translations arrive from an external source (i.e. a CMS), validate the `unknown` input before calling `
|
|
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.
|
|
783
780
|
|
|
784
781
|
Enable validation by adding `dictionarySchemaOutput` to `i18n/i18n.codegen.json`:
|
|
785
782
|
|
|
@@ -791,35 +788,53 @@ Enable validation by adding `dictionarySchemaOutput` to `i18n/i18n.codegen.json`
|
|
|
791
788
|
|
|
792
789
|
Add `zod` as a dependency in the consumer app (required peer of `@xndrjs/i18n`).
|
|
793
790
|
|
|
794
|
-
Codegen emits `DICTIONARY_SPEC` and `
|
|
791
|
+
Codegen emits `DICTIONARY_SPEC`, `validateExternalDictionary()`, `validateExternalDictionaryPartial()`, `validateExternalNamespacePartial()`, and `validateExternalKey()`. Validation runs in two phases:
|
|
795
792
|
|
|
796
793
|
1. **Normalize** — parse ICU templates, extract variables, check required keys (missing keys fail; extra keys are ignored; partial locales are OK).
|
|
797
794
|
2. **Validate** — compare extracted arguments against the static `Params` schema via Zod.
|
|
798
795
|
|
|
799
796
|
```ts
|
|
800
797
|
import { formatIssues } from "@xndrjs/i18n/validation";
|
|
801
|
-
import {
|
|
802
|
-
|
|
798
|
+
import {
|
|
799
|
+
validateExternalDictionaryPartial,
|
|
800
|
+
validateExternalKey,
|
|
801
|
+
} from "./i18n/generated/dictionary-schema.generated.js";
|
|
802
|
+
import { createI18n } from "./i18n";
|
|
803
803
|
|
|
804
804
|
const raw: unknown = await loadTranslations();
|
|
805
805
|
|
|
806
|
-
const
|
|
806
|
+
const { t, set } = await createI18n({})
|
|
807
|
+
.withNamespaces(["default", "billing"])
|
|
808
|
+
.withLocale("en")
|
|
809
|
+
.load();
|
|
810
|
+
|
|
811
|
+
const result = validateExternalDictionaryPartial(raw);
|
|
807
812
|
if (!result.ok) {
|
|
808
813
|
console.error(formatIssues(result.issues));
|
|
809
814
|
return;
|
|
810
815
|
}
|
|
811
816
|
|
|
812
|
-
|
|
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
|
+
}
|
|
813
826
|
```
|
|
814
827
|
|
|
815
|
-
For a
|
|
828
|
+
For a single key patch (multi mode):
|
|
816
829
|
|
|
817
830
|
```ts
|
|
818
|
-
import {
|
|
831
|
+
import { validateExternalKey } from "./i18n/generated/dictionary-schema.generated.js";
|
|
832
|
+
|
|
833
|
+
const { set } = await createI18n({}).withNamespaces(["billing"]).withLocale("it").load();
|
|
819
834
|
|
|
820
|
-
const result =
|
|
835
|
+
const result = validateExternalKey("billing", "invoice_summary", rawLocales);
|
|
821
836
|
if (result.ok) {
|
|
822
|
-
|
|
837
|
+
set("billing", "invoice_summary", result.data.invoice_summary.it!);
|
|
823
838
|
}
|
|
824
839
|
```
|
|
825
840
|
|
|
@@ -829,13 +844,11 @@ Low-level API is also available from `@xndrjs/i18n/validation` for custom wiring
|
|
|
829
844
|
|
|
830
845
|
Both providers share this behavior:
|
|
831
846
|
|
|
832
|
-
- **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.
|
|
833
848
|
- **`getAll()`** — returns a deep-frozen snapshot of the current dictionary (not a live reference).
|
|
834
|
-
-
|
|
835
|
-
-
|
|
836
|
-
-
|
|
837
|
-
- **`setNamespace(ns, values)`** — (multi only) replaces one namespace and invalidates only its cache entries.
|
|
838
|
-
- **`mergeNamespace(ns, values)`** — (multi only) merges locale entries per translation key into an existing namespace (or registers it when not loaded yet). Used by generated `ensureNamespacesLoaded*` helpers in multi-mode split/custom delivery.
|
|
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()`.
|
|
839
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.
|
|
840
853
|
- **ICU syntax error** — throws `[i18n ICU Syntax Error] ...`.
|
|
841
854
|
- **Formatting error** (missing/invalid params) — throws `[i18n Formatting Error] ...`.
|
|
@@ -909,7 +922,7 @@ pnpm run demo:multi # tsx multi/src/index.ts
|
|
|
909
922
|
|
|
910
923
|
1. Add the key with its ICU strings to the relevant JSON file under `translations/`.
|
|
911
924
|
2. Run `pnpm --filter @xndrjs/i18n-demo i18n:codegen:multi` (or `i18n:codegen:single`).
|
|
912
|
-
3. The generated `MyProjectParams` updates; TypeScript will flag any
|
|
925
|
+
3. The generated `MyProjectParams` updates; TypeScript will flag any `t()` call sites that now need (or no longer need) parameters.
|
|
913
926
|
|
|
914
927
|
## Adding a namespace
|
|
915
928
|
|
|
@@ -922,7 +935,7 @@ pnpm run demo:multi # tsx multi/src/index.ts
|
|
|
922
935
|
1. Split the flat JSON into per-domain files.
|
|
923
936
|
2. Change `dictionary` to `namespaces` in the config.
|
|
924
937
|
3. Re-run codegen — types and API switch from flat to namespaced.
|
|
925
|
-
4. Update call sites: `
|
|
938
|
+
4. Update call sites: `t(key, locale)` → `t(namespace, key, locale)` on scopes (or use locale-bound scopes from `load()`).
|
|
926
939
|
|
|
927
940
|
## Tech stack
|
|
928
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":";;;;;;;AAAO,IAAM,2BAAA,GAA8B,CAAC,MAAA,EAAQ,KAAA,EAAO,KAAK;AAMzD,IAAM,uBAAA,GAA0B,0BAAA;AAChC,IAAM,2BAAA,GACX,mEAAA;;;ACPK,IAAM,0BAAA,GAA6B,0BAAA;AASnC,SAAS,oCACd,iBAAA,EAC0B;AAC1B,EAAA,MAAM,SAAmC,EAAC;AAE1C,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA,EAAG;AAC/D,IAAA,IAAI,CAAC,0BAAA,CAA2B,IAAA,CAAK,IAAI,CAAA,EAAG;AAC1C,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,CAAC,mBAAA,EAAqB,IAAI,CAAA;AAAA,QAChC,OAAA,EAAS,CAAA,4BAAA,EAA+B,IAAI,CAAA,cAAA,EAAiB,2BAA2B,MAAM,CAAA;AAAA,OAC/F,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,CAAC,mBAAA,EAAqB,IAAI,CAAA;AAAA,QAChC,OAAA,EAAS,kBAAkB,IAAI,CAAA,mCAAA;AAAA,OAChC,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,YAAA,uBAAmB,GAAA,EAAoB;AAC7C,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA,EAAG;AAC/D,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,YAAA,GAAe,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AAC5C,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAA,CAAO,IAAA,CAAK;AAAA,UACV,IAAA,EAAM,CAAC,mBAAA,EAAqB,IAAI,CAAA;AAAA,UAChC,SAAS,CAAA,QAAA,EAAW,MAAM,CAAA,mBAAA,EAAsB,YAAY,UAAU,IAAI,CAAA,EAAA;AAAA,SAC3E,CAAA;AAAA,MACH,CAAA,MAAO;AACL,QAAA,YAAA,CAAa,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;ACtCA,IAAM,uBAAuB,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAA;AACjF,IAAM,uBAAA,GAA0B,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAC,CAAC,CAAC,CAAA;AAExE,IAAM,cAAA,GAAiB,CAAC,WAAA,EAAa,iBAAA,EAAmB,QAAQ;AAGvE,IAAM,kBAAA,GAAqB;AAAA,EACzB,YAAY,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACvC,UAAA,EAAY,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,EAAO,EAAG,CAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAC,CAAC,EAAE,QAAA,EAAS;AAAA,EAC7D,kBAAkB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EAC7C,WAAA,EAAa,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAC7B,gBAAA,EAAkB,EACf,MAAA,EAAO,CACP,IAAI,CAAC,CAAA,CACL,UAAS,CACT,QAAA;AAAA,IACC;AAAA,GACF;AAAA,EACF,cAAA,EAAgB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAChC,wBAAwB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACnD,UAAA,EAAY,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,CAAC,CAAA,CAAE,QAAA,EAAS;AAAA,EAChD,wBAAwB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACnD,eAAA,EAAiB,CAAA,CAAE,IAAA,CAAK,2BAA2B,EAAE,QAAA,EAAS;AAAA,EAC9D,cAAA,EAAgB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAChC,cAAA,EAAgB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAChC,gBAAgB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EAC3C,yBAAyB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACpD,cAAA,EAAgB,qBAAqB,QAAA,EAAS;AAAA,EAC9C,aAAa,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACxC,QAAA,EAAU,EAAE,IAAA,CAAK,cAAc,EAAE,QAAA,EAAS,CAAE,QAAQ,WAAW,CAAA;AAAA,EAC/D,iBAAA,EAAmB,wBAAwB,QAAA,EAAS;AAAA,EACpD,gBAAgB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AACpC,CAAA;AAEO,IAAM,oBAAoB,MAAA,CAAO,IAAA;AAAA,EACtC;AACF;AAEmC,CAAA,CAChC,MAAA,CAAO,kBAAkB,CAAA,CACzB,QAAO,CACP,WAAA,CAAY,CAAC,MAAA,EAAQ,GAAA,KAAQ;AAC5B,EAAA,MAAM,aAAA,GAAgB,OAAO,UAAA,KAAe,MAAA;AAC5C,EAAA,MAAM,aAAA,GAAgB,OAAO,UAAA,KAAe,MAAA;AAE5C,EAAA,IAAI,kBAAkB,aAAA,EAAe;AACnC,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,OAAO,UAAA,EAAY;AACrB,IAAA,KAAA,MAAW,SAAA,IAAa,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,UAAU,CAAA,EAAG;AACtD,MAAA,IAAI,CAAC,uBAAA,CAAwB,IAAA,CAAK,SAAS,CAAA,EAAG;AAC5C,QAAA,GAAA,CAAI,QAAA,CAAS;AAAA,UACX,IAAA,EAAM,QAAA;AAAA,UACN,IAAA,EAAM,CAAC,YAAA,EAAc,SAAS,CAAA;AAAA,UAC9B,OAAA,EAAS,CAAA,wBAAA,EAA2B,SAAS,CAAA,GAAA,EAAM,2BAA2B,CAAA,EAAA;AAAA,SAC/E,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IACE,MAAA,CAAO,qBAAqB,MAAA,IAC5B,CAAC,wBAAwB,IAAA,CAAK,MAAA,CAAO,gBAAgB,CAAA,EACrD;AACA,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,CAAC,kBAAkB,CAAA;AAAA,MACzB,OAAA,EAAS,CAAA,wBAAA,EAA2B,MAAA,CAAO,gBAAgB,MAAM,2BAA2B,CAAA,EAAA;AAAA,KAC7F,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,MAAA,CAAO,aAAa,QAAA,EAAU;AAChC,IAAA,IAAI,CAAC,OAAO,iBAAA,EAAmB;AAC7B,MAAA,GAAA,CAAI,QAAA,CAAS;AAAA,QACX,IAAA,EAAM,QAAA;AAAA,QACN,IAAA,EAAM,CAAC,mBAAmB,CAAA;AAAA,QAC1B,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH,CAAA,MAAO;AACL,MAAA,KAAA,MAAW,KAAA,IAAS,mCAAA,CAAoC,MAAA,CAAO,iBAAiB,CAAA,EAAG;AACjF,QAAA,GAAA,CAAI,QAAA,CAAS;AAAA,UACX,IAAA,EAAM,QAAA;AAAA,UACN,MAAM,KAAA,CAAM,IAAA;AAAA,UACZ,SAAS,KAAA,CAAM;AAAA,SAChB,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAA,MAAA,IAAW,MAAA,CAAO,iBAAA,KAAsB,MAAA,EAAW;AACjD,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,CAAC,mBAAmB,CAAA;AAAA,MAC1B,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,MAAA,CAAO,UAAA,KAAe,MAAA,IAAa,MAAA,CAAO,aAAa,WAAA,EAAa;AACtE,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,CAAC,YAAY,CAAA;AAAA,MACnB,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AACF,CAAC;AAKI,SAAS,yBACd,MAAA,EACQ;AACR,EAAA,OAAO,MAAA,CAAO,cAAA,IAAkB,IAAA,CAAK,OAAA,CAAQ,OAAO,WAAW,CAAA;AACjE;AAEA,IAAM,2BAAA,GAA8B,yBAAA;AAG7B,SAAS,4BACd,MAAA,EACQ;AACR,EAAA,OACE,MAAA,CAAO,oBACP,IAAA,CAAK,IAAA,CAAK,KAAK,OAAA,CAAQ,MAAA,CAAO,WAAW,CAAA,EAAG,2BAA2B,CAAA;AAE3E;;;ACxIO,SAAS,iBAAiB,OAAA,EAAyB;AACxD,EAAA,MAAM,QAAQ,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,CAAE,OAAO,OAAO,CAAA;AACnD,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAM,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,OAAO,CAAC,CAAA,CAAE,WAAA,EAAY,GAAI,KAAK,KAAA,CAAM,CAAC,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAClF;AAEO,SAAS,oBAAoB,OAAA,EAIlC;AACA,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB,GAAG,OAAO,CAAA,MAAA,CAAA;AAAA,IAC1B,cAAA,EAAgB,GAAG,OAAO,CAAA,MAAA,CAAA;AAAA,IAC1B,cAAA,EAAgB,GAAG,OAAO,CAAA,MAAA;AAAA,GAC5B;AACF;;;ACdA,IAAM,aAAA,GAAgB,WAAA;AACtB,IAAM,gBAAA,GAAmB,cAAA;AAElB,SAAS,kBAAA,CAAmB,MAAiB,OAAA,EAAqC;AACvF,EAAA,MAAM,SAAA,GAAY,oBAAoB,OAAO,CAAA;AAC7C,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,WAAA,EAAa,GAAG,aAAa,CAAA,wBAAA,CAAA;AAAA,IAC7B,gBAAA,EAAkB,GAAG,aAAa,CAAA,wBAAA,CAAA;AAAA,IAClC,cAAA,EAAgB,GAAG,aAAa,CAAA,sBAAA,CAAA;AAAA,IAChC,gBAAgB,SAAA,CAAU,cAAA;AAAA,IAC1B,gBAAgB,SAAA,CAAU,cAAA;AAAA,IAC1B,gBAAgB,SAAA,CAAU,cAAA;AAAA,IAC1B,WAAA,EAAa;AAAA,GACf;AAEA,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,GAAG,gBAAgB,CAAA,kBAAA,CAAA;AAAA,MAC/B,GAAG;AAAA,KACL;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,UAAA,EAAY;AAAA,MACV,OAAA,EAAS,GAAG,gBAAgB,CAAA,aAAA;AAAA,KAC9B;AAAA,IACA,GAAG;AAAA,GACL;AACF;AC7BO,SAAS,kBAAA,CAAmB,YAAoB,MAAA,EAAkC;AACvF,EAAA,EAAA,CAAG,SAAA,CAAUA,KAAK,OAAA,CAAQ,UAAU,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAC1D,EAAA,EAAA,CAAG,aAAA,CAAc,YAAY,CAAA,EAAG,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAC;AAAA,CAAI,CAAA;AACrE","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"]}
|