@xndrjs/i18n 0.3.3 → 0.5.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +193 -44
  2. package/dist/codegen/index.d.ts +85 -0
  3. package/dist/codegen/index.js +181 -0
  4. package/dist/codegen/index.js.map +1 -0
  5. package/dist/index.d.ts +34 -18
  6. package/dist/index.js +174 -145
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +86 -4
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +6 -1
  11. package/src/IcuTranslationProviderMulti.test.ts +48 -1
  12. package/src/IcuTranslationProviderMulti.ts +41 -61
  13. package/src/IcuTranslationProviderSingle.test.ts +67 -0
  14. package/src/IcuTranslationProviderSingle.ts +27 -51
  15. package/src/audit/audit-dictionaries.ts +4 -1
  16. package/src/audit/run-audit.test.ts +35 -1
  17. package/src/audit/run-audit.ts +15 -7
  18. package/src/codegen/codegen-config-schema.ts +68 -1
  19. package/src/codegen/config.test.ts +174 -39
  20. package/src/codegen/config.ts +14 -5
  21. package/src/codegen/constants.ts +8 -0
  22. package/src/codegen/delivery-artifacts.test.ts +142 -0
  23. package/src/codegen/delivery-artifacts.ts +124 -0
  24. package/src/codegen/emit/dictionary-file.test.ts +190 -0
  25. package/src/codegen/emit/dictionary-file.ts +165 -2
  26. package/src/codegen/emit/dictionary-schema-file.ts +5 -0
  27. package/src/codegen/emit/instance-file.ts +119 -38
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
  29. package/src/codegen/emit/namespace-loaders-file.ts +118 -32
  30. package/src/codegen/emit/types-file.test.ts +114 -0
  31. package/src/codegen/emit/types-file.ts +48 -11
  32. package/src/codegen/generate-i18n-types.test.ts +765 -22
  33. package/src/codegen/generate-i18n-types.ts +111 -58
  34. package/src/codegen/icu-analysis.ts +9 -2
  35. package/src/codegen/locale-fallback.ts +19 -10
  36. package/src/codegen/locale-policy.ts +4 -0
  37. package/src/codegen/paths.ts +9 -5
  38. package/src/codegen/project-locales-set-namespace.test.ts +11 -9
  39. package/src/codegen/read-dictionary.test.ts +474 -2
  40. package/src/codegen/read-dictionary.ts +164 -15
  41. package/src/codegen/write-file-if-changed.test.ts +42 -0
  42. package/src/codegen/write-file-if-changed.ts +20 -0
  43. package/src/codegen-config/build-config.ts +34 -0
  44. package/src/codegen-config/codegen-config.test.ts +32 -0
  45. package/src/codegen-config/index.ts +21 -0
  46. package/src/codegen-config/write-config.ts +8 -0
  47. package/src/deep-freeze.test.ts +13 -0
  48. package/src/deep-freeze.ts +5 -0
  49. package/src/format-core.ts +91 -0
  50. package/src/index.ts +8 -5
  51. package/src/project-locales.test.ts +129 -10
  52. package/src/project-locales.ts +90 -3
  53. package/src/setup/setup-i18n.test.ts +1 -1
  54. package/src/setup/setup-i18n.ts +9 -32
  55. package/src/types.ts +19 -4
  56. package/src/validation/normalize.ts +4 -0
  57. package/dist/types-C1CpXVOJ.d.ts +0 -83
  58. package/src/ensure-namespace.integration.test.ts +0 -66
  59. package/src/ensure-namespace.test.ts +0 -125
  60. package/src/ensure-namespace.ts +0 -70
  61. /package/src/{setup → codegen-config}/type-names.ts +0 -0
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
@@ -73,10 +73,11 @@ describe("generate-i18n-types", () => {
73
73
  expect(types).toContain("export const I18N_MODE = 'multi' as const");
74
74
  expect(types).toContain("export type AppLocale = 'en'");
75
75
  expect(factory).toContain("export function createI18n(");
76
- expect(factory).toContain("export function projectLocales(");
77
- expect(factory).toContain("projectNamespacesLocalesCore(dictionary, locales)");
76
+ expect(factory).toContain("options?: { onMissing?: OnMissingTranslation }");
77
+ expect(factory).toContain("export function projectDictionaryLocales(");
78
+ expect(factory).toContain("projectDictionaryLocalesCore(dictionary, locales)");
78
79
  expect(factory).toContain("export function projectNamespaceLocales(");
79
- expect(factory).toContain("projectLocalesCore(dictionary, locales)");
80
+ expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales)");
80
81
  expect(factory).toContain("IcuTranslationProviderMulti");
81
82
  expect(types).toContain("login_button: never");
82
83
  expect(types).toContain("welcome: { name: string }");
@@ -90,6 +91,41 @@ describe("generate-i18n-types", () => {
90
91
  expect(types).toContain("export type AppSchema");
91
92
  });
92
93
 
94
+ it("does not rewrite unchanged generated files on rerun", () => {
95
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
96
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
97
+
98
+ writeFileSync(
99
+ join(tempDir, "src/i18n/translations/translations.json"),
100
+ JSON.stringify({ welcome: { en: "Welcome {name}!" } })
101
+ );
102
+ writeFileSync(
103
+ join(tempDir, "i18n.codegen.json"),
104
+ JSON.stringify({
105
+ dictionary: "src/i18n/translations/translations.json",
106
+ typesOutput: "src/i18n/i18n-types.generated.ts",
107
+ dictionaryOutput: "src/i18n/dictionary.generated.ts",
108
+ instanceOutput: "src/i18n/instance.generated.ts",
109
+ paramsTypeName: "AppParams",
110
+ schemaTypeName: "AppSchema",
111
+ })
112
+ );
113
+
114
+ expect(runCodegen(tempDir).status).toBe(0);
115
+
116
+ const generatedPaths = [
117
+ join(tempDir, "src/i18n/i18n-types.generated.ts"),
118
+ join(tempDir, "src/i18n/dictionary.generated.ts"),
119
+ join(tempDir, "src/i18n/instance.generated.ts"),
120
+ ];
121
+ const mtimesBefore = generatedPaths.map((filePath) => statSync(filePath).mtimeMs);
122
+
123
+ expect(runCodegen(tempDir).status).toBe(0);
124
+
125
+ const mtimesAfter = generatedPaths.map((filePath) => statSync(filePath).mtimeMs);
126
+ expect(mtimesAfter).toEqual(mtimesBefore);
127
+ });
128
+
93
129
  it("generates dictionary schema file when dictionarySchemaOutput is set", () => {
94
130
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
95
131
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
@@ -225,10 +261,10 @@ describe("generate-i18n-types", () => {
225
261
  expect(types).toContain("export const I18N_MODE = 'single' as const");
226
262
  expect(types).toContain("export type AppLocale = 'en'");
227
263
  expect(factory).toContain("export function createI18n(");
228
- expect(factory).toContain("export function projectLocales(");
264
+ expect(factory).toContain("export function projectDictionaryLocales(");
229
265
  expect(factory).toContain("dictionary: AppSchema");
230
- expect(factory).not.toContain("projectNamespaceLocales");
231
- expect(factory).not.toContain("projectNamespacesLocalesCore");
266
+ expect(factory).not.toContain("export function projectNamespaceLocales(");
267
+ expect(factory).not.toContain("projectDictionaryLocalesCore");
232
268
  expect(factory).toContain("IcuTranslationProviderSingle");
233
269
  expect(types).toContain("login_button: never;");
234
270
  expect(types).toContain("welcome: { name: string };");
@@ -297,7 +333,8 @@ describe("generate-i18n-types", () => {
297
333
  expect(types).toContain('"de-CH": "en"');
298
334
  expect(types).toContain("export type AppLocale = 'de-CH' | 'en'");
299
335
  expect(factory).toContain("localeFallback: LOCALE_FALLBACK");
300
- expect(factory).toContain("projectLocalesCore(dictionary, locales, LOCALE_FALLBACK)");
336
+ expect(factory).toContain("...options");
337
+ expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales, LOCALE_FALLBACK)");
301
338
  expect(factory).toContain(
302
339
  "IcuTranslationProviderSingle<AppSchema, AppParams, AppLocale, typeof LOCALE_FALLBACK>"
303
340
  );
@@ -367,7 +404,7 @@ describe("generate-i18n-types", () => {
367
404
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
368
405
  expect(types).not.toContain("export const LOCALE_FALLBACK");
369
406
  expect(factory).not.toContain("localeFallback:");
370
- expect(factory).toContain("projectLocalesCore(dictionary, locales)");
407
+ expect(factory).toContain("projectNamespaceLocalesCore(dictionary, locales)");
371
408
  expect(factory).not.toContain("LOCALE_FALLBACK");
372
409
  expect(types).toContain("export type AppLocale = 'en' | 'it'");
373
410
  });
@@ -450,12 +487,12 @@ describe("generate-i18n-types", () => {
450
487
  expect(types).toContain("export type LoadOnInitNamespace = 'default'");
451
488
  expect(types).toContain("export type LazyNamespace = 'billing'");
452
489
  expect(types).toContain("export type InitialSchema = Pick<AppSchema, LoadOnInitNamespace>");
453
- expect(dictionary).toContain("export const dictionary: InitialSchema");
490
+ expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
454
491
  expect(dictionary).not.toContain("billingNs");
455
- expect(factory).toContain("initialDictionary: InitialSchema = dictionary");
492
+ expect(factory).toContain("dictionary: InitialSchema,");
493
+ expect(factory).not.toContain("import { dictionary }");
456
494
  expect(loaders).toContain("export const namespaceLoaders");
457
- expect(loaders).toContain("export async function ensureNamespacesLoaded(");
458
- expect(loaders).toContain("namespaces: LazyNamespace[]");
495
+ expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
459
496
  expect(loaders).toContain("import('./translations/billing.json')");
460
497
  });
461
498
 
@@ -494,9 +531,10 @@ describe("generate-i18n-types", () => {
494
531
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
495
532
 
496
533
  expect(types).not.toContain("LazyNamespace");
497
- expect(dictionary).toContain("export const dictionary: AppSchema");
534
+ expect(dictionary).toContain("export const defaultDictionary: AppSchema");
498
535
  expect(dictionary).toContain("billingNs");
499
- expect(factory).toContain("initialDictionary: AppSchema = dictionary");
536
+ expect(factory).toContain("dictionary: AppSchema,");
537
+ expect(factory).not.toContain("import { dictionary }");
500
538
  expect(result.stdout).not.toContain("namespace-loaders.generated.ts");
501
539
  });
502
540
 
@@ -706,8 +744,9 @@ describe("generate-i18n-types", () => {
706
744
  const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
707
745
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
708
746
  expect(dictionary).toContain("from './i18n-types.generated'");
709
- expect(factory).toContain("from './dictionary.generated'");
747
+ expect(factory).not.toContain("from './dictionary.generated'");
710
748
  expect(factory).toContain("from './i18n-types.generated'");
749
+ expect(factory).toContain("dictionary: AppSchema,");
711
750
  expect(dictionary).not.toContain(".generated.js");
712
751
  expect(dictionary).not.toContain(".generated.ts");
713
752
  expect(factory).not.toContain(".generated.js");
@@ -739,7 +778,7 @@ describe("generate-i18n-types", () => {
739
778
  expect(result.status).toBe(0);
740
779
 
741
780
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
742
- expect(factory).toContain("from './dictionary.generated.ts'");
781
+ expect(factory).not.toContain("from './dictionary.generated.ts'");
743
782
  expect(factory).toContain("from './i18n-types.generated.ts'");
744
783
  });
745
784
 
@@ -768,7 +807,7 @@ describe("generate-i18n-types", () => {
768
807
  expect(result.status).toBe(0);
769
808
 
770
809
  const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
771
- expect(factory).toContain("from './dictionary.generated.js'");
810
+ expect(factory).not.toContain("from './dictionary.generated.js'");
772
811
  expect(factory).toContain("from './i18n-types.generated.js'");
773
812
  });
774
813
 
@@ -846,7 +885,8 @@ describe("generate-i18n-types", () => {
846
885
  );
847
886
  const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
848
887
  expect(dictionary).toContain("from './translations/translations.json'");
849
- expect(types).toContain("typeof import('./translations/translations.json')");
888
+ expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
889
+ expect(types).not.toContain("typeof import");
850
890
  });
851
891
 
852
892
  it("supports mixed json and yaml namespaces", () => {
@@ -891,8 +931,9 @@ describe("generate-i18n-types", () => {
891
931
  const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
892
932
  expect(dictionary).toContain("from '../translations/default.json'");
893
933
  expect(dictionary).toContain("from './translations/billing.json'");
894
- expect(types).toContain("default: typeof import('../translations/default.json')");
895
- expect(types).toContain("billing: typeof import('./translations/billing.json')");
934
+ expect(types).toContain("login_button: Partial<Record<AppLocale, string>>;");
935
+ expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
936
+ expect(types).not.toContain("typeof import");
896
937
  });
897
938
 
898
939
  it("generates lazy loaders that import compiled json for yaml namespaces", () => {
@@ -960,7 +1001,709 @@ describe("generate-i18n-types", () => {
960
1001
 
961
1002
  const result = runCodegen(tempDir);
962
1003
  expect(result.status).not.toBe(0);
963
- expect(result.stderr).toContain("unsupported dictionary extension");
1004
+ expect(result.stderr).toMatch(/unsupported dictionary (extension|format)/i);
1005
+ });
1006
+
1007
+ it("generates split-by-locale delivery with per-locale files and defaultDictionaryFor", () => {
1008
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1009
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1010
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1011
+
1012
+ writeFileSync(
1013
+ join(tempDir, "src/i18n/translations/default.json"),
1014
+ JSON.stringify({
1015
+ welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
1016
+ })
1017
+ );
1018
+ writeFileSync(
1019
+ join(tempDir, "src/i18n/translations/user.json"),
1020
+ JSON.stringify({
1021
+ profile_title: { en: "Your profile", it: "Il tuo profilo" },
1022
+ })
1023
+ );
1024
+ writeFileSync(
1025
+ join(tempDir, "src/i18n/translations/billing.json"),
1026
+ JSON.stringify({
1027
+ invoice_summary: {
1028
+ en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
1029
+ it: "Hai {count} fatture",
1030
+ },
1031
+ })
1032
+ );
1033
+ writeFileSync(
1034
+ join(tempDir, "i18n.codegen.json"),
1035
+ JSON.stringify({
1036
+ namespaces: {
1037
+ default: "src/i18n/translations/default.json",
1038
+ user: "src/i18n/translations/user.json",
1039
+ billing: "src/i18n/translations/billing.json",
1040
+ },
1041
+ loadOnInit: ["default"],
1042
+ delivery: "split-by-locale",
1043
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1044
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1045
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1046
+ namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1047
+ paramsTypeName: "AppParams",
1048
+ schemaTypeName: "AppSchema",
1049
+ })
1050
+ );
1051
+
1052
+ const result = runCodegen(tempDir);
1053
+ expect(result.status).toBe(0);
1054
+ expect(result.stdout).toContain("default.en.json");
1055
+ expect(result.stdout).toContain("user.it.json");
1056
+ expect(result.stdout).toContain("billing.en.json");
1057
+
1058
+ const userEn = JSON.parse(
1059
+ readFileSync(join(tempDir, "src/i18n/generated/translations/user.en.json"), "utf8")
1060
+ ) as Record<string, Record<string, string>>;
1061
+ expect(userEn.profile_title).toEqual({ en: "Your profile" });
1062
+
1063
+ const billingIt = JSON.parse(
1064
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.it.json"), "utf8")
1065
+ ) as Record<string, Record<string, string>>;
1066
+ expect(billingIt.invoice_summary).toEqual({ it: "Hai {count} fatture" });
1067
+
1068
+ expect(() =>
1069
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1070
+ ).toThrow();
1071
+
1072
+ 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
+ const loaders = readFileSync(
1078
+ join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1079
+ "utf8"
1080
+ );
1081
+
1082
+ expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
1083
+ expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1084
+ expect(types).not.toContain("typeof import");
1085
+ expect(types).toContain("export type AppLocale = 'en' | 'it'");
1086
+ expect(types).toContain("invoice_summary: { count: number }");
1087
+
1088
+ expect(dictionary).toContain("import defaultEn from './translations/default.en.json';");
1089
+ expect(dictionary).toContain("import defaultIt from './translations/default.it.json';");
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");
1097
+
1098
+ expect(loaders).toContain(
1099
+ "[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
1100
+ );
1101
+ expect(loaders).toContain("billing: (locale) => {");
1102
+ expect(loaders).toContain('case "it":');
1103
+ expect(loaders).toContain(
1104
+ "return import('./translations/billing.it.json').then((m) => m.default);"
1105
+ );
1106
+ expect(loaders).toContain('case "en":');
1107
+ expect(loaders).toContain(
1108
+ "return import('./translations/billing.en.json').then((m) => m.default);"
1109
+ );
1110
+ expect(loaders).toContain("user: (locale) => {");
1111
+ expect(loaders).not.toContain("import('./translations/billing.json')");
1112
+ });
1113
+
1114
+ it("writes delivery json under deliveryOutput when it differs from typesOutput directory", () => {
1115
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1116
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1117
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1118
+
1119
+ writeFileSync(
1120
+ join(tempDir, "src/i18n/translations/default.json"),
1121
+ JSON.stringify({
1122
+ welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
1123
+ })
1124
+ );
1125
+ writeFileSync(
1126
+ join(tempDir, "src/i18n/translations/billing.yaml"),
1127
+ `invoice_summary:
1128
+ en: You have {count} invoices
1129
+ it: Hai {count} fatture
1130
+ `
1131
+ );
1132
+ writeFileSync(
1133
+ join(tempDir, "i18n.codegen.json"),
1134
+ JSON.stringify({
1135
+ namespaces: {
1136
+ default: "src/i18n/translations/default.json",
1137
+ billing: "src/i18n/translations/billing.yaml",
1138
+ },
1139
+ loadOnInit: ["default"],
1140
+ delivery: "split-by-locale",
1141
+ deliveryOutput: "public/i18n",
1142
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1143
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1144
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1145
+ namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1146
+ paramsTypeName: "AppParams",
1147
+ schemaTypeName: "AppSchema",
1148
+ })
1149
+ );
1150
+
1151
+ const result = runCodegen(tempDir);
1152
+ expect(result.status).toBe(0);
1153
+ expect(result.stdout).toContain("public/i18n/translations/billing.en.json");
1154
+ expect(result.stdout).toContain("public/i18n/translations/billing.it.json");
1155
+
1156
+ expect(() =>
1157
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
1158
+ ).toThrow();
1159
+
1160
+ const billingEn = JSON.parse(
1161
+ readFileSync(join(tempDir, "public/i18n/translations/billing.en.json"), "utf8")
1162
+ ) as Record<string, Record<string, string>>;
1163
+ expect(billingEn.invoice_summary).toEqual({ en: "You have {count} invoices" });
1164
+
1165
+ const dictionary = readFileSync(
1166
+ join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
1167
+ "utf8"
1168
+ );
1169
+ const loaders = readFileSync(
1170
+ join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1171
+ "utf8"
1172
+ );
1173
+
1174
+ expect(dictionary).toContain(
1175
+ "import defaultEn from '../../../public/i18n/translations/default.en.json';"
1176
+ );
1177
+ expect(dictionary).toContain(
1178
+ "import defaultIt from '../../../public/i18n/translations/default.it.json';"
1179
+ );
1180
+ expect(loaders).toContain(
1181
+ "return import('../../../public/i18n/translations/billing.en.json').then((m) => m.default);"
1182
+ );
1183
+ expect(loaders).toContain(
1184
+ "return import('../../../public/i18n/translations/billing.it.json').then((m) => m.default);"
1185
+ );
1186
+ });
1187
+
1188
+ it("applies localeFallback when generating split-by-locale files", () => {
1189
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1190
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1191
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1192
+
1193
+ writeFileSync(
1194
+ join(tempDir, "src/i18n/translations/default.json"),
1195
+ JSON.stringify({
1196
+ login_button: { en: "Login", it: "Accedi" },
1197
+ })
1198
+ );
1199
+ writeFileSync(
1200
+ join(tempDir, "i18n.codegen.json"),
1201
+ JSON.stringify({
1202
+ namespaces: {
1203
+ default: "src/i18n/translations/default.json",
1204
+ },
1205
+ delivery: "split-by-locale",
1206
+ localeFallback: {
1207
+ en: null,
1208
+ "de-DE": "en",
1209
+ "de-CH": "de-DE",
1210
+ it: "en",
1211
+ },
1212
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1213
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1214
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1215
+ paramsTypeName: "AppParams",
1216
+ schemaTypeName: "AppSchema",
1217
+ })
1218
+ );
1219
+
1220
+ const result = runCodegen(tempDir);
1221
+ expect(result.status).toBe(0);
1222
+ expect(result.stdout).toContain("default.de-CH.json");
1223
+
1224
+ const deChSplit = JSON.parse(
1225
+ readFileSync(join(tempDir, "src/i18n/generated/translations/default.de-CH.json"), "utf8")
1226
+ ) as Record<string, Record<string, string>>;
1227
+ expect(deChSplit).toEqual({
1228
+ login_button: { "de-CH": "Login" },
1229
+ });
1230
+
1231
+ const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1232
+ expect(types).toContain("export type AppLocale = 'de-CH' | 'de-DE' | 'en' | 'it'");
1233
+ });
1234
+
1235
+ it("splits yaml sources into per-locale json without a canonical intermediate", () => {
1236
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1237
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1238
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1239
+
1240
+ writeFileSync(
1241
+ join(tempDir, "src/i18n/translations/billing.yaml"),
1242
+ `invoice_summary:
1243
+ en: You have {count} invoices
1244
+ it: Hai {count} fatture
1245
+ `
1246
+ );
1247
+ writeFileSync(
1248
+ join(tempDir, "i18n.codegen.json"),
1249
+ JSON.stringify({
1250
+ namespaces: {
1251
+ billing: "src/i18n/translations/billing.yaml",
1252
+ },
1253
+ delivery: "split-by-locale",
1254
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1255
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1256
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1257
+ paramsTypeName: "AppParams",
1258
+ schemaTypeName: "AppSchema",
1259
+ })
1260
+ );
1261
+
1262
+ const result = runCodegen(tempDir);
1263
+ expect(result.status).toBe(0);
1264
+ expect(result.stdout).toContain("billing.en.json");
1265
+ expect(result.stdout).toContain("billing.it.json");
1266
+
1267
+ expect(() =>
1268
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1269
+ ).toThrow();
1270
+
1271
+ const enSplit = JSON.parse(
1272
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
1273
+ ) as Record<string, Record<string, string>>;
1274
+ expect(enSplit.invoice_summary).toEqual({ en: "You have {count} invoices" });
1275
+
1276
+ 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
+ expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1282
+ expect(types).not.toContain("typeof import");
1283
+ expect(dictionary).toContain("import billingEn from './translations/billing.en.json';");
1284
+ expect(dictionary).toContain(
1285
+ "export function defaultDictionaryFor(locale: AppLocale): AppSchema"
1286
+ );
1287
+ expect(dictionary).toContain("billing: billingByLocale[locale],");
1288
+ });
1289
+
1290
+ it("emits defaultDictionaryFor in single mode with split-by-locale delivery", () => {
1291
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1292
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1293
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1294
+
1295
+ const sourcePath = join(tempDir, "src/i18n/translations/translations.json");
1296
+ writeFileSync(
1297
+ sourcePath,
1298
+ JSON.stringify({
1299
+ welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
1300
+ })
1301
+ );
1302
+ writeFileSync(
1303
+ join(tempDir, "i18n.codegen.json"),
1304
+ JSON.stringify({
1305
+ dictionary: "src/i18n/translations/translations.json",
1306
+ delivery: "split-by-locale",
1307
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1308
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1309
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1310
+ paramsTypeName: "AppParams",
1311
+ schemaTypeName: "AppSchema",
1312
+ })
1313
+ );
1314
+
1315
+ const result = runCodegen(tempDir);
1316
+ expect(result.status).toBe(0);
1317
+
1318
+ const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
1319
+ string,
1320
+ Record<string, string>
1321
+ >;
1322
+ expect(sourceAfter.welcome).toEqual({
1323
+ en: "Welcome {name}!",
1324
+ it: "Benvenuto {name}!",
1325
+ });
1326
+
1327
+ const dictionary = readFileSync(
1328
+ join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
1329
+ "utf8"
1330
+ );
1331
+ const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1332
+ expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
1333
+ expect(types).not.toContain("typeof import");
1334
+ expect(dictionary).toContain("import defaultEn from './translations/translations.en.json';");
1335
+ expect(dictionary).toContain("import defaultIt from './translations/translations.it.json';");
1336
+ expect(dictionary).toContain(
1337
+ "export function defaultDictionaryFor(locale: AppLocale): AppSchema"
1338
+ );
1339
+ expect(dictionary).toContain("return defaultByLocale[locale];");
1340
+ expect(dictionary).not.toContain("export const defaultDictionary");
1341
+ });
1342
+
1343
+ it("keeps canonical output when delivery is explicitly set to canonical", () => {
1344
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1345
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1346
+
1347
+ writeFileSync(
1348
+ join(tempDir, "src/i18n/translations/default.json"),
1349
+ JSON.stringify({ welcome: { en: "Welcome {name}!" } })
1350
+ );
1351
+ writeFileSync(
1352
+ join(tempDir, "src/i18n/translations/billing.json"),
1353
+ JSON.stringify({ invoice_summary: { en: "Invoice" } })
1354
+ );
1355
+ writeFileSync(
1356
+ join(tempDir, "i18n.codegen.json"),
1357
+ JSON.stringify({
1358
+ namespaces: {
1359
+ default: "src/i18n/translations/default.json",
1360
+ billing: "src/i18n/translations/billing.json",
1361
+ },
1362
+ loadOnInit: ["default"],
1363
+ delivery: "canonical",
1364
+ typesOutput: "src/i18n/i18n-types.generated.ts",
1365
+ dictionaryOutput: "src/i18n/dictionary.generated.ts",
1366
+ instanceOutput: "src/i18n/instance.generated.ts",
1367
+ namespaceLoadersOutput: "src/i18n/namespace-loaders.generated.ts",
1368
+ paramsTypeName: "AppParams",
1369
+ schemaTypeName: "AppSchema",
1370
+ })
1371
+ );
1372
+
1373
+ const result = runCodegen(tempDir);
1374
+ expect(result.status).toBe(0);
1375
+
1376
+ const dictionary = readFileSync(join(tempDir, "src/i18n/dictionary.generated.ts"), "utf8");
1377
+ const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
1378
+
1379
+ expect(dictionary).toContain("export const defaultDictionary: InitialSchema");
1380
+ expect(dictionary).not.toContain("defaultDictionaryFor");
1381
+ expect(loaders).toContain("[K in LazyNamespace]: () => Promise<AppSchema[K]>");
1382
+ expect(loaders).toContain("import('./translations/billing.json')");
1383
+ expect(loaders).not.toContain("billing: {");
1384
+ expect(result.stdout).not.toContain(".en.json");
1385
+ expect(result.stdout).not.toContain(".it.json");
1386
+ });
1387
+
1388
+ it("generates custom delivery with per-area json files", () => {
1389
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1390
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1391
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1392
+
1393
+ writeFileSync(
1394
+ join(tempDir, "src/i18n/translations/default.json"),
1395
+ JSON.stringify({
1396
+ some_key: { it: "Ciao", fr: "Hallo", "en-US": "Hello" },
1397
+ some_other_key: { "en-US": "Computer", fr: "Ordinateur" },
1398
+ })
1399
+ );
1400
+ writeFileSync(
1401
+ join(tempDir, "src/i18n/translations/billing.json"),
1402
+ JSON.stringify({
1403
+ invoice_summary: {
1404
+ "en-US": "You have {count, plural, one {1 invoice} other {{count} invoices}}",
1405
+ it: "Hai {count} fatture",
1406
+ },
1407
+ })
1408
+ );
1409
+ writeFileSync(
1410
+ join(tempDir, "i18n.codegen.json"),
1411
+ JSON.stringify({
1412
+ namespaces: {
1413
+ default: "src/i18n/translations/default.json",
1414
+ billing: "src/i18n/translations/billing.json",
1415
+ },
1416
+ loadOnInit: ["default"],
1417
+ delivery: "custom",
1418
+ deliveryArtifacts: {
1419
+ eu: ["it", "fr"],
1420
+ us: ["en-US"],
1421
+ },
1422
+ localeFallback: {
1423
+ "en-US": null,
1424
+ it: "en-US",
1425
+ fr: null,
1426
+ },
1427
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1428
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1429
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1430
+ namespaceLoadersOutput: "src/i18n/generated/namespace-loaders.generated.ts",
1431
+ paramsTypeName: "AppParams",
1432
+ schemaTypeName: "AppSchema",
1433
+ })
1434
+ );
1435
+
1436
+ const result = runCodegen(tempDir);
1437
+ expect(result.status).toBe(0);
1438
+ expect(result.stdout).toContain("default.eu.json");
1439
+ expect(result.stdout).toContain("default.us.json");
1440
+ expect(result.stdout).toContain("billing.eu.json");
1441
+ expect(result.stdout).toContain("billing.us.json");
1442
+
1443
+ const defaultEu = JSON.parse(
1444
+ readFileSync(join(tempDir, "src/i18n/generated/translations/default.eu.json"), "utf8")
1445
+ ) as Record<string, Record<string, string>>;
1446
+ expect(defaultEu).toEqual({
1447
+ some_key: { it: "Ciao", fr: "Hallo" },
1448
+ some_other_key: { it: "Computer", fr: "Ordinateur" },
1449
+ });
1450
+
1451
+ const defaultUs = JSON.parse(
1452
+ readFileSync(join(tempDir, "src/i18n/generated/translations/default.us.json"), "utf8")
1453
+ ) as Record<string, Record<string, string>>;
1454
+ expect(defaultUs).toEqual({
1455
+ some_key: { "en-US": "Hello" },
1456
+ some_other_key: { "en-US": "Computer" },
1457
+ });
1458
+
1459
+ expect(() =>
1460
+ readFileSync(join(tempDir, "src/i18n/generated/translations/default.json"), "utf8")
1461
+ ).toThrow();
1462
+
1463
+ 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
+ const loaders = readFileSync(
1469
+ join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1470
+ "utf8"
1471
+ );
1472
+ const factory = readFileSync(join(tempDir, "src/i18n/generated/instance.generated.ts"), "utf8");
1473
+
1474
+ const billingEu = JSON.parse(
1475
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.eu.json"), "utf8")
1476
+ ) as Record<string, Record<string, string>>;
1477
+ expect(billingEu.invoice_summary).toEqual({ it: "Hai {count} fatture" });
1478
+
1479
+ const billingUs = JSON.parse(
1480
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.us.json"), "utf8")
1481
+ ) as Record<string, Record<string, string>>;
1482
+ expect(billingUs.invoice_summary).toEqual({
1483
+ "en-US": "You have {count, plural, one {1 invoice} other {{count} invoices}}",
1484
+ });
1485
+
1486
+ expect(() =>
1487
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1488
+ ).toThrow();
1489
+
1490
+ expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
1491
+ expect(types).toContain("export const LOCALE_DELIVERY_AREA = {");
1492
+ expect(types).toContain('"it": "eu"');
1493
+ expect(types).toContain('"fr": "eu"');
1494
+ expect(types).toContain('"en-US": "us"');
1495
+ expect(types).toContain("} as const satisfies Record<AppLocale, AppDeliveryArea>;");
1496
+ expect(types).toContain("export type AppLocale = 'en-US' | 'fr' | 'it';");
1497
+ expect(types).toContain("export const LOCALE_FALLBACK");
1498
+ expect(types).toContain('"it": "en-US"');
1499
+ expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
1500
+ expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1501
+ expect(types).not.toContain("typeof import");
1502
+ expect(types).toContain("invoice_summary: { count: number }");
1503
+
1504
+ expect(dictionary).toContain("import defaultEu from './translations/default.eu.json';");
1505
+ expect(dictionary).toContain("import defaultUs from './translations/default.us.json';");
1506
+ expect(dictionary).toContain(
1507
+ "export function defaultDictionaryFor(area: AppDeliveryArea): InitialSchema"
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");
1513
+
1514
+ expect(loaders).toContain(
1515
+ "[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
1516
+ );
1517
+ expect(loaders).toContain("billing: (area) => {");
1518
+ expect(loaders).toContain('case "eu":');
1519
+ expect(loaders).toContain('case "us":');
1520
+ expect(loaders).toContain(
1521
+ "return import('./translations/billing.eu.json').then((m) => m.default);"
1522
+ );
1523
+ expect(loaders).toContain(
1524
+ "return import('./translations/billing.us.json').then((m) => m.default);"
1525
+ );
1526
+ expect(loaders).not.toContain("import('./translations/billing.json')");
1527
+
1528
+ expect(factory).toContain("export function projectDictionaryForDeliveryArea(");
1529
+ expect(factory).toContain("export function projectNamespaceForDeliveryArea(");
1530
+ expect(factory).toContain(
1531
+ "projectDictionaryForDeliveryAreaCore(dictionary, areaLocales, LOCALE_FALLBACK)"
1532
+ );
1533
+ expect(factory).toContain(
1534
+ "projectNamespaceForDeliveryAreaCore(dictionary, areaLocales, LOCALE_FALLBACK)"
1535
+ );
1536
+ });
1537
+
1538
+ it("fails when deliveryArtifacts does not partition request locales", () => {
1539
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1540
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1541
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1542
+
1543
+ writeFileSync(
1544
+ join(tempDir, "src/i18n/translations/default.json"),
1545
+ JSON.stringify({
1546
+ some_key: { it: "Ciao", fr: "Hallo", "en-US": "Hello" },
1547
+ })
1548
+ );
1549
+ writeFileSync(
1550
+ join(tempDir, "i18n.codegen.json"),
1551
+ JSON.stringify({
1552
+ namespaces: {
1553
+ default: "src/i18n/translations/default.json",
1554
+ },
1555
+ delivery: "custom",
1556
+ deliveryArtifacts: {
1557
+ eu: ["it", "fr"],
1558
+ },
1559
+ localeFallback: {
1560
+ "en-US": null,
1561
+ it: "en-US",
1562
+ fr: null,
1563
+ },
1564
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1565
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1566
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1567
+ paramsTypeName: "AppParams",
1568
+ schemaTypeName: "AppSchema",
1569
+ })
1570
+ );
1571
+
1572
+ const result = runCodegen(tempDir);
1573
+ expect(result.status).not.toBe(0);
1574
+ expect(result.stderr).toContain("missing locales");
1575
+ expect(result.stderr).toContain("en-US");
1576
+ });
1577
+
1578
+ it("splits yaml sources into per-area json without a canonical intermediate", () => {
1579
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1580
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1581
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1582
+
1583
+ writeFileSync(
1584
+ join(tempDir, "src/i18n/translations/billing.yaml"),
1585
+ `invoice_summary:
1586
+ en-US: You have {count} invoices
1587
+ it: Hai {count} fatture
1588
+ `
1589
+ );
1590
+ writeFileSync(
1591
+ join(tempDir, "i18n.codegen.json"),
1592
+ JSON.stringify({
1593
+ namespaces: {
1594
+ billing: "src/i18n/translations/billing.yaml",
1595
+ },
1596
+ delivery: "custom",
1597
+ deliveryArtifacts: {
1598
+ eu: ["it"],
1599
+ us: ["en-US"],
1600
+ },
1601
+ localeFallback: {
1602
+ "en-US": null,
1603
+ it: "en-US",
1604
+ },
1605
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1606
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1607
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1608
+ paramsTypeName: "AppParams",
1609
+ schemaTypeName: "AppSchema",
1610
+ })
1611
+ );
1612
+
1613
+ const result = runCodegen(tempDir);
1614
+ expect(result.status).toBe(0);
1615
+ expect(result.stdout).toContain("billing.eu.json");
1616
+ expect(result.stdout).toContain("billing.us.json");
1617
+
1618
+ expect(() =>
1619
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1620
+ ).toThrow();
1621
+
1622
+ const euSplit = JSON.parse(
1623
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.eu.json"), "utf8")
1624
+ ) as Record<string, Record<string, string>>;
1625
+ expect(euSplit.invoice_summary).toEqual({ it: "Hai {count} fatture" });
1626
+
1627
+ const usSplit = JSON.parse(
1628
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.us.json"), "utf8")
1629
+ ) as Record<string, Record<string, string>>;
1630
+ expect(usSplit.invoice_summary).toEqual({ "en-US": "You have {count} invoices" });
1631
+
1632
+ 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
+ expect(types).toContain("export type AppDeliveryArea = 'eu' | 'us';");
1638
+ expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1639
+ expect(types).not.toContain("typeof import");
1640
+ expect(dictionary).toContain("import billingEu from './translations/billing.eu.json';");
1641
+ expect(dictionary).toContain("import billingUs from './translations/billing.us.json';");
1642
+ expect(dictionary).toContain(
1643
+ "export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
1644
+ );
1645
+ expect(dictionary).toContain("billing: billingByArea[area],");
1646
+ });
1647
+
1648
+ it("emits defaultDictionaryFor in single mode with custom delivery", () => {
1649
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1650
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1651
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1652
+
1653
+ const sourcePath = join(tempDir, "src/i18n/translations/translations.json");
1654
+ writeFileSync(
1655
+ sourcePath,
1656
+ JSON.stringify({
1657
+ some_key: { it: "Ciao", "en-US": "Hello" },
1658
+ })
1659
+ );
1660
+ writeFileSync(
1661
+ join(tempDir, "i18n.codegen.json"),
1662
+ JSON.stringify({
1663
+ dictionary: "src/i18n/translations/translations.json",
1664
+ delivery: "custom",
1665
+ deliveryArtifacts: {
1666
+ eu: ["it"],
1667
+ us: ["en-US"],
1668
+ },
1669
+ localeFallback: {
1670
+ "en-US": null,
1671
+ it: "en-US",
1672
+ },
1673
+ typesOutput: "src/i18n/generated/i18n-types.generated.ts",
1674
+ dictionaryOutput: "src/i18n/generated/dictionary.generated.ts",
1675
+ instanceOutput: "src/i18n/generated/instance.generated.ts",
1676
+ paramsTypeName: "AppParams",
1677
+ schemaTypeName: "AppSchema",
1678
+ })
1679
+ );
1680
+
1681
+ const result = runCodegen(tempDir);
1682
+ expect(result.status).toBe(0);
1683
+
1684
+ const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
1685
+ string,
1686
+ Record<string, string>
1687
+ >;
1688
+ expect(sourceAfter.some_key).toEqual({
1689
+ it: "Ciao",
1690
+ "en-US": "Hello",
1691
+ });
1692
+
1693
+ const dictionary = readFileSync(
1694
+ join(tempDir, "src/i18n/generated/dictionary.generated.ts"),
1695
+ "utf8"
1696
+ );
1697
+ const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1698
+ expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
1699
+ expect(types).not.toContain("typeof import");
1700
+ expect(dictionary).toContain("import defaultEu from './translations/translations.eu.json';");
1701
+ expect(dictionary).toContain("import defaultUs from './translations/translations.us.json';");
1702
+ expect(dictionary).toContain(
1703
+ "export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
1704
+ );
1705
+ expect(dictionary).toContain("return defaultByArea[area];");
1706
+ expect(dictionary).not.toContain("export const defaultDictionary");
964
1707
  });
965
1708
 
966
1709
  it("fails when yaml dictionary has invalid shape", () => {