@xndrjs/i18n 0.3.3-alpha.0 → 0.4.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 +64 -42
- package/dist/index.d.ts +1 -14
- package/dist/index.js +2 -74
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +82 -4
- package/package.json +1 -6
- package/src/IcuTranslationProviderMulti.test.ts +1 -1
- package/src/IcuTranslationProviderMulti.ts +1 -1
- package/src/codegen/emit/dictionary-file.ts +2 -2
- package/src/codegen/emit/instance-file.ts +5 -10
- package/src/codegen/emit/namespace-loaders-file.ts +4 -26
- package/src/codegen/generate-i18n-types.test.ts +14 -9
- package/src/codegen/generate-i18n-types.ts +0 -11
- package/src/codegen/project-locales-set-namespace.test.ts +6 -4
- package/src/index.ts +0 -3
- package/src/setup/setup-i18n.ts +3 -2
- 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/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`.
|
|
@@ -21,14 +21,14 @@ The core idea: your ICU strings live in local JSON files that act as **type-safe
|
|
|
21
21
|
### Install
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install @xndrjs/i18n
|
|
25
|
-
|
|
26
|
-
npm install zod
|
|
24
|
+
npm install @xndrjs/i18n zod
|
|
25
|
+
npm install -D tsx
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
`tsx`
|
|
28
|
+
`tsx` and `zod` are **peer dependencies** of `@xndrjs/i18n`:
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
- **`tsx`** — required by `xndrjs-i18n-codegen` and `xndrjs-i18n-audit` (CLIs run TypeScript directly). Install as a **devDependency** — only needed at build/CI time.
|
|
31
|
+
- **`zod`** — required by codegen/audit config validation (`i18n.codegen.json`) and by `@xndrjs/i18n/validation` when using `dictionarySchemaOutput` and `validateExternalDictionary()`. Use a **devDependency** if validation runs only at build time; a regular **dependency** if you validate CMS payloads in production.
|
|
32
32
|
|
|
33
33
|
### Quick setup (single file)
|
|
34
34
|
|
|
@@ -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` (the codegen fallback dictionary).
|
|
200
|
+
- **`instance.generated.ts`** — exports `createI18n(dictionary)` (required argument; no default import of the fallback dictionary), typed `projectLocales()` (full schema), and in multi mode `projectNamespaceLocales()` (single namespace); `LOCALE_FALLBACK` wired in when configured.
|
|
199
201
|
- **`i18n.ts`** (optional, hand-written) — app-owned singleton if desired.
|
|
200
202
|
|
|
201
203
|
Example generated types (multi-namespace):
|
|
@@ -227,12 +229,13 @@ export type MyProjectSchema = {
|
|
|
227
229
|
|
|
228
230
|
### 4. Runtime provider
|
|
229
231
|
|
|
230
|
-
Create a provider with the generated factory (no side effects at import time):
|
|
232
|
+
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
233
|
|
|
232
234
|
```ts
|
|
233
235
|
import { createI18n } from "./i18n";
|
|
236
|
+
import { defaultDictionary } from "./i18n/generated/dictionary.generated.js";
|
|
234
237
|
|
|
235
|
-
const i18n = createI18n();
|
|
238
|
+
const i18n = createI18n(defaultDictionary);
|
|
236
239
|
// or with an external dictionary at init:
|
|
237
240
|
const i18n = createI18n(externalDictionary);
|
|
238
241
|
```
|
|
@@ -241,7 +244,8 @@ Optionally, wrap it in an app-owned singleton (`i18n.ts`):
|
|
|
241
244
|
|
|
242
245
|
```ts
|
|
243
246
|
import { createI18n } from "./instance.generated.js";
|
|
244
|
-
|
|
247
|
+
import { defaultDictionary } from "./dictionary.generated.js";
|
|
248
|
+
export const i18n = createI18n(defaultDictionary);
|
|
245
249
|
```
|
|
246
250
|
|
|
247
251
|
### Locale-bound provider (`forLocale`)
|
|
@@ -249,7 +253,7 @@ export const i18n = createI18n();
|
|
|
249
253
|
Bind a locale once and omit it on every `.get()`:
|
|
250
254
|
|
|
251
255
|
```ts
|
|
252
|
-
const i18n = createI18n();
|
|
256
|
+
const i18n = createI18n(defaultDictionary);
|
|
253
257
|
const i18nEn = i18n.forLocale("en");
|
|
254
258
|
|
|
255
259
|
i18nEn.get("login_button"); // single-file
|
|
@@ -312,9 +316,10 @@ import {
|
|
|
312
316
|
projectLocales,
|
|
313
317
|
projectNamespaceLocales,
|
|
314
318
|
} from "./i18n/generated/instance.generated.js";
|
|
319
|
+
import { defaultDictionary } from "./i18n/generated/dictionary.generated.js";
|
|
315
320
|
import billingDictionary from "./i18n/translations/billing.json";
|
|
316
321
|
|
|
317
|
-
const i18n = createI18n();
|
|
322
|
+
const i18n = createI18n(defaultDictionary);
|
|
318
323
|
i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, [activeLocale]));
|
|
319
324
|
i18n.setAll(projectLocales(fullDictionary, [activeLocale])); // multi: all namespaces
|
|
320
325
|
```
|
|
@@ -393,23 +398,23 @@ Specify **exactly one** of `dictionary` (single-file) or `namespaces` (multi-fil
|
|
|
393
398
|
}
|
|
394
399
|
```
|
|
395
400
|
|
|
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
|
|
401
|
+
| Field | Description |
|
|
402
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
403
|
+
| `dictionary` | Path to a single dictionary file (`.json`, `.yaml`, or `.yml`) for the flat API. Mutually exclusive with `namespaces`. |
|
|
404
|
+
| `namespaces` | Map of `namespace -> dictionary path` (`.json`, `.yaml`, or `.yml`) for the namespaced API. Mutually exclusive with `dictionary`. |
|
|
405
|
+
| `defaultNamespace` | Optional. Namespace label used internally in single-file mode (default `"default"`). Not exposed in the flat API. |
|
|
406
|
+
| `typesOutput` | Output path for the generated types. |
|
|
407
|
+
| `dictionaryOutput` | Output path for the generated dictionary manifest. |
|
|
408
|
+
| `instanceOutput` | Output path for the generated factory (`createI18n`). |
|
|
409
|
+
| `importExtension` | Optional. Relative import suffix between generated `.ts` modules: `"none"` (default, extensionless), `".ts"`, or `".js"`. |
|
|
410
|
+
| `factoryName` | Name of the exported factory function (default `createI18n`). |
|
|
411
|
+
| `paramsTypeName` / `schemaTypeName` | Names of the exported types (customizable per project). |
|
|
412
|
+
| `localeTypeName` | Name of the exported locale union type (default `MyProjectLocale`). |
|
|
413
|
+
| `localeFallback` | Optional map of `locale -> next locale | null` for runtime fallback resolution. |
|
|
414
|
+
| `localeFallbackConstName` | Name of the generated fallback constant (default `LOCALE_FALLBACK`). |
|
|
415
|
+
| `dictionarySchemaOutput` | Optional path for generated external dictionary validation (`dictionary-schema.generated.ts`). Requires `zod` in the consumer app. |
|
|
416
|
+
| `loadOnInit` | Multi mode only. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default). |
|
|
417
|
+
| `namespaceLoadersOutput` | Output path for generated `namespaceLoaders` (dynamic `import()` per lazy namespace). Defaults to `{dirname(instanceOutput)}/namespace-loaders.generated.ts`. Required when lazy namespaces exist. |
|
|
413
418
|
|
|
414
419
|
> Paths are resolved relative to the directory containing `i18n.codegen.json` (e.g. `i18n/` when using `xndrjs-i18n-setup .`).
|
|
415
420
|
|
|
@@ -453,7 +458,7 @@ Mixed namespaces are supported: some namespaces can stay `.json` while others us
|
|
|
453
458
|
|
|
454
459
|
```ts
|
|
455
460
|
import { i18n } from "./i18n"; // app singleton from i18n.ts
|
|
456
|
-
// or: import { createI18n } from './i18n'; const i18n = createI18n();
|
|
461
|
+
// or: import { createI18n, defaultDictionary } from './i18n'; const i18n = createI18n(defaultDictionary);
|
|
457
462
|
|
|
458
463
|
i18n.get("default", "login_button", "it"); // "Accedi"
|
|
459
464
|
i18n.get("default", "welcome", "en", { name: "Ada" }); // "Welcome Ada!"
|
|
@@ -469,7 +474,7 @@ i18n.get("billing", "login_button", "it"); // ✗ key not in namespace
|
|
|
469
474
|
|
|
470
475
|
```ts
|
|
471
476
|
import { i18n } from "./i18n"; // app singleton from i18n.ts
|
|
472
|
-
// or: import { createI18n } from './i18n'; const i18n = createI18n();
|
|
477
|
+
// or: import { createI18n, defaultDictionary } from './i18n'; const i18n = createI18n(defaultDictionary);
|
|
473
478
|
|
|
474
479
|
i18n.get("login_button", "it");
|
|
475
480
|
i18n.get("welcome", "en", { name: "Ada" });
|
|
@@ -487,7 +492,7 @@ i18n.setNamespace("billing", externalBillingPayload);
|
|
|
487
492
|
|
|
488
493
|
### Lazy namespace loading (multi mode)
|
|
489
494
|
|
|
490
|
-
Split namespaces across chunks by listing only the namespaces you need at startup in `loadOnInit`. Codegen emits
|
|
495
|
+
Split namespaces across chunks by listing only the namespaces you need at startup in `loadOnInit`. Codegen emits typed `namespaceLoaders` — one dynamic `import()` per lazy namespace. `.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering.
|
|
491
496
|
|
|
492
497
|
```json
|
|
493
498
|
{
|
|
@@ -496,7 +501,6 @@ Split namespaces across chunks by listing only the namespaces you need at startu
|
|
|
496
501
|
"billing": "translations/billing.yaml"
|
|
497
502
|
},
|
|
498
503
|
"loadOnInit": ["default"],
|
|
499
|
-
"dictionarySchemaOutput": "generated/dictionary-schema.generated.ts",
|
|
500
504
|
"namespaceLoadersOutput": "generated/namespace-loaders.generated.ts"
|
|
501
505
|
}
|
|
502
506
|
```
|
|
@@ -504,23 +508,41 @@ Split namespaces across chunks by listing only the namespaces you need at startu
|
|
|
504
508
|
Codegen also emits `LoadOnInitNamespace`, `LazyNamespace`, and `InitialSchema` types. The generated factory accepts a partial `InitialSchema` at init time.
|
|
505
509
|
|
|
506
510
|
```ts
|
|
507
|
-
import { i18n,
|
|
511
|
+
import { i18n, namespaceLoaders, type LazyNamespace } from "./i18n";
|
|
508
512
|
|
|
509
513
|
i18n.get("default", "login_button", "en"); // available immediately
|
|
510
514
|
|
|
511
|
-
|
|
515
|
+
if (!i18n.hasNamespace("billing")) {
|
|
516
|
+
i18n.setNamespace("billing", await namespaceLoaders.billing());
|
|
517
|
+
}
|
|
512
518
|
i18n.get("billing", "invoice_summary", "en", { count: 12 });
|
|
513
519
|
|
|
514
520
|
// batch preload
|
|
515
|
-
await
|
|
521
|
+
await Promise.all(
|
|
522
|
+
(["user", "billing"] as const satisfies readonly LazyNamespace[]).map(async (namespace) => {
|
|
523
|
+
if (i18n.hasNamespace(namespace)) return;
|
|
524
|
+
i18n.setNamespace(namespace, await namespaceLoaders[namespace]());
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Optional locale projection before register:
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
import { projectNamespaceLocales } from "./i18n/generated/instance.generated.js";
|
|
533
|
+
|
|
534
|
+
const billing = await namespaceLoaders.billing();
|
|
535
|
+
i18n.setNamespace("billing", projectNamespaceLocales(billing, [userLocale]));
|
|
516
536
|
```
|
|
517
537
|
|
|
518
538
|
Calling `.get()` on a namespace that is not loaded throws:
|
|
519
539
|
|
|
520
|
-
`[i18n] Namespace not loaded: "billing".
|
|
540
|
+
`[i18n] Namespace not loaded: "billing". Register it with setNamespace() before calling .get().`
|
|
521
541
|
|
|
522
542
|
When `loadOnInit` is omitted, behavior is unchanged: all namespaces are statically imported.
|
|
523
543
|
|
|
544
|
+
Fetch/CMS hydration is app-owned: fetch → `validateExternalNamespace()` (requires `dictionarySchemaOutput` + `zod`) → `setNamespace()`.
|
|
545
|
+
|
|
524
546
|
### External dictionary validation
|
|
525
547
|
|
|
526
548
|
When translations arrive from an external source (i.e. a CMS), validate the `unknown` input before calling `setAll()` or `setNamespace()`.
|
|
@@ -533,7 +555,7 @@ Enable validation by adding `dictionarySchemaOutput` to `i18n/i18n.codegen.json`
|
|
|
533
555
|
}
|
|
534
556
|
```
|
|
535
557
|
|
|
536
|
-
Add `zod` as a dependency in the consumer app (
|
|
558
|
+
Add `zod` as a dependency in the consumer app (required peer of `@xndrjs/i18n`).
|
|
537
559
|
|
|
538
560
|
Codegen emits `DICTIONARY_SPEC` and `validateExternalDictionary()`. Validation runs in two phases:
|
|
539
561
|
|
|
@@ -641,5 +663,5 @@ pnpm run demo:multi # tsx multi/src/index.ts
|
|
|
641
663
|
- **TypeScript 6** (`strict`, `resolveJsonModule`, `moduleResolution: "bundler"`)
|
|
642
664
|
- **[intl-messageformat](https://www.npmjs.com/package/intl-messageformat)** — runtime ICU formatting
|
|
643
665
|
- **[@formatjs/icu-messageformat-parser](https://www.npmjs.com/package/@formatjs/icu-messageformat-parser)** — build-time AST parsing
|
|
644
|
-
- **[zod](https://www.npmjs.com/package/zod)** —
|
|
666
|
+
- **[zod](https://www.npmjs.com/package/zod)** — peer dependency (config validation, external dictionary validation)
|
|
645
667
|
- **[tsx](https://www.npmjs.com/package/tsx)** — runs TypeScript scripts directly
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { V as ValidationResult } from './types-C1CpXVOJ.js';
|
|
2
|
-
import '@formatjs/icu-messageformat-parser';
|
|
3
|
-
|
|
4
1
|
type LocaleDictionary = Record<string, string>;
|
|
5
2
|
type KeyDictionary = Record<string, LocaleDictionary>;
|
|
6
3
|
type MultiDictionary = Record<string, KeyDictionary>;
|
|
@@ -122,14 +119,4 @@ declare function projectLocales<T extends KeyDictionary>(dictionary: T, locales:
|
|
|
122
119
|
*/
|
|
123
120
|
declare function projectNamespacesLocales<T extends MultiDictionary>(dictionary: T, locales: readonly string[], localeFallback?: LocaleFallbackMap$1): T;
|
|
124
121
|
|
|
125
|
-
|
|
126
|
-
provider: Pick<{
|
|
127
|
-
hasNamespace(namespace: NS): boolean;
|
|
128
|
-
setNamespace(namespace: NS, values: Schema[NS]): void;
|
|
129
|
-
}, "hasNamespace" | "setNamespace">;
|
|
130
|
-
resolveLoader: (namespace: NS) => () => Promise<unknown>;
|
|
131
|
-
validate: (namespace: NS, raw: unknown) => ValidationResult<Schema[NS]>;
|
|
132
|
-
}
|
|
133
|
-
declare function ensureNamespacesLoadedImpl<Schema extends MultiDictionary, NS extends keyof Schema & string>(options: EnsureNamespacesLoadedOptions<Schema, NS>, namespaces: NS[]): Promise<void>;
|
|
134
|
-
|
|
135
|
-
export { type EnsureNamespacesLoadedOptions, IcuTranslationProviderMulti, type IcuTranslationProviderOptions, IcuTranslationProviderSingle, type KeyDictionary, type LocaleDictionary, type LocaleFallbackMap$1 as LocaleFallbackMap, type LocaleOfMulti, type LocaleOfSingle, type MultiDictionary, type RequestLocale, type ResolvedLocaleTemplate, type TranslationProviderMulti, type TranslationProviderMultiForLocale, type TranslationProviderSingle, type TranslationProviderSingleForLocale, ensureNamespacesLoadedImpl, formatLocaleFallbackChain, projectLocales, projectNamespacesLocales, resolveLocaleTemplate, validateLocaleFallback };
|
|
122
|
+
export { IcuTranslationProviderMulti, type IcuTranslationProviderOptions, IcuTranslationProviderSingle, type KeyDictionary, type LocaleDictionary, type LocaleFallbackMap$1 as LocaleFallbackMap, type LocaleOfMulti, type LocaleOfSingle, type MultiDictionary, type RequestLocale, type ResolvedLocaleTemplate, type TranslationProviderMulti, type TranslationProviderMultiForLocale, type TranslationProviderSingle, type TranslationProviderSingleForLocale, formatLocaleFallbackChain, projectLocales, projectNamespacesLocales, resolveLocaleTemplate, validateLocaleFallback };
|
package/dist/index.js
CHANGED
|
@@ -177,7 +177,7 @@ var IcuTranslationProviderMulti = class {
|
|
|
177
177
|
get(namespace, key, locale, ...args) {
|
|
178
178
|
if (!this.loadedNamespaces.has(namespace)) {
|
|
179
179
|
throw new Error(
|
|
180
|
-
`[i18n] Namespace not loaded: "${String(namespace)}".
|
|
180
|
+
`[i18n] Namespace not loaded: "${String(namespace)}". Register it with setNamespace() before calling .get().`
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
183
|
const params = args[0];
|
|
@@ -280,78 +280,6 @@ function projectNamespacesLocales(dictionary, locales, localeFallback) {
|
|
|
280
280
|
return result;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
function issueMessage(issue) {
|
|
285
|
-
switch (issue.kind) {
|
|
286
|
-
case "missing_key":
|
|
287
|
-
return `Missing required key at ${issue.path.join(".")}`;
|
|
288
|
-
case "invalid_input":
|
|
289
|
-
case "invalid_locale_value":
|
|
290
|
-
case "icu_syntax_error":
|
|
291
|
-
case "locale_args_mismatch":
|
|
292
|
-
case "variable_mismatch":
|
|
293
|
-
case "variable_type_mismatch":
|
|
294
|
-
return issue.message;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function formatIssues(issues) {
|
|
298
|
-
return issues.map((issue) => {
|
|
299
|
-
if ("path" in issue && issue.path.length > 0) {
|
|
300
|
-
return `[${issue.path.join(".")}] ${issue.kind}: ${issueMessage(issue)}`;
|
|
301
|
-
}
|
|
302
|
-
return `${issue.kind}: ${issueMessage(issue)}`;
|
|
303
|
-
}).join("\n");
|
|
304
|
-
}
|
|
305
|
-
var DictionaryValidationError = class extends Error {
|
|
306
|
-
constructor(issues) {
|
|
307
|
-
super(formatIssues(issues));
|
|
308
|
-
this.issues = issues;
|
|
309
|
-
this.name = "DictionaryValidationError";
|
|
310
|
-
}
|
|
311
|
-
issues;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
// src/ensure-namespace.ts
|
|
315
|
-
var inFlightByProvider = /* @__PURE__ */ new WeakMap();
|
|
316
|
-
async function ensureOneNamespace(options, namespace) {
|
|
317
|
-
if (options.provider.hasNamespace(namespace)) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
const providerKey = options.provider;
|
|
321
|
-
let inFlight = inFlightByProvider.get(providerKey);
|
|
322
|
-
if (!inFlight) {
|
|
323
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
324
|
-
inFlightByProvider.set(providerKey, inFlight);
|
|
325
|
-
}
|
|
326
|
-
const existing = inFlight.get(namespace);
|
|
327
|
-
if (existing) {
|
|
328
|
-
return existing;
|
|
329
|
-
}
|
|
330
|
-
const promise = (async () => {
|
|
331
|
-
try {
|
|
332
|
-
if (options.provider.hasNamespace(namespace)) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const raw = await options.resolveLoader(namespace)();
|
|
336
|
-
const result = options.validate(namespace, raw);
|
|
337
|
-
if (!result.ok) {
|
|
338
|
-
throw new DictionaryValidationError(result.issues);
|
|
339
|
-
}
|
|
340
|
-
options.provider.setNamespace(namespace, result.data);
|
|
341
|
-
} finally {
|
|
342
|
-
inFlight.delete(namespace);
|
|
343
|
-
}
|
|
344
|
-
})();
|
|
345
|
-
inFlight.set(namespace, promise);
|
|
346
|
-
return promise;
|
|
347
|
-
}
|
|
348
|
-
async function ensureNamespacesLoadedImpl(options, namespaces) {
|
|
349
|
-
if (namespaces.length === 0) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
await Promise.all(namespaces.map((namespace) => ensureOneNamespace(options, namespace)));
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export { IcuTranslationProviderMulti, IcuTranslationProviderSingle, ensureNamespacesLoadedImpl, formatLocaleFallbackChain, projectLocales, projectNamespacesLocales, resolveLocaleTemplate, validateLocaleFallback };
|
|
283
|
+
export { IcuTranslationProviderMulti, IcuTranslationProviderSingle, formatLocaleFallbackChain, projectLocales, projectNamespacesLocales, resolveLocaleTemplate, validateLocaleFallback };
|
|
356
284
|
//# sourceMappingURL=index.js.map
|
|
357
285
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/deep-freeze.ts","../src/resolve-locale.ts","../src/IcuTranslationProviderSingle.ts","../src/IcuTranslationProviderMulti.ts","../src/project-locales.ts","../src/validation/errors.ts","../src/ensure-namespace.ts"],"names":["IntlMessageFormat"],"mappings":";;;;;AAAO,SAAS,WAAc,KAAA,EAAa;AACzC,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,OAAO,KAAA,KAAU,QAAA,EAAU;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,OAAO,KAAK,CAAA;AAEnB,EAAA,KAAA,MAAW,KAAA,IAAS,MAAA,CAAO,MAAA,CAAO,KAAK,CAAA,EAAG;AACxC,IAAA,UAAA,CAAW,KAAK,CAAA;AAAA,EAClB;AAEA,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,eAAkB,KAAA,EAAa;AAC7C,EAAA,OAAO,UAAA,CAAW,eAAA,CAAgB,KAAK,CAAC,CAAA;AAC1C;;;ACRO,SAAS,uBAAuB,GAAA,EAA8B;AACnE,EAAA,KAAA,MAAW,WAAA,IAAe,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG;AAC1C,IAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,IAAA,IAAI,OAAA,GAA8B,WAAA;AAElC,IAAA,OAAO,YAAY,MAAA,EAAW;AAC5B,MAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA,EAAG;AACxB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oDAAA,EAAuD,OAAO,CAAA,CAAA,CAAG,CAAA;AAAA,MACnF;AACA,MAAA,OAAA,CAAQ,IAAI,OAAO,CAAA;AAEnB,MAAA,MAAM,IAAA,GAAkC,IAAI,OAAO,CAAA;AACnD,MAAA,IAAI,IAAA,KAAS,MAAA,IAAa,IAAA,KAAS,IAAA,EAAM;AACvC,QAAA;AAAA,MACF;AACA,MAAA,OAAA,GAAU,IAAA;AAAA,IACZ;AAAA,EACF;AACF;AAEO,SAAS,qBAAA,CACd,WAAA,EACA,eAAA,EACA,WAAA,EACoC;AACpC,EAAA,MAAM,KAAA,GAAkB,CAAC,eAAe,CAAA;AACxC,EAAA,IAAI,aAAA,GAAgB,eAAA;AAEpB,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,MAAM,QAAA,GAAW,cAAc,aAAa,CAAA;AAC5C,IAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,MAAA,OAAO,EAAE,QAAA,EAAU,cAAA,EAAgB,aAAA,EAAe,eAAe,KAAA,EAAM;AAAA,IACzE;AAEA,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,QAAA,GAAW,YAAY,aAAa,CAAA;AAC1C,IAAA,IAAI,QAAA,KAAa,MAAA,IAAa,QAAA,KAAa,IAAA,EAAM;AAC/C,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,IAAI,KAAA,CAAM,QAAA,CAAS,QAAQ,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,0CAAA,EAA6C,CAAC,GAAG,KAAA,EAAO,QAAQ,CAAA,CAAE,IAAA,CAAK,UAAK,CAAC,CAAA;AAAA,OAC/E;AAAA,IACF;AAEA,IAAA,KAAA,CAAM,KAAK,QAAQ,CAAA;AACnB,IAAA,aAAA,GAAgB,QAAA;AAAA,EAClB;AACF;AAEO,SAAS,yBAAA,CACd,iBACA,WAAA,EACQ;AACR,EAAA,IAAI,CAAC,WAAA,EAAa;AAChB,IAAA,OAAO,eAAA;AAAA,EACT;AAEA,EAAA,MAAM,KAAA,GAAkB,CAAC,eAAe,CAAA;AACxC,EAAA,IAAI,OAAA,GAAU,eAAA;AAEd,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,MAAM,QAAA,GAAW,YAAY,OAAO,CAAA;AACpC,IAAA,IAAI,QAAA,KAAa,MAAA,IAAa,QAAA,KAAa,IAAA,EAAM;AAC/C,MAAA;AAAA,IACF;AACA,IAAA,IAAI,KAAA,CAAM,QAAA,CAAS,QAAQ,CAAA,EAAG;AAC5B,MAAA,KAAA,CAAM,KAAK,QAAQ,CAAA;AACnB,MAAA;AAAA,IACF;AACA,IAAA,KAAA,CAAM,KAAK,QAAQ,CAAA;AACnB,IAAA,OAAA,GAAU,QAAA;AAAA,EACZ;AAEA,EAAA,OAAO,KAAA,CAAM,KAAK,UAAK,CAAA;AACzB;;;AC3CO,IAAM,wCAAN,MAMiE;AAAA,EACtE,WAAA,CACmB,UAMR,MAAA,EACT;AAPiB,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAMR,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EACR;AAAA,EAPgB,QAAA;AAAA,EAMR,MAAA;AAAA,EAGX,GAAA,CACE,QACG,IAAA,EACK;AACR,IAAA,OACE,KAAK,QAAA,CAAS,GAAA,CAKd,KAAK,IAAA,CAAK,MAAA,EAAQ,GAAG,IAAI,CAAA;AAAA,EAC7B;AACF,CAAA;AAEO,IAAM,+BAAN,MAKgE;AAAA,EAC7D,UAAA;AAAA,EACA,gBAAqC,EAAC;AAAA,EAC7B,cAAA;AAAA,EAEjB,WAAA,CAAY,YAAoB,OAAA,EAAmD;AACjF,IAAA,IAAA,CAAK,UAAA,GAAa,gBAAgB,UAAU,CAAA;AAC5C,IAAA,IAAI,SAAS,cAAA,EAAgB;AAC3B,MAAA,sBAAA,CAAuB,QAAQ,cAAc,CAAA;AAC7C,MAAA,IAAA,CAAK,iBAAiB,OAAA,CAAQ,cAAA;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,GAAA,CACE,GAAA,EACA,MAAA,EAAA,GACG,IAAA,EACK;AACR,IAAA,MAAM,MAAA,GAAS,KAAK,CAAC,CAAA;AACrB,IAAA,MAAM,QAAA,GAAW,sBAAsB,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG,MAAA,EAAQ,KAAK,cAAc,CAAA;AAExF,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,KAAA,GAAQ,yBAAA,CAA0B,MAAA,EAAQ,IAAA,CAAK,cAAc,CAAA;AACnE,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,kCAAkC,MAAA,CAAO,GAAG,CAAC,CAAA,GAAA,EAAM,MAAM,sBAAsB,KAAK,CAAA,CAAA;AAAA,OACtF;AAAA,IACF;AAEA,IAAA,MAAM,EAAE,QAAA,EAAU,cAAA,EAAe,GAAI,QAAA;AAErC,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,EAAG;AACvC,MAAA,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,GAAI,EAAC;AAAA,IACxC;AAEA,IAAA,MAAM,QAAA,GAAW,OAAO,GAAG,CAAA;AAE3B,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,CAAE,QAAQ,CAAA,EAAG;AACjD,MAAA,IAAI;AACF,QAAA,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,CAAE,QAAQ,IAAI,IAAI,iBAAA;AAAA,UACjD,QAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,qDAAqD,MAAA,CAAO,GAAG,CAAC,CAAA,GAAA,EAAM,cAAc,MAAM,OAAO,CAAA;AAAA,SACnG;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,OAAO,KAAK,aAAA,CAAc,cAAc,EAAE,QAAQ,CAAA,CAAE,OAAO,MAAM,CAAA;AAAA,IACnE,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,kEAAkE,MAAA,CAAO,GAAG,CAAC,CAAA,GAAA,EAAM,cAAc,MAAM,OAAO,CAAA;AAAA,OAChH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UACE,MAAA,EACyF;AACzF,IAAA,OAAO,IAAI,qCAAA,CAAsC,IAAA,EAAM,MAAM,CAAA;AAAA,EAC/D;AAAA,EAEA,MAAA,GAAiB;AACf,IAAA,OAAO,cAAA,CAAe,KAAK,UAAU,CAAA;AAAA,EACvC;AAAA,EAEA,OAAO,MAAA,EAAsB;AAC3B,IAAA,IAAA,CAAK,UAAA,GAAa,gBAAgB,MAAM,CAAA;AACxC,IAAA,IAAA,CAAK,gBAAgB,EAAC;AAAA,EACxB;AACF;AC1GO,IAAM,uCAAN,MAMgE;AAAA,EACrE,WAAA,CACmB,UAMR,MAAA,EACT;AAPiB,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAMR,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EACR;AAAA,EAPgB,QAAA;AAAA,EAMR,MAAA;AAAA,EAGX,GAAA,CACE,SAAA,EACA,GAAA,EAAA,GACG,IAAA,EACK;AACR,IAAA,OACE,IAAA,CAAK,SAAS,GAAA,CASd,SAAA,EAAW,KAAK,IAAA,CAAK,MAAA,EAAQ,GAAG,IAAI,CAAA;AAAA,EACxC;AACF,CAAA;AAEO,IAAM,8BAAN,MAK+D;AAAA,EAC5D,UAAA;AAAA,EACA,gBAAoC,EAAC;AAAA,EAC5B,cAAA;AAAA,EACT,gBAAA;AAAA,EAER,WAAA,CAAY,YAA6B,OAAA,EAAmD;AAC1F,IAAA,IAAA,CAAK,UAAA,GAAa,gBAAgB,UAAU,CAAA;AAC5C,IAAA,IAAA,CAAK,mBAAmB,IAAI,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,UAAU,CAAC,CAAA;AACvD,IAAA,IAAI,SAAS,cAAA,EAAgB;AAC3B,MAAA,sBAAA,CAAuB,QAAQ,cAAc,CAAA;AAC7C,MAAA,IAAA,CAAK,iBAAiB,OAAA,CAAQ,cAAA;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,GAAA,CACE,SAAA,EACA,GAAA,EACA,MAAA,EAAA,GACG,IAAA,EACK;AACR,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,CAAiB,GAAA,CAAI,SAAmB,CAAA,EAAG;AACnD,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,iCAAiC,MAAA,CAAO,SAAS,CAAC,CAAA,uCAAA,EAA0C,MAAA,CAAO,SAAS,CAAC,CAAA,UAAA;AAAA,OAC/G;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,KAAK,CAAC,CAAA;AACrB,IAAA,MAAM,WAAA,GACJ,IAAA,CAAK,UAAA,CAAW,SAAmB,IACjC,GAAG,CAAA;AACP,IAAA,MAAM,QAAA,GAAW,qBAAA,CAAsB,WAAA,EAAa,MAAA,EAAQ,KAAK,cAAc,CAAA;AAE/E,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,KAAA,GAAQ,yBAAA,CAA0B,MAAA,EAAQ,IAAA,CAAK,cAAc,CAAA;AACnE,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,yCAAA,EAA4C,MAAA,CAAO,SAAS,CAAC,CAAA,QAAA,EAAW,MAAA,CAAO,GAAG,CAAC,CAAA,GAAA,EAAM,MAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAA;AAAA,OAC5H;AAAA,IACF;AAEA,IAAA,MAAM,EAAE,QAAA,EAAU,cAAA,EAAe,GAAI,QAAA;AAErC,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,EAAG;AACvC,MAAA,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,GAAI,EAAC;AAAA,IACxC;AAEA,IAAA,IAAI,CAAC,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,CAAE,SAAmB,CAAA,EAAG;AAC5D,MAAA,IAAA,CAAK,aAAA,CAAc,cAAc,CAAA,CAAE,SAAmB,IAAI,EAAC;AAAA,IAC7D;AAEA,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,aAAA,CAAc,cAAc,EAAE,SAAmB,CAAA;AAC7E,IAAA,MAAM,QAAA,GAAW,OAAO,GAAG,CAAA;AAE3B,IAAA,IAAI,CAAC,cAAA,CAAe,QAAQ,CAAA,EAAG;AAC7B,MAAA,IAAI;AACF,QAAA,cAAA,CAAe,QAAQ,CAAA,GAAI,IAAIA,iBAAAA,CAAkB,UAAU,cAAc,CAAA;AAAA,MAC3E,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,mCAAA,EAAsC,MAAA,CAAO,SAAS,CAAC,CAAA,QAAA,EAAW,MAAA,CAAO,GAAG,CAAC,CAAA,GAAA,EAAM,cAAc,CAAA,GAAA,EAAM,OAAO,CAAA;AAAA,SAChH;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,OAAO,cAAA,CAAe,QAAQ,CAAA,CAAE,MAAA,CAAO,MAAM,CAAA;AAAA,IAC/C,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,mCAAA,EAAsC,MAAA,CAAO,SAAS,CAAC,CAAA,QAAA,EAAW,MAAA,CAAO,GAAG,CAAC,CAAA,GAAA,EAAM,cAAc,CAAA,GAAA,EAAM,OAAO,CAAA;AAAA,OAChH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UACE,MAAA,EACwF;AACxF,IAAA,OAAO,IAAI,oCAAA,CAAqC,IAAA,EAAM,MAAM,CAAA;AAAA,EAC9D;AAAA,EAEA,MAAA,GAAiB;AACf,IAAA,OAAO,cAAA,CAAe,KAAK,UAAU,CAAA;AAAA,EACvC;AAAA,EAEA,aAA+C,SAAA,EAAwB;AACrE,IAAA,OAAO,IAAA,CAAK,gBAAA,CAAiB,GAAA,CAAI,SAAS,CAAA;AAAA,EAC5C;AAAA,EAEA,OAAO,MAAA,EAAsB;AAC3B,IAAA,IAAA,CAAK,UAAA,GAAa,gBAAgB,MAAM,CAAA;AACxC,IAAA,IAAA,CAAK,gBAAgB,EAAC;AACtB,IAAA,IAAA,CAAK,mBAAmB,IAAI,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,EACrD;AAAA,EAEA,YAAA,CAAsC,WAAe,MAAA,EAA0B;AAC7E,IAAA,IAAA,CAAK,UAAA,GAAa;AAAA,MAChB,GAAG,IAAA,CAAK,UAAA;AAAA,MACR,CAAC,SAAS,GAAG,eAAA,CAAgB,MAAM;AAAA,KACrC;AACA,IAAA,IAAA,CAAK,gBAAA,CAAiB,IAAI,SAAmB,CAAA;AAE7C,IAAA,KAAA,MAAW,MAAA,IAAU,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,aAAa,CAAA,EAAG;AACpD,MAAA,OAAO,IAAA,CAAK,aAAA,CAAc,MAAM,CAAA,GAAI,SAAmB,CAAA;AAAA,IACzD;AAAA,EACF;AACF;;;AC/LA,SAAS,oBAAA,CACP,UAAA,EACA,OAAA,EACA,cAAA,EACG;AACH,EAAA,MAAM,gBAAgB,CAAC,GAAG,IAAI,GAAA,CAAI,OAAO,CAAC,CAAA;AAC1C,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,YAAY,KAAK,MAAA,CAAO,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC5D,IAAA,MAAM,YAAoC,EAAC;AAE3C,IAAA,KAAA,MAAW,UAAU,aAAA,EAAe;AAClC,MAAA,MAAM,QAAA,GAAW,qBAAA,CAAsB,YAAA,EAAc,MAAA,EAAQ,cAAc,CAAA;AAC3E,MAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,QAAA,SAAA,CAAU,MAAM,IAAI,QAAA,CAAS,QAAA;AAAA,MAC/B;AAAA,IACF;AAEA,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA,CAAE,SAAS,CAAA,EAAG;AACrC,MAAA,MAAA,CAAO,GAAc,CAAA,GAAI,SAAA;AAAA,IAC3B;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAOO,SAAS,cAAA,CACd,UAAA,EACA,OAAA,EACA,cAAA,EACG;AACH,EAAA,OAAO,oBAAA,CAAqB,UAAA,EAAY,OAAA,EAAS,cAAc,CAAA;AACjE;AAKO,SAAS,wBAAA,CACd,UAAA,EACA,OAAA,EACA,cAAA,EACG;AACH,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,KAAA,MAAW,SAAA,IAAa,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,EAA2B;AACvE,IAAA,MAAM,mBAAA,GAAsB,WAAW,SAAS,CAAA;AAChD,IAAA,IAAI,wBAAwB,MAAA,EAAW;AACrC,MAAA;AAAA,IACF;AAEA,IAAA,MAAA,CAAO,SAAS,CAAA,GAAI,oBAAA;AAAA,MAClB,mBAAA;AAAA,MACA,OAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AChEA,SAAS,aAAa,KAAA,EAAgC;AACpD,EAAA,QAAQ,MAAM,IAAA;AAAM,IAClB,KAAK,aAAA;AACH,MAAA,OAAO,CAAA,wBAAA,EAA2B,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA;AAAA,IACxD,KAAK,eAAA;AAAA,IACL,KAAK,sBAAA;AAAA,IACL,KAAK,kBAAA;AAAA,IACL,KAAK,sBAAA;AAAA,IACL,KAAK,mBAAA;AAAA,IACL,KAAK,wBAAA;AACH,MAAA,OAAO,KAAA,CAAM,OAAA;AAAA;AAEnB;AAEO,SAAS,aAAa,MAAA,EAA4C;AACvE,EAAA,OAAO,MAAA,CACJ,GAAA,CAAI,CAAC,KAAA,KAAU;AACd,IAAA,IAAI,MAAA,IAAU,KAAA,IAAS,KAAA,CAAM,IAAA,CAAK,SAAS,CAAA,EAAG;AAC5C,MAAA,OAAO,CAAA,CAAA,EAAI,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAA,EAAA,EAAK,KAAA,CAAM,IAAI,CAAA,EAAA,EAAK,YAAA,CAAa,KAAK,CAAC,CAAA,CAAA;AAAA,IACxE;AACA,IAAA,OAAO,GAAG,KAAA,CAAM,IAAI,CAAA,EAAA,EAAK,YAAA,CAAa,KAAK,CAAC,CAAA,CAAA;AAAA,EAC9C,CAAC,CAAA,CACA,IAAA,CAAK,IAAI,CAAA;AACd;AAEO,IAAM,yBAAA,GAAN,cAAwC,KAAA,CAAM;AAAA,EACnD,YAA4B,MAAA,EAAoC;AAC9D,IAAA,KAAA,CAAM,YAAA,CAAa,MAAM,CAAC,CAAA;AADA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAE1B,IAAA,IAAA,CAAK,IAAA,GAAO,2BAAA;AAAA,EACd;AAAA,EAH4B,MAAA;AAI9B,CAAA;;;AChBA,IAAM,kBAAA,uBAAyB,OAAA,EAA4C;AAE3E,eAAe,kBAAA,CACb,SACA,SAAA,EACe;AACf,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,YAAA,CAAa,SAAS,CAAA,EAAG;AAC5C,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,cAAc,OAAA,CAAQ,QAAA;AAC5B,EAAA,IAAI,QAAA,GAAW,kBAAA,CAAmB,GAAA,CAAI,WAAW,CAAA;AACjD,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,QAAA,uBAAe,GAAA,EAAI;AACnB,IAAA,kBAAA,CAAmB,GAAA,CAAI,aAAa,QAAQ,CAAA;AAAA,EAC9C;AAEA,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,SAAS,CAAA;AACvC,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,WAAW,YAAY;AAC3B,IAAA,IAAI;AACF,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,YAAA,CAAa,SAAS,CAAA,EAAG;AAC5C,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,aAAA,CAAc,SAAS,CAAA,EAAE;AACnD,MAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,QAAA,CAAS,SAAA,EAAW,GAAG,CAAA;AAC9C,MAAA,IAAI,CAAC,OAAO,EAAA,EAAI;AACd,QAAA,MAAM,IAAI,yBAAA,CAA0B,MAAA,CAAO,MAAM,CAAA;AAAA,MACnD;AAEA,MAAA,OAAA,CAAQ,QAAA,CAAS,YAAA,CAAa,SAAA,EAAW,MAAA,CAAO,IAAI,CAAA;AAAA,IACtD,CAAA,SAAE;AACA,MAAA,QAAA,CAAU,OAAO,SAAS,CAAA;AAAA,IAC5B;AAAA,EACF,CAAA,GAAG;AAEH,EAAA,QAAA,CAAS,GAAA,CAAI,WAAW,OAAO,CAAA;AAC/B,EAAA,OAAO,OAAA;AACT;AAEA,eAAsB,0BAAA,CAGpB,SAAoD,UAAA,EAAiC;AACrF,EAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAC3B,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,CAAQ,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,CAAC,cAAc,kBAAA,CAAmB,OAAA,EAAS,SAAS,CAAC,CAAC,CAAA;AACzF","file":"index.js","sourcesContent":["export function deepFreeze<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n Object.freeze(value);\n\n for (const child of Object.values(value)) {\n deepFreeze(child);\n }\n\n return value;\n}\n\nexport function cloneAndFreeze<T>(value: T): T {\n return deepFreeze(structuredClone(value));\n}\n","export type LocaleFallbackMap = Record<string, string | null>;\n\nexport interface ResolvedLocaleTemplate {\n template: string;\n resolvedLocale: string;\n fallbackChain: readonly string[];\n}\n\nexport function validateLocaleFallback(map: LocaleFallbackMap): void {\n for (const startLocale of Object.keys(map)) {\n const visited = new Set<string>();\n let current: string | undefined = startLocale;\n\n while (current !== undefined) {\n if (visited.has(current)) {\n throw new Error(`[i18n] Circular locale fallback detected involving \"${current}\"`);\n }\n visited.add(current);\n\n const next: string | null | undefined = map[current];\n if (next === undefined || next === null) {\n break;\n }\n current = next;\n }\n }\n}\n\nexport function resolveLocaleTemplate(\n localeByKey: Record<string, string | undefined> | undefined,\n requestedLocale: string,\n fallbackMap?: LocaleFallbackMap\n): ResolvedLocaleTemplate | undefined {\n const chain: string[] = [requestedLocale];\n let currentLocale = requestedLocale;\n\n while (true) {\n const template = localeByKey?.[currentLocale];\n if (template !== undefined) {\n return { template, resolvedLocale: currentLocale, fallbackChain: chain };\n }\n\n if (!fallbackMap) {\n return undefined;\n }\n\n const fallback = fallbackMap[currentLocale];\n if (fallback === undefined || fallback === null) {\n return undefined;\n }\n\n if (chain.includes(fallback)) {\n throw new Error(\n `[i18n] Circular locale fallback detected: ${[...chain, fallback].join(\" → \")}`\n );\n }\n\n chain.push(fallback);\n currentLocale = fallback;\n }\n}\n\nexport function formatLocaleFallbackChain(\n requestedLocale: string,\n fallbackMap?: LocaleFallbackMap\n): string {\n if (!fallbackMap) {\n return requestedLocale;\n }\n\n const chain: string[] = [requestedLocale];\n let current = requestedLocale;\n\n while (true) {\n const fallback = fallbackMap[current];\n if (fallback === undefined || fallback === null) {\n break;\n }\n if (chain.includes(fallback)) {\n chain.push(fallback);\n break;\n }\n chain.push(fallback);\n current = fallback;\n }\n\n return chain.join(\" → \");\n}\n","import { IntlMessageFormat } from \"intl-messageformat\";\nimport { cloneAndFreeze } from \"./deep-freeze.js\";\nimport {\n formatLocaleFallbackChain,\n resolveLocaleTemplate,\n validateLocaleFallback,\n} from \"./resolve-locale.js\";\nimport type {\n IcuTranslationProviderOptions,\n KeyDictionary,\n LocaleFallbackMap,\n LocaleOfSingle,\n SingleCompiledCache,\n} from \"./types.js\";\n\nexport interface TranslationProviderSingleForLocale<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n Locale extends string,\n> {\n readonly locale: Locale;\n get<K extends keyof Schema & string>(\n key: K,\n ...params: Params[K] extends never ? [] : [params: Params[K]]\n ): string;\n}\n\nexport interface TranslationProviderSingle<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n RequestLocales extends string = LocaleOfSingle<Schema>,\n> {\n get<K extends keyof Schema & string>(\n key: K,\n locale: RequestLocales,\n ...params: Params[K] extends never ? [] : [params: Params[K]]\n ): string;\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): TranslationProviderSingleForLocale<Schema, Params, Locale>;\n getAll(): Schema;\n setAll(values: Schema): void;\n}\n\nexport class IcuTranslationProviderSingleForLocale<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n RequestLocales extends string,\n Locale extends RequestLocales,\n Fallback extends LocaleFallbackMap | undefined,\n> implements TranslationProviderSingleForLocale<Schema, Params, Locale> {\n constructor(\n private readonly provider: IcuTranslationProviderSingle<\n Schema,\n Params,\n RequestLocales,\n Fallback\n >,\n readonly locale: Locale\n ) {}\n\n get<K extends keyof Schema & string>(\n key: K,\n ...args: Params[K] extends never ? [] : [params: Params[K]]\n ): string {\n return (\n this.provider.get as (\n key: K,\n locale: Locale,\n ...params: Params[K] extends never ? [] : [params: Params[K]]\n ) => string\n )(key, this.locale, ...args);\n }\n}\n\nexport class IcuTranslationProviderSingle<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n RequestLocales extends string = LocaleOfSingle<Schema>,\n Fallback extends LocaleFallbackMap | undefined = undefined,\n> implements TranslationProviderSingle<Schema, Params, RequestLocales> {\n private dictionary: Schema;\n private compiledCache: SingleCompiledCache = {};\n private readonly localeFallback?: Fallback;\n\n constructor(dictionary: Schema, options?: IcuTranslationProviderOptions<Fallback>) {\n this.dictionary = structuredClone(dictionary);\n if (options?.localeFallback) {\n validateLocaleFallback(options.localeFallback);\n this.localeFallback = options.localeFallback;\n }\n }\n\n get<K extends keyof Schema & string>(\n key: K,\n locale: RequestLocales,\n ...args: Params[K] extends never ? [] : [params: Params[K]]\n ): string {\n const params = args[0] as Record<string, unknown> | undefined;\n const resolved = resolveLocaleTemplate(this.dictionary[key], locale, this.localeFallback);\n\n if (!resolved) {\n const chain = formatLocaleFallbackChain(locale, this.localeFallback);\n throw new Error(\n `[i18n] Missing key or locale: \"${String(key)}\" [${locale}] (fallback chain: ${chain})`\n );\n }\n\n const { template, resolvedLocale } = resolved;\n\n if (!this.compiledCache[resolvedLocale]) {\n this.compiledCache[resolvedLocale] = {};\n }\n\n const cacheKey = String(key);\n\n if (!this.compiledCache[resolvedLocale][cacheKey]) {\n try {\n this.compiledCache[resolvedLocale][cacheKey] = new IntlMessageFormat(\n template,\n resolvedLocale\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n ICU Syntax Error] Dictionary error for key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n try {\n return this.compiledCache[resolvedLocale][cacheKey].format(params) as string;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n Formatting Error] Invalid or missing parameters for key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): IcuTranslationProviderSingleForLocale<Schema, Params, RequestLocales, Locale, Fallback> {\n return new IcuTranslationProviderSingleForLocale(this, locale);\n }\n\n getAll(): Schema {\n return cloneAndFreeze(this.dictionary);\n }\n\n setAll(values: Schema): void {\n this.dictionary = structuredClone(values);\n this.compiledCache = {};\n }\n}\n","import { IntlMessageFormat } from \"intl-messageformat\";\nimport { cloneAndFreeze } from \"./deep-freeze.js\";\nimport {\n formatLocaleFallbackChain,\n resolveLocaleTemplate,\n validateLocaleFallback,\n} from \"./resolve-locale.js\";\nimport type {\n IcuTranslationProviderOptions,\n LocaleFallbackMap,\n LocaleOfMulti,\n MultiCompiledCache,\n MultiDictionary,\n} from \"./types.js\";\n\nexport interface TranslationProviderMultiForLocale<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n Locale extends string,\n> {\n readonly locale: Locale;\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n ...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string;\n}\n\nexport interface TranslationProviderMulti<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n RequestLocales extends string = LocaleOfMulti<Schema>,\n> {\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n locale: RequestLocales,\n ...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string;\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): TranslationProviderMultiForLocale<Schema, Params, Locale>;\n getAll(): Schema;\n hasNamespace<NS extends keyof Schema & string>(namespace: NS): boolean;\n setAll(values: Schema): void;\n setNamespace<NS extends keyof Schema>(namespace: NS, values: Schema[NS]): void;\n}\n\nexport class IcuTranslationProviderMultiForLocale<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n RequestLocales extends string,\n Locale extends RequestLocales,\n Fallback extends LocaleFallbackMap | undefined,\n> implements TranslationProviderMultiForLocale<Schema, Params, Locale> {\n constructor(\n private readonly provider: IcuTranslationProviderMulti<\n Schema,\n Params,\n RequestLocales,\n Fallback\n >,\n readonly locale: Locale\n ) {}\n\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n ...args: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string {\n return (\n this.provider.get as <\n NS extends keyof Schema,\n K extends keyof Schema[NS] & keyof Params[NS] & string,\n >(\n namespace: NS,\n key: K,\n locale: Locale,\n ...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ) => string\n )(namespace, key, this.locale, ...args);\n }\n}\n\nexport class IcuTranslationProviderMulti<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n RequestLocales extends string = LocaleOfMulti<Schema>,\n Fallback extends LocaleFallbackMap | undefined = undefined,\n> implements TranslationProviderMulti<Schema, Params, RequestLocales> {\n private dictionary: Schema;\n private compiledCache: MultiCompiledCache = {};\n private readonly localeFallback?: Fallback;\n private loadedNamespaces: Set<string>;\n\n constructor(dictionary: Partial<Schema>, options?: IcuTranslationProviderOptions<Fallback>) {\n this.dictionary = structuredClone(dictionary) as Schema;\n this.loadedNamespaces = new Set(Object.keys(dictionary));\n if (options?.localeFallback) {\n validateLocaleFallback(options.localeFallback);\n this.localeFallback = options.localeFallback;\n }\n }\n\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n locale: RequestLocales,\n ...args: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string {\n if (!this.loadedNamespaces.has(namespace as string)) {\n throw new Error(\n `[i18n] Namespace not loaded: \"${String(namespace)}\". Call ensureNamespacesLoaded(i18n, [\"${String(namespace)}\"]) first.`\n );\n }\n\n const params = args[0] as Record<string, unknown> | undefined;\n const localeByKey = (\n this.dictionary[namespace as string] as Record<string, Record<string, string>>\n )?.[key];\n const resolved = resolveLocaleTemplate(localeByKey, locale, this.localeFallback);\n\n if (!resolved) {\n const chain = formatLocaleFallbackChain(locale, this.localeFallback);\n throw new Error(\n `[i18n] Missing key or locale: namespace \"${String(namespace)}\", key \"${String(key)}\" [${locale}] (fallback chain: ${chain})`\n );\n }\n\n const { template, resolvedLocale } = resolved;\n\n if (!this.compiledCache[resolvedLocale]) {\n this.compiledCache[resolvedLocale] = {};\n }\n\n if (!this.compiledCache[resolvedLocale][namespace as string]) {\n this.compiledCache[resolvedLocale][namespace as string] = {};\n }\n\n const namespaceCache = this.compiledCache[resolvedLocale][namespace as string]!;\n const cacheKey = String(key);\n\n if (!namespaceCache[cacheKey]) {\n try {\n namespaceCache[cacheKey] = new IntlMessageFormat(template, resolvedLocale);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n ICU Syntax Error] namespace \"${String(namespace)}\", key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n try {\n return namespaceCache[cacheKey].format(params) as string;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n Formatting Error] namespace \"${String(namespace)}\", key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): IcuTranslationProviderMultiForLocale<Schema, Params, RequestLocales, Locale, Fallback> {\n return new IcuTranslationProviderMultiForLocale(this, locale);\n }\n\n getAll(): Schema {\n return cloneAndFreeze(this.dictionary);\n }\n\n hasNamespace<NS extends keyof Schema & string>(namespace: NS): boolean {\n return this.loadedNamespaces.has(namespace);\n }\n\n setAll(values: Schema): void {\n this.dictionary = structuredClone(values);\n this.compiledCache = {};\n this.loadedNamespaces = new Set(Object.keys(values));\n }\n\n setNamespace<NS extends keyof Schema>(namespace: NS, values: Schema[NS]): void {\n this.dictionary = {\n ...this.dictionary,\n [namespace]: structuredClone(values),\n };\n this.loadedNamespaces.add(namespace as string);\n\n for (const locale of Object.keys(this.compiledCache)) {\n delete this.compiledCache[locale]?.[namespace as string];\n }\n }\n}\n","import { resolveLocaleTemplate } from \"./resolve-locale.js\";\nimport type { KeyDictionary, LocaleFallbackMap, MultiDictionary } from \"./types.js\";\n\nfunction projectKeyDictionary<T extends KeyDictionary>(\n dictionary: T,\n locales: readonly string[],\n localeFallback?: LocaleFallbackMap\n): T {\n const targetLocales = [...new Set(locales)];\n const result = {} as T;\n\n for (const [key, localesByKey] of Object.entries(dictionary)) {\n const projected: Record<string, string> = {};\n\n for (const locale of targetLocales) {\n const resolved = resolveLocaleTemplate(localesByKey, locale, localeFallback);\n if (resolved !== undefined) {\n projected[locale] = resolved.template;\n }\n }\n\n if (Object.keys(projected).length > 0) {\n result[key as keyof T] = projected as T[keyof T];\n }\n }\n\n return result;\n}\n\n/**\n * Builds a dictionary that contains only the requested locales.\n * For each key and target locale, copies the direct template when present;\n * otherwise resolves through `localeFallback` (same rules as `.get()` at runtime).\n */\nexport function projectLocales<T extends KeyDictionary>(\n dictionary: T,\n locales: readonly string[],\n localeFallback?: LocaleFallbackMap\n): T {\n return projectKeyDictionary(dictionary, locales, localeFallback);\n}\n\n/**\n * Projects every namespace in a multi-namespace dictionary.\n */\nexport function projectNamespacesLocales<T extends MultiDictionary>(\n dictionary: T,\n locales: readonly string[],\n localeFallback?: LocaleFallbackMap\n): T {\n const result = {} as T;\n\n for (const namespace of Object.keys(dictionary) as (keyof T & string)[]) {\n const namespaceDictionary = dictionary[namespace];\n if (namespaceDictionary === undefined) {\n continue;\n }\n\n result[namespace] = projectKeyDictionary(\n namespaceDictionary,\n locales,\n localeFallback\n ) as T[typeof namespace];\n }\n\n return result;\n}\n","import type { ValidationIssue, ValidationResult } from \"./types.js\";\n\nfunction issueMessage(issue: ValidationIssue): string {\n switch (issue.kind) {\n case \"missing_key\":\n return `Missing required key at ${issue.path.join(\".\")}`;\n case \"invalid_input\":\n case \"invalid_locale_value\":\n case \"icu_syntax_error\":\n case \"locale_args_mismatch\":\n case \"variable_mismatch\":\n case \"variable_type_mismatch\":\n return issue.message;\n }\n}\n\nexport function formatIssues(issues: readonly ValidationIssue[]): string {\n return issues\n .map((issue) => {\n if (\"path\" in issue && issue.path.length > 0) {\n return `[${issue.path.join(\".\")}] ${issue.kind}: ${issueMessage(issue)}`;\n }\n return `${issue.kind}: ${issueMessage(issue)}`;\n })\n .join(\"\\n\");\n}\n\nexport class DictionaryValidationError extends Error {\n constructor(public readonly issues: readonly ValidationIssue[]) {\n super(formatIssues(issues));\n this.name = \"DictionaryValidationError\";\n }\n}\n\nexport function assertValidDictionary<T>(\n result: ValidationResult<T>\n): asserts result is { ok: true; data: T } {\n if (!result.ok) {\n throw new DictionaryValidationError(result.issues);\n }\n}\n","import type { MultiDictionary } from \"./types.js\";\nimport type { ValidationResult } from \"./validation/types.js\";\nimport { DictionaryValidationError } from \"./validation/errors.js\";\n\nexport interface EnsureNamespacesLoadedOptions<\n Schema extends MultiDictionary,\n NS extends keyof Schema & string,\n> {\n provider: Pick<\n { hasNamespace(namespace: NS): boolean; setNamespace(namespace: NS, values: Schema[NS]): void },\n \"hasNamespace\" | \"setNamespace\"\n >;\n resolveLoader: (namespace: NS) => () => Promise<unknown>;\n validate: (namespace: NS, raw: unknown) => ValidationResult<Schema[NS]>;\n}\n\nconst inFlightByProvider = new WeakMap<object, Map<string, Promise<void>>>();\n\nasync function ensureOneNamespace<Schema extends MultiDictionary, NS extends keyof Schema & string>(\n options: EnsureNamespacesLoadedOptions<Schema, NS>,\n namespace: NS\n): Promise<void> {\n if (options.provider.hasNamespace(namespace)) {\n return;\n }\n\n const providerKey = options.provider as object;\n let inFlight = inFlightByProvider.get(providerKey);\n if (!inFlight) {\n inFlight = new Map();\n inFlightByProvider.set(providerKey, inFlight);\n }\n\n const existing = inFlight.get(namespace);\n if (existing) {\n return existing;\n }\n\n const promise = (async () => {\n try {\n if (options.provider.hasNamespace(namespace)) {\n return;\n }\n\n const raw = await options.resolveLoader(namespace)();\n const result = options.validate(namespace, raw);\n if (!result.ok) {\n throw new DictionaryValidationError(result.issues);\n }\n\n options.provider.setNamespace(namespace, result.data);\n } finally {\n inFlight!.delete(namespace);\n }\n })();\n\n inFlight.set(namespace, promise);\n return promise;\n}\n\nexport async function ensureNamespacesLoadedImpl<\n Schema extends MultiDictionary,\n NS extends keyof Schema & string,\n>(options: EnsureNamespacesLoadedOptions<Schema, NS>, namespaces: NS[]): Promise<void> {\n if (namespaces.length === 0) {\n return;\n }\n\n await Promise.all(namespaces.map((namespace) => ensureOneNamespace(options, namespace)));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/deep-freeze.ts","../src/resolve-locale.ts","../src/IcuTranslationProviderSingle.ts","../src/IcuTranslationProviderMulti.ts","../src/project-locales.ts"],"names":["IntlMessageFormat"],"mappings":"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","file":"index.js","sourcesContent":["export function deepFreeze<T>(value: T): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n Object.freeze(value);\n\n for (const child of Object.values(value)) {\n deepFreeze(child);\n }\n\n return value;\n}\n\nexport function cloneAndFreeze<T>(value: T): T {\n return deepFreeze(structuredClone(value));\n}\n","export type LocaleFallbackMap = Record<string, string | null>;\n\nexport interface ResolvedLocaleTemplate {\n template: string;\n resolvedLocale: string;\n fallbackChain: readonly string[];\n}\n\nexport function validateLocaleFallback(map: LocaleFallbackMap): void {\n for (const startLocale of Object.keys(map)) {\n const visited = new Set<string>();\n let current: string | undefined = startLocale;\n\n while (current !== undefined) {\n if (visited.has(current)) {\n throw new Error(`[i18n] Circular locale fallback detected involving \"${current}\"`);\n }\n visited.add(current);\n\n const next: string | null | undefined = map[current];\n if (next === undefined || next === null) {\n break;\n }\n current = next;\n }\n }\n}\n\nexport function resolveLocaleTemplate(\n localeByKey: Record<string, string | undefined> | undefined,\n requestedLocale: string,\n fallbackMap?: LocaleFallbackMap\n): ResolvedLocaleTemplate | undefined {\n const chain: string[] = [requestedLocale];\n let currentLocale = requestedLocale;\n\n while (true) {\n const template = localeByKey?.[currentLocale];\n if (template !== undefined) {\n return { template, resolvedLocale: currentLocale, fallbackChain: chain };\n }\n\n if (!fallbackMap) {\n return undefined;\n }\n\n const fallback = fallbackMap[currentLocale];\n if (fallback === undefined || fallback === null) {\n return undefined;\n }\n\n if (chain.includes(fallback)) {\n throw new Error(\n `[i18n] Circular locale fallback detected: ${[...chain, fallback].join(\" → \")}`\n );\n }\n\n chain.push(fallback);\n currentLocale = fallback;\n }\n}\n\nexport function formatLocaleFallbackChain(\n requestedLocale: string,\n fallbackMap?: LocaleFallbackMap\n): string {\n if (!fallbackMap) {\n return requestedLocale;\n }\n\n const chain: string[] = [requestedLocale];\n let current = requestedLocale;\n\n while (true) {\n const fallback = fallbackMap[current];\n if (fallback === undefined || fallback === null) {\n break;\n }\n if (chain.includes(fallback)) {\n chain.push(fallback);\n break;\n }\n chain.push(fallback);\n current = fallback;\n }\n\n return chain.join(\" → \");\n}\n","import { IntlMessageFormat } from \"intl-messageformat\";\nimport { cloneAndFreeze } from \"./deep-freeze.js\";\nimport {\n formatLocaleFallbackChain,\n resolveLocaleTemplate,\n validateLocaleFallback,\n} from \"./resolve-locale.js\";\nimport type {\n IcuTranslationProviderOptions,\n KeyDictionary,\n LocaleFallbackMap,\n LocaleOfSingle,\n SingleCompiledCache,\n} from \"./types.js\";\n\nexport interface TranslationProviderSingleForLocale<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n Locale extends string,\n> {\n readonly locale: Locale;\n get<K extends keyof Schema & string>(\n key: K,\n ...params: Params[K] extends never ? [] : [params: Params[K]]\n ): string;\n}\n\nexport interface TranslationProviderSingle<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n RequestLocales extends string = LocaleOfSingle<Schema>,\n> {\n get<K extends keyof Schema & string>(\n key: K,\n locale: RequestLocales,\n ...params: Params[K] extends never ? [] : [params: Params[K]]\n ): string;\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): TranslationProviderSingleForLocale<Schema, Params, Locale>;\n getAll(): Schema;\n setAll(values: Schema): void;\n}\n\nexport class IcuTranslationProviderSingleForLocale<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n RequestLocales extends string,\n Locale extends RequestLocales,\n Fallback extends LocaleFallbackMap | undefined,\n> implements TranslationProviderSingleForLocale<Schema, Params, Locale> {\n constructor(\n private readonly provider: IcuTranslationProviderSingle<\n Schema,\n Params,\n RequestLocales,\n Fallback\n >,\n readonly locale: Locale\n ) {}\n\n get<K extends keyof Schema & string>(\n key: K,\n ...args: Params[K] extends never ? [] : [params: Params[K]]\n ): string {\n return (\n this.provider.get as (\n key: K,\n locale: Locale,\n ...params: Params[K] extends never ? [] : [params: Params[K]]\n ) => string\n )(key, this.locale, ...args);\n }\n}\n\nexport class IcuTranslationProviderSingle<\n Schema extends KeyDictionary,\n Params extends { [K in keyof Schema]: unknown },\n RequestLocales extends string = LocaleOfSingle<Schema>,\n Fallback extends LocaleFallbackMap | undefined = undefined,\n> implements TranslationProviderSingle<Schema, Params, RequestLocales> {\n private dictionary: Schema;\n private compiledCache: SingleCompiledCache = {};\n private readonly localeFallback?: Fallback;\n\n constructor(dictionary: Schema, options?: IcuTranslationProviderOptions<Fallback>) {\n this.dictionary = structuredClone(dictionary);\n if (options?.localeFallback) {\n validateLocaleFallback(options.localeFallback);\n this.localeFallback = options.localeFallback;\n }\n }\n\n get<K extends keyof Schema & string>(\n key: K,\n locale: RequestLocales,\n ...args: Params[K] extends never ? [] : [params: Params[K]]\n ): string {\n const params = args[0] as Record<string, unknown> | undefined;\n const resolved = resolveLocaleTemplate(this.dictionary[key], locale, this.localeFallback);\n\n if (!resolved) {\n const chain = formatLocaleFallbackChain(locale, this.localeFallback);\n throw new Error(\n `[i18n] Missing key or locale: \"${String(key)}\" [${locale}] (fallback chain: ${chain})`\n );\n }\n\n const { template, resolvedLocale } = resolved;\n\n if (!this.compiledCache[resolvedLocale]) {\n this.compiledCache[resolvedLocale] = {};\n }\n\n const cacheKey = String(key);\n\n if (!this.compiledCache[resolvedLocale][cacheKey]) {\n try {\n this.compiledCache[resolvedLocale][cacheKey] = new IntlMessageFormat(\n template,\n resolvedLocale\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n ICU Syntax Error] Dictionary error for key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n try {\n return this.compiledCache[resolvedLocale][cacheKey].format(params) as string;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n Formatting Error] Invalid or missing parameters for key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): IcuTranslationProviderSingleForLocale<Schema, Params, RequestLocales, Locale, Fallback> {\n return new IcuTranslationProviderSingleForLocale(this, locale);\n }\n\n getAll(): Schema {\n return cloneAndFreeze(this.dictionary);\n }\n\n setAll(values: Schema): void {\n this.dictionary = structuredClone(values);\n this.compiledCache = {};\n }\n}\n","import { IntlMessageFormat } from \"intl-messageformat\";\nimport { cloneAndFreeze } from \"./deep-freeze.js\";\nimport {\n formatLocaleFallbackChain,\n resolveLocaleTemplate,\n validateLocaleFallback,\n} from \"./resolve-locale.js\";\nimport type {\n IcuTranslationProviderOptions,\n LocaleFallbackMap,\n LocaleOfMulti,\n MultiCompiledCache,\n MultiDictionary,\n} from \"./types.js\";\n\nexport interface TranslationProviderMultiForLocale<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n Locale extends string,\n> {\n readonly locale: Locale;\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n ...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string;\n}\n\nexport interface TranslationProviderMulti<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n RequestLocales extends string = LocaleOfMulti<Schema>,\n> {\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n locale: RequestLocales,\n ...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string;\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): TranslationProviderMultiForLocale<Schema, Params, Locale>;\n getAll(): Schema;\n hasNamespace<NS extends keyof Schema & string>(namespace: NS): boolean;\n setAll(values: Schema): void;\n setNamespace<NS extends keyof Schema>(namespace: NS, values: Schema[NS]): void;\n}\n\nexport class IcuTranslationProviderMultiForLocale<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n RequestLocales extends string,\n Locale extends RequestLocales,\n Fallback extends LocaleFallbackMap | undefined,\n> implements TranslationProviderMultiForLocale<Schema, Params, Locale> {\n constructor(\n private readonly provider: IcuTranslationProviderMulti<\n Schema,\n Params,\n RequestLocales,\n Fallback\n >,\n readonly locale: Locale\n ) {}\n\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n ...args: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string {\n return (\n this.provider.get as <\n NS extends keyof Schema,\n K extends keyof Schema[NS] & keyof Params[NS] & string,\n >(\n namespace: NS,\n key: K,\n locale: Locale,\n ...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ) => string\n )(namespace, key, this.locale, ...args);\n }\n}\n\nexport class IcuTranslationProviderMulti<\n Schema extends MultiDictionary,\n Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },\n RequestLocales extends string = LocaleOfMulti<Schema>,\n Fallback extends LocaleFallbackMap | undefined = undefined,\n> implements TranslationProviderMulti<Schema, Params, RequestLocales> {\n private dictionary: Schema;\n private compiledCache: MultiCompiledCache = {};\n private readonly localeFallback?: Fallback;\n private loadedNamespaces: Set<string>;\n\n constructor(dictionary: Partial<Schema>, options?: IcuTranslationProviderOptions<Fallback>) {\n this.dictionary = structuredClone(dictionary) as Schema;\n this.loadedNamespaces = new Set(Object.keys(dictionary));\n if (options?.localeFallback) {\n validateLocaleFallback(options.localeFallback);\n this.localeFallback = options.localeFallback;\n }\n }\n\n get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(\n namespace: NS,\n key: K,\n locale: RequestLocales,\n ...args: Params[NS][K] extends never ? [] : [params: Params[NS][K]]\n ): string {\n if (!this.loadedNamespaces.has(namespace as string)) {\n throw new Error(\n `[i18n] Namespace not loaded: \"${String(namespace)}\". Register it with setNamespace() before calling .get().`\n );\n }\n\n const params = args[0] as Record<string, unknown> | undefined;\n const localeByKey = (\n this.dictionary[namespace as string] as Record<string, Record<string, string>>\n )?.[key];\n const resolved = resolveLocaleTemplate(localeByKey, locale, this.localeFallback);\n\n if (!resolved) {\n const chain = formatLocaleFallbackChain(locale, this.localeFallback);\n throw new Error(\n `[i18n] Missing key or locale: namespace \"${String(namespace)}\", key \"${String(key)}\" [${locale}] (fallback chain: ${chain})`\n );\n }\n\n const { template, resolvedLocale } = resolved;\n\n if (!this.compiledCache[resolvedLocale]) {\n this.compiledCache[resolvedLocale] = {};\n }\n\n if (!this.compiledCache[resolvedLocale][namespace as string]) {\n this.compiledCache[resolvedLocale][namespace as string] = {};\n }\n\n const namespaceCache = this.compiledCache[resolvedLocale][namespace as string]!;\n const cacheKey = String(key);\n\n if (!namespaceCache[cacheKey]) {\n try {\n namespaceCache[cacheKey] = new IntlMessageFormat(template, resolvedLocale);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n ICU Syntax Error] namespace \"${String(namespace)}\", key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n try {\n return namespaceCache[cacheKey].format(params) as string;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[i18n Formatting Error] namespace \"${String(namespace)}\", key \"${String(key)}\" [${resolvedLocale}]: ${message}`\n );\n }\n }\n\n forLocale<Locale extends RequestLocales>(\n locale: Locale\n ): IcuTranslationProviderMultiForLocale<Schema, Params, RequestLocales, Locale, Fallback> {\n return new IcuTranslationProviderMultiForLocale(this, locale);\n }\n\n getAll(): Schema {\n return cloneAndFreeze(this.dictionary);\n }\n\n hasNamespace<NS extends keyof Schema & string>(namespace: NS): boolean {\n return this.loadedNamespaces.has(namespace);\n }\n\n setAll(values: Schema): void {\n this.dictionary = structuredClone(values);\n this.compiledCache = {};\n this.loadedNamespaces = new Set(Object.keys(values));\n }\n\n setNamespace<NS extends keyof Schema>(namespace: NS, values: Schema[NS]): void {\n this.dictionary = {\n ...this.dictionary,\n [namespace]: structuredClone(values),\n };\n this.loadedNamespaces.add(namespace as string);\n\n for (const locale of Object.keys(this.compiledCache)) {\n delete this.compiledCache[locale]?.[namespace as string];\n }\n }\n}\n","import { resolveLocaleTemplate } from \"./resolve-locale.js\";\nimport type { KeyDictionary, LocaleFallbackMap, MultiDictionary } from \"./types.js\";\n\nfunction projectKeyDictionary<T extends KeyDictionary>(\n dictionary: T,\n locales: readonly string[],\n localeFallback?: LocaleFallbackMap\n): T {\n const targetLocales = [...new Set(locales)];\n const result = {} as T;\n\n for (const [key, localesByKey] of Object.entries(dictionary)) {\n const projected: Record<string, string> = {};\n\n for (const locale of targetLocales) {\n const resolved = resolveLocaleTemplate(localesByKey, locale, localeFallback);\n if (resolved !== undefined) {\n projected[locale] = resolved.template;\n }\n }\n\n if (Object.keys(projected).length > 0) {\n result[key as keyof T] = projected as T[keyof T];\n }\n }\n\n return result;\n}\n\n/**\n * Builds a dictionary that contains only the requested locales.\n * For each key and target locale, copies the direct template when present;\n * otherwise resolves through `localeFallback` (same rules as `.get()` at runtime).\n */\nexport function projectLocales<T extends KeyDictionary>(\n dictionary: T,\n locales: readonly string[],\n localeFallback?: LocaleFallbackMap\n): T {\n return projectKeyDictionary(dictionary, locales, localeFallback);\n}\n\n/**\n * Projects every namespace in a multi-namespace dictionary.\n */\nexport function projectNamespacesLocales<T extends MultiDictionary>(\n dictionary: T,\n locales: readonly string[],\n localeFallback?: LocaleFallbackMap\n): T {\n const result = {} as T;\n\n for (const namespace of Object.keys(dictionary) as (keyof T & string)[]) {\n const namespaceDictionary = dictionary[namespace];\n if (namespaceDictionary === undefined) {\n continue;\n }\n\n result[namespace] = projectKeyDictionary(\n namespaceDictionary,\n locales,\n localeFallback\n ) as T[typeof namespace];\n }\n\n return result;\n}\n"]}
|
|
@@ -1,7 +1,85 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { d as NormalizedKeyDictionary, e as NormalizedMultiDictionary, P as ParsedKeyEntry, f as ParsedLocaleEntry, g as VariableType, h as extractVariables, m as mergeVariableSpecs, v as variableSpecsEqual } from '../types-C1CpXVOJ.js';
|
|
1
|
+
import { parse } from '@formatjs/icu-messageformat-parser';
|
|
3
2
|
import { z } from 'zod';
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
type VariableType = "string" | "number" | "date";
|
|
5
|
+
type VariableSpec = Record<string, VariableType>;
|
|
6
|
+
type VariableRole = "simple" | "plural" | "selectordinal" | "select" | "number" | "date" | "time";
|
|
7
|
+
type VariableMeta = {
|
|
8
|
+
type: VariableType;
|
|
9
|
+
roles: Set<VariableRole>;
|
|
10
|
+
};
|
|
11
|
+
type VariableMetaSpec = Record<string, VariableMeta>;
|
|
12
|
+
type IcuAst = ReturnType<typeof parse>;
|
|
13
|
+
declare function extractVariables(nodes: IcuAst): VariableSpec;
|
|
14
|
+
declare function mergeVariableSpecs(target: VariableSpec, source: VariableSpec): VariableSpec;
|
|
15
|
+
declare function variableSpecsEqual(a: VariableSpec, b: VariableSpec): boolean;
|
|
16
|
+
|
|
17
|
+
type ParsedLocaleEntry = {
|
|
18
|
+
readonly template: string;
|
|
19
|
+
readonly args: VariableSpec;
|
|
20
|
+
};
|
|
21
|
+
type ParsedKeyEntry = {
|
|
22
|
+
readonly locales: Readonly<Record<string, ParsedLocaleEntry>>;
|
|
23
|
+
readonly mergedArgs: VariableSpec;
|
|
24
|
+
};
|
|
25
|
+
type NormalizedKeyDictionary = Readonly<Record<string, ParsedKeyEntry>>;
|
|
26
|
+
type NormalizedMultiDictionary = Readonly<Record<string, NormalizedKeyDictionary>>;
|
|
27
|
+
type NormalizedDictionary = {
|
|
28
|
+
readonly mode: "single";
|
|
29
|
+
readonly keys: NormalizedKeyDictionary;
|
|
30
|
+
} | {
|
|
31
|
+
readonly mode: "multi";
|
|
32
|
+
readonly namespaces: NormalizedMultiDictionary;
|
|
33
|
+
};
|
|
34
|
+
type DictionarySpec = {
|
|
35
|
+
readonly mode: "single";
|
|
36
|
+
readonly requiredKeys: readonly string[];
|
|
37
|
+
readonly argsByKey: Readonly<Record<string, VariableSpec>>;
|
|
38
|
+
} | {
|
|
39
|
+
readonly mode: "multi";
|
|
40
|
+
readonly requiredKeys: Readonly<Record<string, readonly string[]>>;
|
|
41
|
+
readonly argsByKey: Readonly<Record<string, Readonly<Record<string, VariableSpec>>>>;
|
|
42
|
+
};
|
|
43
|
+
type ValidationIssue = {
|
|
44
|
+
readonly kind: "invalid_input";
|
|
45
|
+
readonly message: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: "missing_key";
|
|
48
|
+
readonly path: readonly string[];
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: "invalid_locale_value";
|
|
51
|
+
readonly path: readonly string[];
|
|
52
|
+
readonly message: string;
|
|
53
|
+
} | {
|
|
54
|
+
readonly kind: "icu_syntax_error";
|
|
55
|
+
readonly path: readonly string[];
|
|
56
|
+
readonly message: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: "locale_args_mismatch";
|
|
59
|
+
readonly path: readonly string[];
|
|
60
|
+
readonly locales: Readonly<Record<string, VariableSpec>>;
|
|
61
|
+
readonly message: string;
|
|
62
|
+
} | {
|
|
63
|
+
readonly kind: "variable_mismatch";
|
|
64
|
+
readonly path: readonly string[];
|
|
65
|
+
readonly expected: VariableSpec;
|
|
66
|
+
readonly found: VariableSpec;
|
|
67
|
+
readonly message: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: "variable_type_mismatch";
|
|
70
|
+
readonly path: readonly string[];
|
|
71
|
+
readonly variable: string;
|
|
72
|
+
readonly expected: VariableType;
|
|
73
|
+
readonly found: VariableType;
|
|
74
|
+
readonly message: string;
|
|
75
|
+
};
|
|
76
|
+
type ValidationResult<T> = {
|
|
77
|
+
readonly ok: true;
|
|
78
|
+
readonly data: T;
|
|
79
|
+
} | {
|
|
80
|
+
readonly ok: false;
|
|
81
|
+
readonly issues: readonly ValidationIssue[];
|
|
82
|
+
};
|
|
5
83
|
|
|
6
84
|
declare function normalizeDictionary(input: unknown, spec: DictionarySpec): ValidationResult<NormalizedDictionary>;
|
|
7
85
|
|
|
@@ -42,4 +120,4 @@ declare function parseTemplate(template: string): ParseTemplateResult;
|
|
|
42
120
|
declare function validateExternalDictionary<TSchema>(input: unknown, spec: DictionarySpec): ValidationResult<TSchema>;
|
|
43
121
|
declare function validateExternalNamespace<TNamespaceSchema>(namespace: string, input: unknown, spec: DictionarySpec): ValidationResult<TNamespaceSchema>;
|
|
44
122
|
|
|
45
|
-
export { DictionarySpec, DictionaryValidationError, NormalizedDictionary, type ParseTemplateResult, ValidationIssue, ValidationResult, VariableSpec, assertValidDictionary, createArgsSchema, createNormalizedDictionarySchema, formatIssues, normalizeDictionary, parseTemplate, toDictionary, toNamespaceDictionary, validateExternalDictionary, validateExternalNamespace, validateNormalizedDictionary };
|
|
123
|
+
export { type DictionarySpec, DictionaryValidationError, type NormalizedDictionary, type NormalizedKeyDictionary, type NormalizedMultiDictionary, type ParseTemplateResult, type ParsedKeyEntry, type ParsedLocaleEntry, type ValidationIssue, type ValidationResult, type VariableSpec, type VariableType, assertValidDictionary, createArgsSchema, createNormalizedDictionarySchema, extractVariables, formatIssues, mergeVariableSpecs, normalizeDictionary, parseTemplate, toDictionary, toNamespaceDictionary, validateExternalDictionary, validateExternalNamespace, validateNormalizedDictionary, variableSpecsEqual };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xndrjs/i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Compiler-first, type-safe ICU MessageFormat i18n with runtime dictionary override from external sources.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,11 +45,6 @@
|
|
|
45
45
|
"tsx": ">=4",
|
|
46
46
|
"zod": "^4.0.0"
|
|
47
47
|
},
|
|
48
|
-
"peerDependenciesMeta": {
|
|
49
|
-
"zod": {
|
|
50
|
-
"optional": true
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
48
|
"devDependencies": {
|
|
54
49
|
"tsup": "^8.5.1",
|
|
55
50
|
"tsx": "^4.22.4",
|
|
@@ -161,7 +161,7 @@ describe("IcuTranslationProviderMulti", () => {
|
|
|
161
161
|
expect(() =>
|
|
162
162
|
partial.get("billing", "invoice_summary", "en", { count: 1, name: "Ada" })
|
|
163
163
|
).toThrow(
|
|
164
|
-
'[i18n] Namespace not loaded: "billing".
|
|
164
|
+
'[i18n] Namespace not loaded: "billing". Register it with setNamespace() before calling .get().'
|
|
165
165
|
);
|
|
166
166
|
});
|
|
167
167
|
|
|
@@ -110,7 +110,7 @@ export class IcuTranslationProviderMulti<
|
|
|
110
110
|
): string {
|
|
111
111
|
if (!this.loadedNamespaces.has(namespace as string)) {
|
|
112
112
|
throw new Error(
|
|
113
|
-
`[i18n] Namespace not loaded: "${String(namespace)}".
|
|
113
|
+
`[i18n] Namespace not loaded: "${String(namespace)}". Register it with setNamespace() before calling .get().`
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -48,7 +48,7 @@ export function formatDictionaryFile(options: DictionaryFileOptions): string {
|
|
|
48
48
|
`${GENERATED_FILE_BANNER}` +
|
|
49
49
|
`import ${importId} from '${importPath}.json';\n` +
|
|
50
50
|
`import type { ${schemaTypeName} } from '${typesImport}';\n\n` +
|
|
51
|
-
`export const
|
|
51
|
+
`export const defaultDictionary: ${schemaTypeName} = ${importId};\n`
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -72,6 +72,6 @@ export function formatDictionaryFile(options: DictionaryFileOptions): string {
|
|
|
72
72
|
`${GENERATED_FILE_BANNER}` +
|
|
73
73
|
`${imports}\n` +
|
|
74
74
|
`import type { ${dictionaryTypeName} } from '${typesImport}';\n\n` +
|
|
75
|
-
`export const
|
|
75
|
+
`export const defaultDictionary: ${dictionaryTypeName} = {\n${objectEntries}\n};\n`
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -5,7 +5,6 @@ export interface InstanceFileOptions {
|
|
|
5
5
|
isSingle: boolean;
|
|
6
6
|
hasLazy: boolean;
|
|
7
7
|
typesOutputPath: string;
|
|
8
|
-
dictionaryOutputPath: string;
|
|
9
8
|
paramsTypeName: string;
|
|
10
9
|
schemaTypeName: string;
|
|
11
10
|
localeTypeName: string;
|
|
@@ -22,7 +21,6 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
22
21
|
isSingle,
|
|
23
22
|
hasLazy,
|
|
24
23
|
typesOutputPath,
|
|
25
|
-
dictionaryOutputPath,
|
|
26
24
|
paramsTypeName,
|
|
27
25
|
schemaTypeName,
|
|
28
26
|
localeTypeName,
|
|
@@ -36,11 +34,9 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
36
34
|
|
|
37
35
|
const providerClass = isSingle ? "IcuTranslationProviderSingle" : "IcuTranslationProviderMulti";
|
|
38
36
|
const typesModule = toModuleBasename(typesOutputPath);
|
|
39
|
-
const dictionaryModule = toModuleBasename(dictionaryOutputPath);
|
|
40
37
|
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const initialDictionaryImport = hasLazy
|
|
38
|
+
const dictionaryParamType = hasLazy ? "InitialSchema" : schemaTypeName;
|
|
39
|
+
const schemaTypesImport = hasLazy
|
|
44
40
|
? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from '${typesImport}';\n`
|
|
45
41
|
: `import type { ${paramsTypeName}, ${schemaTypeName} } from '${typesImport}';\n`;
|
|
46
42
|
const providerTypeArgs = hasLocaleFallback
|
|
@@ -77,14 +73,13 @@ export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
|
77
73
|
return (
|
|
78
74
|
`${GENERATED_FILE_BANNER}` +
|
|
79
75
|
`import { ${providerClass}, ${coreImports} } from '@xndrjs/i18n';\n` +
|
|
80
|
-
|
|
81
|
-
initialDictionaryImport +
|
|
76
|
+
schemaTypesImport +
|
|
82
77
|
typesImportLine +
|
|
83
78
|
`\n` +
|
|
84
79
|
`export function ${factoryName}(\n` +
|
|
85
|
-
`
|
|
80
|
+
` dictionary: ${dictionaryParamType},\n` +
|
|
86
81
|
`) {\n` +
|
|
87
|
-
` return new ${providerClass}<${providerTypeArgs}>(
|
|
82
|
+
` return new ${providerClass}<${providerTypeArgs}>(dictionary${providerOptions});\n` +
|
|
88
83
|
`}\n` +
|
|
89
84
|
projectLocalesBlock
|
|
90
85
|
);
|
|
@@ -6,14 +6,9 @@ export function formatNamespaceLoadersFile(
|
|
|
6
6
|
lazyEntries: (NamespaceEntry & { absolutePath: string })[],
|
|
7
7
|
schemaTypeName: string,
|
|
8
8
|
typesModule: string,
|
|
9
|
-
dictionarySchemaModule: string,
|
|
10
|
-
instanceModule: string,
|
|
11
|
-
factoryName: string,
|
|
12
9
|
importExtension: ImportExtension
|
|
13
10
|
): string {
|
|
14
|
-
const dictionarySchemaImport = toRelativeModuleImport(dictionarySchemaModule, importExtension);
|
|
15
11
|
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
16
|
-
const instanceImport = toRelativeModuleImport(instanceModule, importExtension);
|
|
17
12
|
const loaderEntries = lazyEntries
|
|
18
13
|
.map((entry) => {
|
|
19
14
|
const importPath = toImportPath(loadersOutputPath, entry.absolutePath);
|
|
@@ -23,26 +18,9 @@ export function formatNamespaceLoadersFile(
|
|
|
23
18
|
|
|
24
19
|
return (
|
|
25
20
|
`${GENERATED_FILE_BANNER}` +
|
|
26
|
-
`import {
|
|
27
|
-
`
|
|
28
|
-
`
|
|
29
|
-
`
|
|
30
|
-
`export const namespaceLoaders: Record<\n` +
|
|
31
|
-
` LazyNamespace,\n` +
|
|
32
|
-
` () => Promise<${schemaTypeName}[LazyNamespace]>\n` +
|
|
33
|
-
`> = {\n${loaderEntries}\n};\n\n` +
|
|
34
|
-
`export async function ensureNamespacesLoaded(\n` +
|
|
35
|
-
` i18n: ReturnType<typeof ${factoryName}>,\n` +
|
|
36
|
-
` namespaces: LazyNamespace[],\n` +
|
|
37
|
-
`): Promise<void> {\n` +
|
|
38
|
-
` return ensureNamespacesLoadedImpl<${schemaTypeName}, LazyNamespace>(\n` +
|
|
39
|
-
` {\n` +
|
|
40
|
-
` provider: i18n,\n` +
|
|
41
|
-
` resolveLoader: (namespace) => namespaceLoaders[namespace],\n` +
|
|
42
|
-
` validate: (namespace, raw) => validateExternalNamespace(namespace, raw),\n` +
|
|
43
|
-
` },\n` +
|
|
44
|
-
` namespaces,\n` +
|
|
45
|
-
` );\n` +
|
|
46
|
-
`}\n`
|
|
21
|
+
`import type { ${schemaTypeName}, LazyNamespace } from '${typesImport}';\n\n` +
|
|
22
|
+
`export const namespaceLoaders: {\n` +
|
|
23
|
+
` [K in LazyNamespace]: () => Promise<${schemaTypeName}[K]>;\n` +
|
|
24
|
+
`} = {\n${loaderEntries}\n};\n`
|
|
47
25
|
);
|
|
48
26
|
}
|
|
@@ -450,12 +450,15 @@ describe("generate-i18n-types", () => {
|
|
|
450
450
|
expect(types).toContain("export type LoadOnInitNamespace = 'default'");
|
|
451
451
|
expect(types).toContain("export type LazyNamespace = 'billing'");
|
|
452
452
|
expect(types).toContain("export type InitialSchema = Pick<AppSchema, LoadOnInitNamespace>");
|
|
453
|
-
expect(dictionary).toContain("export const
|
|
453
|
+
expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
|
|
454
454
|
expect(dictionary).not.toContain("billingNs");
|
|
455
|
-
expect(factory).toContain("
|
|
455
|
+
expect(factory).toContain("dictionary: InitialSchema,");
|
|
456
|
+
expect(factory).not.toContain("import { dictionary }");
|
|
456
457
|
expect(loaders).toContain("export const namespaceLoaders");
|
|
457
|
-
expect(loaders).toContain("
|
|
458
|
-
expect(loaders).toContain("
|
|
458
|
+
expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
|
|
459
|
+
expect(loaders).not.toContain("ensureNamespacesLoaded");
|
|
460
|
+
expect(loaders).not.toContain("ensureNamespacesLoadedImpl");
|
|
461
|
+
expect(loaders).not.toContain("validateExternalNamespace");
|
|
459
462
|
expect(loaders).toContain("import('./translations/billing.json')");
|
|
460
463
|
});
|
|
461
464
|
|
|
@@ -494,9 +497,10 @@ describe("generate-i18n-types", () => {
|
|
|
494
497
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
495
498
|
|
|
496
499
|
expect(types).not.toContain("LazyNamespace");
|
|
497
|
-
expect(dictionary).toContain("export const
|
|
500
|
+
expect(dictionary).toContain("export const defaultDictionary: AppSchema");
|
|
498
501
|
expect(dictionary).toContain("billingNs");
|
|
499
|
-
expect(factory).toContain("
|
|
502
|
+
expect(factory).toContain("dictionary: AppSchema,");
|
|
503
|
+
expect(factory).not.toContain("import { dictionary }");
|
|
500
504
|
expect(result.stdout).not.toContain("namespace-loaders.generated.ts");
|
|
501
505
|
});
|
|
502
506
|
|
|
@@ -706,8 +710,9 @@ describe("generate-i18n-types", () => {
|
|
|
706
710
|
const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
|
|
707
711
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
708
712
|
expect(dictionary).toContain("from './i18n-types.generated'");
|
|
709
|
-
expect(factory).toContain("from './dictionary.generated'");
|
|
713
|
+
expect(factory).not.toContain("from './dictionary.generated'");
|
|
710
714
|
expect(factory).toContain("from './i18n-types.generated'");
|
|
715
|
+
expect(factory).toContain("dictionary: AppSchema,");
|
|
711
716
|
expect(dictionary).not.toContain(".generated.js");
|
|
712
717
|
expect(dictionary).not.toContain(".generated.ts");
|
|
713
718
|
expect(factory).not.toContain(".generated.js");
|
|
@@ -739,7 +744,7 @@ describe("generate-i18n-types", () => {
|
|
|
739
744
|
expect(result.status).toBe(0);
|
|
740
745
|
|
|
741
746
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
742
|
-
expect(factory).toContain("from './dictionary.generated.ts'");
|
|
747
|
+
expect(factory).not.toContain("from './dictionary.generated.ts'");
|
|
743
748
|
expect(factory).toContain("from './i18n-types.generated.ts'");
|
|
744
749
|
});
|
|
745
750
|
|
|
@@ -768,7 +773,7 @@ describe("generate-i18n-types", () => {
|
|
|
768
773
|
expect(result.status).toBe(0);
|
|
769
774
|
|
|
770
775
|
const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
|
|
771
|
-
expect(factory).toContain("from './dictionary.generated.js'");
|
|
776
|
+
expect(factory).not.toContain("from './dictionary.generated.js'");
|
|
772
777
|
expect(factory).toContain("from './i18n-types.generated.js'");
|
|
773
778
|
});
|
|
774
779
|
|
|
@@ -46,12 +46,6 @@ function main() {
|
|
|
46
46
|
const isSingle = Boolean(config.dictionary);
|
|
47
47
|
const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
|
|
48
48
|
|
|
49
|
-
if (hasLazy && !config.dictionarySchemaOutput) {
|
|
50
|
-
fail(
|
|
51
|
-
'[Codegen Error] Lazy namespaces require "dictionarySchemaOutput" for validateExternalNamespace.'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
49
|
const typesOutputPath = path.resolve(projectRoot, config.typesOutput);
|
|
56
50
|
const dictionaryOutputPath = path.resolve(projectRoot, config.dictionaryOutput);
|
|
57
51
|
const instanceOutputPath = path.resolve(projectRoot, config.instanceOutput);
|
|
@@ -124,7 +118,6 @@ function main() {
|
|
|
124
118
|
isSingle,
|
|
125
119
|
hasLazy,
|
|
126
120
|
typesOutputPath,
|
|
127
|
-
dictionaryOutputPath,
|
|
128
121
|
paramsTypeName,
|
|
129
122
|
schemaTypeName,
|
|
130
123
|
localeTypeName,
|
|
@@ -173,7 +166,6 @@ function main() {
|
|
|
173
166
|
config.namespaceLoadersOutput ??
|
|
174
167
|
path.join(path.dirname(config.instanceOutput), "namespace-loaders.generated.ts")
|
|
175
168
|
);
|
|
176
|
-
const dictionarySchemaOutputPath = path.resolve(projectRoot, config.dictionarySchemaOutput!);
|
|
177
169
|
const lazyEntriesWithPaths = lazyEntries.map((entry) => ({
|
|
178
170
|
...entry,
|
|
179
171
|
absolutePath: path.resolve(projectRoot, entry.filePath),
|
|
@@ -183,9 +175,6 @@ function main() {
|
|
|
183
175
|
lazyEntriesWithPaths,
|
|
184
176
|
schemaTypeName,
|
|
185
177
|
typesModule,
|
|
186
|
-
toModuleBasename(dictionarySchemaOutputPath),
|
|
187
|
-
toModuleBasename(instanceOutputPath),
|
|
188
|
-
factoryName,
|
|
189
178
|
importExtension
|
|
190
179
|
);
|
|
191
180
|
|
|
@@ -124,9 +124,10 @@ describe("codegen projectLocales + setNamespace", () => {
|
|
|
124
124
|
join(tempDir, "src/hydrate-billing.ts"),
|
|
125
125
|
[
|
|
126
126
|
`import { createI18n, projectNamespaceLocales } from "./i18n/instance.generated.js";`,
|
|
127
|
+
`import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
|
|
127
128
|
`import billingDictionary from "./i18n/translations/billing.json";`,
|
|
128
129
|
``,
|
|
129
|
-
`const i18n = createI18n();`,
|
|
130
|
+
`const i18n = createI18n(defaultDictionary);`,
|
|
130
131
|
`i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, ["en"]));`,
|
|
131
132
|
].join("\n")
|
|
132
133
|
);
|
|
@@ -145,16 +146,17 @@ describe("codegen projectLocales + setNamespace", () => {
|
|
|
145
146
|
join(tempDir, "src/hydrate-all.ts"),
|
|
146
147
|
[
|
|
147
148
|
`import { createI18n, projectLocales } from "./i18n/instance.generated.js";`,
|
|
149
|
+
`import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
|
|
148
150
|
`import type { AppSchema } from "./i18n/i18n-types.generated.js";`,
|
|
149
151
|
`import billingDictionary from "./i18n/translations/billing.json";`,
|
|
150
|
-
`import
|
|
152
|
+
`import defaultNs from "./i18n/translations/default.json";`,
|
|
151
153
|
``,
|
|
152
154
|
`const fullDictionary = {`,
|
|
153
|
-
` default:
|
|
155
|
+
` default: defaultNs,`,
|
|
154
156
|
` billing: billingDictionary,`,
|
|
155
157
|
`} satisfies AppSchema;`,
|
|
156
158
|
``,
|
|
157
|
-
`const i18n = createI18n();`,
|
|
159
|
+
`const i18n = createI18n(defaultDictionary);`,
|
|
158
160
|
`i18n.setAll(projectLocales(fullDictionary, ["en"]));`,
|
|
159
161
|
].join("\n")
|
|
160
162
|
);
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,3 @@ export {
|
|
|
29
29
|
export type { ResolvedLocaleTemplate } from "./resolve-locale.js";
|
|
30
30
|
|
|
31
31
|
export { projectLocales, projectNamespacesLocales } from "./project-locales.js";
|
|
32
|
-
|
|
33
|
-
export { ensureNamespacesLoadedImpl } from "./ensure-namespace.js";
|
|
34
|
-
export type { EnsureNamespacesLoadedOptions } from "./ensure-namespace.js";
|
package/src/setup/setup-i18n.ts
CHANGED
|
@@ -32,11 +32,12 @@ const DEFAULT_STARTER = {
|
|
|
32
32
|
const DEFAULT_IMPORT_SUFFIX = importExtensionSuffix(DEFAULT_IMPORT_EXTENSION);
|
|
33
33
|
|
|
34
34
|
const INDEX_TS =
|
|
35
|
-
`import { createI18n } from "./generated/instance.generated${DEFAULT_IMPORT_SUFFIX}";\n
|
|
35
|
+
`import { createI18n } from "./generated/instance.generated${DEFAULT_IMPORT_SUFFIX}";\n` +
|
|
36
|
+
`import { defaultDictionary } from "./generated/dictionary.generated${DEFAULT_IMPORT_SUFFIX}";\n\n` +
|
|
36
37
|
`export * from "./generated/instance.generated${DEFAULT_IMPORT_SUFFIX}";\n` +
|
|
37
38
|
`export * from "./generated/dictionary.generated${DEFAULT_IMPORT_SUFFIX}";\n` +
|
|
38
39
|
`export * from "./generated/i18n-types.generated${DEFAULT_IMPORT_SUFFIX}";\n\n` +
|
|
39
|
-
`export const i18n = createI18n();\n`;
|
|
40
|
+
`export const i18n = createI18n(defaultDictionary);\n`;
|
|
40
41
|
|
|
41
42
|
function writeJson(filePath: string, value: unknown): void {
|
|
42
43
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
package/dist/types-C1CpXVOJ.d.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { parse } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
|
|
3
|
-
type VariableType = "string" | "number" | "date";
|
|
4
|
-
type VariableSpec = Record<string, VariableType>;
|
|
5
|
-
type VariableRole = "simple" | "plural" | "selectordinal" | "select" | "number" | "date" | "time";
|
|
6
|
-
type VariableMeta = {
|
|
7
|
-
type: VariableType;
|
|
8
|
-
roles: Set<VariableRole>;
|
|
9
|
-
};
|
|
10
|
-
type VariableMetaSpec = Record<string, VariableMeta>;
|
|
11
|
-
type IcuAst = ReturnType<typeof parse>;
|
|
12
|
-
declare function extractVariables(nodes: IcuAst): VariableSpec;
|
|
13
|
-
declare function mergeVariableSpecs(target: VariableSpec, source: VariableSpec): VariableSpec;
|
|
14
|
-
declare function variableSpecsEqual(a: VariableSpec, b: VariableSpec): boolean;
|
|
15
|
-
|
|
16
|
-
type ParsedLocaleEntry = {
|
|
17
|
-
readonly template: string;
|
|
18
|
-
readonly args: VariableSpec;
|
|
19
|
-
};
|
|
20
|
-
type ParsedKeyEntry = {
|
|
21
|
-
readonly locales: Readonly<Record<string, ParsedLocaleEntry>>;
|
|
22
|
-
readonly mergedArgs: VariableSpec;
|
|
23
|
-
};
|
|
24
|
-
type NormalizedKeyDictionary = Readonly<Record<string, ParsedKeyEntry>>;
|
|
25
|
-
type NormalizedMultiDictionary = Readonly<Record<string, NormalizedKeyDictionary>>;
|
|
26
|
-
type NormalizedDictionary = {
|
|
27
|
-
readonly mode: "single";
|
|
28
|
-
readonly keys: NormalizedKeyDictionary;
|
|
29
|
-
} | {
|
|
30
|
-
readonly mode: "multi";
|
|
31
|
-
readonly namespaces: NormalizedMultiDictionary;
|
|
32
|
-
};
|
|
33
|
-
type DictionarySpec = {
|
|
34
|
-
readonly mode: "single";
|
|
35
|
-
readonly requiredKeys: readonly string[];
|
|
36
|
-
readonly argsByKey: Readonly<Record<string, VariableSpec>>;
|
|
37
|
-
} | {
|
|
38
|
-
readonly mode: "multi";
|
|
39
|
-
readonly requiredKeys: Readonly<Record<string, readonly string[]>>;
|
|
40
|
-
readonly argsByKey: Readonly<Record<string, Readonly<Record<string, VariableSpec>>>>;
|
|
41
|
-
};
|
|
42
|
-
type ValidationIssue = {
|
|
43
|
-
readonly kind: "invalid_input";
|
|
44
|
-
readonly message: string;
|
|
45
|
-
} | {
|
|
46
|
-
readonly kind: "missing_key";
|
|
47
|
-
readonly path: readonly string[];
|
|
48
|
-
} | {
|
|
49
|
-
readonly kind: "invalid_locale_value";
|
|
50
|
-
readonly path: readonly string[];
|
|
51
|
-
readonly message: string;
|
|
52
|
-
} | {
|
|
53
|
-
readonly kind: "icu_syntax_error";
|
|
54
|
-
readonly path: readonly string[];
|
|
55
|
-
readonly message: string;
|
|
56
|
-
} | {
|
|
57
|
-
readonly kind: "locale_args_mismatch";
|
|
58
|
-
readonly path: readonly string[];
|
|
59
|
-
readonly locales: Readonly<Record<string, VariableSpec>>;
|
|
60
|
-
readonly message: string;
|
|
61
|
-
} | {
|
|
62
|
-
readonly kind: "variable_mismatch";
|
|
63
|
-
readonly path: readonly string[];
|
|
64
|
-
readonly expected: VariableSpec;
|
|
65
|
-
readonly found: VariableSpec;
|
|
66
|
-
readonly message: string;
|
|
67
|
-
} | {
|
|
68
|
-
readonly kind: "variable_type_mismatch";
|
|
69
|
-
readonly path: readonly string[];
|
|
70
|
-
readonly variable: string;
|
|
71
|
-
readonly expected: VariableType;
|
|
72
|
-
readonly found: VariableType;
|
|
73
|
-
readonly message: string;
|
|
74
|
-
};
|
|
75
|
-
type ValidationResult<T> = {
|
|
76
|
-
readonly ok: true;
|
|
77
|
-
readonly data: T;
|
|
78
|
-
} | {
|
|
79
|
-
readonly ok: false;
|
|
80
|
-
readonly issues: readonly ValidationIssue[];
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export { type DictionarySpec as D, type NormalizedDictionary as N, type ParsedKeyEntry as P, type ValidationResult as V, type VariableSpec as a, type ValidationIssue as b, type VariableMetaSpec as c, type NormalizedKeyDictionary as d, type NormalizedMultiDictionary as e, type ParsedLocaleEntry as f, type VariableType as g, extractVariables as h, mergeVariableSpecs as m, variableSpecsEqual as v };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { ensureNamespacesLoadedImpl } from "./ensure-namespace.js";
|
|
3
|
-
import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
|
|
4
|
-
|
|
5
|
-
type TestSchema = {
|
|
6
|
-
default: {
|
|
7
|
-
login_button: { en: string };
|
|
8
|
-
};
|
|
9
|
-
user: {
|
|
10
|
-
greeting: { en: string };
|
|
11
|
-
};
|
|
12
|
-
billing: {
|
|
13
|
-
invoice_summary: { en: string };
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type TestParams = {
|
|
18
|
-
default: { login_button: never };
|
|
19
|
-
user: { greeting: { name: string } };
|
|
20
|
-
billing: { invoice_summary: { count: number } };
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const namespaces: TestSchema = {
|
|
24
|
-
default: { login_button: { en: "Login" } },
|
|
25
|
-
user: { greeting: { en: "Hello {name}!" } },
|
|
26
|
-
billing: {
|
|
27
|
-
invoice_summary: {
|
|
28
|
-
en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
describe("ensureNamespacesLoadedImpl integration", () => {
|
|
34
|
-
it("mirrors the lazy codegen flow: partial init, preload batch, sync get", async () => {
|
|
35
|
-
const i18n = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
36
|
-
default: namespaces.default,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(i18n.hasNamespace("default")).toBe(true);
|
|
40
|
-
expect(i18n.hasNamespace("user")).toBe(false);
|
|
41
|
-
expect(i18n.hasNamespace("billing")).toBe(false);
|
|
42
|
-
|
|
43
|
-
expect(i18n.get("default", "login_button", "en")).toBe("Login");
|
|
44
|
-
|
|
45
|
-
expect(() => i18n.get("billing", "invoice_summary", "en", { count: 1 })).toThrow(
|
|
46
|
-
'Namespace not loaded: "billing"'
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
await ensureNamespacesLoadedImpl(
|
|
50
|
-
{
|
|
51
|
-
provider: i18n,
|
|
52
|
-
resolveLoader: (namespace) => async () => namespaces[namespace],
|
|
53
|
-
validate: (_namespace, raw) => ({ ok: true, data: raw as TestSchema["billing"] }),
|
|
54
|
-
},
|
|
55
|
-
["user", "billing"]
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
expect(i18n.get("user", "greeting", "en", { name: "Ada" })).toBe("Hello Ada!");
|
|
59
|
-
expect(i18n.get("billing", "invoice_summary", "en", { count: 2 })).toBe("You have 2 invoices");
|
|
60
|
-
|
|
61
|
-
i18n.setNamespace("billing", {
|
|
62
|
-
invoice_summary: { en: "{count, plural, one {1 bill} other {{count} bills}}" },
|
|
63
|
-
});
|
|
64
|
-
expect(i18n.get("billing", "invoice_summary", "en", { count: 3 })).toBe("3 bills");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { ensureNamespacesLoadedImpl } from "./ensure-namespace.js";
|
|
3
|
-
import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
|
|
4
|
-
import type { ValidationResult } from "./validation/types.js";
|
|
5
|
-
|
|
6
|
-
type TestSchema = {
|
|
7
|
-
default: {
|
|
8
|
-
hello: { en: string };
|
|
9
|
-
};
|
|
10
|
-
billing: {
|
|
11
|
-
invoice: { en: string };
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type TestParams = {
|
|
16
|
-
default: { hello: never };
|
|
17
|
-
billing: { invoice: never };
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const defaultNs: TestSchema["default"] = { hello: { en: "Hello" } };
|
|
21
|
-
const billingNs: TestSchema["billing"] = { invoice: { en: "Invoice" } };
|
|
22
|
-
|
|
23
|
-
function createProvider(initial: Partial<TestSchema> = { default: defaultNs }) {
|
|
24
|
-
return new IcuTranslationProviderMulti<TestSchema, TestParams>(initial);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe("ensureNamespacesLoadedImpl", () => {
|
|
28
|
-
it("is a noop for an empty namespaces array", async () => {
|
|
29
|
-
const provider = createProvider();
|
|
30
|
-
const load = vi.fn();
|
|
31
|
-
|
|
32
|
-
await ensureNamespacesLoadedImpl(
|
|
33
|
-
{
|
|
34
|
-
provider,
|
|
35
|
-
resolveLoader: () => load,
|
|
36
|
-
validate: () => ({ ok: true, data: billingNs }),
|
|
37
|
-
},
|
|
38
|
-
[]
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
expect(load).not.toHaveBeenCalled();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("is a noop when the namespace is already loaded", async () => {
|
|
45
|
-
const provider = createProvider({ default: defaultNs, billing: billingNs });
|
|
46
|
-
const load = vi.fn();
|
|
47
|
-
|
|
48
|
-
await ensureNamespacesLoadedImpl(
|
|
49
|
-
{
|
|
50
|
-
provider,
|
|
51
|
-
resolveLoader: () => load,
|
|
52
|
-
validate: () => ({ ok: true, data: billingNs }),
|
|
53
|
-
},
|
|
54
|
-
["billing"]
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
expect(load).not.toHaveBeenCalled();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("loads multiple namespaces in parallel", async () => {
|
|
61
|
-
const provider = createProvider();
|
|
62
|
-
const loadBilling = vi.fn(async () => billingNs);
|
|
63
|
-
const loadDefault = vi.fn(async () => defaultNs);
|
|
64
|
-
|
|
65
|
-
await ensureNamespacesLoadedImpl(
|
|
66
|
-
{
|
|
67
|
-
provider,
|
|
68
|
-
resolveLoader: (namespace) => (namespace === "billing" ? loadBilling : loadDefault),
|
|
69
|
-
validate: (_namespace, raw) => ({ ok: true, data: raw as TestSchema["billing"] }),
|
|
70
|
-
},
|
|
71
|
-
["billing"]
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
expect(loadBilling).toHaveBeenCalledTimes(1);
|
|
75
|
-
expect(provider.hasNamespace("billing")).toBe(true);
|
|
76
|
-
expect(provider.get("billing", "invoice", "en")).toBe("Invoice");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("dedupes concurrent requests for the same namespace", async () => {
|
|
80
|
-
const provider = createProvider();
|
|
81
|
-
let resolveLoad!: () => void;
|
|
82
|
-
const load = vi.fn(
|
|
83
|
-
() =>
|
|
84
|
-
new Promise<TestSchema["billing"]>((resolve) => {
|
|
85
|
-
resolveLoad = () => resolve(billingNs);
|
|
86
|
-
})
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const options = {
|
|
90
|
-
provider,
|
|
91
|
-
resolveLoader: () => load,
|
|
92
|
-
validate: () => ({ ok: true, data: billingNs }) as const,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const first = ensureNamespacesLoadedImpl(options, ["billing"]);
|
|
96
|
-
const second = ensureNamespacesLoadedImpl(options, ["billing"]);
|
|
97
|
-
|
|
98
|
-
resolveLoad();
|
|
99
|
-
await Promise.all([first, second]);
|
|
100
|
-
|
|
101
|
-
expect(load).toHaveBeenCalledTimes(1);
|
|
102
|
-
expect(provider.hasNamespace("billing")).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("propagates validation failures without marking the namespace loaded", async () => {
|
|
106
|
-
const provider = createProvider();
|
|
107
|
-
const failed: ValidationResult<TestSchema["billing"]> = {
|
|
108
|
-
ok: false,
|
|
109
|
-
issues: [{ kind: "invalid_input", message: "bad data" }],
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
await expect(
|
|
113
|
-
ensureNamespacesLoadedImpl(
|
|
114
|
-
{
|
|
115
|
-
provider,
|
|
116
|
-
resolveLoader: () => async () => ({ bad: true }),
|
|
117
|
-
validate: () => failed,
|
|
118
|
-
},
|
|
119
|
-
["billing"]
|
|
120
|
-
)
|
|
121
|
-
).rejects.toThrow("bad data");
|
|
122
|
-
|
|
123
|
-
expect(provider.hasNamespace("billing")).toBe(false);
|
|
124
|
-
});
|
|
125
|
-
});
|
package/src/ensure-namespace.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { MultiDictionary } from "./types.js";
|
|
2
|
-
import type { ValidationResult } from "./validation/types.js";
|
|
3
|
-
import { DictionaryValidationError } from "./validation/errors.js";
|
|
4
|
-
|
|
5
|
-
export interface EnsureNamespacesLoadedOptions<
|
|
6
|
-
Schema extends MultiDictionary,
|
|
7
|
-
NS extends keyof Schema & string,
|
|
8
|
-
> {
|
|
9
|
-
provider: Pick<
|
|
10
|
-
{ hasNamespace(namespace: NS): boolean; setNamespace(namespace: NS, values: Schema[NS]): void },
|
|
11
|
-
"hasNamespace" | "setNamespace"
|
|
12
|
-
>;
|
|
13
|
-
resolveLoader: (namespace: NS) => () => Promise<unknown>;
|
|
14
|
-
validate: (namespace: NS, raw: unknown) => ValidationResult<Schema[NS]>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const inFlightByProvider = new WeakMap<object, Map<string, Promise<void>>>();
|
|
18
|
-
|
|
19
|
-
async function ensureOneNamespace<Schema extends MultiDictionary, NS extends keyof Schema & string>(
|
|
20
|
-
options: EnsureNamespacesLoadedOptions<Schema, NS>,
|
|
21
|
-
namespace: NS
|
|
22
|
-
): Promise<void> {
|
|
23
|
-
if (options.provider.hasNamespace(namespace)) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const providerKey = options.provider as object;
|
|
28
|
-
let inFlight = inFlightByProvider.get(providerKey);
|
|
29
|
-
if (!inFlight) {
|
|
30
|
-
inFlight = new Map();
|
|
31
|
-
inFlightByProvider.set(providerKey, inFlight);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const existing = inFlight.get(namespace);
|
|
35
|
-
if (existing) {
|
|
36
|
-
return existing;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const promise = (async () => {
|
|
40
|
-
try {
|
|
41
|
-
if (options.provider.hasNamespace(namespace)) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const raw = await options.resolveLoader(namespace)();
|
|
46
|
-
const result = options.validate(namespace, raw);
|
|
47
|
-
if (!result.ok) {
|
|
48
|
-
throw new DictionaryValidationError(result.issues);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
options.provider.setNamespace(namespace, result.data);
|
|
52
|
-
} finally {
|
|
53
|
-
inFlight!.delete(namespace);
|
|
54
|
-
}
|
|
55
|
-
})();
|
|
56
|
-
|
|
57
|
-
inFlight.set(namespace, promise);
|
|
58
|
-
return promise;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function ensureNamespacesLoadedImpl<
|
|
62
|
-
Schema extends MultiDictionary,
|
|
63
|
-
NS extends keyof Schema & string,
|
|
64
|
-
>(options: EnsureNamespacesLoadedOptions<Schema, NS>, namespaces: NS[]): Promise<void> {
|
|
65
|
-
if (namespaces.length === 0) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
await Promise.all(namespaces.map((namespace) => ensureOneNamespace(options, namespace)));
|
|
70
|
-
}
|