@xndrjs/i18n 0.5.0-alpha.0 → 0.6.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 +70 -35
- package/dist/codegen/index.js +7 -0
- package/dist/codegen/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen/codegen-config-schema.ts +8 -0
- package/src/codegen/config.test.ts +49 -0
- package/src/codegen/config.ts +9 -0
- package/src/codegen/emit/dictionary-file.test.ts +32 -0
- package/src/codegen/emit/dictionary-file.ts +6 -2
- package/src/codegen/emit/dictionary-schema-file.ts +2 -2
- package/src/codegen/emit/namespace-loaders-file.test.ts +47 -1
- package/src/codegen/emit/namespace-loaders-file.ts +90 -6
- package/src/codegen/emit/types-file.ts +7 -4
- package/src/codegen/generate-i18n-types.test.ts +41 -56
- package/src/codegen/generate-i18n-types.ts +13 -3
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ 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` (canonical delivery) or `defaultDictionaryFor(locale)` (split
|
|
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.
|
|
202
202
|
|
|
@@ -407,25 +407,25 @@ 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 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
|
|
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
|
|
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
|
-
| `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/`.
|
|
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; **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
|
+
| `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
429
|
|
|
430
430
|
> Paths are resolved relative to the directory containing `i18n.codegen.json` (e.g. `i18n/` when using `xndrjs-i18n-setup .`).
|
|
431
431
|
|
|
@@ -458,7 +458,7 @@ Mixed namespaces are supported: some namespaces can stay `.json` while others us
|
|
|
458
458
|
|
|
459
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
460
|
|
|
461
|
-
Set `"delivery": "split-by-locale"` to emit **one JSON file per locale** instead:
|
|
461
|
+
Set `"delivery": "split-by-locale"` to emit **one JSON file per locale** instead. All namespaces are lazy in this mode — register them with `namespaceLoaders` before rendering:
|
|
462
462
|
|
|
463
463
|
```json
|
|
464
464
|
{
|
|
@@ -467,7 +467,7 @@ Set `"delivery": "split-by-locale"` to emit **one JSON file per locale** instead
|
|
|
467
467
|
"default": "translations/default.json",
|
|
468
468
|
"billing": "translations/billing.yaml"
|
|
469
469
|
},
|
|
470
|
-
"
|
|
470
|
+
"namespaceLoadersOutput": "generated/namespace-loaders.generated.ts"
|
|
471
471
|
}
|
|
472
472
|
```
|
|
473
473
|
|
|
@@ -477,12 +477,35 @@ Codegen writes `{deliveryOutput}/translations/{basename}.{locale}.json` (for exa
|
|
|
477
477
|
|
|
478
478
|
**Generated API changes (opt-in):**
|
|
479
479
|
|
|
480
|
-
| Canonical | Split-by-locale
|
|
481
|
-
| -------------------------------- |
|
|
482
|
-
| `export const defaultDictionary` | `
|
|
483
|
-
| `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)`
|
|
480
|
+
| Canonical | Split-by-locale / custom (all lazy) |
|
|
481
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
482
|
+
| `export const defaultDictionary` | `dictionary.generated.ts` is not generated |
|
|
483
|
+
| `namespaceLoaders.billing()` | `namespaceLoaders.billing(locale)` or `namespaceLoaders.billing(area)` |
|
|
484
|
+
| — | `ensureNamespacesLoadedForLocale(i18n, locale)` or `ensureNamespacesLoadedForArea(i18n, area)` in `namespace-loaders.generated.ts` |
|
|
485
|
+
|
|
486
|
+
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
|
+
|
|
488
|
+
Example init (multi, split-by-locale, all namespaces lazy):
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
import { createI18n } from "./generated/instance.generated.js";
|
|
492
|
+
import { ensureNamespacesLoadedForLocale } from "./generated/namespace-loaders.generated.js";
|
|
493
|
+
|
|
494
|
+
const i18n = createI18n({});
|
|
495
|
+
await ensureNamespacesLoadedForLocale(i18n, activeLocale);
|
|
496
|
+
```
|
|
484
497
|
|
|
485
|
-
Example
|
|
498
|
+
Example init (multi, custom delivery, all namespaces lazy):
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
import { createI18n } from "./generated/instance.generated.js";
|
|
502
|
+
import { ensureNamespacesLoadedForArea } from "./generated/namespace-loaders.generated.js";
|
|
503
|
+
|
|
504
|
+
const i18n = createI18n({});
|
|
505
|
+
await ensureNamespacesLoadedForArea(i18n, activeArea);
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Example `dictionary.generated.ts` (multi, canonical delivery, `loadOnInit: ["default"]`):
|
|
486
509
|
|
|
487
510
|
```ts
|
|
488
511
|
import defaultEn from "./translations/default.en.json";
|
|
@@ -513,19 +536,31 @@ export const namespaceLoaders = {
|
|
|
513
536
|
}
|
|
514
537
|
},
|
|
515
538
|
};
|
|
539
|
+
|
|
540
|
+
export async function ensureNamespacesLoadedForLocale(
|
|
541
|
+
i18n: I18nMultiInstance,
|
|
542
|
+
locale: MyProjectLocale,
|
|
543
|
+
namespaces: readonly LazyNamespace[] = ["billing", "default"] as const
|
|
544
|
+
): Promise<void> {
|
|
545
|
+
await Promise.all(
|
|
546
|
+
namespaces.map(async (namespace) => {
|
|
547
|
+
if (i18n.hasNamespace(namespace)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
i18n.setNamespace(namespace, await namespaceLoaders[namespace](locale));
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
}
|
|
516
554
|
```
|
|
517
555
|
|
|
518
|
-
|
|
556
|
+
Load only the namespaces you need:
|
|
519
557
|
|
|
520
558
|
```ts
|
|
521
559
|
import { createI18n } from "./generated/instance.generated.js";
|
|
522
|
-
import {
|
|
523
|
-
|
|
524
|
-
const i18n = createI18n(defaultDictionaryFor(activeLocale));
|
|
560
|
+
import { ensureNamespacesLoadedForLocale } from "./generated/namespace-loaders.generated.js";
|
|
525
561
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
562
|
+
const i18n = createI18n({});
|
|
563
|
+
await ensureNamespacesLoadedForLocale(i18n, activeLocale, ["billing"]);
|
|
529
564
|
```
|
|
530
565
|
|
|
531
566
|
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.
|
|
@@ -579,7 +614,7 @@ i18n.setNamespace("billing", externalBillingPayload);
|
|
|
579
614
|
|
|
580
615
|
### Lazy namespace loading (multi mode)
|
|
581
616
|
|
|
582
|
-
Split namespaces across chunks
|
|
617
|
+
Split namespaces across chunks with `loadOnInit` in **canonical** delivery only. With `split-by-locale` or `custom`, every namespace is lazy and codegen emits typed `namespaceLoaders` — one dynamic `import()` per lazy namespace (and per locale or delivery area). `.get()` stays synchronous — register lazy namespaces with `setNamespace()` before rendering.
|
|
583
618
|
|
|
584
619
|
```json
|
|
585
620
|
{
|
package/dist/codegen/index.js
CHANGED
|
@@ -122,6 +122,13 @@ z.object(codegenConfigShape).strict().superRefine((config, ctx) => {
|
|
|
122
122
|
message: 'deliveryArtifacts is only allowed when delivery is "custom".'
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
+
if (config.loadOnInit !== void 0 && config.delivery !== "canonical") {
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: "custom",
|
|
128
|
+
path: ["loadOnInit"],
|
|
129
|
+
message: 'loadOnInit is only allowed when delivery is "canonical".'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
125
132
|
});
|
|
126
133
|
function resolveDeliveryOutputDir(config) {
|
|
127
134
|
return config.deliveryOutput ?? path.dirname(config.typesOutput);
|
|
@@ -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\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":"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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"]}
|
package/package.json
CHANGED
|
@@ -99,6 +99,14 @@ export const codegenConfigSchema = z
|
|
|
99
99
|
message: 'deliveryArtifacts is only allowed when delivery is "custom".',
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
if (config.loadOnInit !== undefined && config.delivery !== "canonical") {
|
|
104
|
+
ctx.addIssue({
|
|
105
|
+
code: "custom",
|
|
106
|
+
path: ["loadOnInit"],
|
|
107
|
+
message: 'loadOnInit is only allowed when delivery is "canonical".',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
102
110
|
});
|
|
103
111
|
|
|
104
112
|
export type CodegenConfigInput = z.input<typeof codegenConfigSchema>;
|
|
@@ -207,6 +207,35 @@ describe("loadConfig", () => {
|
|
|
207
207
|
|
|
208
208
|
expect(() => loadConfig(configPath)).toThrow('Locale "fr" appears in both "eu" and "us"');
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
it("throws when loadOnInit is set for split-by-locale delivery", () => {
|
|
212
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
213
|
+
const configPath = writeConfig(tempDir, {
|
|
214
|
+
...validMultiConfig,
|
|
215
|
+
delivery: "split-by-locale",
|
|
216
|
+
loadOnInit: ["default"],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
220
|
+
'loadOnInit is only allowed when delivery is "canonical".'
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("throws when loadOnInit is set for custom delivery", () => {
|
|
225
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
226
|
+
const configPath = writeConfig(tempDir, {
|
|
227
|
+
...validMultiConfig,
|
|
228
|
+
delivery: "custom",
|
|
229
|
+
deliveryArtifacts: {
|
|
230
|
+
eu: ["it"],
|
|
231
|
+
},
|
|
232
|
+
loadOnInit: ["default"],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
236
|
+
'loadOnInit is only allowed when delivery is "canonical".'
|
|
237
|
+
);
|
|
238
|
+
});
|
|
210
239
|
});
|
|
211
240
|
|
|
212
241
|
describe("resolveLoadOnInit", () => {
|
|
@@ -230,6 +259,26 @@ describe("resolveLoadOnInit", () => {
|
|
|
230
259
|
'[Codegen Error] loadOnInit: namespace "missing" is not defined in namespaces config.'
|
|
231
260
|
);
|
|
232
261
|
});
|
|
262
|
+
|
|
263
|
+
it("treats all namespaces as lazy when delivery is split-by-locale", () => {
|
|
264
|
+
const config = { ...validMultiConfig, delivery: "split-by-locale" as const };
|
|
265
|
+
|
|
266
|
+
expect(resolveLoadOnInit(config, entries, false)).toEqual({
|
|
267
|
+
loadOnInitSet: new Set(),
|
|
268
|
+
lazyEntries: entries,
|
|
269
|
+
hasLazy: true,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("treats all namespaces as lazy when delivery is custom", () => {
|
|
274
|
+
const config = { ...validMultiConfig, delivery: "custom" as const };
|
|
275
|
+
|
|
276
|
+
expect(resolveLoadOnInit(config, entries, false)).toEqual({
|
|
277
|
+
loadOnInitSet: new Set(),
|
|
278
|
+
lazyEntries: entries,
|
|
279
|
+
hasLazy: true,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
233
282
|
});
|
|
234
283
|
|
|
235
284
|
describe("resolveImportExtension", () => {
|
package/src/codegen/config.ts
CHANGED
|
@@ -72,6 +72,15 @@ export function resolveLoadOnInit(
|
|
|
72
72
|
return { loadOnInitSet: all, lazyEntries: [], hasLazy: false };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const delivery = config.delivery ?? "canonical";
|
|
76
|
+
if (delivery !== "canonical") {
|
|
77
|
+
return {
|
|
78
|
+
loadOnInitSet: new Set(),
|
|
79
|
+
lazyEntries: entries,
|
|
80
|
+
hasLazy: entries.length > 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
const allNamespaces = new Set(entries.map((entry) => entry.namespace));
|
|
76
85
|
const loadOnInitSet = config.loadOnInit
|
|
77
86
|
? new Set(config.loadOnInit)
|
|
@@ -30,6 +30,38 @@ describe("formatDictionaryFile", () => {
|
|
|
30
30
|
expect(output).not.toContain("defaultDictionaryFor");
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it("emits an empty module when all namespaces are lazy in split mode (multi)", () => {
|
|
34
|
+
const output = formatDictionaryFile({
|
|
35
|
+
isSingle: false,
|
|
36
|
+
hasLazy: true,
|
|
37
|
+
entries: [
|
|
38
|
+
{ namespace: "default", filePath: "src/i18n/translations/default.json" },
|
|
39
|
+
{ namespace: "billing", filePath: "src/i18n/translations/billing.json" },
|
|
40
|
+
],
|
|
41
|
+
eagerEntries: [],
|
|
42
|
+
projectRoot,
|
|
43
|
+
dictionaryOutputPath,
|
|
44
|
+
typesOutputPath,
|
|
45
|
+
schemaTypeName: "AppSchema",
|
|
46
|
+
localeTypeName: "AppLocale",
|
|
47
|
+
importExtension: "none",
|
|
48
|
+
delivery: "split-by-locale",
|
|
49
|
+
requestLocales: ["en", "it"],
|
|
50
|
+
splitPathsByNamespace: {
|
|
51
|
+
default: {
|
|
52
|
+
en: "src/i18n/generated/translations/default.en.json",
|
|
53
|
+
it: "src/i18n/generated/translations/default.it.json",
|
|
54
|
+
},
|
|
55
|
+
billing: {
|
|
56
|
+
en: "src/i18n/generated/translations/billing.en.json",
|
|
57
|
+
it: "src/i18n/generated/translations/billing.it.json",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(output).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
33
65
|
it("emits defaultDictionaryFor with per-locale imports in split mode (multi)", () => {
|
|
34
66
|
const output = formatDictionaryFile({
|
|
35
67
|
isSingle: false,
|
|
@@ -67,7 +67,7 @@ function toPartitionSuffix(paramName: string): string {
|
|
|
67
67
|
function formatPartitionedDictionaryFile(
|
|
68
68
|
options: DictionaryFileOptions,
|
|
69
69
|
{ partitionKeys, paramName, paramTypeName }: PartitionedDictionaryParams
|
|
70
|
-
): string {
|
|
70
|
+
): string | null {
|
|
71
71
|
const {
|
|
72
72
|
isSingle,
|
|
73
73
|
hasLazy,
|
|
@@ -86,6 +86,10 @@ function formatPartitionedDictionaryFile(
|
|
|
86
86
|
const dictionaryTypeName = hasLazy ? "InitialSchema" : schemaTypeName;
|
|
87
87
|
const schemaTypeImport = hasLazy ? `, ${schemaTypeName}` : "";
|
|
88
88
|
|
|
89
|
+
if (eagerEntries.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
89
93
|
const imports = eagerEntries
|
|
90
94
|
.flatMap((entry) =>
|
|
91
95
|
partitionKeys.map((partitionKey) => {
|
|
@@ -164,7 +168,7 @@ function formatPartitionedDictionaryFile(
|
|
|
164
168
|
* Builds `dictionary.generated.ts` for the configured delivery mode:
|
|
165
169
|
* canonical (single bundled JSON per namespace), split-by-locale, or custom areas.
|
|
166
170
|
*/
|
|
167
|
-
export function formatDictionaryFile(options: DictionaryFileOptions): string {
|
|
171
|
+
export function formatDictionaryFile(options: DictionaryFileOptions): string | null {
|
|
168
172
|
const delivery = options.delivery ?? "canonical";
|
|
169
173
|
if (delivery === "split-by-locale") {
|
|
170
174
|
return formatPartitionedDictionaryFile(options, {
|
|
@@ -81,14 +81,14 @@ export function formatDictionarySchemaFile(
|
|
|
81
81
|
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
82
82
|
const namespaceImport = isSingle
|
|
83
83
|
? ""
|
|
84
|
-
: ` validateExternalNamespace as
|
|
84
|
+
: ` validateExternalNamespace as validateExternalNamespaceCore,\n`;
|
|
85
85
|
const namespaceValidator = isSingle
|
|
86
86
|
? ""
|
|
87
87
|
: `export function validateExternalNamespace<NS extends keyof ${schemaTypeName}>(\n` +
|
|
88
88
|
` namespace: NS,\n` +
|
|
89
89
|
` input: unknown,\n` +
|
|
90
90
|
`) {\n` +
|
|
91
|
-
` return
|
|
91
|
+
` return validateExternalNamespaceCore<${schemaTypeName}[NS]>(\n` +
|
|
92
92
|
` namespace as string,\n` +
|
|
93
93
|
` input,\n` +
|
|
94
94
|
` DICTIONARY_SPEC,\n` +
|
|
@@ -17,7 +17,10 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
17
17
|
},
|
|
18
18
|
],
|
|
19
19
|
schemaTypeName: "AppSchema",
|
|
20
|
+
paramsTypeName: "AppParams",
|
|
20
21
|
localeTypeName: "AppLocale",
|
|
22
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
23
|
+
hasLocaleFallback: false,
|
|
21
24
|
typesModule: "i18n-types.generated",
|
|
22
25
|
importExtension: "none",
|
|
23
26
|
projectRoot,
|
|
@@ -29,6 +32,7 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
29
32
|
"billing: () => import('./translations/billing.json').then((m) => m.default),"
|
|
30
33
|
);
|
|
31
34
|
expect(output).not.toContain("AppLocale");
|
|
35
|
+
expect(output).not.toContain("ensureNamespacesLoadedForLocale");
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
it("emits ns(locale) loaders in split mode", () => {
|
|
@@ -40,6 +44,11 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
40
44
|
filePath: "src/i18n/translations/billing.json",
|
|
41
45
|
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
42
46
|
},
|
|
47
|
+
{
|
|
48
|
+
namespace: "default",
|
|
49
|
+
filePath: "src/i18n/translations/default.json",
|
|
50
|
+
absolutePath: path.join(projectRoot, "src/i18n/translations/default.json"),
|
|
51
|
+
},
|
|
43
52
|
{
|
|
44
53
|
namespace: "user",
|
|
45
54
|
filePath: "src/i18n/translations/user.json",
|
|
@@ -47,10 +56,14 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
47
56
|
},
|
|
48
57
|
],
|
|
49
58
|
schemaTypeName: "AppSchema",
|
|
59
|
+
paramsTypeName: "AppParams",
|
|
50
60
|
localeTypeName: "AppLocale",
|
|
61
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
62
|
+
hasLocaleFallback: true,
|
|
51
63
|
typesModule: "i18n-types.generated",
|
|
52
64
|
importExtension: "none",
|
|
53
65
|
projectRoot,
|
|
66
|
+
isSingle: false,
|
|
54
67
|
delivery: "split-by-locale",
|
|
55
68
|
requestLocales: ["en", "it", "de-CH"],
|
|
56
69
|
splitPathsByNamespace: {
|
|
@@ -59,6 +72,11 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
59
72
|
it: "src/i18n/generated/translations/billing.it.json",
|
|
60
73
|
"de-CH": "src/i18n/generated/translations/billing.de-CH.json",
|
|
61
74
|
},
|
|
75
|
+
default: {
|
|
76
|
+
en: "src/i18n/generated/translations/default.en.json",
|
|
77
|
+
it: "src/i18n/generated/translations/default.it.json",
|
|
78
|
+
"de-CH": "src/i18n/generated/translations/default.de-CH.json",
|
|
79
|
+
},
|
|
62
80
|
user: {
|
|
63
81
|
en: "src/i18n/generated/translations/user.en.json",
|
|
64
82
|
it: "src/i18n/generated/translations/user.it.json",
|
|
@@ -78,6 +96,12 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
78
96
|
expect(output).toContain("user: (locale) => {");
|
|
79
97
|
expect(output).not.toContain("billing: {");
|
|
80
98
|
expect(output).not.toContain("import('./translations/billing.json')");
|
|
99
|
+
expect(output).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
100
|
+
expect(output).toContain("i18n: I18nMultiInstance,");
|
|
101
|
+
expect(output).toContain(
|
|
102
|
+
'namespaces: readonly LazyNamespace[] = ["billing", "default", "user"] as const,'
|
|
103
|
+
);
|
|
104
|
+
expect(output.match(/from '\.\/i18n-types\.generated'/g)?.length).toBe(1);
|
|
81
105
|
});
|
|
82
106
|
|
|
83
107
|
it("throws when a split path is missing", () => {
|
|
@@ -92,7 +116,10 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
92
116
|
},
|
|
93
117
|
],
|
|
94
118
|
schemaTypeName: "AppSchema",
|
|
119
|
+
paramsTypeName: "AppParams",
|
|
95
120
|
localeTypeName: "AppLocale",
|
|
121
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
122
|
+
hasLocaleFallback: false,
|
|
96
123
|
typesModule: "i18n-types.generated",
|
|
97
124
|
importExtension: "none",
|
|
98
125
|
projectRoot,
|
|
@@ -116,12 +143,21 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
116
143
|
filePath: "src/i18n/translations/billing.json",
|
|
117
144
|
absolutePath: path.join(projectRoot, "src/i18n/translations/billing.json"),
|
|
118
145
|
},
|
|
146
|
+
{
|
|
147
|
+
namespace: "default",
|
|
148
|
+
filePath: "src/i18n/translations/default.json",
|
|
149
|
+
absolutePath: path.join(projectRoot, "src/i18n/translations/default.json"),
|
|
150
|
+
},
|
|
119
151
|
],
|
|
120
152
|
schemaTypeName: "AppSchema",
|
|
153
|
+
paramsTypeName: "AppParams",
|
|
121
154
|
localeTypeName: "AppLocale",
|
|
155
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
156
|
+
hasLocaleFallback: true,
|
|
122
157
|
typesModule: "i18n-types.generated",
|
|
123
158
|
importExtension: "none",
|
|
124
159
|
projectRoot,
|
|
160
|
+
isSingle: false,
|
|
125
161
|
delivery: "custom",
|
|
126
162
|
deliveryAreaTypeName: "AppDeliveryArea",
|
|
127
163
|
deliveryAreaNames: ["eu", "us"],
|
|
@@ -130,6 +166,10 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
130
166
|
eu: "src/i18n/generated/translations/billing.eu.json",
|
|
131
167
|
us: "src/i18n/generated/translations/billing.us.json",
|
|
132
168
|
},
|
|
169
|
+
default: {
|
|
170
|
+
eu: "src/i18n/generated/translations/default.eu.json",
|
|
171
|
+
us: "src/i18n/generated/translations/default.us.json",
|
|
172
|
+
},
|
|
133
173
|
},
|
|
134
174
|
});
|
|
135
175
|
|
|
@@ -142,8 +182,11 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
142
182
|
expect(output).toContain(
|
|
143
183
|
"return import('./generated/translations/billing.eu.json').then((m) => m.default);"
|
|
144
184
|
);
|
|
145
|
-
expect(output).not.toContain("AppLocale");
|
|
146
185
|
expect(output).not.toContain("import('./translations/billing.json')");
|
|
186
|
+
expect(output).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
187
|
+
expect(output).toContain(
|
|
188
|
+
'namespaces: readonly LazyNamespace[] = ["billing", "default"] as const,'
|
|
189
|
+
);
|
|
147
190
|
});
|
|
148
191
|
|
|
149
192
|
it("throws when a custom delivery split path is missing", () => {
|
|
@@ -158,7 +201,10 @@ describe("formatNamespaceLoadersFile", () => {
|
|
|
158
201
|
},
|
|
159
202
|
],
|
|
160
203
|
schemaTypeName: "AppSchema",
|
|
204
|
+
paramsTypeName: "AppParams",
|
|
161
205
|
localeTypeName: "AppLocale",
|
|
206
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
207
|
+
hasLocaleFallback: false,
|
|
162
208
|
typesModule: "i18n-types.generated",
|
|
163
209
|
importExtension: "none",
|
|
164
210
|
projectRoot,
|
|
@@ -11,10 +11,14 @@ export interface NamespaceLoadersFileOptions {
|
|
|
11
11
|
loadersOutputPath: string;
|
|
12
12
|
lazyEntries: (NamespaceEntry & { absolutePath: string })[];
|
|
13
13
|
schemaTypeName: string;
|
|
14
|
+
paramsTypeName: string;
|
|
14
15
|
localeTypeName: string;
|
|
16
|
+
localeFallbackConstName: string;
|
|
17
|
+
hasLocaleFallback: boolean;
|
|
15
18
|
typesModule: string;
|
|
16
19
|
importExtension: ImportExtension;
|
|
17
20
|
projectRoot: string;
|
|
21
|
+
isSingle?: boolean;
|
|
18
22
|
delivery?: DeliveryMode;
|
|
19
23
|
splitPathsByNamespace?: Record<string, Record<string, string>>;
|
|
20
24
|
requestLocales?: readonly string[];
|
|
@@ -26,6 +30,84 @@ interface PartitionedNamespaceLoadersParams {
|
|
|
26
30
|
partitionKeys: readonly string[];
|
|
27
31
|
paramName: string;
|
|
28
32
|
paramTypeName: string;
|
|
33
|
+
loadNamespacesFunctionName: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatI18nMultiInstanceType(options: NamespaceLoadersFileOptions): string {
|
|
37
|
+
const {
|
|
38
|
+
schemaTypeName,
|
|
39
|
+
paramsTypeName,
|
|
40
|
+
localeTypeName,
|
|
41
|
+
localeFallbackConstName,
|
|
42
|
+
hasLocaleFallback,
|
|
43
|
+
} = options;
|
|
44
|
+
|
|
45
|
+
const typeArgs = hasLocaleFallback
|
|
46
|
+
? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}`
|
|
47
|
+
: `${schemaTypeName}, ${paramsTypeName}`;
|
|
48
|
+
|
|
49
|
+
return `IcuTranslationProviderMulti<${typeArgs}>`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatDefaultNamespacesLiteral(lazyEntries: NamespaceEntry[]): string {
|
|
53
|
+
return [...lazyEntries]
|
|
54
|
+
.map((entry) => entry.namespace)
|
|
55
|
+
.sort()
|
|
56
|
+
.map((namespace) => JSON.stringify(namespace))
|
|
57
|
+
.join(", ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatLoadNamespacesHelper(
|
|
61
|
+
options: NamespaceLoadersFileOptions,
|
|
62
|
+
{ paramName, paramTypeName, loadNamespacesFunctionName }: PartitionedNamespaceLoadersParams
|
|
63
|
+
): string {
|
|
64
|
+
const { lazyEntries } = options;
|
|
65
|
+
const defaultNamespaces = formatDefaultNamespacesLiteral(lazyEntries);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
`\ntype I18nMultiInstance = ${formatI18nMultiInstanceType(options)};\n\n` +
|
|
69
|
+
`export async function ${loadNamespacesFunctionName}(\n` +
|
|
70
|
+
` i18n: I18nMultiInstance,\n` +
|
|
71
|
+
` ${paramName}: ${paramTypeName},\n` +
|
|
72
|
+
` namespaces: readonly LazyNamespace[] = [${defaultNamespaces}] as const,\n` +
|
|
73
|
+
`): Promise<void> {\n` +
|
|
74
|
+
` await Promise.all(\n` +
|
|
75
|
+
` namespaces.map(async (namespace) => {\n` +
|
|
76
|
+
` if (i18n.hasNamespace(namespace)) {\n` +
|
|
77
|
+
` return;\n` +
|
|
78
|
+
` }\n` +
|
|
79
|
+
` i18n.setNamespace(namespace, await namespaceLoaders[namespace](${paramName}));\n` +
|
|
80
|
+
` }),\n` +
|
|
81
|
+
` );\n` +
|
|
82
|
+
`}\n`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatPartitionedTypesImport(
|
|
87
|
+
options: NamespaceLoadersFileOptions,
|
|
88
|
+
paramTypeName: string
|
|
89
|
+
): string {
|
|
90
|
+
const {
|
|
91
|
+
schemaTypeName,
|
|
92
|
+
paramsTypeName,
|
|
93
|
+
localeTypeName,
|
|
94
|
+
localeFallbackConstName,
|
|
95
|
+
hasLocaleFallback,
|
|
96
|
+
typesModule,
|
|
97
|
+
importExtension,
|
|
98
|
+
isSingle,
|
|
99
|
+
} = options;
|
|
100
|
+
|
|
101
|
+
if (isSingle) {
|
|
102
|
+
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fallbackImport = hasLocaleFallback ? `${localeFallbackConstName}, ` : "";
|
|
106
|
+
const paramTypeImport = paramTypeName === localeTypeName ? "" : `, type ${paramTypeName}`;
|
|
107
|
+
return (
|
|
108
|
+
`import { IcuTranslationProviderMulti } from '@xndrjs/i18n';\n` +
|
|
109
|
+
`import { ${fallbackImport}type ${localeTypeName}, type ${paramsTypeName}, type ${schemaTypeName}, type LazyNamespace${paramTypeImport} } from '${toRelativeModuleImport(typesModule, importExtension)}';\n\n`
|
|
110
|
+
);
|
|
29
111
|
}
|
|
30
112
|
|
|
31
113
|
/**
|
|
@@ -35,19 +117,18 @@ interface PartitionedNamespaceLoadersParams {
|
|
|
35
117
|
*/
|
|
36
118
|
function formatPartitionedNamespaceLoadersFile(
|
|
37
119
|
options: NamespaceLoadersFileOptions,
|
|
38
|
-
|
|
120
|
+
params: PartitionedNamespaceLoadersParams
|
|
39
121
|
): string {
|
|
122
|
+
const { partitionKeys, paramName, paramTypeName } = params;
|
|
40
123
|
const {
|
|
41
124
|
loadersOutputPath,
|
|
42
125
|
lazyEntries,
|
|
43
126
|
schemaTypeName,
|
|
44
|
-
typesModule,
|
|
45
|
-
importExtension,
|
|
46
127
|
projectRoot,
|
|
47
128
|
splitPathsByNamespace = {},
|
|
48
129
|
} = options;
|
|
49
130
|
|
|
50
|
-
const typesImport =
|
|
131
|
+
const typesImport = formatPartitionedTypesImport(options, paramTypeName);
|
|
51
132
|
const loaderEntries = lazyEntries
|
|
52
133
|
.map((entry) => {
|
|
53
134
|
const switchCases = partitionKeys
|
|
@@ -80,10 +161,11 @@ function formatPartitionedNamespaceLoadersFile(
|
|
|
80
161
|
|
|
81
162
|
return (
|
|
82
163
|
`${GENERATED_FILE_BANNER}` +
|
|
83
|
-
|
|
164
|
+
typesImport +
|
|
84
165
|
`export const namespaceLoaders: {\n` +
|
|
85
166
|
` [K in LazyNamespace]: (${paramName}: ${paramTypeName}) => Promise<${schemaTypeName}[K]>;\n` +
|
|
86
|
-
`} = {\n${loaderEntries}\n};\n`
|
|
167
|
+
`} = {\n${loaderEntries}\n};\n` +
|
|
168
|
+
(options.isSingle ? "" : formatLoadNamespacesHelper(options, params))
|
|
87
169
|
);
|
|
88
170
|
}
|
|
89
171
|
|
|
@@ -118,6 +200,7 @@ export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions)
|
|
|
118
200
|
partitionKeys: options.requestLocales ?? [],
|
|
119
201
|
paramName: "locale",
|
|
120
202
|
paramTypeName: options.localeTypeName,
|
|
203
|
+
loadNamespacesFunctionName: "ensureNamespacesLoadedForLocale",
|
|
121
204
|
});
|
|
122
205
|
}
|
|
123
206
|
if (delivery === "custom") {
|
|
@@ -128,6 +211,7 @@ export function formatNamespaceLoadersFile(options: NamespaceLoadersFileOptions)
|
|
|
128
211
|
partitionKeys: options.deliveryAreaNames ?? [],
|
|
129
212
|
paramName: "area",
|
|
130
213
|
paramTypeName: options.deliveryAreaTypeName,
|
|
214
|
+
loadNamespacesFunctionName: "ensureNamespacesLoadedForArea",
|
|
131
215
|
});
|
|
132
216
|
}
|
|
133
217
|
return formatCanonicalNamespaceLoadersFile(options);
|
|
@@ -12,10 +12,13 @@ export function formatLazyTypesBlock(
|
|
|
12
12
|
lazyEntries: NamespaceEntry[],
|
|
13
13
|
schemaTypeName: string
|
|
14
14
|
): string {
|
|
15
|
-
const loadOnInitUnion =
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const loadOnInitUnion =
|
|
16
|
+
loadOnInitSet.size > 0
|
|
17
|
+
? [...loadOnInitSet]
|
|
18
|
+
.sort()
|
|
19
|
+
.map((namespace) => `'${namespace}'`)
|
|
20
|
+
.join(" | ")
|
|
21
|
+
: "never";
|
|
19
22
|
const lazyUnion = lazyEntries.map((entry) => `'${entry.namespace}'`).join(" | ");
|
|
20
23
|
|
|
21
24
|
return (
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
2
10
|
import { tmpdir } from "node:os";
|
|
3
11
|
import { join } from "node:path";
|
|
4
12
|
import { spawnSync } from "node:child_process";
|
|
@@ -168,6 +176,7 @@ describe("generate-i18n-types", () => {
|
|
|
168
176
|
expect(schema).toContain("mode: 'multi' as const");
|
|
169
177
|
expect(schema).toContain("validateExternalDictionary");
|
|
170
178
|
expect(schema).toContain("validateExternalNamespace");
|
|
179
|
+
expect(schema).toContain("validateExternalNamespaceCore");
|
|
171
180
|
expect(schema).toContain('"name": "string"');
|
|
172
181
|
});
|
|
173
182
|
|
|
@@ -228,6 +237,7 @@ describe("generate-i18n-types", () => {
|
|
|
228
237
|
expect(schema).toContain("mode: 'single' as const");
|
|
229
238
|
expect(schema).toContain("validateExternalDictionary");
|
|
230
239
|
expect(schema).not.toContain("validateExternalNamespaceImpl");
|
|
240
|
+
expect(schema).not.toContain("validateExternalNamespaceCore");
|
|
231
241
|
});
|
|
232
242
|
|
|
233
243
|
it("generates flat types for single-file config", () => {
|
|
@@ -1038,7 +1048,6 @@ describe("generate-i18n-types", () => {
|
|
|
1038
1048
|
user: "src/i18n/translations/user.json",
|
|
1039
1049
|
billing: "src/i18n/translations/billing.json",
|
|
1040
1050
|
},
|
|
1041
|
-
loadOnInit: ["default"],
|
|
1042
1051
|
delivery: "split-by-locale",
|
|
1043
1052
|
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
1044
1053
|
dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
|
|
@@ -1070,34 +1079,28 @@ describe("generate-i18n-types", () => {
|
|
|
1070
1079
|
).toThrow();
|
|
1071
1080
|
|
|
1072
1081
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1073
|
-
const dictionary = readFileSync(
|
|
1074
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1075
|
-
"utf8"
|
|
1076
|
-
);
|
|
1077
1082
|
const loaders = readFileSync(
|
|
1078
1083
|
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1079
1084
|
"utf8"
|
|
1080
1085
|
);
|
|
1081
1086
|
|
|
1087
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1088
|
+
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1089
|
+
|
|
1082
1090
|
expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
|
|
1083
1091
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1084
1092
|
expect(types).not.toContain("typeof import");
|
|
1085
1093
|
expect(types).toContain("export type AppLocale = 'en' | 'it'");
|
|
1086
1094
|
expect(types).toContain("invoice_summary: { count: number }");
|
|
1087
1095
|
|
|
1088
|
-
expect(
|
|
1089
|
-
expect(
|
|
1090
|
-
expect(dictionary).toContain(
|
|
1091
|
-
"export function defaultDictionaryFor(locale: AppLocale): InitialSchema"
|
|
1092
|
-
);
|
|
1093
|
-
expect(dictionary).toContain("default: defaultByLocale[locale],");
|
|
1094
|
-
expect(dictionary).not.toContain("as unknown as");
|
|
1095
|
-
expect(dictionary).not.toContain("export const defaultDictionary");
|
|
1096
|
-
expect(dictionary).not.toContain("billingEn");
|
|
1096
|
+
expect(types).toContain("export type LoadOnInitNamespace = never;");
|
|
1097
|
+
expect(types).toContain("export type LazyNamespace = 'default' | 'user' | 'billing';");
|
|
1097
1098
|
|
|
1098
1099
|
expect(loaders).toContain(
|
|
1099
1100
|
"[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
|
|
1100
1101
|
);
|
|
1102
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1103
|
+
expect(loaders).toContain("default: (locale) => {");
|
|
1101
1104
|
expect(loaders).toContain("billing: (locale) => {");
|
|
1102
1105
|
expect(loaders).toContain('case "it":');
|
|
1103
1106
|
expect(loaders).toContain(
|
|
@@ -1136,7 +1139,6 @@ describe("generate-i18n-types", () => {
|
|
|
1136
1139
|
default: "src/i18n/translations/default.json",
|
|
1137
1140
|
billing: "src/i18n/translations/billing.yaml",
|
|
1138
1141
|
},
|
|
1139
|
-
loadOnInit: ["default"],
|
|
1140
1142
|
delivery: "split-by-locale",
|
|
1141
1143
|
deliveryOutput: "public/i18n",
|
|
1142
1144
|
typesOutput: "src/i18n/generated/i18n-types.generated.ts",
|
|
@@ -1162,20 +1164,15 @@ describe("generate-i18n-types", () => {
|
|
|
1162
1164
|
) as Record<string, Record<string, string>>;
|
|
1163
1165
|
expect(billingEn.invoice_summary).toEqual({ en: "You have {count} invoices" });
|
|
1164
1166
|
|
|
1165
|
-
const dictionary = readFileSync(
|
|
1166
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1167
|
-
"utf8"
|
|
1168
|
-
);
|
|
1169
1167
|
const loaders = readFileSync(
|
|
1170
1168
|
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1171
1169
|
"utf8"
|
|
1172
1170
|
);
|
|
1173
1171
|
|
|
1174
|
-
expect(dictionary).
|
|
1175
|
-
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
"import defaultIt from '../../../public/i18n/translations/default.it.json';"
|
|
1172
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1173
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1174
|
+
expect(loaders).toContain(
|
|
1175
|
+
"return import('../../../public/i18n/translations/default.en.json').then((m) => m.default);"
|
|
1179
1176
|
);
|
|
1180
1177
|
expect(loaders).toContain(
|
|
1181
1178
|
"return import('../../../public/i18n/translations/billing.en.json').then((m) => m.default);"
|
|
@@ -1274,17 +1271,15 @@ describe("generate-i18n-types", () => {
|
|
|
1274
1271
|
expect(enSplit.invoice_summary).toEqual({ en: "You have {count} invoices" });
|
|
1275
1272
|
|
|
1276
1273
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1277
|
-
const dictionary = readFileSync(
|
|
1278
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1279
|
-
"utf8"
|
|
1280
|
-
);
|
|
1281
1274
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1282
1275
|
expect(types).not.toContain("typeof import");
|
|
1283
|
-
expect(
|
|
1284
|
-
expect(dictionary).
|
|
1285
|
-
|
|
1276
|
+
expect(types).toContain("export type LazyNamespace = 'billing';");
|
|
1277
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1278
|
+
const loaders = readFileSync(
|
|
1279
|
+
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1280
|
+
"utf8"
|
|
1286
1281
|
);
|
|
1287
|
-
expect(
|
|
1282
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
|
|
1288
1283
|
});
|
|
1289
1284
|
|
|
1290
1285
|
it("emits defaultDictionaryFor in single mode with split-by-locale delivery", () => {
|
|
@@ -1413,7 +1408,6 @@ describe("generate-i18n-types", () => {
|
|
|
1413
1408
|
default: "src/i18n/translations/default.json",
|
|
1414
1409
|
billing: "src/i18n/translations/billing.json",
|
|
1415
1410
|
},
|
|
1416
|
-
loadOnInit: ["default"],
|
|
1417
1411
|
delivery: "custom",
|
|
1418
1412
|
deliveryArtifacts: {
|
|
1419
1413
|
eu: ["it", "fr"],
|
|
@@ -1461,10 +1455,6 @@ describe("generate-i18n-types", () => {
|
|
|
1461
1455
|
).toThrow();
|
|
1462
1456
|
|
|
1463
1457
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1464
|
-
const dictionary = readFileSync(
|
|
1465
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1466
|
-
"utf8"
|
|
1467
|
-
);
|
|
1468
1458
|
const loaders = readFileSync(
|
|
1469
1459
|
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1470
1460
|
"utf8"
|
|
@@ -1501,19 +1491,17 @@ describe("generate-i18n-types", () => {
|
|
|
1501
1491
|
expect(types).not.toContain("typeof import");
|
|
1502
1492
|
expect(types).toContain("invoice_summary: { count: number }");
|
|
1503
1493
|
|
|
1504
|
-
expect(
|
|
1505
|
-
expect(
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
);
|
|
1509
|
-
expect(dictionary).toContain("default: defaultByArea[area],");
|
|
1510
|
-
expect(dictionary).not.toContain("as unknown as");
|
|
1511
|
-
expect(dictionary).not.toContain("export const defaultDictionary");
|
|
1512
|
-
expect(dictionary).not.toContain("billingEu");
|
|
1494
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1495
|
+
expect(result.stdout).not.toContain("dictionary.generated.ts");
|
|
1496
|
+
|
|
1497
|
+
expect(types).toContain("export type LoadOnInitNamespace = never;");
|
|
1498
|
+
expect(types).toContain("export type LazyNamespace = 'default' | 'billing';");
|
|
1513
1499
|
|
|
1514
1500
|
expect(loaders).toContain(
|
|
1515
1501
|
"[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
|
|
1516
1502
|
);
|
|
1503
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
1504
|
+
expect(loaders).toContain("default: (area) => {");
|
|
1517
1505
|
expect(loaders).toContain("billing: (area) => {");
|
|
1518
1506
|
expect(loaders).toContain('case "eu":');
|
|
1519
1507
|
expect(loaders).toContain('case "us":');
|
|
@@ -1630,19 +1618,16 @@ describe("generate-i18n-types", () => {
|
|
|
1630
1618
|
expect(usSplit.invoice_summary).toEqual({ "en-US": "You have {count} invoices" });
|
|
1631
1619
|
|
|
1632
1620
|
const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
|
|
1633
|
-
const dictionary = readFileSync(
|
|
1634
|
-
join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
|
|
1635
|
-
"utf8"
|
|
1636
|
-
);
|
|
1637
1621
|
expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
|
|
1638
1622
|
expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
|
|
1639
1623
|
expect(types).not.toContain("typeof import");
|
|
1640
|
-
expect(
|
|
1641
|
-
expect(
|
|
1642
|
-
|
|
1643
|
-
|
|
1624
|
+
expect(types).toContain("export type LazyNamespace = 'billing';");
|
|
1625
|
+
expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
|
|
1626
|
+
const loaders = readFileSync(
|
|
1627
|
+
join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
|
|
1628
|
+
"utf8"
|
|
1644
1629
|
);
|
|
1645
|
-
expect(
|
|
1630
|
+
expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
|
|
1646
1631
|
});
|
|
1647
1632
|
|
|
1648
1633
|
it("emits defaultDictionaryFor in single mode with custom delivery", () => {
|
|
@@ -188,15 +188,21 @@ function main() {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
writeFileIfChanged(typesOutputPath, typesContent);
|
|
191
|
-
writeFileIfChanged(dictionaryOutputPath, dictionaryContent);
|
|
192
|
-
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
193
191
|
|
|
194
192
|
const generatedFiles = [
|
|
195
193
|
path.relative(projectRoot, typesOutputPath),
|
|
196
|
-
path.relative(projectRoot, dictionaryOutputPath),
|
|
197
194
|
path.relative(projectRoot, instanceOutputPath),
|
|
198
195
|
];
|
|
199
196
|
|
|
197
|
+
if (dictionaryContent !== null) {
|
|
198
|
+
writeFileIfChanged(dictionaryOutputPath, dictionaryContent);
|
|
199
|
+
generatedFiles.splice(1, 0, path.relative(projectRoot, dictionaryOutputPath));
|
|
200
|
+
} else if (fs.existsSync(dictionaryOutputPath)) {
|
|
201
|
+
fs.unlinkSync(dictionaryOutputPath);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
205
|
+
|
|
200
206
|
if (config.dictionarySchemaOutput) {
|
|
201
207
|
const dictionarySchemaOutputPath = path.resolve(projectRoot, config.dictionarySchemaOutput);
|
|
202
208
|
const dictionarySpecBlock = formatDictionarySpecBlock(isSingle, entries, argsSpecByNamespace);
|
|
@@ -226,10 +232,14 @@ function main() {
|
|
|
226
232
|
loadersOutputPath: namespaceLoadersOutputPath,
|
|
227
233
|
lazyEntries: lazyEntriesWithPaths,
|
|
228
234
|
schemaTypeName,
|
|
235
|
+
paramsTypeName,
|
|
229
236
|
localeTypeName,
|
|
237
|
+
localeFallbackConstName,
|
|
238
|
+
hasLocaleFallback: Boolean(config.localeFallback),
|
|
230
239
|
typesModule,
|
|
231
240
|
importExtension,
|
|
232
241
|
projectRoot,
|
|
242
|
+
isSingle,
|
|
233
243
|
delivery,
|
|
234
244
|
splitPathsByNamespace,
|
|
235
245
|
...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
|