@xndrjs/i18n 0.7.0 → 0.8.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.
Files changed (69) hide show
  1. package/README.md +67 -872
  2. package/dist/codegen/index.d.ts +117 -51
  3. package/dist/codegen/index.js +1585 -93
  4. package/dist/codegen/index.js.map +1 -1
  5. package/dist/index.d.ts +100 -234
  6. package/dist/index.js +74 -638
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +5 -0
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/IcuTranslationProviderMulti.test.ts +18 -40
  12. package/src/IcuTranslationProviderMulti.ts +27 -37
  13. package/src/audit/audit-dictionaries.ts +1 -1
  14. package/src/audit/run-audit.test.ts +6 -7
  15. package/src/builder-load-registry.ts +26 -5
  16. package/src/builder-loaders.ts +8 -11
  17. package/src/builder-types.test.ts +11 -42
  18. package/src/builder.test.ts +82 -243
  19. package/src/builder.ts +8 -110
  20. package/src/codegen/codegen-config-schema.ts +82 -77
  21. package/src/codegen/config.test.ts +55 -143
  22. package/src/codegen/config.ts +10 -67
  23. package/src/codegen/dictionary-spec-contract.test.ts +28 -0
  24. package/src/codegen/dictionary-spec-contract.ts +138 -0
  25. package/src/codegen/emit/dictionary-schema-file.ts +7 -43
  26. package/src/codegen/emit/instance-file.test.ts +35 -52
  27. package/src/codegen/emit/instance-file.ts +170 -268
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
  29. package/src/codegen/emit/namespace-loaders-file.ts +147 -90
  30. package/src/codegen/emit/types-file.test.ts +20 -41
  31. package/src/codegen/emit/types-file.ts +48 -74
  32. package/src/codegen/generate-i18n-types.test.ts +163 -492
  33. package/src/codegen/generate-i18n-types.ts +7 -269
  34. package/src/codegen/paths.ts +0 -14
  35. package/src/codegen/project-locales-set-namespace.test.ts +44 -86
  36. package/src/codegen/read-dictionary.test.ts +27 -9
  37. package/src/codegen/read-dictionary.ts +18 -40
  38. package/src/codegen/regenerate-namespaces.ts +180 -0
  39. package/src/codegen/run-codegen.test.ts +230 -0
  40. package/src/codegen/run-codegen.ts +252 -0
  41. package/src/codegen/types.ts +0 -6
  42. package/src/codegen-config/build-config.ts +4 -23
  43. package/src/codegen-config/codegen-config.test.ts +10 -6
  44. package/src/codegen-config/index.ts +18 -3
  45. package/src/engine.ts +3 -27
  46. package/src/fetch-artifact.ts +16 -0
  47. package/src/i18n-handle.ts +202 -0
  48. package/src/index.ts +15 -34
  49. package/src/project-locales.ts +2 -2
  50. package/src/scope-multi.ts +1 -20
  51. package/src/scope-types.ts +14 -6
  52. package/src/scope.test.ts +3 -312
  53. package/src/serialized-state.test.ts +114 -0
  54. package/src/serialized-state.ts +28 -0
  55. package/src/setup/setup-i18n.test.ts +22 -33
  56. package/src/setup/setup-i18n.ts +22 -27
  57. package/src/types.ts +3 -3
  58. package/src/validation/index.ts +4 -0
  59. package/src/IcuTranslationProviderSingle.test.ts +0 -177
  60. package/src/IcuTranslationProviderSingle.ts +0 -114
  61. package/src/builder-multi.ts +0 -481
  62. package/src/codegen/emit/dictionary-file.test.ts +0 -215
  63. package/src/codegen/emit/dictionary-file.ts +0 -244
  64. package/src/deep-freeze.test.ts +0 -40
  65. package/src/deep-freeze.ts +0 -22
  66. package/src/patch-key.test.ts +0 -186
  67. package/src/patch-key.ts +0 -140
  68. package/src/scope-single.ts +0 -85
  69. package/src/single-builder.ts +0 -153
@@ -1,12 +1,4 @@
1
- import {
2
- mkdtempSync,
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- rmSync,
7
- statSync,
8
- writeFileSync,
9
- } from "node:fs";
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
10
2
  import { tmpdir } from "node:os";
11
3
  import { join } from "node:path";
12
4
  import { spawnSync } from "node:child_process";
@@ -65,11 +57,8 @@ describe("generate-i18n-types", () => {
65
57
  default: "src/i18n/translations/default.json",
66
58
  billing: "src/i18n/translations/billing.json",
67
59
  },
68
- typesOutput: "src/i18n/i18n-types.generated.ts",
69
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
70
- instanceOutput: "src/i18n/instance.generated.ts",
71
- paramsTypeName: "AppParams",
72
- schemaTypeName: "AppSchema",
60
+ projectName: "App",
61
+ codegenPath: "src/i18n",
73
62
  })
74
63
  );
75
64
 
