@xndrjs/i18n 0.6.0-alpha.0 → 0.6.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 +125 -28
- package/dist/codegen/index.d.ts +5 -3
- package/dist/codegen/index.js +8 -2
- package/dist/codegen/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen/codegen-config-schema.ts +19 -1
- package/src/codegen/config.test.ts +30 -1
- package/src/codegen/config.ts +1 -1
- package/src/codegen/delivery-artifacts.test.ts +24 -0
- package/src/codegen/delivery-artifacts.ts +29 -0
- package/src/codegen/emit/namespace-loaders-file.test.ts +6 -0
- package/src/codegen/emit/namespace-loaders-file.ts +8 -4
- package/src/codegen/emit/types-file.test.ts +5 -0
- package/src/codegen/emit/types-file.ts +12 -2
- package/src/codegen/generate-i18n-types.test.ts +41 -0
- package/src/codegen/generate-i18n-types.ts +2 -1
- package/src/codegen-config/index.ts +1 -0
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ Variables found across **all locales** of the same key are merged. If parsing fa
|
|
|
195
195
|
|
|
196
196
|
### 3. Generated files
|
|
197
197
|
|
|
198
|
-
- **`i18n-types.generated.ts`** — `I18N_MODE`, `MyProjectParams`, `MyProjectSchema`.
|
|
198
|
+
- **`i18n-types.generated.ts`** — `I18N_MODE`, `MyProjectParams`, `MyProjectSchema`. With `delivery: "custom"`, also `MyProjectDeliveryArea`, `DELIVERY_ARTIFACTS`, `LOCALE_DELIVERY_AREA`, and `MyProjectDeliveryArtifacts`.
|
|
199
199
|
- **`dictionary.generated.ts`** — imports the JSON files and exports `defaultDictionary` (canonical delivery) or `defaultDictionaryFor(locale)` / `defaultDictionaryFor(area)` when eager namespaces exist in split/custom delivery. Omitted when every namespace is lazy in split/custom delivery.
|
|
200
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.
|
|
@@ -407,25 +407,26 @@ Specify **exactly one** of `dictionary` (single-file) or `namespaces` (multi-fil
|
|
|
407
407
|
}
|
|
408
408
|
```
|
|
409
409
|
|
|
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
|
|
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
|
|
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; **canonical delivery only**. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default). Not allowed with `split-by-locale` or `custom` — those modes load every namespace through `namespaceLoaders`.
|
|
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.
|
|
428
|
-
| `
|
|
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` | Optional. Output path for `dictionary.generated.ts` when codegen emits it (canonical delivery, or split/custom with eager namespaces). Omit in split/custom when every namespace is lazy — codegen does not write the file; if a stale manifest exists at the default `{dirname(typesOutput)}/dictionary.generated.ts`, it is removed. Explicit `dictionaryOutput` still controls which path is cleaned up. |
|
|
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; **canonical delivery only**. Namespaces to include in the initial bundle via static imports. When omitted, all namespaces are eager (default). Not allowed with `split-by-locale` or `custom` — those modes load every namespace through `namespaceLoaders`. |
|
|
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. `"custom"` — named delivery areas via `deliveryArtifacts`; emits `{basename}.{area}.json`, area-scoped loaders, and typed `DELIVERY_ARTIFACTS` / `LOCALE_DELIVERY_AREA` in `i18n-types.generated.ts`. |
|
|
428
|
+
| `deliveryArtifacts` | Required when `delivery` is `"custom"`. Map of delivery area → locale list (e.g. `{ "eu": ["en", "it"], "amer": ["en-US"] }`). Codegen validates structure and that locales partition the project locale set. Emitted as `DELIVERY_ARTIFACTS` (area → locales) and `LOCALE_DELIVERY_AREA` (locale → area). |
|
|
429
|
+
| `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/`. |
|
|
429
430
|
|
|
430
431
|
> Paths are resolved relative to the directory containing `i18n.codegen.json` (e.g. `i18n/` when using `xndrjs-i18n-setup .`).
|
|
431
432
|
|
|
@@ -482,6 +483,7 @@ Codegen writes `{deliveryOutput}/translations/{basename}.{locale}.json` (for exa
|
|
|
482
483
|
| `export const defaultDictionary` | `dictionary.generated.ts` is not generated |
|
|
483
484
|
| `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)` or `namespaceLoaders.billing(area)` |
|
|
484
485
|
| — | `ensureNamespacesLoadedForLocale(i18n, locale)` or `ensureNamespacesLoadedForArea(i18n, area)` in `namespace-loaders.generated.ts` |
|
|
486
|
+
| — | `DELIVERY_ARTIFACTS`, `LOCALE_DELIVERY_AREA` in `i18n-types.generated.ts` (`custom` only) |
|
|
485
487
|
|
|
486
488
|
When `loadOnInit` lists eager namespaces in **canonical** delivery only, `dictionary.generated.ts` still exports `defaultDictionary` with static imports. In split/custom delivery, eager slices use `defaultDictionaryFor(locale)` or `defaultDictionaryFor(area)` when configured.
|
|
487
489
|
|
|
@@ -505,6 +507,55 @@ const i18n = createI18n({});
|
|
|
505
507
|
await ensureNamespacesLoadedForArea(i18n, activeArea);
|
|
506
508
|
```
|
|
507
509
|
|
|
510
|
+
Custom delivery config and typed artifacts (no need to read `deliveryArtifacts` from JSON at runtime):
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
{
|
|
514
|
+
"delivery": "custom",
|
|
515
|
+
"deliveryArtifacts": {
|
|
516
|
+
"eu": ["en", "it", "fr"],
|
|
517
|
+
"amer": ["en-US", "es-AR"]
|
|
518
|
+
},
|
|
519
|
+
"namespaces": {
|
|
520
|
+
"default": "translations/default.json",
|
|
521
|
+
"billing": "translations/billing.yaml"
|
|
522
|
+
},
|
|
523
|
+
"namespaceLoadersOutput": "generated/namespace-loaders.generated.ts"
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Codegen emits the partition in `i18n-types.generated.ts`:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
export type MyProjectDeliveryArea = "amer" | "eu";
|
|
531
|
+
|
|
532
|
+
export const DELIVERY_ARTIFACTS = {
|
|
533
|
+
amer: ["en-US", "es-AR"] as const,
|
|
534
|
+
eu: ["en", "fr", "it"] as const,
|
|
535
|
+
} as const satisfies Record<MyProjectDeliveryArea, readonly MyProjectLocale[]>;
|
|
536
|
+
|
|
537
|
+
export type MyProjectDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;
|
|
538
|
+
|
|
539
|
+
export const LOCALE_DELIVERY_AREA = {
|
|
540
|
+
"en-US": "amer",
|
|
541
|
+
"es-AR": "amer",
|
|
542
|
+
en: "eu",
|
|
543
|
+
fr: "eu",
|
|
544
|
+
it: "eu",
|
|
545
|
+
} as const satisfies Record<MyProjectLocale, MyProjectDeliveryArea>;
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
import {
|
|
550
|
+
DELIVERY_ARTIFACTS,
|
|
551
|
+
LOCALE_DELIVERY_AREA,
|
|
552
|
+
type MyProjectDeliveryArea,
|
|
553
|
+
} from "./generated/i18n-types.generated.js";
|
|
554
|
+
|
|
555
|
+
const euLocales = DELIVERY_ARTIFACTS.eu; // readonly ["en", "fr", "it"]
|
|
556
|
+
const areaForEnUs: MyProjectDeliveryArea = LOCALE_DELIVERY_AREA["en-US"]; // "amer"
|
|
557
|
+
```
|
|
558
|
+
|
|
508
559
|
Example `dictionary.generated.ts` (multi, canonical delivery, `loadOnInit: ["default"]`):
|
|
509
560
|
|
|
510
561
|
```ts
|
|
@@ -533,6 +584,10 @@ export const namespaceLoaders = {
|
|
|
533
584
|
return import("./translations/billing.it.json").then((m) => m.default);
|
|
534
585
|
case "de-CH":
|
|
535
586
|
return import("./translations/billing.de-CH.json").then((m) => m.default);
|
|
587
|
+
default:
|
|
588
|
+
throw new Error(
|
|
589
|
+
`[i18n] No translation artifact for namespace "billing" and locale "${String(locale)}".`
|
|
590
|
+
);
|
|
536
591
|
}
|
|
537
592
|
},
|
|
538
593
|
};
|
|
@@ -544,9 +599,6 @@ export async function ensureNamespacesLoadedForLocale(
|
|
|
544
599
|
): Promise<void> {
|
|
545
600
|
await Promise.all(
|
|
546
601
|
namespaces.map(async (namespace) => {
|
|
547
|
-
if (i18n.hasNamespace(namespace)) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
602
|
i18n.setNamespace(namespace, await namespaceLoaders[namespace](locale));
|
|
551
603
|
})
|
|
552
604
|
);
|
|
@@ -614,10 +666,20 @@ i18n.setNamespace("billing", externalBillingPayload);
|
|
|
614
666
|
|
|
615
667
|
### Lazy namespace loading (multi mode)
|
|
616
668
|
|
|
617
|
-
|
|
669
|
+
`.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering. The pattern depends on `delivery`:
|
|
670
|
+
|
|
671
|
+
| Delivery | Loading pattern |
|
|
672
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
673
|
+
| `canonical` | `loadOnInit` for eager namespaces; manual `namespaceLoaders.ns()` for lazy ones. Use `hasNamespace` to skip already-loaded namespaces. |
|
|
674
|
+
| `split-by-locale` / `custom` | Every namespace is lazy. Prefer `ensureNamespacesLoadedForLocale` / `ensureNamespacesLoadedForArea` — they always call `setNamespace()` (safe when switching locale or area on a shared `i18n` instance). |
|
|
675
|
+
|
|
676
|
+
#### Canonical delivery — `loadOnInit` and manual loaders
|
|
677
|
+
|
|
678
|
+
Split namespaces across chunks with `loadOnInit` in **canonical** delivery only. Codegen emits typed `namespaceLoaders` with one dynamic `import()` per lazy namespace.
|
|
618
679
|
|
|
619
680
|
```json
|
|
620
681
|
{
|
|
682
|
+
"delivery": "canonical",
|
|
621
683
|
"namespaces": {
|
|
622
684
|
"default": "translations/default.json",
|
|
623
685
|
"billing": "translations/billing.yaml"
|
|
@@ -629,6 +691,8 @@ Split namespaces across chunks with `loadOnInit` in **canonical** delivery only.
|
|
|
629
691
|
|
|
630
692
|
Codegen also emits `LoadOnInitNamespace`, `LazyNamespace`, and `InitialSchema` types. The generated factory accepts a partial `InitialSchema` at init time.
|
|
631
693
|
|
|
694
|
+
Use `hasNamespace` when loading lazily by hand — namespaces loaded once stay in memory:
|
|
695
|
+
|
|
632
696
|
```ts
|
|
633
697
|
import { i18n, namespaceLoaders, type LazyNamespace } from "./i18n";
|
|
634
698
|
|
|
@@ -648,16 +712,49 @@ await Promise.all(
|
|
|
648
712
|
);
|
|
649
713
|
```
|
|
650
714
|
|
|
651
|
-
|
|
715
|
+
#### Split-by-locale / custom — `ensureNamespacesLoaded*`
|
|
716
|
+
|
|
717
|
+
With `split-by-locale` or `custom`, codegen emits `ensureNamespacesLoadedForLocale` / `ensureNamespacesLoadedForArea`. These helpers **always** reload the requested namespaces — do not guard with `hasNamespace` when switching locale or delivery area on the same instance:
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
import { createI18n } from "./generated/instance.generated.js";
|
|
721
|
+
import {
|
|
722
|
+
ensureNamespacesLoadedForLocale,
|
|
723
|
+
ensureNamespacesLoadedForArea,
|
|
724
|
+
} from "./generated/namespace-loaders.generated.js";
|
|
725
|
+
|
|
726
|
+
const i18n = createI18n({});
|
|
727
|
+
|
|
728
|
+
// split-by-locale
|
|
729
|
+
await ensureNamespacesLoadedForLocale(i18n, "it");
|
|
730
|
+
i18n.get("billing", "invoice_summary", "it", { count: 3 });
|
|
731
|
+
|
|
732
|
+
await ensureNamespacesLoadedForLocale(i18n, "en"); // reloads billing for en
|
|
733
|
+
i18n.get("billing", "invoice_summary", "en", { count: 3 });
|
|
734
|
+
|
|
735
|
+
// custom delivery
|
|
736
|
+
await ensureNamespacesLoadedForArea(i18n, "eu", ["billing"]);
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Route-scoped subset:
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
await ensureNamespacesLoadedForLocale(i18n, activeLocale, ["billing"]);
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Manual loader calls (without the helper) follow the same rule — call `setNamespace` again when the locale or area changes:
|
|
652
746
|
|
|
653
747
|
```ts
|
|
654
748
|
const locale = "it" as const;
|
|
655
749
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
750
|
+
i18n.setNamespace("billing", await namespaceLoaders.billing(locale));
|
|
751
|
+
|
|
752
|
+
// after locale change on the same instance:
|
|
753
|
+
i18n.setNamespace("billing", await namespaceLoaders.billing("en"));
|
|
659
754
|
```
|
|
660
755
|
|
|
756
|
+
Generated loaders throw if locale/area does not match a known artifact (no silent `undefined` into `setNamespace`).
|
|
757
|
+
|
|
661
758
|
Optional locale projection before register:
|
|
662
759
|
|
|
663
760
|
```ts
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ declare const codegenConfigShape: {
|
|
|
7
7
|
namespaces: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
8
8
|
defaultNamespace: z.ZodOptional<z.ZodString>;
|
|
9
9
|
typesOutput: z.ZodString;
|
|
10
|
-
dictionaryOutput: z.ZodString
|
|
10
|
+
dictionaryOutput: z.ZodOptional<z.ZodString>;
|
|
11
11
|
instanceOutput: z.ZodString;
|
|
12
12
|
dictionarySchemaOutput: z.ZodOptional<z.ZodString>;
|
|
13
13
|
loadOnInit: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -37,7 +37,7 @@ declare const codegenConfigSchema: z.ZodObject<{
|
|
|
37
37
|
namespaces: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
38
38
|
defaultNamespace: z.ZodOptional<z.ZodString>;
|
|
39
39
|
typesOutput: z.ZodString;
|
|
40
|
-
dictionaryOutput: z.ZodString
|
|
40
|
+
dictionaryOutput: z.ZodOptional<z.ZodString>;
|
|
41
41
|
instanceOutput: z.ZodString;
|
|
42
42
|
dictionarySchemaOutput: z.ZodOptional<z.ZodString>;
|
|
43
43
|
loadOnInit: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -64,6 +64,8 @@ declare const codegenConfigSchema: z.ZodObject<{
|
|
|
64
64
|
type CodegenConfigInput = z.input<typeof codegenConfigSchema>;
|
|
65
65
|
type CodegenConfig = z.infer<typeof codegenConfigSchema>;
|
|
66
66
|
declare function resolveDeliveryOutputDir(config: Pick<CodegenConfig, "typesOutput" | "deliveryOutput">): string;
|
|
67
|
+
/** Resolved path for dictionary.generated.ts (explicit config or default next to typesOutput). */
|
|
68
|
+
declare function resolveDictionaryOutputPath(config: Pick<CodegenConfig, "typesOutput" | "dictionaryOutput">): string;
|
|
67
69
|
|
|
68
70
|
type DeliveryArtifactsMap = Record<string, readonly string[]>;
|
|
69
71
|
|
|
@@ -82,4 +84,4 @@ declare function typeNamesForProject(project: string): {
|
|
|
82
84
|
|
|
83
85
|
declare function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void;
|
|
84
86
|
|
|
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 };
|
|
87
|
+
export { type CodegenConfig, type CodegenConfigInput, DELIVERY_MODES, type DeliveryArtifactsMap, type DeliveryMode, SUPPORTED_IMPORT_EXTENSIONS, type SetupMode, type SupportedImportExtension, buildCodegenConfig, codegenConfigKeys, inferProjectName, resolveDeliveryOutputDir, resolveDictionaryOutputPath, typeNamesForProject, writeCodegenConfig };
|
package/dist/codegen/index.js
CHANGED
|
@@ -53,7 +53,9 @@ var codegenConfigShape = {
|
|
|
53
53
|
namespaces: z.record(z.string(), z.string().min(1)).optional(),
|
|
54
54
|
defaultNamespace: z.string().min(1).optional(),
|
|
55
55
|
typesOutput: z.string().min(1),
|
|
56
|
-
dictionaryOutput: z.string().min(1)
|
|
56
|
+
dictionaryOutput: z.string().min(1).optional().describe(
|
|
57
|
+
'Output path for dictionary.generated.ts when emitted (canonical, or split/custom with eager namespaces). Omitted in split/custom when every namespace is lazy \u2014 defaults to "{dirname(typesOutput)}/dictionary.generated.ts" for stale-file cleanup only.'
|
|
58
|
+
),
|
|
57
59
|
instanceOutput: z.string().min(1),
|
|
58
60
|
dictionarySchemaOutput: z.string().min(1).optional(),
|
|
59
61
|
loadOnInit: z.array(z.string().min(1)).optional(),
|
|
@@ -133,6 +135,10 @@ z.object(codegenConfigShape).strict().superRefine((config, ctx) => {
|
|
|
133
135
|
function resolveDeliveryOutputDir(config) {
|
|
134
136
|
return config.deliveryOutput ?? path.dirname(config.typesOutput);
|
|
135
137
|
}
|
|
138
|
+
var DEFAULT_DICTIONARY_BASENAME = "dictionary.generated.ts";
|
|
139
|
+
function resolveDictionaryOutputPath(config) {
|
|
140
|
+
return config.dictionaryOutput ?? path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME);
|
|
141
|
+
}
|
|
136
142
|
|
|
137
143
|
// src/codegen-config/type-names.ts
|
|
138
144
|
function inferProjectName(dirName) {
|
|
@@ -183,6 +189,6 @@ function writeCodegenConfig(configPath, config) {
|
|
|
183
189
|
`);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
export { DELIVERY_MODES, SUPPORTED_IMPORT_EXTENSIONS, buildCodegenConfig, codegenConfigKeys, inferProjectName, resolveDeliveryOutputDir, typeNamesForProject, writeCodegenConfig };
|
|
192
|
+
export { DELIVERY_MODES, SUPPORTED_IMPORT_EXTENSIONS, buildCodegenConfig, codegenConfigKeys, inferProjectName, resolveDeliveryOutputDir, resolveDictionaryOutputPath, typeNamesForProject, writeCodegenConfig };
|
|
187
193
|
//# sourceMappingURL=index.js.map
|
|
188
194
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/codegen/constants.ts","../../src/codegen/delivery-artifacts.ts","../../src/codegen/codegen-config-schema.ts","../../src/codegen-config/type-names.ts","../../src/codegen-config/build-config.ts","../../src/codegen-config/write-config.ts"],"names":["path"],"mappings":"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","file":"index.js","sourcesContent":["export const SUPPORTED_IMPORT_EXTENSIONS = [\"none\", \".ts\", \".js\"] as const;\nexport type SupportedImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];\n\n// Translation keys and namespace names become TypeScript identifiers and\n// template-literal type segments in the generated code, so they must be valid\n// identifiers (e.g. \"app.title\" would emit the broken type `\"app.title:...\"`).\nexport const IDENTIFIER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;\nexport const IDENTIFIER_NAME_REQUIREMENT =\n \"allowed: letters, digits, underscore; must not start with a digit\";\n","/** Valid TypeScript identifier / filename segment for a delivery area id. */\nexport const DELIVERY_AREA_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n\nexport type DeliveryArtifactsMap = Record<string, readonly string[]>;\n\nexport type DeliveryArtifactsIssue = {\n path: (string | number)[];\n message: string;\n};\n\nexport function getDeliveryArtifactsStructureIssues(\n deliveryArtifacts: DeliveryArtifactsMap\n): DeliveryArtifactsIssue[] {\n const issues: DeliveryArtifactsIssue[] = [];\n\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n if (!DELIVERY_AREA_NAME_PATTERN.test(area)) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Invalid delivery area name \"${area}\": must match ${DELIVERY_AREA_NAME_PATTERN.source}`,\n });\n }\n\n if (locales.length === 0) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Delivery area \"${area}\" must include at least one locale.`,\n });\n }\n }\n\n const localeToArea = new Map<string, string>();\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n for (const locale of locales) {\n const previousArea = localeToArea.get(locale);\n if (previousArea) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Locale \"${locale}\" appears in both \"${previousArea}\" and \"${area}\".`,\n });\n } else {\n localeToArea.set(locale, area);\n }\n }\n }\n\n return issues;\n}\n\nexport function getDeliveryArtifactsPartitionIssues(\n deliveryArtifacts: DeliveryArtifactsMap,\n requestLocales: ReadonlySet<string>\n): DeliveryArtifactsIssue[] {\n const issues: DeliveryArtifactsIssue[] = [];\n const artifactLocales = new Set<string>();\n\n for (const locales of Object.values(deliveryArtifacts)) {\n for (const locale of locales) {\n artifactLocales.add(locale);\n }\n }\n\n const missing = [...requestLocales].filter((locale) => !artifactLocales.has(locale)).sort();\n if (missing.length > 0) {\n issues.push({\n path: [\"deliveryArtifacts\"],\n message: `deliveryArtifacts is missing locales required by dictionaries and localeFallback: ${missing.join(\", \")}`,\n });\n }\n\n const excess = [...artifactLocales].filter((locale) => !requestLocales.has(locale)).sort();\n if (excess.length > 0) {\n issues.push({\n path: [\"deliveryArtifacts\"],\n message: `deliveryArtifacts includes locales not required by dictionaries and localeFallback: ${excess.join(\", \")}`,\n });\n }\n\n return issues;\n}\n\n/** Pre-emit validation gate for `deliveryArtifacts` in custom delivery mode. */\nexport function getDeliveryArtifactsIssues(\n deliveryArtifacts: DeliveryArtifactsMap,\n requestLocales: ReadonlySet<string>\n): DeliveryArtifactsIssue[] {\n return [\n ...getDeliveryArtifactsStructureIssues(deliveryArtifacts),\n ...getDeliveryArtifactsPartitionIssues(deliveryArtifacts, requestLocales),\n ];\n}\n\nexport function getDeliveryAreaNames(deliveryArtifacts: DeliveryArtifactsMap): string[] {\n return Object.keys(deliveryArtifacts).sort();\n}\n\nexport function getLocaleDeliveryAreaMap(\n deliveryArtifacts: DeliveryArtifactsMap\n): Record<string, string> {\n const localeToArea: Record<string, string> = {};\n\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n for (const locale of locales) {\n localeToArea[locale] = area;\n }\n }\n\n return localeToArea;\n}\n\nexport function formatLocaleDeliveryAreaBlock(\n deliveryArtifacts: DeliveryArtifactsMap,\n constName: string,\n localeTypeName: string,\n deliveryAreaTypeName: string\n): string {\n const localeToArea = getLocaleDeliveryAreaMap(deliveryArtifacts);\n const lines = Object.keys(localeToArea)\n .sort()\n .map((locale) => ` ${JSON.stringify(locale)}: ${JSON.stringify(localeToArea[locale])},`)\n .join(\"\\n\");\n\n return `export const ${constName} = {\\n${lines}\\n} as const satisfies Record<${localeTypeName}, ${deliveryAreaTypeName}>;\\n\\n`;\n}\n","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 if (config.loadOnInit !== undefined && config.delivery !== \"canonical\") {\n ctx.addIssue({\n code: \"custom\",\n path: [\"loadOnInit\"],\n message: 'loadOnInit is only allowed when delivery is \"canonical\".',\n });\n }\n });\n\nexport type CodegenConfigInput = z.input<typeof codegenConfigSchema>;\nexport type CodegenConfig = z.infer<typeof codegenConfigSchema>;\n\nexport function resolveDeliveryOutputDir(\n config: Pick<CodegenConfig, \"typesOutput\" | \"deliveryOutput\">\n): string {\n return config.deliveryOutput ?? path.dirname(config.typesOutput);\n}\n\nexport function formatCodegenConfigIssues(error: z.ZodError): string {\n const issueLines = error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"(root)\";\n return ` - ${path}: ${issue.message}`;\n });\n\n return [\n \"[Codegen Error] Invalid i18n.codegen.json:\",\n ...issueLines,\n \"\",\n `Allowed keys: ${codegenConfigKeys.join(\", \")}`,\n ].join(\"\\n\");\n}\n","export function inferProjectName(dirName: string): string {\n const parts = dirName.split(/[-_]+/).filter(Boolean);\n if (parts.length === 0) {\n return \"App\";\n }\n\n return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(\"\");\n}\n\nexport function typeNamesForProject(project: string): {\n paramsTypeName: string;\n schemaTypeName: string;\n localeTypeName: string;\n} {\n return {\n paramsTypeName: `${project}Params`,\n schemaTypeName: `${project}Schema`,\n localeTypeName: `${project}Locale`,\n };\n}\n","import type { CodegenConfigInput } from \"../codegen/codegen-config-schema.js\";\nimport { typeNamesForProject } from \"./type-names.js\";\n\nexport type SetupMode = \"single\" | \"multi\";\n\nconst GENERATED_DIR = \"generated\";\nconst TRANSLATIONS_DIR = \"translations\";\n\nexport function buildCodegenConfig(mode: SetupMode, project: string): CodegenConfigInput {\n const typeNames = typeNamesForProject(project);\n const base = {\n typesOutput: `${GENERATED_DIR}/i18n-types.generated.ts`,\n dictionaryOutput: `${GENERATED_DIR}/dictionary.generated.ts`,\n instanceOutput: `${GENERATED_DIR}/instance.generated.ts`,\n paramsTypeName: typeNames.paramsTypeName,\n schemaTypeName: typeNames.schemaTypeName,\n localeTypeName: typeNames.localeTypeName,\n factoryName: \"createI18n\",\n } satisfies CodegenConfigInput;\n\n if (mode === \"single\") {\n return {\n dictionary: `${TRANSLATIONS_DIR}/translations.json`,\n ...base,\n };\n }\n\n return {\n namespaces: {\n default: `${TRANSLATIONS_DIR}/default.json`,\n },\n ...base,\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { CodegenConfigInput } from \"../codegen/codegen-config-schema.js\";\n\nexport function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void {\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/codegen/constants.ts","../../src/codegen/delivery-artifacts.ts","../../src/codegen/codegen-config-schema.ts","../../src/codegen-config/type-names.ts","../../src/codegen-config/build-config.ts","../../src/codegen-config/write-config.ts"],"names":["path"],"mappings":";;;;;;;AAAO,IAAM,2BAAA,GAA8B,CAAC,MAAA,EAAQ,KAAA,EAAO,KAAK;AAMzD,IAAM,uBAAA,GAA0B,0BAAA;AAChC,IAAM,2BAAA,GACX,mEAAA;;;ACPK,IAAM,0BAAA,GAA6B,0BAAA;AASnC,SAAS,oCACd,iBAAA,EAC0B;AAC1B,EAAA,MAAM,SAAmC,EAAC;AAE1C,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA,EAAG;AAC/D,IAAA,IAAI,CAAC,0BAAA,CAA2B,IAAA,CAAK,IAAI,CAAA,EAAG;AAC1C,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,CAAC,mBAAA,EAAqB,IAAI,CAAA;AAAA,QAChC,OAAA,EAAS,CAAA,4BAAA,EAA+B,IAAI,CAAA,cAAA,EAAiB,2BAA2B,MAAM,CAAA;AAAA,OAC/F,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,CAAC,mBAAA,EAAqB,IAAI,CAAA;AAAA,QAChC,OAAA,EAAS,kBAAkB,IAAI,CAAA,mCAAA;AAAA,OAChC,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,MAAM,YAAA,uBAAmB,GAAA,EAAoB;AAC7C,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA,EAAG;AAC/D,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,YAAA,GAAe,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AAC5C,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAA,CAAO,IAAA,CAAK;AAAA,UACV,IAAA,EAAM,CAAC,mBAAA,EAAqB,IAAI,CAAA;AAAA,UAChC,SAAS,CAAA,QAAA,EAAW,MAAM,CAAA,mBAAA,EAAsB,YAAY,UAAU,IAAI,CAAA,EAAA;AAAA,SAC3E,CAAA;AAAA,MACH,CAAA,MAAO;AACL,QAAA,YAAA,CAAa,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;ACtCA,IAAM,uBAAuB,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,IAAU,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAA;AACjF,IAAM,uBAAA,GAA0B,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAC,CAAC,CAAC,CAAA;AAExE,IAAM,cAAA,GAAiB,CAAC,WAAA,EAAa,iBAAA,EAAmB,QAAQ;AAGvE,IAAM,kBAAA,GAAqB;AAAA,EACzB,YAAY,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACvC,UAAA,EAAY,CAAA,CAAE,MAAA,CAAO,CAAA,CAAE,MAAA,EAAO,EAAG,CAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAC,CAAC,EAAE,QAAA,EAAS;AAAA,EAC7D,kBAAkB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EAC7C,WAAA,EAAa,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAC7B,gBAAA,EAAkB,EACf,MAAA,EAAO,CACP,IAAI,CAAC,CAAA,CACL,UAAS,CACT,QAAA;AAAA,IACC;AAAA,GACF;AAAA,EACF,cAAA,EAAgB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAChC,wBAAwB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACnD,UAAA,EAAY,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,CAAC,CAAA,CAAE,QAAA,EAAS;AAAA,EAChD,wBAAwB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACnD,eAAA,EAAiB,CAAA,CAAE,IAAA,CAAK,2BAA2B,EAAE,QAAA,EAAS;AAAA,EAC9D,cAAA,EAAgB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAChC,cAAA,EAAgB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAChC,gBAAgB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EAC3C,yBAAyB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACpD,cAAA,EAAgB,qBAAqB,QAAA,EAAS;AAAA,EAC9C,aAAa,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA,EACxC,QAAA,EAAU,EAAE,IAAA,CAAK,cAAc,EAAE,QAAA,EAAS,CAAE,QAAQ,WAAW,CAAA;AAAA,EAC/D,iBAAA,EAAmB,wBAAwB,QAAA,EAAS;AAAA,EACpD,gBAAgB,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AACpC,CAAA;AAEO,IAAM,oBAAoB,MAAA,CAAO,IAAA;AAAA,EACtC;AACF;AAEmC,CAAA,CAChC,MAAA,CAAO,kBAAkB,CAAA,CACzB,QAAO,CACP,WAAA,CAAY,CAAC,MAAA,EAAQ,GAAA,KAAQ;AAC5B,EAAA,MAAM,aAAA,GAAgB,OAAO,UAAA,KAAe,MAAA;AAC5C,EAAA,MAAM,aAAA,GAAgB,OAAO,UAAA,KAAe,MAAA;AAE5C,EAAA,IAAI,kBAAkB,aAAA,EAAe;AACnC,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,OAAO,UAAA,EAAY;AACrB,IAAA,KAAA,MAAW,SAAA,IAAa,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,UAAU,CAAA,EAAG;AACtD,MAAA,IAAI,CAAC,uBAAA,CAAwB,IAAA,CAAK,SAAS,CAAA,EAAG;AAC5C,QAAA,GAAA,CAAI,QAAA,CAAS;AAAA,UACX,IAAA,EAAM,QAAA;AAAA,UACN,IAAA,EAAM,CAAC,YAAA,EAAc,SAAS,CAAA;AAAA,UAC9B,OAAA,EAAS,CAAA,wBAAA,EAA2B,SAAS,CAAA,GAAA,EAAM,2BAA2B,CAAA,EAAA;AAAA,SAC/E,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IACE,MAAA,CAAO,qBAAqB,MAAA,IAC5B,CAAC,wBAAwB,IAAA,CAAK,MAAA,CAAO,gBAAgB,CAAA,EACrD;AACA,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,CAAC,kBAAkB,CAAA;AAAA,MACzB,OAAA,EAAS,CAAA,wBAAA,EAA2B,MAAA,CAAO,gBAAgB,MAAM,2BAA2B,CAAA,EAAA;AAAA,KAC7F,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,MAAA,CAAO,aAAa,QAAA,EAAU;AAChC,IAAA,IAAI,CAAC,OAAO,iBAAA,EAAmB;AAC7B,MAAA,GAAA,CAAI,QAAA,CAAS;AAAA,QACX,IAAA,EAAM,QAAA;AAAA,QACN,IAAA,EAAM,CAAC,mBAAmB,CAAA;AAAA,QAC1B,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH,CAAA,MAAO;AACL,MAAA,KAAA,MAAW,KAAA,IAAS,mCAAA,CAAoC,MAAA,CAAO,iBAAiB,CAAA,EAAG;AACjF,QAAA,GAAA,CAAI,QAAA,CAAS;AAAA,UACX,IAAA,EAAM,QAAA;AAAA,UACN,MAAM,KAAA,CAAM,IAAA;AAAA,UACZ,SAAS,KAAA,CAAM;AAAA,SAChB,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAA,MAAA,IAAW,MAAA,CAAO,iBAAA,KAAsB,MAAA,EAAW;AACjD,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,CAAC,mBAAmB,CAAA;AAAA,MAC1B,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,MAAA,CAAO,UAAA,KAAe,MAAA,IAAa,MAAA,CAAO,aAAa,WAAA,EAAa;AACtE,IAAA,GAAA,CAAI,QAAA,CAAS;AAAA,MACX,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,CAAC,YAAY,CAAA;AAAA,MACnB,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AACF,CAAC;AAKI,SAAS,yBACd,MAAA,EACQ;AACR,EAAA,OAAO,MAAA,CAAO,cAAA,IAAkB,IAAA,CAAK,OAAA,CAAQ,OAAO,WAAW,CAAA;AACjE;AAEA,IAAM,2BAAA,GAA8B,yBAAA;AAG7B,SAAS,4BACd,MAAA,EACQ;AACR,EAAA,OACE,MAAA,CAAO,oBACP,IAAA,CAAK,IAAA,CAAK,KAAK,OAAA,CAAQ,MAAA,CAAO,WAAW,CAAA,EAAG,2BAA2B,CAAA;AAE3E;;;ACxIO,SAAS,iBAAiB,OAAA,EAAyB;AACxD,EAAA,MAAM,QAAQ,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,CAAE,OAAO,OAAO,CAAA;AACnD,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAM,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,OAAO,CAAC,CAAA,CAAE,WAAA,EAAY,GAAI,KAAK,KAAA,CAAM,CAAC,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAClF;AAEO,SAAS,oBAAoB,OAAA,EAIlC;AACA,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB,GAAG,OAAO,CAAA,MAAA,CAAA;AAAA,IAC1B,cAAA,EAAgB,GAAG,OAAO,CAAA,MAAA,CAAA;AAAA,IAC1B,cAAA,EAAgB,GAAG,OAAO,CAAA,MAAA;AAAA,GAC5B;AACF;;;ACdA,IAAM,aAAA,GAAgB,WAAA;AACtB,IAAM,gBAAA,GAAmB,cAAA;AAElB,SAAS,kBAAA,CAAmB,MAAiB,OAAA,EAAqC;AACvF,EAAA,MAAM,SAAA,GAAY,oBAAoB,OAAO,CAAA;AAC7C,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,WAAA,EAAa,GAAG,aAAa,CAAA,wBAAA,CAAA;AAAA,IAC7B,gBAAA,EAAkB,GAAG,aAAa,CAAA,wBAAA,CAAA;AAAA,IAClC,cAAA,EAAgB,GAAG,aAAa,CAAA,sBAAA,CAAA;AAAA,IAChC,gBAAgB,SAAA,CAAU,cAAA;AAAA,IAC1B,gBAAgB,SAAA,CAAU,cAAA;AAAA,IAC1B,gBAAgB,SAAA,CAAU,cAAA;AAAA,IAC1B,WAAA,EAAa;AAAA,GACf;AAEA,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,GAAG,gBAAgB,CAAA,kBAAA,CAAA;AAAA,MAC/B,GAAG;AAAA,KACL;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,UAAA,EAAY;AAAA,MACV,OAAA,EAAS,GAAG,gBAAgB,CAAA,aAAA;AAAA,KAC9B;AAAA,IACA,GAAG;AAAA,GACL;AACF;AC7BO,SAAS,kBAAA,CAAmB,YAAoB,MAAA,EAAkC;AACvF,EAAA,EAAA,CAAG,SAAA,CAAUA,KAAK,OAAA,CAAQ,UAAU,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAC1D,EAAA,EAAA,CAAG,aAAA,CAAc,YAAY,CAAA,EAAG,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAC;AAAA,CAAI,CAAA;AACrE","file":"index.js","sourcesContent":["export const SUPPORTED_IMPORT_EXTENSIONS = [\"none\", \".ts\", \".js\"] as const;\nexport type SupportedImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];\n\n// Translation keys and namespace names become TypeScript identifiers and\n// template-literal type segments in the generated code, so they must be valid\n// identifiers (e.g. \"app.title\" would emit the broken type `\"app.title:...\"`).\nexport const IDENTIFIER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;\nexport const IDENTIFIER_NAME_REQUIREMENT =\n \"allowed: letters, digits, underscore; must not start with a digit\";\n","/** Valid TypeScript identifier / filename segment for a delivery area id. */\nexport const DELIVERY_AREA_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n\nexport type DeliveryArtifactsMap = Record<string, readonly string[]>;\n\nexport type DeliveryArtifactsIssue = {\n path: (string | number)[];\n message: string;\n};\n\nexport function getDeliveryArtifactsStructureIssues(\n deliveryArtifacts: DeliveryArtifactsMap\n): DeliveryArtifactsIssue[] {\n const issues: DeliveryArtifactsIssue[] = [];\n\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n if (!DELIVERY_AREA_NAME_PATTERN.test(area)) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Invalid delivery area name \"${area}\": must match ${DELIVERY_AREA_NAME_PATTERN.source}`,\n });\n }\n\n if (locales.length === 0) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Delivery area \"${area}\" must include at least one locale.`,\n });\n }\n }\n\n const localeToArea = new Map<string, string>();\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n for (const locale of locales) {\n const previousArea = localeToArea.get(locale);\n if (previousArea) {\n issues.push({\n path: [\"deliveryArtifacts\", area],\n message: `Locale \"${locale}\" appears in both \"${previousArea}\" and \"${area}\".`,\n });\n } else {\n localeToArea.set(locale, area);\n }\n }\n }\n\n return issues;\n}\n\nexport function getDeliveryArtifactsPartitionIssues(\n deliveryArtifacts: DeliveryArtifactsMap,\n requestLocales: ReadonlySet<string>\n): DeliveryArtifactsIssue[] {\n const issues: DeliveryArtifactsIssue[] = [];\n const artifactLocales = new Set<string>();\n\n for (const locales of Object.values(deliveryArtifacts)) {\n for (const locale of locales) {\n artifactLocales.add(locale);\n }\n }\n\n const missing = [...requestLocales].filter((locale) => !artifactLocales.has(locale)).sort();\n if (missing.length > 0) {\n issues.push({\n path: [\"deliveryArtifacts\"],\n message: `deliveryArtifacts is missing locales required by dictionaries and localeFallback: ${missing.join(\", \")}`,\n });\n }\n\n const excess = [...artifactLocales].filter((locale) => !requestLocales.has(locale)).sort();\n if (excess.length > 0) {\n issues.push({\n path: [\"deliveryArtifacts\"],\n message: `deliveryArtifacts includes locales not required by dictionaries and localeFallback: ${excess.join(\", \")}`,\n });\n }\n\n return issues;\n}\n\n/** Pre-emit validation gate for `deliveryArtifacts` in custom delivery mode. */\nexport function getDeliveryArtifactsIssues(\n deliveryArtifacts: DeliveryArtifactsMap,\n requestLocales: ReadonlySet<string>\n): DeliveryArtifactsIssue[] {\n return [\n ...getDeliveryArtifactsStructureIssues(deliveryArtifacts),\n ...getDeliveryArtifactsPartitionIssues(deliveryArtifacts, requestLocales),\n ];\n}\n\nexport function getDeliveryAreaNames(deliveryArtifacts: DeliveryArtifactsMap): string[] {\n return Object.keys(deliveryArtifacts).sort();\n}\n\nexport function getLocaleDeliveryAreaMap(\n deliveryArtifacts: DeliveryArtifactsMap\n): Record<string, string> {\n const localeToArea: Record<string, string> = {};\n\n for (const [area, locales] of Object.entries(deliveryArtifacts)) {\n for (const locale of locales) {\n localeToArea[locale] = area;\n }\n }\n\n return localeToArea;\n}\n\nexport function formatLocaleDeliveryAreaBlock(\n deliveryArtifacts: DeliveryArtifactsMap,\n constName: string,\n localeTypeName: string,\n deliveryAreaTypeName: string\n): string {\n const localeToArea = getLocaleDeliveryAreaMap(deliveryArtifacts);\n const lines = Object.keys(localeToArea)\n .sort()\n .map((locale) => ` ${JSON.stringify(locale)}: ${JSON.stringify(localeToArea[locale])},`)\n .join(\"\\n\");\n\n return `export const ${constName} = {\\n${lines}\\n} as const satisfies Record<${localeTypeName}, ${deliveryAreaTypeName}>;\\n\\n`;\n}\n\nexport function getDeliveryArtifactsTypeName(deliveryAreaTypeName: string): string {\n return deliveryAreaTypeName.endsWith(\"DeliveryArea\")\n ? deliveryAreaTypeName.replace(/DeliveryArea$/, \"DeliveryArtifacts\")\n : `${deliveryAreaTypeName}Artifacts`;\n}\n\nexport function formatDeliveryArtifactsBlock(\n deliveryArtifacts: DeliveryArtifactsMap,\n constName: string,\n localeTypeName: string,\n deliveryAreaTypeName: string\n): string {\n const deliveryArtifactsTypeName = getDeliveryArtifactsTypeName(deliveryAreaTypeName);\n const areaEntries = getDeliveryAreaNames(deliveryArtifacts)\n .map((area) => {\n const locales = [...deliveryArtifacts[area]!]\n .sort()\n .map((locale) => JSON.stringify(locale))\n .join(\", \");\n return ` ${JSON.stringify(area)}: [${locales}] as const,`;\n })\n .join(\"\\n\");\n\n return (\n `export const ${constName} = {\\n${areaEntries}\\n} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;\\n\\n` +\n `export type ${deliveryArtifactsTypeName} = typeof ${constName};\\n\\n`\n );\n}\n","import { z } from \"zod\";\nimport path from \"node:path\";\nimport {\n IDENTIFIER_NAME_PATTERN,\n IDENTIFIER_NAME_REQUIREMENT,\n SUPPORTED_IMPORT_EXTENSIONS,\n} from \"./constants.js\";\nimport { getDeliveryArtifactsStructureIssues } from \"./delivery-artifacts.js\";\n\nconst localeFallbackSchema = z.record(z.string(), z.union([z.string(), z.null()]));\nconst deliveryArtifactsSchema = z.record(z.string(), z.array(z.string().min(1)));\n\nexport const DELIVERY_MODES = [\"canonical\", \"split-by-locale\", \"custom\"] as const;\nexport type DeliveryMode = (typeof DELIVERY_MODES)[number];\n\nconst codegenConfigShape = {\n dictionary: z.string().min(1).optional(),\n namespaces: z.record(z.string(), z.string().min(1)).optional(),\n defaultNamespace: z.string().min(1).optional(),\n typesOutput: z.string().min(1),\n dictionaryOutput: z\n .string()\n .min(1)\n .optional()\n .describe(\n 'Output path for dictionary.generated.ts when emitted (canonical, or split/custom with eager namespaces). Omitted in split/custom when every namespace is lazy — defaults to \"{dirname(typesOutput)}/dictionary.generated.ts\" for stale-file cleanup only.'\n ),\n instanceOutput: z.string().min(1),\n dictionarySchemaOutput: z.string().min(1).optional(),\n loadOnInit: z.array(z.string().min(1)).optional(),\n namespaceLoadersOutput: z.string().min(1).optional(),\n importExtension: z.enum(SUPPORTED_IMPORT_EXTENSIONS).optional(),\n paramsTypeName: z.string().min(1),\n schemaTypeName: z.string().min(1),\n localeTypeName: z.string().min(1).optional(),\n localeFallbackConstName: z.string().min(1).optional(),\n localeFallback: localeFallbackSchema.optional(),\n factoryName: z.string().min(1).optional(),\n delivery: z.enum(DELIVERY_MODES).optional().default(\"canonical\"),\n deliveryArtifacts: deliveryArtifactsSchema.optional(),\n deliveryOutput: z.string().min(1).optional(),\n};\n\nexport const codegenConfigKeys = Object.keys(\n codegenConfigShape\n) as (keyof typeof codegenConfigShape)[];\n\nexport const codegenConfigSchema = z\n .object(codegenConfigShape)\n .strict()\n .superRefine((config, ctx) => {\n const hasDictionary = config.dictionary !== undefined;\n const hasNamespaces = config.namespaces !== undefined;\n\n if (hasDictionary === hasNamespaces) {\n ctx.addIssue({\n code: \"custom\",\n message: 'Specify exactly one of \"dictionary\" or \"namespaces\".',\n });\n }\n\n if (config.namespaces) {\n for (const namespace of Object.keys(config.namespaces)) {\n if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"namespaces\", namespace],\n message: `Invalid namespace name \"${namespace}\" (${IDENTIFIER_NAME_REQUIREMENT}).`,\n });\n }\n }\n }\n\n if (\n config.defaultNamespace !== undefined &&\n !IDENTIFIER_NAME_PATTERN.test(config.defaultNamespace)\n ) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"defaultNamespace\"],\n message: `Invalid namespace name \"${config.defaultNamespace}\" (${IDENTIFIER_NAME_REQUIREMENT}).`,\n });\n }\n\n if (config.delivery === \"custom\") {\n if (!config.deliveryArtifacts) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"deliveryArtifacts\"],\n message: 'deliveryArtifacts is required when delivery is \"custom\".',\n });\n } else {\n for (const issue of getDeliveryArtifactsStructureIssues(config.deliveryArtifacts)) {\n ctx.addIssue({\n code: \"custom\",\n path: issue.path,\n message: issue.message,\n });\n }\n }\n } else if (config.deliveryArtifacts !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n path: [\"deliveryArtifacts\"],\n message: 'deliveryArtifacts is only allowed when delivery is \"custom\".',\n });\n }\n\n if (config.loadOnInit !== undefined && config.delivery !== \"canonical\") {\n ctx.addIssue({\n code: \"custom\",\n path: [\"loadOnInit\"],\n message: 'loadOnInit is only allowed when delivery is \"canonical\".',\n });\n }\n });\n\nexport type CodegenConfigInput = z.input<typeof codegenConfigSchema>;\nexport type CodegenConfig = z.infer<typeof codegenConfigSchema>;\n\nexport function resolveDeliveryOutputDir(\n config: Pick<CodegenConfig, \"typesOutput\" | \"deliveryOutput\">\n): string {\n return config.deliveryOutput ?? path.dirname(config.typesOutput);\n}\n\nconst DEFAULT_DICTIONARY_BASENAME = \"dictionary.generated.ts\";\n\n/** Resolved path for dictionary.generated.ts (explicit config or default next to typesOutput). */\nexport function resolveDictionaryOutputPath(\n config: Pick<CodegenConfig, \"typesOutput\" | \"dictionaryOutput\">\n): string {\n return (\n config.dictionaryOutput ??\n path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME)\n );\n}\n\nexport function formatCodegenConfigIssues(error: z.ZodError): string {\n const issueLines = error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"(root)\";\n return ` - ${path}: ${issue.message}`;\n });\n\n return [\n \"[Codegen Error] Invalid i18n.codegen.json:\",\n ...issueLines,\n \"\",\n `Allowed keys: ${codegenConfigKeys.join(\", \")}`,\n ].join(\"\\n\");\n}\n","export function inferProjectName(dirName: string): string {\n const parts = dirName.split(/[-_]+/).filter(Boolean);\n if (parts.length === 0) {\n return \"App\";\n }\n\n return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(\"\");\n}\n\nexport function typeNamesForProject(project: string): {\n paramsTypeName: string;\n schemaTypeName: string;\n localeTypeName: string;\n} {\n return {\n paramsTypeName: `${project}Params`,\n schemaTypeName: `${project}Schema`,\n localeTypeName: `${project}Locale`,\n };\n}\n","import type { CodegenConfigInput } from \"../codegen/codegen-config-schema.js\";\nimport { typeNamesForProject } from \"./type-names.js\";\n\nexport type SetupMode = \"single\" | \"multi\";\n\nconst GENERATED_DIR = \"generated\";\nconst TRANSLATIONS_DIR = \"translations\";\n\nexport function buildCodegenConfig(mode: SetupMode, project: string): CodegenConfigInput {\n const typeNames = typeNamesForProject(project);\n const base = {\n typesOutput: `${GENERATED_DIR}/i18n-types.generated.ts`,\n dictionaryOutput: `${GENERATED_DIR}/dictionary.generated.ts`,\n instanceOutput: `${GENERATED_DIR}/instance.generated.ts`,\n paramsTypeName: typeNames.paramsTypeName,\n schemaTypeName: typeNames.schemaTypeName,\n localeTypeName: typeNames.localeTypeName,\n factoryName: \"createI18n\",\n } satisfies CodegenConfigInput;\n\n if (mode === \"single\") {\n return {\n dictionary: `${TRANSLATIONS_DIR}/translations.json`,\n ...base,\n };\n }\n\n return {\n namespaces: {\n default: `${TRANSLATIONS_DIR}/default.json`,\n },\n ...base,\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { CodegenConfigInput } from \"../codegen/codegen-config-schema.js\";\n\nexport function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void {\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -18,7 +18,13 @@ const codegenConfigShape = {
|
|
|
18
18
|
namespaces: z.record(z.string(), z.string().min(1)).optional(),
|
|
19
19
|
defaultNamespace: z.string().min(1).optional(),
|
|
20
20
|
typesOutput: z.string().min(1),
|
|
21
|
-
dictionaryOutput: z
|
|
21
|
+
dictionaryOutput: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(
|
|
26
|
+
'Output path for dictionary.generated.ts when emitted (canonical, or split/custom with eager namespaces). Omitted in split/custom when every namespace is lazy — defaults to "{dirname(typesOutput)}/dictionary.generated.ts" for stale-file cleanup only.'
|
|
27
|
+
),
|
|
22
28
|
instanceOutput: z.string().min(1),
|
|
23
29
|
dictionarySchemaOutput: z.string().min(1).optional(),
|
|
24
30
|
loadOnInit: z.array(z.string().min(1)).optional(),
|
|
@@ -118,6 +124,18 @@ export function resolveDeliveryOutputDir(
|
|
|
118
124
|
return config.deliveryOutput ?? path.dirname(config.typesOutput);
|
|
119
125
|
}
|
|
120
126
|
|
|
127
|
+
const DEFAULT_DICTIONARY_BASENAME = "dictionary.generated.ts";
|
|
128
|
+
|
|
129
|
+
/** Resolved path for dictionary.generated.ts (explicit config or default next to typesOutput). */
|
|
130
|
+
export function resolveDictionaryOutputPath(
|
|
131
|
+
config: Pick<CodegenConfig, "typesOutput" | "dictionaryOutput">
|
|
132
|
+
): string {
|
|
133
|
+
return (
|
|
134
|
+
config.dictionaryOutput ??
|
|
135
|
+
path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
export function formatCodegenConfigIssues(error: z.ZodError): string {
|
|
122
140
|
const issueLines = error.issues.map((issue) => {
|
|
123
141
|
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { codegenConfigKeys } from "./codegen-config-schema.js";
|
|
5
|
+
import { codegenConfigKeys, resolveDictionaryOutputPath } from "./codegen-config-schema.js";
|
|
6
6
|
import { loadConfig, resolveDeliveryOutputDir, resolveLoadOnInit } from "./config.js";
|
|
7
7
|
import { resolveImportExtension } from "./paths.js";
|
|
8
8
|
|
|
@@ -42,6 +42,35 @@ describe("loadConfig", () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it("accepts split-by-locale config without dictionaryOutput", () => {
|
|
46
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
47
|
+
const { dictionaryOutput: _dictionaryOutput, ...configWithoutDictionaryOutput } =
|
|
48
|
+
validMultiConfig;
|
|
49
|
+
const configPath = writeConfig(tempDir, {
|
|
50
|
+
...configWithoutDictionaryOutput,
|
|
51
|
+
delivery: "split-by-locale",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(loadConfig(configPath)).toEqual({
|
|
55
|
+
...configWithoutDictionaryOutput,
|
|
56
|
+
delivery: "split-by-locale",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("resolves dictionaryOutput next to typesOutput when omitted", () => {
|
|
61
|
+
expect(
|
|
62
|
+
resolveDictionaryOutputPath({
|
|
63
|
+
typesOutput: "generated/i18n-types.generated.ts",
|
|
64
|
+
})
|
|
65
|
+
).toBe("generated/dictionary.generated.ts");
|
|
66
|
+
expect(
|
|
67
|
+
resolveDictionaryOutputPath({
|
|
68
|
+
typesOutput: "generated/i18n-types.generated.ts",
|
|
69
|
+
dictionaryOutput: "custom/dictionary.generated.ts",
|
|
70
|
+
})
|
|
71
|
+
).toBe("custom/dictionary.generated.ts");
|
|
72
|
+
});
|
|
73
|
+
|
|
45
74
|
it("throws on invalid config JSON", () => {
|
|
46
75
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
47
76
|
const configPath = join(tempDir, "i18n.codegen.json");
|
package/src/codegen/config.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
formatCodegenConfigIssues,
|
|
7
7
|
} from "./codegen-config-schema.js";
|
|
8
8
|
|
|
9
|
-
export { resolveDeliveryOutputDir } from "./codegen-config-schema.js";
|
|
9
|
+
export { resolveDeliveryOutputDir, resolveDictionaryOutputPath } from "./codegen-config-schema.js";
|
|
10
10
|
|
|
11
11
|
/** Parses and validates `i18n.codegen.json`; first step of the codegen pipeline. */
|
|
12
12
|
export function loadConfig(configPath: string): CodegenConfig {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
formatDeliveryArtifactsBlock,
|
|
3
4
|
formatLocaleDeliveryAreaBlock,
|
|
4
5
|
getDeliveryAreaNames,
|
|
5
6
|
getDeliveryArtifactsIssues,
|
|
6
7
|
getDeliveryArtifactsPartitionIssues,
|
|
7
8
|
getDeliveryArtifactsStructureIssues,
|
|
9
|
+
getDeliveryArtifactsTypeName,
|
|
8
10
|
getLocaleDeliveryAreaMap,
|
|
9
11
|
} from "./delivery-artifacts.js";
|
|
10
12
|
|
|
@@ -139,4 +141,26 @@ describe("delivery-artifacts", () => {
|
|
|
139
141
|
"} as const satisfies Record<AppLocale, AppDeliveryArea>;\n\n"
|
|
140
142
|
);
|
|
141
143
|
});
|
|
144
|
+
|
|
145
|
+
it("formats delivery artifacts block for generated types", () => {
|
|
146
|
+
expect(getDeliveryArtifactsTypeName("AppDeliveryArea")).toBe("AppDeliveryArtifacts");
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
formatDeliveryArtifactsBlock(
|
|
150
|
+
{
|
|
151
|
+
eu: ["it", "fr"],
|
|
152
|
+
us: ["en-US"],
|
|
153
|
+
},
|
|
154
|
+
"DELIVERY_ARTIFACTS",
|
|
155
|
+
"AppLocale",
|
|
156
|
+
"AppDeliveryArea"
|
|
157
|
+
)
|
|
158
|
+
).toBe(
|
|
159
|
+
"export const DELIVERY_ARTIFACTS = {\n" +
|
|
160
|
+
' "eu": ["fr", "it"] as const,\n' +
|
|
161
|
+
' "us": ["en-US"] as const,\n' +
|
|
162
|
+
"} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;\n\n" +
|
|
163
|
+
"export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;\n\n"
|
|
164
|
+
);
|
|
165
|
+
});
|
|
142
166
|
});
|
|
@@ -122,3 +122,32 @@ export function formatLocaleDeliveryAreaBlock(
|
|
|
122
122
|
|
|
123
123
|
return `export const ${constName} = {\n${lines}\n} as const satisfies Record<${localeTypeName}, ${deliveryAreaTypeName}>;\n\n`;
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
export function getDeliveryArtifactsTypeName(deliveryAreaTypeName: string): string {
|
|
127
|
+
return deliveryAreaTypeName.endsWith("DeliveryArea")
|
|
128
|
+
? deliveryAreaTypeName.replace(/DeliveryArea$/, "DeliveryArtifacts")
|
|
129
|
+
: `${deliveryAreaTypeName}Artifacts`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function formatDeliveryArtifactsBlock(
|
|
133
|
+
deliveryArtifacts: DeliveryArtifactsMap,
|
|
134
|
+
constName: string,
|
|
135
|
+
localeTypeName: string,
|
|
136
|
+
deliveryAreaTypeName: string
|
|
137
|
+
): string {
|
|
138
|
+
const deliveryArtifactsTypeName = getDeliveryArtifactsTypeName(deliveryAreaTypeName);
|
|
139
|
+
const areaEntries = getDeliveryAreaNames(deliveryArtifacts)
|
|
140
|
+
.map((area) => {
|
|
141
|
+
const locales = [...deliveryArtifacts[area]!]
|
|
142
|
+
.sort()
|
|
143
|
+
.map((locale) => JSON.stringify(locale))
|
|
144
|
+
.join(", ");
|
|
145
|
+
return ` ${JSON.stringify(area)}: [${locales}] as const,`;
|
|
146
|
+
})
|
|
147
|
+
.join("\n");
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
`export const ${constName} = {\n${areaEntries}\n} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;\n\n` +
|
|
151
|
+
`export type ${deliveryArtifactsTypeName} = typeof ${constName};\n\n`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -90,6 +90,9 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
90
90
|
expect(output).toContain('case "en":');
|
|
91
91
|
expect(output).toContain('case "it":');
|
|
92
92
|
expect(output).toContain('case "de-CH":');
|
|
93
|
+
expect(output).toContain(
|
|
94
|
+
'throw new Error(`[i18n] No translation artifact for namespace "billing" and locale "${String(locale)}".`);'
|
|
95
|
+
);
|
|
93
96
|
expect(output).toContain(
|
|
94
97
|
"return import('./generated/translations/billing.en.json').then((m) => m.default);"
|
|
95
98
|
);
|
|
@@ -179,6 +182,9 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
179
182
|
expect(output).toContain("billing: (area) => {");
|
|
180
183
|
expect(output).toContain('case "eu":');
|
|
181
184
|
expect(output).toContain('case "us":');
|
|
185
|
+
expect(output).toContain(
|
|
186
|
+
'throw new Error(`[i18n] No translation artifact for namespace "billing" and area "${String(area)}".`);'
|
|
187
|
+
);
|
|
182
188
|
expect(output).toContain(
|
|
183
189
|
"return import('./generated/translations/billing.eu.json').then((m) => m.default);"
|
|
184
190
|
);
|
|
@@ -73,9 +73,6 @@ function formatLoadNamespacesHelper(
|
|
|
73
73
|
`): Promise<void> {\n` +
|
|
74
74
|
` await Promise.all(\n` +
|
|
75
75
|
` namespaces.map(async (namespace) => {\n` +
|
|
76
|
-
` if (i18n.hasNamespace(namespace)) {\n` +
|
|
77
|
-
` return;\n` +
|
|
78
|
-
` }\n` +
|
|
79
76
|
` i18n.setNamespace(namespace, await namespaceLoaders[namespace](${paramName}));\n` +
|
|
80
77
|
` }),\n` +
|
|
81
78
|
` );\n` +
|
|
@@ -110,8 +107,14 @@ function formatPartitionedTypesImport(
|
|
|
110
107
|
);
|
|
111
108
|
}
|
|
112
109
|
|
|
110
|
+
function formatSwitchDefaultCase(namespace: string, paramName: string): string {
|
|
111
|
+
return (
|
|
112
|
+
` default:\n` +
|
|
113
|
+
` throw new Error(\`[i18n] No translation artifact for namespace ${JSON.stringify(namespace)} and ${paramName} "\${String(${paramName})}".\`);\n`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
113
117
|
/**
|
|
114
|
-
* Emits lazy loaders for split-by-locale and custom delivery.
|
|
115
118
|
* Each namespace gets a function that dynamic-imports the JSON for the given
|
|
116
119
|
* partition key (locale or area) via a switch.
|
|
117
120
|
*/
|
|
@@ -153,6 +156,7 @@ function formatPartitionedNamespaceLoadersFile(
|
|
|
153
156
|
return (
|
|
154
157
|
` ${entry.namespace}: (${paramName}) => {\n` +
|
|
155
158
|
` switch (${paramName}) {\n${switchCases}\n` +
|
|
159
|
+
`${formatSwitchDefaultCase(entry.namespace, paramName)}` +
|
|
156
160
|
` }\n` +
|
|
157
161
|
` },`
|
|
158
162
|
);
|
|
@@ -105,6 +105,11 @@ describe("formatTypesFile", () => {
|
|
|
105
105
|
|
|
106
106
|
expect(output).toContain("export type AppLocale = 'en-US' | 'fr' | 'it';");
|
|
107
107
|
expect(output).toContain("export type AppDeliveryArea = 'eu' | 'us';");
|
|
108
|
+
expect(output).toContain("export const DELIVERY_ARTIFACTS = {");
|
|
109
|
+
expect(output).toContain('"eu": ["fr", "it"] as const');
|
|
110
|
+
expect(output).toContain('"us": ["en-US"] as const');
|
|
111
|
+
expect(output).toContain("} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;");
|
|
112
|
+
expect(output).toContain("export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;");
|
|
108
113
|
expect(output).toContain("export const LOCALE_DELIVERY_AREA = {");
|
|
109
114
|
expect(output).toContain('"it": "eu"');
|
|
110
115
|
expect(output).toContain('"en-US": "us"');
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
formatDeliveryArtifactsBlock,
|
|
3
|
+
formatLocaleDeliveryAreaBlock,
|
|
4
|
+
type DeliveryArtifactsMap,
|
|
5
|
+
} from "../delivery-artifacts.js";
|
|
2
6
|
import { formatLocaleFallbackBlock } from "../locale-fallback.js";
|
|
3
7
|
import { GENERATED_FILE_BANNER } from "../paths.js";
|
|
4
8
|
import type { NamespaceEntry } from "../types.js";
|
|
@@ -90,7 +94,13 @@ export function formatTypesFile(options: TypesFileOptions): string {
|
|
|
90
94
|
deliveryAreaTypeName && deliveryAreaUnion
|
|
91
95
|
? `export type ${deliveryAreaTypeName} = ${deliveryAreaUnion};\n\n` +
|
|
92
96
|
(deliveryArtifacts
|
|
93
|
-
?
|
|
97
|
+
? formatDeliveryArtifactsBlock(
|
|
98
|
+
deliveryArtifacts,
|
|
99
|
+
"DELIVERY_ARTIFACTS",
|
|
100
|
+
localeTypeName,
|
|
101
|
+
deliveryAreaTypeName
|
|
102
|
+
) +
|
|
103
|
+
formatLocaleDeliveryAreaBlock(
|
|
94
104
|
deliveryArtifacts,
|
|
95
105
|
localeDeliveryAreaConstName,
|
|
96
106
|
localeTypeName,
|
|
@@ -1114,6 +1114,42 @@ describe("generate-i18n-types", () => {
|
|
|
1114
1114
|
expect(loaders).not.toContain("import('./translations/billing.json')");
|
|
1115
1115
|
});
|
|
1116
1116
|
|
|
1117
|
+
it("omits dictionaryOutput in split mode and removes stale dictionary at the default path", () => {
|
|
1118
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1119
|
+
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
1120
|
+
mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
|
|
1121
|
+
|
|
1122
|
+
writeFileSync(
|
|
1123
|
+
join(tempDir, "src/i18n/translations/default.json"),
|
|
1124
|
+
JSON.stringify({
|
|
1125
|
+
welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
|
|
1126
|
+
})
|
|
1127
|
+
);
|
|
1128
|
+
writeFileSync(
|
|
1129
|
+
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1130
|
+
"// stale dictionary manifest\n"
|
|
1131
|
+
);
|
|
1132
|
+
writeFileSync(
|
|
1133
|
+
join(tempDir, "i18n.codegen.json"),
|
|
1134
|
+
JSON.stringify({
|
|
1135
|
+
namespaces: {
|
|
1136
|
+
default: "src/i18n/translations/default.json",
|
|
1137
|
+
},
|
|
1138
|
+
delivery: "split-by-locale",
|
|
1139
|
+
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1140
|
+
instanceOutput: "src/i18n/generated/instance.generated.ts",
|
|
1141
|
+
namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
|
|
1142
|
+
paramsTypeName: "AppParams",
|
|
1143
|
+
schemaTypeName: "AppSchema",
|
|
1144
|
+
})
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
const result = runCodegen(tempDir);
|
|
1148
|
+
expect(result.status).toBe(0);
|
|
1149
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1150
|
+
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1117
1153
|
it("writes delivery json under deliveryOutput when it differs from typesOutput directory", () => {
|
|
1118
1154
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
|
|
1119
1155
|
mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
|
|
@@ -1478,6 +1514,11 @@ describe("generate-i18n-types", () => {
|
|
|
1478
1514
|
).toThrow();
|
|
1479
1515
|
|
|
1480
1516
|
expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
|
|
1517
|
+
expect(types).toContain("export const DELIVERY_ARTIFACTS = {");
|
|
1518
|
+
expect(types).toContain('"eu": ["fr", "it"] as const');
|
|
1519
|
+
expect(types).toContain('"us": ["en-US"] as const');
|
|
1520
|
+
expect(types).toContain("} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;");
|
|
1521
|
+
expect(types).toContain("export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;");
|
|
1481
1522
|
expect(types).toContain("export const LOCALE_DELIVERY_AREA = {");
|
|
1482
1523
|
expect(types).toContain('"it": "eu"');
|
|
1483
1524
|
expect(types).toContain('"fr": "eu"');
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
loadConfig,
|
|
5
5
|
resolveDeliveryOutputDir,
|
|
6
|
+
resolveDictionaryOutputPath,
|
|
6
7
|
resolveLoadOnInit,
|
|
7
8
|
resolveNamespaces,
|
|
8
9
|
} from "./config.js";
|
|
@@ -98,7 +99,7 @@ function main() {
|
|
|
98
99
|
const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
|
|
99
100
|
|
|
100
101
|
const typesOutputPath = path.resolve(projectRoot, config.typesOutput);
|
|
101
|
-
const dictionaryOutputPath = path.resolve(projectRoot, config
|
|
102
|
+
const dictionaryOutputPath = path.resolve(projectRoot, resolveDictionaryOutputPath(config));
|
|
102
103
|
const instanceOutputPath = path.resolve(projectRoot, config.instanceOutput);
|
|
103
104
|
const typesModule = toModuleBasename(typesOutputPath);
|
|
104
105
|
|