@xndrjs/i18n 0.6.1 → 0.7.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 (51) hide show
  1. package/README.md +154 -141
  2. package/dist/codegen/index.js.map +1 -1
  3. package/dist/index.d.ts +268 -69
  4. package/dist/index.js +572 -65
  5. package/dist/index.js.map +1 -1
  6. package/dist/validation/index.d.ts +23 -7
  7. package/dist/validation/index.js +196 -89
  8. package/dist/validation/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/IcuTranslationProviderMulti.test.ts +80 -234
  11. package/src/IcuTranslationProviderMulti.ts +101 -112
  12. package/src/IcuTranslationProviderSingle.test.ts +30 -309
  13. package/src/IcuTranslationProviderSingle.ts +42 -75
  14. package/src/builder-load-registry.ts +18 -0
  15. package/src/builder-loaders.ts +18 -0
  16. package/src/builder-multi.ts +481 -0
  17. package/src/builder-types.test.ts +80 -0
  18. package/src/builder-types.ts +24 -0
  19. package/src/builder.test.ts +421 -0
  20. package/src/builder.ts +112 -0
  21. package/src/codegen/delivery-artifacts.test.ts +2 -1
  22. package/src/codegen/delivery-artifacts.ts +2 -1
  23. package/src/codegen/emit/dictionary-file.test.ts +0 -7
  24. package/src/codegen/emit/dictionary-schema-file.ts +55 -42
  25. package/src/codegen/emit/instance-file.test.ts +88 -0
  26. package/src/codegen/emit/instance-file.ts +164 -17
  27. package/src/codegen/emit/namespace-loaders-file.test.ts +4 -18
  28. package/src/codegen/emit/namespace-loaders-file.ts +4 -54
  29. package/src/codegen/emit/types-file.test.ts +0 -2
  30. package/src/codegen/generate-i18n-types.test.ts +8 -303
  31. package/src/codegen/generate-i18n-types.ts +16 -1
  32. package/src/codegen/project-locales-set-namespace.test.ts +25 -32
  33. package/src/engine.ts +71 -0
  34. package/src/index.ts +45 -7
  35. package/src/patch-key.test.ts +186 -0
  36. package/src/patch-key.ts +140 -0
  37. package/src/project-locales.test.ts +16 -6
  38. package/src/project-locales.ts +17 -6
  39. package/src/scope-multi.ts +124 -0
  40. package/src/scope-single.ts +85 -0
  41. package/src/scope-types.ts +27 -0
  42. package/src/scope.test.ts +481 -0
  43. package/src/single-builder.ts +153 -0
  44. package/src/types.ts +16 -0
  45. package/src/validation/create-normalized-schema.ts +1 -1
  46. package/src/validation/errors.ts +2 -0
  47. package/src/validation/index.ts +135 -27
  48. package/src/validation/normalize.ts +176 -46
  49. package/src/validation/types.ts +4 -0
  50. package/src/validation/validate-normalized.ts +43 -1
  51. package/src/validation/validation.test.ts +200 -7
@@ -174,39 +174,12 @@ describe("generate-i18n-types", () => {
174
174
  const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
175
175
  expect(schema).toContain("export const DICTIONARY_SPEC");
176
176
  expect(schema).toContain("mode: 'multi' as const");
177
- expect(schema).toContain("validateExternalDictionary");
178
- expect(schema).toContain("validateExternalNamespace");
179
- expect(schema).toContain("validateExternalNamespaceCore");
177
+ expect(schema).toContain("validateExternalDictionaryPartial");
178
+ expect(schema).toContain("validateExternalNamespacePartial");
179
+ expect(schema).toContain("validateExternalKey");
180
180
  expect(schema).toContain('"name": "string"');
181
181
  });
182
182
 