@@ -79,13 +68,11 @@ describe("generate-i18n-types", () => {
79
68
  const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
80
69
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
81
70
  expect(types).toContain("export const I18N_MODE = 'multi' as const");
82
- expect(types).toContain("export type AppLocale = 'en'");
71
+ expect(types).toContain('export const AppLocales = ["en"] as const;');
72
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
83
73
  expect(factory).toContain("export function createI18n(");
84
- expect(factory).toContain("options?: { onMissing?: OnMissingTranslation }");
85
- expect(factory).toContain("export function projectDictionaryLocales(");
86
- expect(factory).toContain("projectDictionaryLocalesCore(dictionary, locales)");
87
- expect(factory).toContain("export function projectNamespaceLocales(");
88
- expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales)");
74
+ expect(factory).toContain("onMissing?: OnMissingTranslation");
75
+ expect(factory).toContain("state?: { dictionary: InitialSchema");
89
76
  expect(factory).toContain("IcuTranslationProviderMulti");
90
77
  expect(types).toContain("login_button: never");
91
78
  expect(types).toContain("welcome: { name: string }");
@@ -110,12 +97,11 @@ describe("generate-i18n-types", () => {
110
97
  writeFileSync(
111
98
  join(tempDir, "i18n.codegen.json"),
112
99
  JSON.stringify({
113
- dictionary: "src/i18n/translations/translations.json",
114
- typesOutput: "src/i18n/i18n-types.generated.ts",
115
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
116
- instanceOutput: "src/i18n/instance.generated.ts",
117
- paramsTypeName: "AppParams",
118
- schemaTypeName: "AppSchema",
100
+ namespaces: {
101
+ default: "src/i18n/translations/translations.json",
102
+ },
103
+ projectName: "App",
104
+ codegenPath: "src/i18n",
119
105
  })
120
106
  );
121
107
 
@@ -123,8 +109,8 @@ describe("generate-i18n-types", () => {
123
109
 
124
110
  const generatedPaths = [
125
111
  join(tempDir, "src/i18n/i18n-types.generated.ts"),
126
- join(tempDir, "src/i18n/dictionary.generated.ts"),
127
112
  join(tempDir, "src/i18n/instance.generated.ts"),
113
+ join(tempDir, "src/i18n/namespace-loaders.generated.ts"),
128
114
  ];
129
115
  const mtimesBefore = generatedPaths.map((filePath) => statSync(filePath).mtimeMs);
130
116
 
@@ -134,7 +120,7 @@ describe("generate-i18n-types", () => {
134
120
  expect(mtimesAfter).toEqual(mtimesBefore);
135
121
  });
136
122
 
137
- it("generates dictionary schema file when dictionarySchemaOutput is set", () => {
123
+ it("always generates dictionary schema file under codegenPath", () => {
138
124
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
139
125
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
140
126
 
@@ -159,12 +145,8 @@ describe("generate-i18n-types", () => {
159
145
  default: "src/i18n/translations/default.json",
160
146
  billing: "src/i18n/translations/billing.json",
161
147
  },
162
- typesOutput: "src/i18n/i18n-types.generated.ts",
163
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
164
- instanceOutput: "src/i18n/instance.generated.ts",
165
- dictionarySchemaOutput: "src/i18n/dictionary-schema.generated.ts",
166
- paramsTypeName: "AppParams",
167
- schemaTypeName: "AppSchema",
148
+ projectName: "App",
149
+ codegenPath: "src/i18n",
168
150
  })
169
151
  );
170
152
 
@@ -180,12 +162,12 @@ describe("generate-i18n-types", () => {
180
162
  expect(schema).toContain('"name": "string"');
181
163
  });
182
164
 
183
- it("generates single-mode dictionary schema without namespace validator", () => {
165
+ it("generates multi-mode dictionary schema with namespace validator", () => {
184
166
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
185
167
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
186
168
 
187
169
  writeFileSync(
188
- join(tempDir, "src/i18n/translations/translations.json"),
170
+ join(tempDir, "src/i18n/translations/default.json"),
189
171
  JSON.stringify({
190
172
  welcome: { en: "Welcome {name}!" },
191
173
  })
@@ -193,13 +175,11 @@ describe("generate-i18n-types", () => {
193
175
  writeFileSync(
194
176
  join(tempDir, "i18n.codegen.json"),
195
177
  JSON.stringify({
196
- dictionary: "src/i18n/translations/translations.json",
197
- typesOutput: "src/i18n/i18n-types.generated.ts",
198
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
199
- instanceOutput: "src/i18n/instance.generated.ts",
200
- dictionarySchemaOutput: "src/i18n/dictionary-schema.generated.ts",
201
- paramsTypeName: "AppParams",
202
- schemaTypeName: "AppSchema",
178
+ namespaces: {
179
+ default: "src/i18n/translations/default.json",
180
+ },
181
+ projectName: "App",
182
+ codegenPath: "src/i18n",
203
183
  })
204
184
  );
205
185
 
@@ -207,49 +187,12 @@ describe("generate-i18n-types", () => {
207
187
  expect(result.status).toBe(0);
208
188
 
209
189
  const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
210
- expect(schema).toContain("mode: 'single' as const");
190
+ expect(schema).toContain("mode: 'multi' as const");
211
191
  expect(schema).toContain("validateExternalDictionaryPartial");
192
+ expect(schema).toContain("validateExternalNamespacePartial");
212
193
  expect(schema).toContain("validateExternalKey");
213
194
  });
214
195
 
215
- it("generates flat types for single-file config", () => {
216
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
217
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
218
-
219
- writeFileSync(
220
- join(tempDir, "src/i18n/translations/translations.json"),
221
- JSON.stringify({
222
- login_button: { en: "Login" },
223
- welcome: { en: "Welcome {name}!" },
224
- })
225
- );
226
- writeFileSync(
227
- join(tempDir, "i18n.codegen.json"),
228
- JSON.stringify({
229
- dictionary: "src/i18n/translations/translations.json",
230
- typesOutput: "src/i18n/i18n-types.generated.ts",
231
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
232
- instanceOutput: "src/i18n/instance.generated.ts",
233
- paramsTypeName: "AppParams",
234
- schemaTypeName: "AppSchema",
235
- })
236
- );
237
-
238
- const result = runCodegen(tempDir);
239
- expect(result.status).toBe(0);
240
-
241
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
242
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
243
- expect(types).toContain("export const I18N_MODE = 'single' as const");
244
- expect(types).toContain("export type AppLocale = 'en'");
245
- expect(factory).toContain("export function createI18n(");
246
- expect(factory).toContain("export function projectDictionaryLocales(");
247
- expect(factory).toContain("dictionary: AppSchema");
248
- expect(factory).toContain("IcuTranslationProviderSingle");
249
- expect(types).toContain("login_button: never;");
250
- expect(types).toContain("welcome: { name: string };");
251
- });
252
-
253
196
  it("fails with a non-zero exit code on malformed ICU", () => {
254
197
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
255
198
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
@@ -263,12 +206,11 @@ describe("generate-i18n-types", () => {
263
206
  writeFileSync(
264
207
  join(tempDir, "i18n.codegen.json"),
265
208
  JSON.stringify({
266
- dictionary: "src/i18n/translations/translations.json",
267
- typesOutput: "src/i18n/i18n-types.generated.ts",
268
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
269
- instanceOutput: "src/i18n/instance.generated.ts",
270
- paramsTypeName: "AppParams",
271
- schemaTypeName: "AppSchema",
209
+ namespaces: {
210
+ default: "src/i18n/translations/translations.json",
211
+ },
212
+ projectName: "App",
213
+ codegenPath: "src/i18n",
272
214
  })
273
215
  );
274
216
 
@@ -290,12 +232,11 @@ describe("generate-i18n-types", () => {
290
232
  writeFileSync(
291
233
  join(tempDir, "i18n.codegen.json"),
292
234
  JSON.stringify({
293
- dictionary: "src/i18n/translations/translations.json",
294
- typesOutput: "src/i18n/i18n-types.generated.ts",
295
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
296
- instanceOutput: "src/i18n/instance.generated.ts",
297
- paramsTypeName: "AppParams",
298
- schemaTypeName: "AppSchema",
235
+ namespaces: {
236
+ default: "src/i18n/translations/translations.json",
237
+ },
238
+ projectName: "App",
239
+ codegenPath: "src/i18n",
299
240
  localeFallback: {
300
241
  en: null,
301
242
  "de-CH": "en",
@@ -310,12 +251,12 @@ describe("generate-i18n-types", () => {
310
251
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
311
252
  expect(types).toContain("export const LOCALE_FALLBACK");
312
253
  expect(types).toContain('"de-CH": "en"');
313
- expect(types).toContain("export type AppLocale = 'de-CH' | 'en'");
254
+ expect(types).toContain('export const AppLocales = ["de-CH", "en"] as const;');
255
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
314
256
  expect(factory).toContain("localeFallback: LOCALE_FALLBACK");
315
- expect(factory).toContain("...options");
316
- expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales, LOCALE_FALLBACK)");
257
+ expect(factory).toContain("...providerOptions");
317
258
  expect(factory).toContain(
318
- "IcuTranslationProviderSingle<AppSchema, AppParams, AppLocale, typeof LOCALE_FALLBACK>"
259
+ "IcuTranslationProviderMulti<AppSchema, AppParams, AppLocale, typeof LOCALE_FALLBACK>"
319
260
  );
320
261
  });
321
262
 
@@ -332,12 +273,11 @@ describe("generate-i18n-types", () => {
332
273
  writeFileSync(
333
274
  join(tempDir, "i18n.codegen.json"),
334
275
  JSON.stringify({
335
- dictionary: "src/i18n/translations/translations.json",
336
- typesOutput: "src/i18n/i18n-types.generated.ts",
337
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
338
- instanceOutput: "src/i18n/instance.generated.ts",
339
- paramsTypeName: "AppParams",
340
- schemaTypeName: "AppSchema",
276
+ namespaces: {
277
+ default: "src/i18n/translations/translations.json",
278
+ },
279
+ projectName: "App",
280
+ codegenPath: "src/i18n",
341
281
  localeFallback: {
342
282
  en: null,
343
283
  it: "en",
@@ -351,7 +291,8 @@ describe("generate-i18n-types", () => {
351
291
  const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
352
292
  expect(types).toContain('"fr": null');
353
293
  expect(types).toContain('"it": "en"');
354
- expect(types).toContain("export type AppLocale = 'en' | 'fr' | 'it'");
294
+ expect(types).toContain('export const AppLocales = ["en", "fr", "it"] as const;');
295
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
355
296
  });
356
297
 
357
298
  it("does not emit LOCALE_FALLBACK when localeFallback is absent from config", () => {
@@ -367,12 +308,11 @@ describe("generate-i18n-types", () => {
367
308
  writeFileSync(
368
309
  join(tempDir, "i18n.codegen.json"),
369
310
  JSON.stringify({
370
- dictionary: "src/i18n/translations/translations.json",
371
- typesOutput: "src/i18n/i18n-types.generated.ts",
372
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
373
- instanceOutput: "src/i18n/instance.generated.ts",
374
- paramsTypeName: "AppParams",
375
- schemaTypeName: "AppSchema",
311
+ namespaces: {
312
+ default: "src/i18n/translations/translations.json",
313
+ },
314
+ projectName: "App",
315
+ codegenPath: "src/i18n",
376
316
  })
377
317
  );
378
318
 
@@ -380,9 +320,8 @@ describe("generate-i18n-types", () => {
380
320
  expect(result.status).toBe(0);
381
321
 
382
322
  const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
383
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
384
- expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales)");
385
- expect(types).toContain("export type AppLocale = 'en' | 'it'");
323
+ expect(types).toContain('export const AppLocales = ["en", "it"] as const;');
324
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
386
325
  });
387
326
 
388
327
  it("fails on circular locale fallback in config", () => {
@@ -398,12 +337,11 @@ describe("generate-i18n-types", () => {
398
337
  writeFileSync(
399
338
  join(tempDir, "i18n.codegen.json"),
400
339
  JSON.stringify({
401
- dictionary: "src/i18n/translations/translations.json",
402
- typesOutput: "src/i18n/i18n-types.generated.ts",
403
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
404
- instanceOutput: "src/i18n/instance.generated.ts",
405
- paramsTypeName: "AppParams",
406
- schemaTypeName: "AppSchema",
340
+ namespaces: {
341
+ default: "src/i18n/translations/translations.json",
342
+ },
343
+ projectName: "App",
344
+ codegenPath: "src/i18n",
407
345
  localeFallback: {
408
346
  a: "b",
409
347
  b: "a",
@@ -416,7 +354,7 @@ describe("generate-i18n-types", () => {
416
354
  expect(result.stderr).toContain("Circular locale fallback");
417
355
  });
418
356
 
419
- it("generates lazy namespace loaders when loadOnInit is a subset", () => {
357
+ it("generates namespace loaders for all namespaces under split-by-locale", () => {
420
358
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
421
359
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
422
360
 
@@ -441,14 +379,8 @@ describe("generate-i18n-types", () => {
441
379
  default: "src/i18n/translations/default.json",
442
380
  billing: "src/i18n/translations/billing.json",
443
381
  },
444
- loadOnInit: ["default"],
445
- typesOutput: "src/i18n/i18n-types.generated.ts",
446
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
447
- instanceOutput: "src/i18n/instance.generated.ts",
448
- dictionarySchemaOutput: "src/i18n/dictionary-schema.generated.ts",
449
- namespaceLoadersOutput: "src/i18n/namespace-loaders.generated.ts",
450
- paramsTypeName: "AppParams",
451
- schemaTypeName: "AppSchema",
382
+ projectName: "App",
383
+ codegenPath: "src/i18n",
452
384
  })
453
385
  );
454
386
 
@@ -456,82 +388,23 @@ describe("generate-i18n-types", () => {
456
388
  expect(result.status).toBe(0);
457
389
 
458
390
  const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
459
- const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
460
391
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
461
392
  const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
462
393
 
463
- expect(types).toContain("export type LoadOnInitNamespace = 'default'");
464
- expect(types).toContain("export type LazyNamespace = 'billing'");
465
- expect(types).toContain("export type InitialSchema = Pick<AppSchema, LoadOnInitNamespace>");
466
- expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
467
- expect(factory).toContain("dictionary: InitialSchema,");
468
- expect(loaders).toContain("export const namespaceLoaders");
469
- expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
470
- expect(loaders).toContain("import('./translations/billing.json')");
471
- });
472
-
473
- it("keeps eager output when loadOnInit is omitted", () => {
474
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
475
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
476
-
477
- writeFileSync(
478
- join(tempDir, "src/i18n/translations/default.json"),
479
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
480
- );
481
- writeFileSync(
482
- join(tempDir, "src/i18n/translations/billing.json"),
483
- JSON.stringify({ invoice_summary: { en: "Invoice" } })
484
- );
485
- writeFileSync(
486
- join(tempDir, "i18n.codegen.json"),
487
- JSON.stringify({
488
- namespaces: {
489
- default: "src/i18n/translations/default.json",
490
- billing: "src/i18n/translations/billing.json",
491
- },
492
- typesOutput: "src/i18n/i18n-types.generated.ts",
493
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
494
- instanceOutput: "src/i18n/instance.generated.ts",
495
- paramsTypeName: "AppParams",
496
- schemaTypeName: "AppSchema",
497
- })
394
+ expect(types).toContain("export type LazyNamespace = 'default' | 'billing'");
395
+ expect(types).toContain("export type InitialSchema = Record<string, never>");
396
+ expect(factory).toContain(
397
+ "state?: { dictionary: InitialSchema; resources?: readonly (readonly [string, string])[] }"
498
398
  );
499
-
500
- const result = runCodegen(tempDir);
501
- expect(result.status).toBe(0);
502
-
503
- const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
504
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
505
-
506
- expect(dictionary).toContain("export const defaultDictionary: AppSchema");
507
- expect(dictionary).toContain("billingNs");
508
- expect(factory).toContain("dictionary: AppSchema,");
509
- });
510
-
511
- it("fails when loadOnInit is used in single mode", () => {
512
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
513
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
514
-
515
- writeFileSync(
516
- join(tempDir, "src/i18n/translations/translations.json"),
517
- JSON.stringify({ welcome: { en: "Welcome" } })
399
+ expect(factory).toContain("normalizeI18nCreateInput");
400
+ expect(factory).toContain("seedBuilderResources");
401
+ expect(loaders).toContain("export const namespaceLoaders");
402
+ expect(loaders).toContain(
403
+ "[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
518
404
  );
519
- writeFileSync(
520
- join(tempDir, "i18n.codegen.json"),
521
- JSON.stringify({
522
- dictionary: "src/i18n/translations/translations.json",
523
- loadOnInit: ["default"],
524
- typesOutput: "src/i18n/i18n-types.generated.ts",
525
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
526
- instanceOutput: "src/i18n/instance.generated.ts",
527
- paramsTypeName: "AppParams",
528
- schemaTypeName: "AppSchema",
529
- })
405
+ expect(loaders).toContain(
406
+ "return import('./translations/billing.en.json').then((m) => m.default);"
530
407
  );
531
-
532
- const result = runCodegen(tempDir);
533
- expect(result.status).not.toBe(0);
534
- expect(result.stderr).toContain("loadOnInit");
535
408
  });
536
409
 
537
410
  it("infers number when English plural and Italian simple interpolation share a key", () => {
@@ -550,12 +423,11 @@ describe("generate-i18n-types", () => {
550
423
  writeFileSync(
551
424
  join(tempDir, "i18n.codegen.json"),
552
425
  JSON.stringify({
553
- dictionary: "src/i18n/translations/translations.json",
554
- typesOutput: "src/i18n/i18n-types.generated.ts",
555
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
556
- instanceOutput: "src/i18n/instance.generated.ts",
557
- paramsTypeName: "AppParams",
558
- schemaTypeName: "AppSchema",
426
+ namespaces: {
427
+ default: "src/i18n/translations/translations.json",
428
+ },
429
+ projectName: "App",
430
+ codegenPath: "src/i18n",
559
431
  })
560
432
  );
561
433
 
@@ -582,12 +454,11 @@ describe("generate-i18n-types", () => {
582
454
  writeFileSync(
583
455
  join(tempDir, "i18n.codegen.json"),
584
456
  JSON.stringify({
585
- dictionary: "src/i18n/translations/translations.json",
586
- typesOutput: "src/i18n/i18n-types.generated.ts",
587
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
588
- instanceOutput: "src/i18n/instance.generated.ts",
589
- paramsTypeName: "AppParams",
590
- schemaTypeName: "AppSchema",
457
+ namespaces: {
458
+ default: "src/i18n/translations/translations.json",
459
+ },
460
+ projectName: "App",
461
+ codegenPath: "src/i18n",
591
462
  })
592
463
  );
593
464
 
@@ -612,12 +483,11 @@ describe("generate-i18n-types", () => {
612
483
  writeFileSync(
613
484
  join(tempDir, "i18n.codegen.json"),
614
485
  JSON.stringify({
615
- dictionary: "src/i18n/translations/translations.json",
616
- typesOutput: "src/i18n/i18n-types.generated.ts",
617
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
618
- instanceOutput: "src/i18n/instance.generated.ts",
619
- paramsTypeName: "AppParams",
620
- schemaTypeName: "AppSchema",
486
+ namespaces: {
487
+ default: "src/i18n/translations/translations.json",
488
+ },
489
+ projectName: "App",
490
+ codegenPath: "src/i18n",
621
491
  })
622
492
  );
623
493
 
@@ -642,12 +512,11 @@ describe("generate-i18n-types", () => {
642
512
  writeFileSync(
643
513
  join(tempDir, "i18n.codegen.json"),
644
514
  JSON.stringify({
645
- dictionary: "src/i18n/translations/translations.json",
646
- typesOutput: "src/i18n/i18n-types.generated.ts",
647
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
648
- instanceOutput: "src/i18n/instance.generated.ts",
649
- paramsTypeName: "AppParams",
650
- schemaTypeName: "AppSchema",
515
+ namespaces: {
516
+ default: "src/i18n/translations/translations.json",
517
+ },
518
+ projectName: "App",
519
+ codegenPath: "src/i18n",
651
520
  })
652
521
  );
653
522
 
@@ -670,11 +539,8 @@ describe("generate-i18n-types", () => {
670
539
  namespaces: {
671
540
  default: "src/i18n/translations/default.json",
672
541
  },
673
- typesOutput: "src/i18n/i18n-types.generated.ts",
674
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
675
- instanceOutput: "src/i18n/instance.generated.ts",
676
- paramsTypeName: "AppParams",
677
- schemaTypeName: "AppSchema",
542
+ projectName: "App",
543
+ codegenPath: "src/i18n",
678
544
  })
679
545
  );
680
546
 
@@ -699,108 +565,22 @@ describe("generate-i18n-types", () => {
699
565
  writeFileSync(
700
566
  join(tempDir, "i18n.codegen.json"),
701
567
  JSON.stringify({
702
- dictionary: "src/i18n/translations/translations.json",
703
- typesOutput: "src/i18n/i18n-types.generated.ts",
704
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
705
- instanceOutput: "src/i18n/instance.generated.ts",
706
- paramsTypeName: "AppParams",
707
- schemaTypeName: "AppSchema",
568
+ namespaces: {
569
+ default: "src/i18n/translations/translations.json",
570
+ },
571
+ projectName: "App",
572
+ codegenPath: "src/i18n",
708
573
  })
709
574
  );
710
575
 
711
576
  const result = runCodegen(tempDir);
712
577
  expect(result.status).toBe(0);
713
578
 
714
- const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
715
579
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
716
- expect(dictionary).toContain("from './i18n-types.generated'");
580
+ const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
717
581
  expect(factory).toContain("from './i18n-types.generated'");
718
- expect(factory).toContain("dictionary: AppSchema,");
719
- });
720
-
721
- it("supports importExtension .ts", () => {
722
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
723
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
724
-
725
- writeFileSync(
726
- join(tempDir, "src/i18n/translations/translations.json"),
727
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
728
- );
729
- writeFileSync(
730
- join(tempDir, "i18n.codegen.json"),
731
- JSON.stringify({
732
- dictionary: "src/i18n/translations/translations.json",
733
- typesOutput: "src/i18n/i18n-types.generated.ts",
734
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
735
- instanceOutput: "src/i18n/instance.generated.ts",
736
- importExtension: ".ts",
737
- paramsTypeName: "AppParams",
738
- schemaTypeName: "AppSchema",
739
- })
740
- );
741
-
742
- const result = runCodegen(tempDir);
743
- expect(result.status).toBe(0);
744
-
745
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
746
- expect(factory).toContain("from './i18n-types.generated.ts'");
747
- });
748
-
749
- it("supports importExtension .js for NodeNext ESM projects", () => {
750
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
751
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
752
-
753
- writeFileSync(
754
- join(tempDir, "src/i18n/translations/translations.json"),
755
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
756
- );
757
- writeFileSync(
758
- join(tempDir, "i18n.codegen.json"),
759
- JSON.stringify({
760
- dictionary: "src/i18n/translations/translations.json",
761
- typesOutput: "src/i18n/i18n-types.generated.ts",
762
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
763
- instanceOutput: "src/i18n/instance.generated.ts",
764
- importExtension: ".js",
765
- paramsTypeName: "AppParams",
766
- schemaTypeName: "AppSchema",
767
- })
768
- );
769
-
770
- const result = runCodegen(tempDir);
771
- expect(result.status).toBe(0);
772
-
773
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
774
- expect(factory).toContain("from './i18n-types.generated.js'");
775
- });
776
-
777
- it("fails when importExtension is invalid", () => {
778
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
779
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
780
-
781
- writeFileSync(
782
- join(tempDir, "src/i18n/translations/translations.json"),
783
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
784
- );
785
- writeFileSync(
786
- join(tempDir, "i18n.codegen.json"),
787
- JSON.stringify({
788
- dictionary: "src/i18n/translations/translations.json",
789
- typesOutput: "src/i18n/i18n-types.generated.ts",
790
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
791
- instanceOutput: "src/i18n/instance.generated.ts",
792
- importExtension: ".mjs",
793
- paramsTypeName: "AppParams",
794
- schemaTypeName: "AppSchema",
795
- })
796
- );
797
-
798
- const result = runCodegen(tempDir);
799
- expect(result.status).not.toBe(0);
800
- expect(result.stderr).toContain("Invalid i18n.codegen.json");
801
- expect(result.stderr).toContain("importExtension");
802
- expect(result.stderr).toContain('one of "none"|".ts"|".js"');
803
- expect(result.stderr).toContain("Allowed keys:");
582
+ expect(factory).toContain("dictionary: InitialSchema");
583
+ expect(loaders).toContain("from './i18n-types.generated'");
804
584
  });
805
585
 
806
586
  it("compiles yaml dictionaries to json and generates json imports", () => {
@@ -822,33 +602,34 @@ describe("generate-i18n-types", () => {
822
602
  writeFileSync(
823
603
  join(tempDir, "i18n.codegen.json"),
824
604
  JSON.stringify({
825
- dictionary: "src/i18n/translations/translations.yaml",
826
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
827
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
828
- instanceOutput: "src/i18n/generated/instance.generated.ts",
829
- paramsTypeName: "AppParams",
830
- schemaTypeName: "AppSchema",
605
+ namespaces: {
606
+ default: "src/i18n/translations/translations.yaml",
607
+ },
608
+ projectName: "App",
609
+ codegenPath: "src/i18n/generated",
831
610
  })
832
611
  );
833
612
 
834
613
  const result = runCodegen(tempDir);
835
614
  expect(result.status).toBe(0);
836
615
  expect(result.stdout).toContain(
837
- "Compiled: src/i18n/translations/translations.yaml → src/i18n/generated/translations/translations.json"
616
+ "Compiled: src/i18n/translations/translations.yaml → src/i18n/generated/translations/translations.en.json"
838
617
  );
839
618
 
840
619
  const compiled = JSON.parse(
841
- readFileSync(join(tempDir, "src/i18n/generated/translations/translations.json"), "utf8")
620
+ readFileSync(join(tempDir, "src/i18n/generated/translations/translations.en.json"), "utf8")
842
621
  ) as Record<string, Record<string, string>>;
843
622
  expect(compiled.welcome?.en).toBe("Line one\nLine two\n");
844
623
 
845
- const dictionary = readFileSync(
846
- join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
624
+ const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
625
+ const loaders = readFileSync(
626
+ join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
847
627
  "utf8"
848
628
  );
849
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
850
- expect(dictionary).toContain("from './translations/translations.json'");
851
629
  expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
630
+ expect(loaders).toContain(
631
+ "return import('./translations/translations.en.json').then((m) => m.default);"
632
+ );
852
633
  });
853
634
 
854
635
  it("supports mixed json and yaml namespaces", () => {
@@ -875,26 +656,27 @@ describe("generate-i18n-types", () => {
875
656
  default: "src/i18n/translations/default.json",
876
657
  billing: "src/i18n/translations/billing.yaml",
877
658
  },
878
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
879
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
880
- instanceOutput: "src/i18n/generated/instance.generated.ts",
881
- paramsTypeName: "AppParams",
882
- schemaTypeName: "AppSchema",
659
+ projectName: "App",
660
+ codegenPath: "src/i18n/generated",
883
661
  })
884
662
  );
885
663
 
886
664
  const result = runCodegen(tempDir);
887
665
  expect(result.status).toBe(0);
888
666
 
889
- const dictionary = readFileSync(
890
- join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
667
+ const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
668
+ const loaders = readFileSync(
669
+ join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
891
670
  "utf8"
892
671
  );
893
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
894
- expect(dictionary).toContain("from '../translations/default.json'");
895
- expect(dictionary).toContain("from './translations/billing.json'");
896
672
  expect(types).toContain("login_button: Partial<Record<AppLocale, string>>;");
897
673
  expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
674
+ expect(loaders).toContain(
675
+ "return import('./translations/billing.en.json').then((m) => m.default);"
676
+ );
677
+ expect(loaders).toContain(
678
+ "return import('./translations/default.en.json').then((m) => m.default);"
679
+ );
898
680
  });
899
681
 
900
682
  it("generates lazy loaders that import compiled json for yaml namespaces", () => {
@@ -921,14 +703,8 @@ describe("generate-i18n-types", () => {
921
703
  default: "src/i18n/translations/default.json",
922
704
  billing: "src/i18n/translations/billing.yaml",
923
705
  },
924
- loadOnInit: ["default"],
925
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
926
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
927
- instanceOutput: "src/i18n/generated/instance.generated.ts",
928
- dictionarySchemaOutput: "src/i18n/generated/dictionary-schema.generated.ts",
929
- namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
930
- paramsTypeName: "AppParams",
931
- schemaTypeName: "AppSchema",
706
+ projectName: "App",
707
+ codegenPath: "src/i18n/generated",
932
708
  })
933
709
  );
934
710
 
@@ -939,7 +715,9 @@ describe("generate-i18n-types", () => {
939
715
  join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
940
716
  "utf8"
941
717
  );
942
- expect(loaders).toContain("import('./translations/billing.json')");
718
+ expect(loaders).toContain(
719
+ "return import('./translations/billing.en.json').then((m) => m.default);"
720
+ );
943
721
  });
944
722
 
945
723
  it("fails when dictionary extension is unsupported", () => {
@@ -950,12 +728,11 @@ describe("generate-i18n-types", () => {
950
728
  writeFileSync(
951
729
  join(tempDir, "i18n.codegen.json"),
952
730
  JSON.stringify({
953
- dictionary: "src/i18n/translations/translations.toml",
954
- typesOutput: "src/i18n/i18n-types.generated.ts",
955
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
956
- instanceOutput: "src/i18n/instance.generated.ts",
957
- paramsTypeName: "AppParams",
958
- schemaTypeName: "AppSchema",
731
+ namespaces: {
732
+ default: "src/i18n/translations/translations.toml",
733
+ },
734
+ projectName: "App",
735
+ codegenPath: "src/i18n",
959
736
  })
960
737
  );
961
738
 
@@ -964,7 +741,7 @@ describe("generate-i18n-types", () => {
964
741
  expect(result.stderr).toMatch(/unsupported dictionary (extension|format)/i);
965
742
  });
966
743
 
967
- it("generates split-by-locale delivery with per-locale files and defaultDictionaryFor", () => {
744
+ it("generates split-by-locale delivery with per-locale files and namespace loaders", () => {
968
745
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
969
746
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
970
747
  mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
@@ -999,12 +776,8 @@ describe("generate-i18n-types", () => {
999
776
  billing: "src/i18n/translations/billing.json",
1000
777
  },
1001
778
  delivery: "split-by-locale",
1002
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1003
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1004
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1005
- namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1006
- paramsTypeName: "AppParams",
1007
- schemaTypeName: "AppSchema",
779
+ projectName: "App",
780
+ codegenPath: "src/i18n/generated",
1008
781
  })
1009
782
  );
1010
783
 
@@ -1034,14 +807,12 @@ describe("generate-i18n-types", () => {
1034
807
  "utf8"
1035
808
  );
1036
809
 
1037
- expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1038
-
1039
810
  expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
1040
811
  expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1041
- expect(types).toContain("export type AppLocale = 'en' | 'it'");
812
+ expect(types).toContain('export const AppLocales = ["en", "it"] as const;');
813
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
1042
814
  expect(types).toContain("invoice_summary: { count: number }");
1043
815
 
1044
- expect(types).toContain("export type LoadOnInitNamespace = never;");
1045
816
  expect(types).toContain("export type LazyNamespace = 'default' | 'user' | 'billing';");
1046
817
 
1047
818
  expect(loaders).toContain(
@@ -1061,42 +832,7 @@ describe("generate-i18n-types", () => {
1061
832
  expect(loaders).toContain("user: (locale) => {");
1062
833
  });
1063
834
 
1064
- it("omits dictionaryOutput in split mode and removes stale dictionary at the default path", () => {
1065
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1066
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1067
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1068
-
1069
- writeFileSync(
1070
- join(tempDir, "src/i18n/translations/default.json"),
1071
- JSON.stringify({
1072
- welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
1073
- })
1074
- );
1075
- writeFileSync(
1076
- join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
1077
- "// stale dictionary manifest\n"
1078
- );
1079
- writeFileSync(
1080
- join(tempDir, "i18n.codegen.json"),
1081
- JSON.stringify({
1082
- namespaces: {
1083
- default: "src/i18n/translations/default.json",
1084
- },
1085
- delivery: "split-by-locale",
1086
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1087
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1088
- namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1089
- paramsTypeName: "AppParams",
1090
- schemaTypeName: "AppSchema",
1091
- })
1092
- );
1093
-
1094
- const result = runCodegen(tempDir);
1095
- expect(result.status).toBe(0);
1096
- expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1097
- });
1098
-
1099
- it("writes delivery json under deliveryOutput when it differs from typesOutput directory", () => {
835
+ it("writes delivery json under artifactsPath when it differs from codegenPath", () => {
1100
836
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1101
837
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1102
838
  mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
@@ -1122,13 +858,9 @@ describe("generate-i18n-types", () => {
1122
858
  billing: "src/i18n/translations/billing.yaml",
1123
859
  },
1124
860
  delivery: "split-by-locale",
1125
- deliveryOutput: "public/i18n",
1126
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1127
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1128
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1129
- namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1130
- paramsTypeName: "AppParams",
1131
- schemaTypeName: "AppSchema",
861
+ artifactsPath: "public/i18n",
862
+ projectName: "App",
863
+ codegenPath: "src/i18n/generated",
1132
864
  })
1133
865
  );
1134
866
 
@@ -1150,8 +882,6 @@ describe("generate-i18n-types", () => {
1150
882
  join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1151
883
  "utf8"
1152
884
  );
1153
-
1154
- expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1155
885
  expect(loaders).toContain("export const defaultLazyNamespaces");
1156
886
  expect(loaders).toContain(
1157
887
  "return import('../../../public/i18n/translations/default.en.json').then((m) => m.default);"
@@ -1188,11 +918,8 @@ describe("generate-i18n-types", () => {
1188
918
  "de-CH": "de-DE",
1189
919
  it: "en",
1190
920
  },
1191
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1192
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1193
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1194
- paramsTypeName: "AppParams",
1195
- schemaTypeName: "AppSchema",
921
+ projectName: "App",
922
+ codegenPath: "src/i18n/generated",
1196
923
  })
1197
924
  );
1198
925
 
@@ -1208,48 +935,8 @@ describe("generate-i18n-types", () => {
1208
935
  });
1209
936
 
1210
937
  const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1211
- expect(types).toContain("export type AppLocale = 'de-CH' | 'de-DE' | 'en' | 'it'");
1212
- });
1213
-
1214
- it("keeps canonical output when delivery is explicitly set to canonical", () => {
1215
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1216
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1217
-
1218
- writeFileSync(
1219
- join(tempDir, "src/i18n/translations/default.json"),
1220
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
1221
- );
1222
- writeFileSync(
1223
- join(tempDir, "src/i18n/translations/billing.json"),
1224
- JSON.stringify({ invoice_summary: { en: "Invoice" } })
1225
- );
1226
- writeFileSync(
1227
- join(tempDir, "i18n.codegen.json"),
1228
- JSON.stringify({
1229
- namespaces: {
1230
- default: "src/i18n/translations/default.json",
1231
- billing: "src/i18n/translations/billing.json",
1232
- },
1233
- loadOnInit: ["default"],
1234
- delivery: "canonical",
1235
- typesOutput: "src/i18n/i18n-types.generated.ts",
1236
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
1237
- instanceOutput: "src/i18n/instance.generated.ts",
1238
- namespaceLoadersOutput: "src/i18n/namespace-loaders.generated.ts",
1239
- paramsTypeName: "AppParams",
1240
- schemaTypeName: "AppSchema",
1241
- })
1242
- );
1243
-
1244
- const result = runCodegen(tempDir);
1245
- expect(result.status).toBe(0);
1246
-
1247
- const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
1248
- const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
1249
-
1250
- expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
1251
- expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
1252
- expect(loaders).toContain("import('./translations/billing.json')");
938
+ expect(types).toContain('export const AppLocales = ["de-CH", "de-DE", "en", "it"] as const;');
939
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
1253
940
  });
1254
941
 
1255
942
  it("generates custom delivery with per-area json files", () => {
@@ -1290,12 +977,8 @@ describe("generate-i18n-types", () => {
1290
977
  it: "en-US",
1291
978
  fr: null,
1292
979
  },
1293
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1294
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1295
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1296
- namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1297
- paramsTypeName: "AppParams",
1298
- schemaTypeName: "AppSchema",
980
+ projectName: "App",
981
+ codegenPath: "src/i18n/generated",
1299
982
  })
1300
983
  );
1301
984
 
@@ -1349,7 +1032,8 @@ describe("generate-i18n-types", () => {
1349
1032
  readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1350
1033
  ).toThrow();
1351
1034
 
1352
- expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
1035
+ expect(types).toContain('export const AppDeliveryAreas = ["eu", "us"] as const;');
1036
+ expect(types).toContain("export type AppDeliveryArea = (typeof AppDeliveryAreas)[number];");
1353
1037
  expect(types).toContain("export const DELIVERY_ARTIFACTS = {");
1354
1038
  expect(types).toContain('"eu": ["fr", "it"] as const');
1355
1039
  expect(types).toContain('"us": ["en-US"] as const');
@@ -1360,16 +1044,14 @@ describe("generate-i18n-types", () => {
1360
1044
  expect(types).toContain('"fr": "eu"');
1361
1045
  expect(types).toContain('"en-US": "us"');
1362
1046
  expect(types).toContain("} as const satisfies Record<AppLocale, AppDeliveryArea>;");
1363
- expect(types).toContain("export type AppLocale = 'en-US' | 'fr' | 'it';");
1047
+ expect(types).toContain('export const AppLocales = ["en-US", "fr", "it"] as const;');
1048
+ expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
1364
1049
  expect(types).toContain("export const LOCALE_FALLBACK");
1365
1050
  expect(types).toContain('"it": "en-US"');
1366
1051
  expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
1367
1052
  expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1368
1053
  expect(types).toContain("invoice_summary: { count: number }");
1369
1054
 
1370
- expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1371
-
1372
- expect(types).toContain("export type LoadOnInitNamespace = never;");
1373
1055
  expect(types).toContain("export type LazyNamespace = 'default' | 'billing';");
1374
1056
 
1375
1057
  expect(loaders).toContain(
@@ -1387,14 +1069,7 @@ describe("generate-i18n-types", () => {
1387
1069
  "return import('./translations/billing.us.json').then((m) => m.default);"
1388
1070
  );
1389
1071
 
1390
- expect(factory).toContain("export function projectDictionaryForDeliveryArea(");
1391
- expect(factory).toContain("export function projectNamespaceForDeliveryArea(");
1392
- expect(factory).toContain(
1393
- "projectDictionaryForDeliveryAreaCore(dictionary, areaLocales, LOCALE_FALLBACK)"
1394
- );
1395
- expect(factory).toContain(
1396
- "projectNamespaceForDeliveryAreaCore(dictionary, areaLocales, LOCALE_FALLBACK)"
1397
- );
1072
+ expect(factory).toContain("partitionForLocale: (locale) => LOCALE_DELIVERY_AREA[locale]");
1398
1073
  });
1399
1074
 
1400
1075
  it("fails when deliveryArtifacts does not partition request locales", () => {
@@ -1423,11 +1098,8 @@ describe("generate-i18n-types", () => {
1423
1098
  it: "en-US",
1424
1099
  fr: null,
1425
1100
  },
1426
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1427
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1428
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1429
- paramsTypeName: "AppParams",
1430
- schemaTypeName: "AppSchema",
1101
+ projectName: "App",
1102
+ codegenPath: "src/i18n/generated",
1431
1103
  })
1432
1104
  );
1433
1105
 
@@ -1450,12 +1122,11 @@ describe("generate-i18n-types", () => {
1450
1122
  writeFileSync(
1451
1123
  join(tempDir, "i18n.codegen.json"),
1452
1124
  JSON.stringify({
1453
- dictionary: "src/i18n/translations/translations.yaml",
1454
- typesOutput: "src/i18n/i18n-types.generated.ts",
1455
- dictionaryOutput: "src/i18n/dictionary.generated.ts",
1456
- instanceOutput: "src/i18n/instance.generated.ts",
1457
- paramsTypeName: "AppParams",
1458
- schemaTypeName: "AppSchema",
1125
+ namespaces: {
1126
+ default: "src/i18n/translations/translations.yaml",
1127
+ },
1128
+ projectName: "App",
1129
+ codegenPath: "src/i18n",
1459
1130
  })
1460
1131
  );
1461
1132