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