183
- it("does not generate dictionary schema file when dictionarySchemaOutput is omitted", () => {
184
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
185
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
186
-
187
- writeFileSync(
188
- join(tempDir, "src/i18n/translations/translations.json"),
189
- JSON.stringify({
190
- login_button: { en: "Login" },
191
- })
192
- );
193
- writeFileSync(
194
- join(tempDir, "i18n.codegen.json"),
195
- 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
- paramsTypeName: "AppParams",
201
- schemaTypeName: "AppSchema",
202
- })
203
- );
204
-
205
- const result = runCodegen(tempDir);
206
- expect(result.status).toBe(0);
207
- expect(result.stdout).not.toContain("dictionary-schema.generated.ts");
208
- });
209
-
210
183
  it("generates single-mode dictionary schema without namespace validator", () => {
211
184
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
212
185
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
@@ -235,9 +208,8 @@ describe("generate-i18n-types", () => {
235
208
 
236
209
  const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
237
210
  expect(schema).toContain("mode: 'single' as const");
238
- expect(schema).toContain("validateExternalDictionary");
239
- expect(schema).not.toContain("validateExternalNamespaceImpl");
240
- expect(schema).not.toContain("validateExternalNamespaceCore");
211
+ expect(schema).toContain("validateExternalDictionaryPartial");
212
+ expect(schema).toContain("validateExternalKey");
241
213
  });
242
214
 
243
215
  it("generates flat types for single-file config", () => {
@@ -273,12 +245,9 @@ describe("generate-i18n-types", () => {
273
245
  expect(factory).toContain("export function createI18n(");
274
246
  expect(factory).toContain("export function projectDictionaryLocales(");
275
247
  expect(factory).toContain("dictionary: AppSchema");
276
- expect(factory).not.toContain("export function projectNamespaceLocales(");
277
- expect(factory).not.toContain("projectDictionaryLocalesCore");
278
248
  expect(factory).toContain("IcuTranslationProviderSingle");
279
249
  expect(types).toContain("login_button: never;");
280
250
  expect(types).toContain("welcome: { name: string };");
281
- expect(types).not.toContain("default: {");
282
251
  });
283
252
 
284
253
  it("fails with a non-zero exit code on malformed ICU", () => {
@@ -412,10 +381,7 @@ describe("generate-i18n-types", () => {
412
381
 
413
382
  const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
414
383
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
415
- expect(types).not.toContain("export const LOCALE_FALLBACK");
416
- expect(factory).not.toContain("localeFallback:");
417
384
  expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales)");
418
- expect(factory).not.toContain("LOCALE_FALLBACK");
419
385
  expect(types).toContain("export type AppLocale = 'en' | 'it'");
420
386
  });
421
387
 
@@ -498,9 +464,7 @@ describe("generate-i18n-types", () => {
498
464
  expect(types).toContain("export type LazyNamespace = 'billing'");
499
465
  expect(types).toContain("export type InitialSchema = Pick<AppSchema, LoadOnInitNamespace>");
500
466
  expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
501
- expect(dictionary).not.toContain("billingNs");
502
467
  expect(factory).toContain("dictionary: InitialSchema,");
503
- expect(factory).not.toContain("import { dictionary }");
504
468
  expect(loaders).toContain("export const namespaceLoaders");
505
469
  expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
506
470
  expect(loaders).toContain("import('./translations/billing.json')");
@@ -536,16 +500,12 @@ describe("generate-i18n-types", () => {
536
500
  const result = runCodegen(tempDir);
537
501
  expect(result.status).toBe(0);
538
502
 
539
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
540
503
  const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
541
504
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
542
505
 
543
- expect(types).not.toContain("LazyNamespace");
544
506
  expect(dictionary).toContain("export const defaultDictionary: AppSchema");
545
507
  expect(dictionary).toContain("billingNs");
546
508
  expect(factory).toContain("dictionary: AppSchema,");
547
- expect(factory).not.toContain("import { dictionary }");
548
- expect(result.stdout).not.toContain("namespace-loaders.generated.ts");
549
509
  });
550
510
 
551
511
  it("fails when loadOnInit is used in single mode", () => {
@@ -754,13 +714,8 @@ describe("generate-i18n-types", () => {
754
714
  const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
755
715
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
756
716
  expect(dictionary).toContain("from './i18n-types.generated'");
757
- expect(factory).not.toContain("from './dictionary.generated'");
758
717
  expect(factory).toContain("from './i18n-types.generated'");
759
718
  expect(factory).toContain("dictionary: AppSchema,");
760
- expect(dictionary).not.toContain(".generated.js");
761
- expect(dictionary).not.toContain(".generated.ts");
762
- expect(factory).not.toContain(".generated.js");
763
- expect(factory).not.toContain(".generated.ts");
764
719
  });
765
720
 
766
721
  it("supports importExtension .ts", () => {
@@ -788,7 +743,6 @@ describe("generate-i18n-types", () => {
788
743
  expect(result.status).toBe(0);
789
744
 
790
745
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
791
- expect(factory).not.toContain("from './dictionary.generated.ts'");
792
746
  expect(factory).toContain("from './i18n-types.generated.ts'");
793
747
  });
794
748
 
@@ -817,7 +771,6 @@ describe("generate-i18n-types", () => {
817
771
  expect(result.status).toBe(0);
818
772
 
819
773
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
820
- expect(factory).not.toContain("from './dictionary.generated.js'");
821
774
  expect(factory).toContain("from './i18n-types.generated.js'");
822
775
  });
823
776
 
@@ -896,7 +849,6 @@ describe("generate-i18n-types", () => {
896
849
  const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
897
850
  expect(dictionary).toContain("from './translations/translations.json'");
898
851
  expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
899
- expect(types).not.toContain("typeof import");
900
852
  });
901
853
 
902
854
  it("supports mixed json and yaml namespaces", () => {
@@ -943,7 +895,6 @@ describe("generate-i18n-types", () => {
943
895
  expect(dictionary).toContain("from './translations/billing.json'");
944
896
  expect(types).toContain("login_button: Partial<Record<AppLocale, string>>;");
945
897
  expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
946
- expect(types).not.toContain("typeof import");
947
898
  });
948
899
 
949
900
  it("generates lazy loaders that import compiled json for yaml namespaces", () => {
@@ -989,7 +940,6 @@ describe("generate-i18n-types", () => {
989
940
  "utf8"
990
941
  );
991
942
  expect(loaders).toContain("import('./translations/billing.json')");
992
- expect(loaders).not.toContain("billing.yaml");
993
943
  });
994
944
 
995
945
  it("fails when dictionary extension is unsupported", () => {
@@ -1085,11 +1035,9 @@ describe("generate-i18n-types", () => {
1085
1035
  );
1086
1036
 
1087
1037
  expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1088
- expect(result.stdout).not.toContain("dictionary.generated.ts");
1089
1038
 
1090
1039
  expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
1091
1040
  expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1092
- expect(types).not.toContain("typeof import");
1093
1041
  expect(types).toContain("export type AppLocale = 'en' | 'it'");
1094
1042
  expect(types).toContain("invoice_summary: { count: number }");
1095
1043
 
@@ -1099,7 +1047,7 @@ describe("generate-i18n-types", () => {
1099
1047
  expect(loaders).toContain(
1100
1048
  "[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
1101
1049
  );
1102
- expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
1050
+ expect(loaders).toContain("export const defaultLazyNamespaces");
1103
1051
  expect(loaders).toContain("default: (locale) => {");
1104
1052
  expect(loaders).toContain("billing: (locale) => {");
1105
1053
  expect(loaders).toContain('case "it":');
@@ -1111,7 +1059,6 @@ describe("generate-i18n-types", () => {
1111
1059
  "return import('./translations/billing.en.json').then((m) => m.default);"
1112
1060
  );
1113
1061
  expect(loaders).toContain("user: (locale) => {");
1114
- expect(loaders).not.toContain("import('./translations/billing.json')");
1115
1062
  });
1116
1063
 
1117
1064
  it("omits dictionaryOutput in split mode and removes stale dictionary at the default path", () => {
@@ -1147,7 +1094,6 @@ describe("generate-i18n-types", () => {
1147
1094
  const result = runCodegen(tempDir);
1148
1095
  expect(result.status).toBe(0);
1149
1096
  expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1150
- expect(result.stdout).not.toContain("dictionary.generated.ts");
1151
1097
  });
1152
1098
 
1153
1099
  it("writes delivery json under deliveryOutput when it differs from typesOutput directory", () => {
@@ -1206,7 +1152,7 @@ describe("generate-i18n-types", () => {
1206
1152
  );
1207
1153
 
1208
1154
  expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1209
- expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
1155
+ expect(loaders).toContain("export const defaultLazyNamespaces");
1210
1156
  expect(loaders).toContain(
1211
1157
  "return import('../../../public/i18n/translations/default.en.json').then((m) => m.default);"
1212
1158
  );
@@ -1265,112 +1211,6 @@ describe("generate-i18n-types", () => {
1265
1211
  expect(types).toContain("export type AppLocale = 'de-CH' | 'de-DE' | 'en' | 'it'");
1266
1212
  });
1267
1213
 
1268
- it("splits yaml sources into per-locale json without a canonical intermediate", () => {
1269
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1270
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1271
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1272
-
1273
- writeFileSync(
1274
- join(tempDir, "src/i18n/translations/billing.yaml"),
1275
- `invoice_summary:
1276
- en: You have {count} invoices
1277
- it: Hai {count} fatture
1278
- `
1279
- );
1280
- writeFileSync(
1281
- join(tempDir, "i18n.codegen.json"),
1282
- JSON.stringify({
1283
- namespaces: {
1284
- billing: "src/i18n/translations/billing.yaml",
1285
- },
1286
- delivery: "split-by-locale",
1287
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1288
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1289
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1290
- paramsTypeName: "AppParams",
1291
- schemaTypeName: "AppSchema",
1292
- })
1293
- );
1294
-
1295
- const result = runCodegen(tempDir);
1296
- expect(result.status).toBe(0);
1297
- expect(result.stdout).toContain("billing.en.json");
1298
- expect(result.stdout).toContain("billing.it.json");
1299
-
1300
- expect(() =>
1301
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1302
- ).toThrow();
1303
-
1304
- const enSplit = JSON.parse(
1305
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
1306
- ) as Record<string, Record<string, string>>;
1307
- expect(enSplit.invoice_summary).toEqual({ en: "You have {count} invoices" });
1308
-
1309
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1310
- expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1311
- expect(types).not.toContain("typeof import");
1312
- expect(types).toContain("export type LazyNamespace = 'billing';");
1313
- expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1314
- const loaders = readFileSync(
1315
- join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1316
- "utf8"
1317
- );
1318
- expect(loaders).toContain("export async function ensureNamespacesLoadedForLocale(");
1319
- });
1320
-
1321
- it("emits defaultDictionaryFor in single mode with split-by-locale delivery", () => {
1322
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1323
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1324
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1325
-
1326
- const sourcePath = join(tempDir, "src/i18n/translations/translations.json");
1327
- writeFileSync(
1328
- sourcePath,
1329
- JSON.stringify({
1330
- welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
1331
- })
1332
- );
1333
- writeFileSync(
1334
- join(tempDir, "i18n.codegen.json"),
1335
- JSON.stringify({
1336
- dictionary: "src/i18n/translations/translations.json",
1337
- delivery: "split-by-locale",
1338
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1339
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1340
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1341
- paramsTypeName: "AppParams",
1342
- schemaTypeName: "AppSchema",
1343
- })
1344
- );
1345
-
1346
- const result = runCodegen(tempDir);
1347
- expect(result.status).toBe(0);
1348
-
1349
- const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
1350
- string,
1351
- Record<string, string>
1352
- >;
1353
- expect(sourceAfter.welcome).toEqual({
1354
- en: "Welcome {name}!",
1355
- it: "Benvenuto {name}!",
1356
- });
1357
-
1358
- const dictionary = readFileSync(
1359
- join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
1360
- "utf8"
1361
- );
1362
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1363
- expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
1364
- expect(types).not.toContain("typeof import");
1365
- expect(dictionary).toContain("import defaultEn from './translations/translations.en.json';");
1366
- expect(dictionary).toContain("import defaultIt from './translations/translations.it.json';");
1367
- expect(dictionary).toContain(
1368
- "export function defaultDictionaryFor(locale: AppLocale): AppSchema"
1369
- );
1370
- expect(dictionary).toContain("return defaultByLocale[locale];");
1371
- expect(dictionary).not.toContain("export const defaultDictionary");
1372
- });
1373
-
1374
1214
  it("keeps canonical output when delivery is explicitly set to canonical", () => {
1375
1215
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1376
1216
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
@@ -1408,12 +1248,8 @@ describe("generate-i18n-types", () => {
1408
1248
  const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
1409
1249
 
1410
1250
  expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
1411
- expect(dictionary).not.toContain("defaultDictionaryFor");
1412
1251
  expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
1413
1252
  expect(loaders).toContain("import('./translations/billing.json')");
1414
- expect(loaders).not.toContain("billing: {");
1415
- expect(result.stdout).not.toContain(".en.json");
1416
- expect(result.stdout).not.toContain(".it.json");
1417
1253
  });
1418
1254
 
1419
1255
  it("generates custom delivery with per-area json files", () => {
@@ -1529,11 +1365,9 @@ describe("generate-i18n-types", () => {
1529
1365
  expect(types).toContain('"it": "en-US"');
1530
1366
  expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
1531
1367
  expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1532
- expect(types).not.toContain("typeof import");
1533
1368
  expect(types).toContain("invoice_summary: { count: number }");
1534
1369
 
1535
1370
  expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1536
- expect(result.stdout).not.toContain("dictionary.generated.ts");
1537
1371
 
1538
1372
  expect(types).toContain("export type LoadOnInitNamespace = never;");
1539
1373
  expect(types).toContain("export type LazyNamespace = 'default' | 'billing';");
@@ -1541,7 +1375,7 @@ describe("generate-i18n-types", () => {
1541
1375
  expect(loaders).toContain(
1542
1376
  "[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
1543
1377
  );
1544
- expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
1378
+ expect(loaders).toContain("export const defaultLazyNamespaces");
1545
1379
  expect(loaders).toContain("default: (area) => {");
1546
1380
  expect(loaders).toContain("billing: (area) => {");
1547
1381
  expect(loaders).toContain('case "eu":');
@@ -1552,7 +1386,6 @@ describe("generate-i18n-types", () => {
1552
1386
  expect(loaders).toContain(
1553
1387
  "return import('./translations/billing.us.json').then((m) => m.default);"
1554
1388
  );
1555
- expect(loaders).not.toContain("import('./translations/billing.json')");
1556
1389
 
1557
1390
  expect(factory).toContain("export function projectDictionaryForDeliveryArea(");
1558
1391
  expect(factory).toContain("export function projectNamespaceForDeliveryArea(");
@@ -1604,134 +1437,6 @@ describe("generate-i18n-types", () => {
1604
1437
  expect(result.stderr).toContain("en-US");
1605
1438
  });
1606
1439
 
1607
- it("splits yaml sources into per-area json without a canonical intermediate", () => {
1608
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1609
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1610
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1611
-
1612
- writeFileSync(
1613
- join(tempDir, "src/i18n/translations/billing.yaml"),
1614
- `invoice_summary:
1615
- en-US: You have {count} invoices
1616
- it: Hai {count} fatture
1617
- `
1618
- );
1619
- writeFileSync(
1620
- join(tempDir, "i18n.codegen.json"),
1621
- JSON.stringify({
1622
- namespaces: {
1623
- billing: "src/i18n/translations/billing.yaml",
1624
- },
1625
- delivery: "custom",
1626
- deliveryArtifacts: {
1627
- eu: ["it"],
1628
- us: ["en-US"],
1629
- },
1630
- localeFallback: {
1631
- "en-US": null,
1632
- it: "en-US",
1633
- },
1634
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1635
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1636
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1637
- paramsTypeName: "AppParams",
1638
- schemaTypeName: "AppSchema",
1639
- })
1640
- );
1641
-
1642
- const result = runCodegen(tempDir);
1643
- expect(result.status).toBe(0);
1644
- expect(result.stdout).toContain("billing.eu.json");
1645
- expect(result.stdout).toContain("billing.us.json");
1646
-
1647
- expect(() =>
1648
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1649
- ).toThrow();
1650
-
1651
- const euSplit = JSON.parse(
1652
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.eu.json"), "utf8")
1653
- ) as Record<string, Record<string, string>>;
1654
- expect(euSplit.invoice_summary).toEqual({ it: "Hai {count} fatture" });
1655
-
1656
- const usSplit = JSON.parse(
1657
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.us.json"), "utf8")
1658
- ) as Record<string, Record<string, string>>;
1659
- expect(usSplit.invoice_summary).toEqual({ "en-US": "You have {count} invoices" });
1660
-
1661
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1662
- expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
1663
- expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1664
- expect(types).not.toContain("typeof import");
1665
- expect(types).toContain("export type LazyNamespace = 'billing';");
1666
- expect(existsSync(join(tempDir, "src/i18n/generated/dictionary.generated.ts"))).toBe(false);
1667
- const loaders = readFileSync(
1668
- join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1669
- "utf8"
1670
- );
1671
- expect(loaders).toContain("export async function ensureNamespacesLoadedForArea(");
1672
- });
1673
-
1674
- it("emits defaultDictionaryFor in single mode with custom delivery", () => {
1675
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1676
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1677
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1678
-
1679
- const sourcePath = join(tempDir, "src/i18n/translations/translations.json");
1680
- writeFileSync(
1681
- sourcePath,
1682
- JSON.stringify({
1683
- some_key: { it: "Ciao", "en-US": "Hello" },
1684
- })
1685
- );
1686
- writeFileSync(
1687
- join(tempDir, "i18n.codegen.json"),
1688
- JSON.stringify({
1689
- dictionary: "src/i18n/translations/translations.json",
1690
- delivery: "custom",
1691
- deliveryArtifacts: {
1692
- eu: ["it"],
1693
- us: ["en-US"],
1694
- },
1695
- localeFallback: {
1696
- "en-US": null,
1697
- it: "en-US",
1698
- },
1699
- typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1700
- dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1701
- instanceOutput: "src/i18n/generated/instance.generated.ts",
1702
- paramsTypeName: "AppParams",
1703
- schemaTypeName: "AppSchema",
1704
- })
1705
- );
1706
-
1707
- const result = runCodegen(tempDir);
1708
- expect(result.status).toBe(0);
1709
-
1710
- const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
1711
- string,
1712
- Record<string, string>
1713
- >;
1714
- expect(sourceAfter.some_key).toEqual({
1715
- it: "Ciao",
1716
- "en-US": "Hello",
1717
- });
1718
-
1719
- const dictionary = readFileSync(
1720
- join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
1721
- "utf8"
1722
- );
1723
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1724
- expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
1725
- expect(types).not.toContain("typeof import");
1726
- expect(dictionary).toContain("import defaultEu from './translations/translations.eu.json';");
1727
- expect(dictionary).toContain("import defaultUs from './translations/translations.us.json';");
1728
- expect(dictionary).toContain(
1729
- "export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
1730
- );
1731
- expect(dictionary).toContain("return defaultByArea[area];");
1732
- expect(dictionary).not.toContain("export const defaultDictionary");
1733
- });
1734
-
1735
1440
  it("fails when yaml dictionary has invalid shape", () => {
1736
1441
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1737
1442
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
@@ -16,7 +16,7 @@ import { formatInstanceFile } from "./emit/instance-file.js";
16
16
  import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
17
17
  import { formatTypesFile } from "./emit/types-file.js";
18
18
  import { analyzeDictionaries } from "./icu-analysis.js";
19
- import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
19
+ import { getDeliveryArtifactsIssues, getDeliveryArtifactsTypeName } from "./delivery-artifacts.js";
20
20
  import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
21
21
  import { enrichLocaleFallback } from "./locale-policy.js";
22
22
  import { reportCodegenIssues, resolveImportExtension, toModuleBasename } from "./paths.js";
@@ -176,6 +176,15 @@ function main() {
176
176
  isSingle,
177
177
  hasLazy,
178
178
  typesOutputPath,
179
+ ...(hasLazy
180
+ ? {
181
+ namespaceLoadersOutputPath: path.resolve(
182
+ projectRoot,
183
+ config.namespaceLoadersOutput ??
184
+ path.join(path.dirname(config.instanceOutput), "namespace-loaders.generated.ts")
185
+ ),
186
+ }
187
+ : {}),
179
188
  paramsTypeName,
180
189
  schemaTypeName,
181
190
  localeTypeName,
@@ -186,6 +195,12 @@ function main() {
186
195
  namespaceNames: entries.map((entry) => entry.namespace),
187
196
  importExtension,
188
197
  delivery,
198
+ ...(delivery === "custom" && deliveryAreaTypeName
199
+ ? {
200
+ deliveryAreaTypeName,
201
+ deliveryArtifactsTypeName: getDeliveryArtifactsTypeName(deliveryAreaTypeName),
202
+ }
203
+ : {}),
189
204
  });
190
205
 
191
206
  writeFileIfChanged(typesOutputPath, typesContent);
@@ -95,7 +95,7 @@ function writeTscProject(tempDir: string) {
95
95
  );
96
96
  }
97
97
 
98
- describe("codegen projectDictionaryLocales + setNamespace", () => {
98
+ describe("codegen projectNamespaceLocales + load + scope.set", () => {
99
99
  describe("generated instance output", () => {
100
100
  let tempDir: string;
101
101
 
@@ -111,53 +111,46 @@ describe("codegen projectDictionaryLocales + setNamespace", () => {
111
111
 
112
112
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
113
113
  expect(factory).toContain('dictionary: AppSchema["billing"]');
114
+ expect(factory).toContain("export function projectNamespaceLocales(");
114
115
  expect(factory).toContain(
115
116
  "projectDictionaryLocalesCore(dictionary, locales, LOCALE_FALLBACK)"
116
117
  );
117
118
  });
118
119
 
119
- it("passes tsc when hydrating with projectNamespaceLocales before setNamespace", () => {
120
+ it("passes tsc when hydrating with projectNamespaceLocales via load() and scope.set()", () => {
120
121
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
121
122
  setupMultiCodegenFixture(tempDir);
122
123
 
123
124
  writeFileSync(
124
125
  join(tempDir, "src/hydrate-billing.ts"),
125
126
  [
126
- `import { createI18n, projectNamespaceLocales } from "./i18n/instance.generated.js";`,
127
+ `import { createI18nMultiBuilder, IcuTranslationProviderMulti } from "@xndrjs/i18n";`,
128
+ `import { projectNamespaceLocales } from "./i18n/instance.generated.js";`,
127
129
  `import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
130
+ `import type { AppSchema, AppParams } from "./i18n/i18n-types.generated.js";`,
128
131
  `import billingDictionary from "./i18n/translations/billing.json";`,
129
132
  ``,
130
- `const i18n = createI18n(defaultDictionary);`,
131
- `i18n.setNamespace("billing", projectNamespaceLocales(billingDictionary, ["en"]));`,
132
- ].join("\n")
133
- );
134
-
135
- writeTscProject(tempDir);
136
-
137
- const tsc = runTsc(join(tempDir, "tsconfig.json"));
138
- expect(tsc.status, `${tsc.stdout}\n${tsc.stderr}`).toBe(0);
139
- });
140
-
141
- it("passes tsc when replacing the full schema with projectDictionaryLocales before setAll", () => {
142
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
143
- setupMultiCodegenFixture(tempDir);
144
-
145
- writeFileSync(
146
- join(tempDir, "src/hydrate-all.ts"),
147
- [
148
- `import { createI18n, projectDictionaryLocales } from "./i18n/instance.generated.js";`,
149
- `import { defaultDictionary } from "./i18n/dictionary.generated.js";`,
150
- `import type { AppSchema } from "./i18n/i18n-types.generated.js";`,
151
- `import billingDictionary from "./i18n/translations/billing.json";`,
152
- `import defaultNs from "./i18n/translations/default.json";`,
133
+ `async function hydrateBilling() {`,
134
+ ` const billingEn = projectNamespaceLocales(billingDictionary, ["en"]);`,
135
+ ` const engine = new IcuTranslationProviderMulti<AppSchema, AppParams>(defaultDictionary);`,
136
+ ` const view = await createI18nMultiBuilder(engine, {`,
137
+ ` namespaceLoaders: {`,
138
+ ` billing: async () => billingEn,`,
139
+ ` },`,
140
+ ` })`,
141
+ ` .withNamespaces(["billing"])`,
142
+ ` .withLocale("en")`,
143
+ ` .load();`,
153
144
  ``,
154
- `const fullDictionary = {`,
155
- ` default: defaultNs,`,
156
- ` billing: billingDictionary,`,
157
- `} satisfies AppSchema;`,
145
+ ` view.set(`,
146
+ ` "billing",`,
147
+ ` "invoice_summary",`,
148
+ ` "You have {count, plural, one {1 invoice} other {{count} invoices}}"`,
149
+ ` );`,
150
+ ` return view.t("billing", "invoice_summary", { count: 1 });`,
151
+ `}`,
158
152
  ``,
159
- `const i18n = createI18n(defaultDictionary);`,
160
- `i18n.setAll(projectDictionaryLocales(fullDictionary, ["en"]));`,
153
+ `export { hydrateBilling };`,
161
154
  ].join("\n")
162
155
  );
163
156