@xndrjs/i18n 0.3.3 → 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -44
- package/dist/codegen/index.d.ts +85 -0
- package/dist/codegen/index.js +181 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/index.d.ts +34 -18
- package/dist/index.js +174 -145
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +86 -4
- package/dist/validation/index.js.map +1 -1
- package/package.json +6 -1
- package/src/IcuTranslationProviderMulti.test.ts +48 -1
- package/src/IcuTranslationProviderMulti.ts +41 -61
- package/src/IcuTranslationProviderSingle.test.ts +67 -0
- package/src/IcuTranslationProviderSingle.ts +27 -51
- package/src/audit/audit-dictionaries.ts +4 -1
- package/src/audit/run-audit.test.ts +35 -1
- package/src/audit/run-audit.ts +15 -7
- package/src/codegen/codegen-config-schema.ts +68 -1
- package/src/codegen/config.test.ts +174 -39
- package/src/codegen/config.ts +14 -5
- package/src/codegen/constants.ts +8 -0
- package/src/codegen/delivery-artifacts.test.ts +142 -0
- package/src/codegen/delivery-artifacts.ts +124 -0
- package/src/codegen/emit/dictionary-file.test.ts +190 -0
- package/src/codegen/emit/dictionary-file.ts +165 -2
- package/src/codegen/emit/dictionary-schema-file.ts +5 -0
- package/src/codegen/emit/instance-file.ts +119 -38
- package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
- package/src/codegen/emit/namespace-loaders-file.ts +118 -32
- package/src/codegen/emit/types-file.test.ts +114 -0
- package/src/codegen/emit/types-file.ts +48 -11
- package/src/codegen/generate-i18n-types.test.ts +765 -22
- package/src/codegen/generate-i18n-types.ts +111 -58
- package/src/codegen/icu-analysis.ts +9 -2
- package/src/codegen/locale-fallback.ts +19 -10
- package/src/codegen/locale-policy.ts +4 -0
- package/src/codegen/paths.ts +9 -5
- package/src/codegen/project-locales-set-namespace.test.ts +11 -9
- package/src/codegen/read-dictionary.test.ts +474 -2
- package/src/codegen/read-dictionary.ts +164 -15
- package/src/codegen/write-file-if-changed.test.ts +42 -0
- package/src/codegen/write-file-if-changed.ts +20 -0
- package/src/codegen-config/build-config.ts +34 -0
- package/src/codegen-config/codegen-config.test.ts +32 -0
- package/src/codegen-config/index.ts +21 -0
- package/src/codegen-config/write-config.ts +8 -0
- package/src/deep-freeze.test.ts +13 -0
- package/src/deep-freeze.ts +5 -0
- package/src/format-core.ts +91 -0
- package/src/index.ts +8 -5
- package/src/project-locales.test.ts +129 -10
- package/src/project-locales.ts +90 -3
- package/src/setup/setup-i18n.test.ts +1 -1
- package/src/setup/setup-i18n.ts +9 -32
- package/src/types.ts +19 -4
- package/src/validation/normalize.ts +4 -0
- package/dist/types-C1CpXVOJ.d.ts +0 -83
- package/src/ensure-namespace.integration.test.ts +0 -66
- package/src/ensure-namespace.test.ts +0 -125
- package/src/ensure-namespace.ts +0 -70
- /package/src/{setup → codegen-config}/type-names.ts +0 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The core idea: your ICU strings live in local JSON files that act as **type-safe
|
|
|
10
10
|
- **ICU MessageFormat** — full support for interpolation, plurals, and select.
|
|
11
11
|
- **Runtime override** — hydrate translations from an external source via `setAll()` / `setNamespace()` without rebuilding.
|
|
12
12
|
- **Single-file or multi-namespace** — one flat dictionary, or multiple JSON files each bound to a namespace.
|
|
13
|
-
- **Lazy namespace loading** — optional code-splitting via `loadOnInit` and `
|
|
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.
|
|
15
15
|
- **Explicit runtime errors** — malformed ICU (e.g. a corrupt remote payload) or missing parameters throw descriptive errors.
|
|
16
16
|
- **Translation audit** — `xndrjs-i18n-audit` reports missing locales per key (direct vs effective after fallback); optional CI gate via `--fail-on`.
|
|
@@ -75,11 +75,13 @@ npm run i18n:audit
|
|
|
75
75
|
```ts
|
|
76
76
|
// i18n/index.ts
|
|
77
77
|
import { createI18n } from "./generated/instance.generated.js";
|
|
78
|
+
import { defaultDictionary } from "./generated/dictionary.generated.js";
|
|
78
79
|
|
|
79
80
|
export * from "./generated/instance.generated.js";
|
|
81
|
+
export * from "./generated/dictionary.generated.js";
|
|
80
82
|
export * from "./generated/i18n-types.generated.js";
|
|
81
83
|
|
|
82
|
-
export const i18n = createI18n();
|
|
84
|
+
export const i18n = createI18n(defaultDictionary);
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
```ts
|
|
@@ -104,7 +106,7 @@ This creates `i18n/i18n.codegen.json`, starter translation JSON, and `i18n/index
|
|
|
104
106
|
|
|
105
107
|
Use `namespaces` instead of `dictionary` in `i18n/i18n.codegen.json`. See [Configuration](#configuration-i18ncodegenjson) and the [multi-namespace example](#multi-namespace-example) below.
|
|
106
108
|
|
|
107
|
-
For lazy loading, add `loadOnInit
|
|
109
|
+
For lazy loading, add `loadOnInit` and `namespaceLoadersOutput` — see [Lazy namespace loading](#lazy-namespace-loading-multi-mode). Add `dictionarySchemaOutput` only when you validate external CMS/API payloads.
|
|
108
110
|
|
|
109
111
|
## Repository layout
|
|
110
112
|
|
|
@@ -194,8 +196,8 @@ Variables found across **all locales** of the same key are merged. If parsing fa
|
|
|
194
196
|
### 3. Generated files
|
|
195
197
|
|
|
196
198
|
- **`i18n-types.generated.ts`** — `I18N_MODE`, `MyProjectParams`, `MyProjectSchema`.
|
|
197
|
-
- **`dictionary.generated.ts`** — imports the JSON files and
|
|
198
|
-
- **`instance.generated.ts`** — exports `createI18n()
|
|
199
|
+
- **`dictionary.generated.ts`** — imports the JSON files and exports `defaultDictionary` (canonical delivery) or `defaultDictionaryFor(locale)` (split-by-locale delivery).
|
|
200
|
+
- **`instance.generated.ts`** — exports `createI18n(dictionary)` (required argument; no default import of the fallback dictionary), typed `projectDictionaryLocales()` (full schema), and in multi mode `projectNamespaceLocales()` (single namespace); with `delivery: "custom"`, also `projectDictionaryForDeliveryArea()` and `projectNamespaceForDeliveryArea()`; `LOCALE_FALLBACK` wired in when configured.
|
|
199
201
|
- **`i18n.ts`** (optional, hand-written) — app-owned singleton if desired.
|
|
200
202
|
|
|
201
203
|
Example generated types (multi-namespace):
|
|
@@ -219,20 +221,30 @@ export type MyProjectParams = {
|
|
|
219
221
|
};
|
|
220
222
|
|
|
221
223
|
export type MyProjectSchema = {
|
|
222
|
-
default:
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
default: {
|
|
225
|
+
login_button: Partial<Record<MyProjectLocale, string>>;
|
|
226
|
+
welcome: Partial<Record<MyProjectLocale, string>>;
|
|
227
|
+
dashboard_status: Partial<Record<MyProjectLocale, string>>;
|
|
228
|
+
};
|
|
229
|
+
user: {
|
|
230
|
+
profile_title: Partial<Record<MyProjectLocale, string>>;
|
|
231
|
+
greeting: Partial<Record<MyProjectLocale, string>>;
|
|
232
|
+
};
|
|
233
|
+
billing: {
|
|
234
|
+
invoice_summary: Partial<Record<MyProjectLocale, string>>;
|
|
235
|
+
};
|
|
225
236
|
};
|
|
226
237
|
```
|
|
227
238
|
|
|
228
239
|
### 4. Runtime provider
|
|
229
240
|
|
|
230
|
-
Create a provider with the generated factory (no side effects at import time):
|
|
241
|
+
Create a provider with the generated factory (no side effects at import time). Pass the dictionary explicitly — from codegen’s `dictionary.generated.ts`, from lazy loaders, or from an external source — so the factory module does not pull fallback JSON into your bundle:
|
|
231
242
|
|
|
232
243
|
```ts
|
|
233
244
|
import { createI18n } from "./i18n";
|
|
245
|
+
import { defaultDictionary } from "./i18n/generated/dictionary.generated.js";
|
|
234
246
|
|
|
235
|
-
const i18n = createI18n();
|
|
247
|
+
const i18n = createI18n(defaultDictionary);
|
|
236
248
|
// or with an external dictionary at init:
|
|
237
249
|
const i18n = createI18n(externalDictionary);
|
|
238
250
|
```
|
|
@@ -241,7 +253,8 @@ Optionally, wrap it in an app-owned singleton (`i18n.ts`):
|
|
|
241
253
|
|
|
242
254
|
```ts
|
|
243
255
|
import { createI18n } from "./instance.generated.js";
|
|
244
|
-
|
|
256
|
+
import { defaultDictionary } from "./dictionary.generated.js";
|
|
257
|
+
export const i18n = createI18n(defaultDictionary);
|
|
245
258
|
```
|
|
246
259
|
|
|
247
260
|
### Locale-bound provider (`forLocale`)
|
|
@@ -249,7 +262,7 @@ export const i18n = createI18n();
|
|
|
249
262
|
Bind a locale once and omit it on every `.get()`:
|
|
250
263
|
|
|
251
264
|
```ts
|
|
252
|
-
const i18n = createI18n();
|
|
265
|
+
const i18n = createI18n(defaultDictionary);
|
|
253
266
|
const i18nEn = i18n.forLocale("en");
|
|
254
267
|
|
|
255
268
|
i18nEn.get("login_button"); // single-file
|
|
@@ -302,24 +315,25 @@ const localeFallback = {
|
|
|
302
315
|
const i18n = new IcuTranslationProviderMulti(schema, { localeFallback });
|
|
303
316
|
```
|
|
304
317
|
|
|
305
|
-
#### `
|
|
318
|
+
#### `projectDictionaryLocales` / `projectNamespaceLocales`
|
|
306
319
|
|
|
307
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.
|
|
308
321
|
|
|
309
322
|
```ts
|
|
310
323
|
import {
|
|
311
324
|
createI18n,
|
|
312
|
-
|
|
325
|
+
projectDictionaryLocales,
|
|
313
326
|
projectNamespaceLocales,
|
|
314
327
|
} from "./i18n/generated/instance.generated.js";
|
|
328
|
+
import { defaultDictionary } from "./i18n/generated/dictionary.generated.js";
|
|
315
329
|
import billingDictionary from "./i18n/translations/billing.json";
|
|
316
330
|
|
|
317
|
-
const i18n = createI18n();
|
|
331
|
+
const i18n = createI18n(defaultDictionary);
|
|
318
332
|
i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, [activeLocale]));
|
|
319
|
-
i18n.setAll(
|
|
333
|
+
i18n.setAll(projectDictionaryLocales(fullDictionary, [activeLocale])); // multi: all namespaces
|
|
320
334
|
```
|
|
321
335
|
|
|
322
|
-
Codegen emits typed wrappers in `instance.generated.ts` (`
|
|
336
|
+
Codegen emits typed wrappers in `instance.generated.ts` (`projectDictionaryLocales` for the full schema; `projectNamespaceLocales` in multi mode for one namespace). With `delivery: "custom"`, it also emits `projectDictionaryForDeliveryArea` and `projectNamespaceForDeliveryArea`. The low-level `@xndrjs/i18n` exports are `*Core` helpers for tooling without codegen.
|
|
323
337
|
|
|
324
338
|
### 6. Translation audit (`xndrjs-i18n-audit`)
|
|
325
339
|
|
|
@@ -393,29 +407,31 @@ Specify **exactly one** of `dictionary` (single-file) or `namespaces` (multi-fil
|
|
|
393
407
|
}
|
|
394
408
|
```
|
|
395
409
|
|
|
396
|
-
| Field | Description
|
|
397
|
-
| ----------------------------------- |
|
|
398
|
-
| `dictionary` | Path to a single dictionary file (`.json`, `.yaml`, or `.yml`) for the flat API. Mutually exclusive with `namespaces`.
|
|
399
|
-
| `namespaces` | Map of `namespace -> dictionary path` (`.json`, `.yaml`, or `.yml`) for the namespaced API. Mutually exclusive with `dictionary`.
|
|
400
|
-
| `defaultNamespace` | Optional. Namespace label used internally in single-file mode (default `"default"`). Not exposed in the flat API.
|
|
401
|
-
| `typesOutput` | Output path for the generated types.
|
|
402
|
-
| `dictionaryOutput` | Output path for the generated dictionary manifest.
|
|
403
|
-
| `instanceOutput` | Output path for the generated factory (`createI18n`).
|
|
404
|
-
| `importExtension` | Optional. Relative import suffix between generated `.ts` modules: `"none"` (default, extensionless), `".ts"`, or `".js"`.
|
|
405
|
-
| `factoryName` | Name of the exported factory function (default `createI18n`).
|
|
406
|
-
| `paramsTypeName` / `schemaTypeName` | Names of the exported types (customizable per project).
|
|
407
|
-
| `localeTypeName` | Name of the exported locale union type (default `MyProjectLocale`).
|
|
408
|
-
| `localeFallback` | Optional map of `locale -> next locale
|
|
409
|
-
| `localeFallbackConstName` | Name of the generated fallback constant (default `LOCALE_FALLBACK`).
|
|
410
|
-
| `dictionarySchemaOutput` | Optional path for generated external dictionary validation (`dictionary-schema.generated.ts`). Requires `zod` in the consumer app.
|
|
411
|
-
| `loadOnInit` | Multi mode only. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default).
|
|
412
|
-
| `namespaceLoadersOutput` | Output path for generated
|
|
410
|
+
| Field | Description |
|
|
411
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
412
|
+
| `dictionary` | Path to a single dictionary file (`.json`, `.yaml`, or `.yml`) for the flat API. Mutually exclusive with `namespaces`. |
|
|
413
|
+
| `namespaces` | Map of `namespace -> dictionary path` (`.json`, `.yaml`, or `.yml`) for the namespaced API. Mutually exclusive with `dictionary`. |
|
|
414
|
+
| `defaultNamespace` | Optional. Namespace label used internally in single-file mode (default `"default"`). Not exposed in the flat API. |
|
|
415
|
+
| `typesOutput` | Output path for the generated types. |
|
|
416
|
+
| `dictionaryOutput` | Output path for the generated dictionary manifest. |
|
|
417
|
+
| `instanceOutput` | Output path for the generated factory (`createI18n`). |
|
|
418
|
+
| `importExtension` | Optional. Relative import suffix between generated `.ts` modules: `"none"` (default, extensionless), `".ts"`, or `".js"`. |
|
|
419
|
+
| `factoryName` | Name of the exported factory function (default `createI18n`). |
|
|
420
|
+
| `paramsTypeName` / `schemaTypeName` | Names of the exported types (customizable per project). |
|
|
421
|
+
| `localeTypeName` | Name of the exported locale union type (default `MyProjectLocale`). |
|
|
422
|
+
| `localeFallback` | Optional map of `locale -> next locale | null` for runtime fallback resolution. |
|
|
423
|
+
| `localeFallbackConstName` | Name of the generated fallback constant (default `LOCALE_FALLBACK`). |
|
|
424
|
+
| `dictionarySchemaOutput` | Optional path for generated external dictionary validation (`dictionary-schema.generated.ts`). Requires `zod` in the consumer app. |
|
|
425
|
+
| `loadOnInit` | Multi mode only. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default). |
|
|
426
|
+
| `namespaceLoadersOutput` | Output path for generated `namespaceLoaders` (dynamic `import()` per lazy namespace). Defaults to `{dirname(instanceOutput)}/namespace-loaders.generated.ts`. Required when lazy namespaces exist. |
|
|
427
|
+
| `delivery` | Optional. `"canonical"` (default) — one multilocale JSON per namespace. `"split-by-locale"` — emits `{basename}.{locale}.json` under `{deliveryOutput}/translations/` and per-locale loaders. See [Split-by-locale delivery](#split-by-locale-delivery). |
|
|
428
|
+
| `deliveryOutput` | Optional. Directory for compiled and split delivery JSON (files land in `{deliveryOutput}/translations/`). Defaults to `dirname(typesOutput)`. Use e.g. `public/i18n` to ship per-locale JSON from a static host while keeping generated TypeScript under `generated/`. |
|
|
413
429
|
|
|
414
430
|
> Paths are resolved relative to the directory containing `i18n.codegen.json` (e.g. `i18n/` when using `xndrjs-i18n-setup .`).
|
|
415
431
|
|
|
416
432
|
### YAML authoring
|
|
417
433
|
|
|
418
|
-
Dictionary paths may use `.yaml` or `.yml` instead of `.json`. **YAML is an authoring format, not a runtime format** — codegen compiles YAML to JSON under the
|
|
434
|
+
Dictionary paths may use `.yaml` or `.yml` instead of `.json`. **YAML is an authoring format, not a runtime format** — codegen compiles YAML to JSON under the delivery output directory (for example `translations/billing.yaml` → `{deliveryOutput}/translations/billing.json`; default `{deliveryOutput}` is `dirname(typesOutput)`); generated TypeScript imports the compiled JSON at runtime. Edit only the YAML source — the compiled JSON is overwritten on each codegen run.
|
|
419
435
|
|
|
420
436
|
Use YAML when ICU is hard to read on one line: multiple parameters, or plural/select branches. It is not aimed at long legal copy or styled pages — those usually belong in a separate content template with localized fragments inside.
|
|
421
437
|
|
|
@@ -438,6 +454,82 @@ Workflow:
|
|
|
438
454
|
|
|
439
455
|
Mixed namespaces are supported: some namespaces can stay `.json` while others use YAML.
|
|
440
456
|
|
|
457
|
+
### Split-by-locale delivery
|
|
458
|
+
|
|
459
|
+
By default (`delivery: "canonical"`), codegen keeps one multilocale JSON per namespace — either your source file in place (`.json`) or a compiled YAML → JSON under `{deliveryOutput}/translations/` (default: `generated/translations/`).
|
|
460
|
+
|
|
461
|
+
Set `"delivery": "split-by-locale"` to emit **one JSON file per locale** instead:
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"delivery": "split-by-locale",
|
|
466
|
+
"namespaces": {
|
|
467
|
+
"default": "translations/default.json",
|
|
468
|
+
"billing": "translations/billing.yaml"
|
|
469
|
+
},
|
|
470
|
+
"loadOnInit": ["default"]
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Codegen writes `{deliveryOutput}/translations/{basename}.{locale}.json` (for example `user.it.json`, `billing.en.json`). Each file contains the same shape as `projectNamespaceLocalesCore(dict, [locale])` — keys map to a single locale entry. With `localeFallback`, locales such as `de-CH` are resolved at codegen time (for example from `de-DE`).
|
|
475
|
+
|
|
476
|
+
**Authoring is unchanged** — edit the canonical source JSON/YAML. Types (`MyProjectSchema`) are generated explicitly from ICU analysis (keys + `Partial<Record<MyProjectLocale, string>>` per key), not from `typeof import` of JSON files. Audit still reads the config paths.
|
|
477
|
+
|
|
478
|
+
**Generated API changes (opt-in):**
|
|
479
|
+
|
|
480
|
+
| Canonical | Split-by-locale |
|
|
481
|
+
| -------------------------------- | ---------------------------------------------- |
|
|
482
|
+
| `export const defaultDictionary` | `export function defaultDictionaryFor(locale)` |
|
|
483
|
+
| `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)` |
|
|
484
|
+
|
|
485
|
+
Example `dictionary.generated.ts` (multi, `loadOnInit: ["default"]`):
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import defaultEn from "./translations/default.en.json";
|
|
489
|
+
import defaultIt from "./translations/default.it.json";
|
|
490
|
+
|
|
491
|
+
const defaultByLocale = {
|
|
492
|
+
en: defaultEn,
|
|
493
|
+
it: defaultIt,
|
|
494
|
+
} as const satisfies Record<MyProjectLocale, MyProjectSchema["default"]>;
|
|
495
|
+
|
|
496
|
+
export function defaultDictionaryFor(locale: MyProjectLocale): InitialSchema {
|
|
497
|
+
return { default: defaultByLocale[locale] };
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Example `namespace-loaders.generated.ts`:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
export const namespaceLoaders = {
|
|
505
|
+
billing: (locale: MyProjectLocale) => {
|
|
506
|
+
switch (locale) {
|
|
507
|
+
case "en":
|
|
508
|
+
return import("./translations/billing.en.json").then((m) => m.default);
|
|
509
|
+
case "it":
|
|
510
|
+
return import("./translations/billing.it.json").then((m) => m.default);
|
|
511
|
+
case "de-CH":
|
|
512
|
+
return import("./translations/billing.de-CH.json").then((m) => m.default);
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Init with the active locale:
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import { createI18n } from "./generated/instance.generated.js";
|
|
522
|
+
import { defaultDictionaryFor } from "./generated/dictionary.generated.js";
|
|
523
|
+
|
|
524
|
+
const i18n = createI18n(defaultDictionaryFor(activeLocale));
|
|
525
|
+
|
|
526
|
+
if (!i18n.hasNamespace("billing")) {
|
|
527
|
+
i18n.setNamespace("billing", await namespaceLoaders.billing(activeLocale));
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Use split delivery when you want smaller lazy chunks (one locale per dynamic import) or when serving per-locale JSON from `public/` without runtime `projectNamespaceLocales`. Runtime `projectDictionaryLocales` / `projectNamespaceLocales` remain available for external CMS/API payloads.
|
|
532
|
+
|
|
441
533
|
## Usage
|
|
442
534
|
|
|
443
535
|
### Single vs. multi-namespace API
|
|
@@ -453,7 +545,7 @@ Mixed namespaces are supported: some namespaces can stay `.json` while others us
|
|
|
453
545
|
|
|
454
546
|
```ts
|
|
455
547
|
import { i18n } from "./i18n"; // app singleton from i18n.ts
|
|
456
|
-
// or: import { createI18n } from './i18n'; const i18n = createI18n();
|
|
548
|
+
// or: import { createI18n, defaultDictionary } from './i18n'; const i18n = createI18n(defaultDictionary);
|
|
457
549
|
|
|
458
550
|
i18n.get("default", "login_button", "it"); // "Accedi"
|
|
459
551
|
i18n.get("default", "welcome", "en", { name: "Ada" }); // "Welcome Ada!"
|
|
@@ -469,7 +561,7 @@ i18n.get("billing", "login_button", "it"); // ✗ key not in namespace
|
|
|
469
561
|
|
|
470
562
|
```ts
|
|
471
563
|
import { i18n } from "./i18n"; // app singleton from i18n.ts
|
|
472
|
-
// or: import { createI18n } from './i18n'; const i18n = createI18n();
|
|
564
|
+
// or: import { createI18n, defaultDictionary } from './i18n'; const i18n = createI18n(defaultDictionary);
|
|
473
565
|
|
|
474
566
|
i18n.get("login_button", "it");
|
|
475
567
|
i18n.get("welcome", "en", { name: "Ada" });
|
|
@@ -487,7 +579,7 @@ i18n.setNamespace("billing", externalBillingPayload);
|
|
|
487
579
|
|
|
488
580
|
### Lazy namespace loading (multi mode)
|
|
489
581
|
|
|
490
|
-
Split namespaces across chunks by listing only the namespaces you need at startup in `loadOnInit`. Codegen emits dynamic `import()`
|
|
582
|
+
Split namespaces across chunks by listing only the namespaces you need at startup in `loadOnInit`. Codegen emits typed `namespaceLoaders` — one dynamic `import()` per lazy namespace (or per lazy namespace **and locale** when `delivery: "split-by-locale"`). `.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering.
|
|
491
583
|
|
|
492
584
|
```json
|
|
493
585
|
{
|
|
@@ -496,7 +588,6 @@ Split namespaces across chunks by listing only the namespaces you need at startu
|
|
|
496
588
|
"billing": "translations/billing.yaml"
|
|
497
589
|
},
|
|
498
590
|
"loadOnInit": ["default"],
|
|
499
|
-
"dictionarySchemaOutput": "generated/dictionary-schema.generated.ts",
|
|
500
591
|
"namespaceLoadersOutput": "generated/namespace-loaders.generated.ts"
|
|
501
592
|
}
|
|
502
593
|
```
|
|
@@ -504,23 +595,51 @@ Split namespaces across chunks by listing only the namespaces you need at startu
|
|
|
504
595
|
Codegen also emits `LoadOnInitNamespace`, `LazyNamespace`, and `InitialSchema` types. The generated factory accepts a partial `InitialSchema` at init time.
|
|
505
596
|
|
|
506
597
|
```ts
|
|
507
|
-
import { i18n,
|
|
598
|
+
import { i18n, namespaceLoaders, type LazyNamespace } from "./i18n";
|
|
508
599
|
|
|
509
600
|
i18n.get("default", "login_button", "en"); // available immediately
|
|
510
601
|
|
|
511
|
-
|
|
602
|
+
if (!i18n.hasNamespace("billing")) {
|
|
603
|
+
i18n.setNamespace("billing", await namespaceLoaders.billing());
|
|
604
|
+
}
|
|
512
605
|
i18n.get("billing", "invoice_summary", "en", { count: 12 });
|
|
513
606
|
|
|
514
607
|
// batch preload
|
|
515
|
-
await
|
|
608
|
+
await Promise.all(
|
|
609
|
+
(["user", "billing"] as const satisfies readonly LazyNamespace[]).map(async (namespace) => {
|
|
610
|
+
if (i18n.hasNamespace(namespace)) return;
|
|
611
|
+
i18n.setNamespace(namespace, await namespaceLoaders[namespace]());
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
With `delivery: "split-by-locale"`, pick the locale on each loader:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
const locale = "it" as const;
|
|
620
|
+
|
|
621
|
+
if (!i18n.hasNamespace("billing")) {
|
|
622
|
+
i18n.setNamespace("billing", await namespaceLoaders.billing(locale));
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
Optional locale projection before register:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
import { projectNamespaceLocales } from "./i18n/generated/instance.generated.js";
|
|
630
|
+
|
|
631
|
+
const billing = await namespaceLoaders.billing();
|
|
632
|
+
i18n.setNamespace("billing", projectNamespaceLocales(billing, [userLocale]));
|
|
516
633
|
```
|
|
517
634
|
|
|
518
635
|
Calling `.get()` on a namespace that is not loaded throws:
|
|
519
636
|
|
|
520
|
-
`[i18n] Namespace not loaded: "billing".
|
|
637
|
+
`[i18n] Namespace not loaded: "billing". Register it with setNamespace() before calling .get().`
|
|
521
638
|
|
|
522
639
|
When `loadOnInit` is omitted, behavior is unchanged: all namespaces are statically imported.
|
|
523
640
|
|
|
641
|
+
Fetch/CMS hydration is app-owned: fetch → `validateExternalNamespace()` (requires `dictionarySchemaOutput` + `zod`) → `setNamespace()`.
|
|
642
|
+
|
|
524
643
|
### External dictionary validation
|
|
525
644
|
|
|
526
645
|
When translations arrive from an external source (i.e. a CMS), validate the `unknown` input before calling `setAll()` or `setNamespace()`.
|
|
@@ -578,10 +697,40 @@ Both providers share this behavior:
|
|
|
578
697
|
- **`hasNamespace(ns)`** — (multi only) returns whether a namespace has been loaded (eager init, lazy load, or `setNamespace`).
|
|
579
698
|
- **`setAll(values)`** — replaces the dictionary and clears the entire cache.
|
|
580
699
|
- **`setNamespace(ns, values)`** — (multi only) replaces one namespace and invalidates only its cache entries.
|
|
581
|
-
- **Missing key/locale** — throws an error if the template is `undefined
|
|
700
|
+
- **Missing key/locale** — by default throws an error if the template is `undefined` (configurable via `onMissing`, see below). An empty string (`""`) is treated as a valid template.
|
|
582
701
|
- **ICU syntax error** — throws `[i18n ICU Syntax Error] ...`.
|
|
583
702
|
- **Formatting error** (missing/invalid params) — throws `[i18n Formatting Error] ...`.
|
|
584
703
|
|
|
704
|
+
### Missing-translation strategy (`onMissing`)
|
|
705
|
+
|
|
706
|
+
Both providers accept an `onMissing` option controlling what happens when no template resolves for a key/locale (after walking the full fallback chain):
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
onMissing?: "throw" | "key" | ((context: MissingTranslationContext) => string);
|
|
710
|
+
// MissingTranslationContext = { namespace?: string; key: string; locale: string; fallbackChain: string }
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
- **`"throw"`** (default) — throws `[i18n] Missing key or locale: ...` including the fallback chain.
|
|
714
|
+
- **`"key"`** — returns the key itself: `key` in single mode, `namespace.key` in multi mode.
|
|
715
|
+
- **Function** — receives the missing-translation context (`namespace` is only set in multi mode) and returns the string to display.
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
const i18n = new IcuTranslationProviderMulti(schema, {
|
|
719
|
+
localeFallback,
|
|
720
|
+
onMissing: ({ namespace, key }) => `[missing: ${namespace}.${key}]`,
|
|
721
|
+
});
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
`onMissing` only applies to missing-template resolution. ICU syntax errors, formatting errors, and (in multi mode) unloaded namespaces still throw.
|
|
725
|
+
|
|
726
|
+
The generated factory accepts `onMissing` as a pass-through option, merged with the codegen-wired `localeFallback`:
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
import { createI18n } from "./i18n/generated/instance.generated.js";
|
|
730
|
+
|
|
731
|
+
const i18n = createI18n(dictionary, { onMissing: "key" });
|
|
732
|
+
```
|
|
733
|
+
|
|
585
734
|
## Commands
|
|
586
735
|
|
|
587
736
|
Run from the repo root:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
declare const DELIVERY_MODES: readonly ["canonical", "split-by-locale", "custom"];
|
|
4
|
+
type DeliveryMode = (typeof DELIVERY_MODES)[number];
|
|
5
|
+
declare const codegenConfigShape: {
|
|
6
|
+
dictionary: z.ZodOptional<z.ZodString>;
|
|
7
|
+
namespaces: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
8
|
+
defaultNamespace: z.ZodOptional<z.ZodString>;
|
|
9
|
+
typesOutput: z.ZodString;
|
|
10
|
+
dictionaryOutput: z.ZodString;
|
|
11
|
+
instanceOutput: z.ZodString;
|
|
12
|
+
dictionarySchemaOutput: z.ZodOptional<z.ZodString>;
|
|
13
|
+
loadOnInit: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
|
+
namespaceLoadersOutput: z.ZodOptional<z.ZodString>;
|
|
15
|
+
importExtension: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
none: "none";
|
|
17
|
+
".ts": ".ts";
|
|
18
|
+
".js": ".js";
|
|
19
|
+
}>>;
|
|
20
|
+
paramsTypeName: z.ZodString;
|
|
21
|
+
schemaTypeName: z.ZodString;
|
|
22
|
+
localeTypeName: z.ZodOptional<z.ZodString>;
|
|
23
|
+
localeFallbackConstName: z.ZodOptional<z.ZodString>;
|
|
24
|
+
localeFallback: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>>;
|
|
25
|
+
factoryName: z.ZodOptional<z.ZodString>;
|
|
26
|
+
delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
custom: "custom";
|
|
28
|
+
canonical: "canonical";
|
|
29
|
+
"split-by-locale": "split-by-locale";
|
|
30
|
+
}>>>;
|
|
31
|
+
deliveryArtifacts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
32
|
+
deliveryOutput: z.ZodOptional<z.ZodString>;
|
|
33
|
+
};
|
|
34
|
+
declare const codegenConfigKeys: (keyof typeof codegenConfigShape)[];
|
|
35
|
+
declare const codegenConfigSchema: z.ZodObject<{
|
|
36
|
+
dictionary: z.ZodOptional<z.ZodString>;
|
|
37
|
+
namespaces: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
38
|
+
defaultNamespace: z.ZodOptional<z.ZodString>;
|
|
39
|
+
typesOutput: z.ZodString;
|
|
40
|
+
dictionaryOutput: z.ZodString;
|
|
41
|
+
instanceOutput: z.ZodString;
|
|
42
|
+
dictionarySchemaOutput: z.ZodOptional<z.ZodString>;
|
|
43
|
+
loadOnInit: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
44
|
+
namespaceLoadersOutput: z.ZodOptional<z.ZodString>;
|
|
45
|
+
importExtension: z.ZodOptional<z.ZodEnum<{
|
|
46
|
+
none: "none";
|
|
47
|
+
".ts": ".ts";
|
|
48
|
+
".js": ".js";
|
|
49
|
+
}>>;
|
|
50
|
+
paramsTypeName: z.ZodString;
|
|
51
|
+
schemaTypeName: z.ZodString;
|
|
52
|
+
localeTypeName: z.ZodOptional<z.ZodString>;
|
|
53
|
+
localeFallbackConstName: z.ZodOptional<z.ZodString>;
|
|
54
|
+
localeFallback: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>>;
|
|
55
|
+
factoryName: z.ZodOptional<z.ZodString>;
|
|
56
|
+
delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
57
|
+
custom: "custom";
|
|
58
|
+
canonical: "canonical";
|
|
59
|
+
"split-by-locale": "split-by-locale";
|
|
60
|
+
}>>>;
|
|
61
|
+
deliveryArtifacts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
62
|
+
deliveryOutput: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, z.core.$strict>;
|
|
64
|
+
type CodegenConfigInput = z.input<typeof codegenConfigSchema>;
|
|
65
|
+
type CodegenConfig = z.infer<typeof codegenConfigSchema>;
|
|
66
|
+
declare function resolveDeliveryOutputDir(config: Pick<CodegenConfig, "typesOutput" | "deliveryOutput">): string;
|
|
67
|
+
|
|
68
|
+
type DeliveryArtifactsMap = Record<string, readonly string[]>;
|
|
69
|
+
|
|
70
|
+
declare const SUPPORTED_IMPORT_EXTENSIONS: readonly ["none", ".ts", ".js"];
|
|
71
|
+
type SupportedImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];
|
|
72
|
+
|
|
73
|
+
type SetupMode = "single" | "multi";
|
|
74
|
+
declare function buildCodegenConfig(mode: SetupMode, project: string): CodegenConfigInput;
|
|
75
|
+
|
|
76
|
+
declare function inferProjectName(dirName: string): string;
|
|
77
|
+
declare function typeNamesForProject(project: string): {
|
|
78
|
+
paramsTypeName: string;
|
|
79
|
+
schemaTypeName: string;
|
|
80
|
+
localeTypeName: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
declare function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void;
|
|
84
|
+
|
|
85
|
+
export { type CodegenConfig, type CodegenConfigInput, DELIVERY_MODES, type DeliveryArtifactsMap, type DeliveryMode, SUPPORTED_IMPORT_EXTENSIONS, type SetupMode, type SupportedImportExtension, buildCodegenConfig, codegenConfigKeys, inferProjectName, resolveDeliveryOutputDir, typeNamesForProject, writeCodegenConfig };
|