@xndrjs/i18n 0.8.0 → 0.8.2-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 (94) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/audit.js +739 -0
  3. package/dist/cli/audit.js.map +1 -0
  4. package/dist/cli/codegen.js +1471 -0
  5. package/dist/cli/codegen.js.map +1 -0
  6. package/dist/cli/setup.js +148 -0
  7. package/dist/cli/setup.js.map +1 -0
  8. package/dist/codegen/index.js +43 -40
  9. package/dist/codegen/index.js.map +1 -1
  10. package/dist/index.d.ts +20 -1
  11. package/dist/index.js +56 -10
  12. package/dist/index.js.map +1 -1
  13. package/package.json +6 -9
  14. package/bin/audit.mjs +0 -14
  15. package/bin/codegen.mjs +0 -17
  16. package/bin/setup.mjs +0 -14
  17. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  18. package/src/IcuTranslationProviderMulti.ts +0 -176
  19. package/src/audit/audit-dictionaries.test.ts +0 -134
  20. package/src/audit/audit-dictionaries.ts +0 -178
  21. package/src/audit/run-audit.test.ts +0 -169
  22. package/src/audit/run-audit.ts +0 -94
  23. package/src/builder-load-registry.ts +0 -39
  24. package/src/builder-loaders.ts +0 -15
  25. package/src/builder-types.test.ts +0 -49
  26. package/src/builder-types.ts +0 -24
  27. package/src/builder.test.ts +0 -260
  28. package/src/builder.ts +0 -10
  29. package/src/codegen/codegen-config-schema.ts +0 -156
  30. package/src/codegen/config.test.ts +0 -231
  31. package/src/codegen/config.ts +0 -42
  32. package/src/codegen/constants.ts +0 -9
  33. package/src/codegen/delivery-artifacts.test.ts +0 -167
  34. package/src/codegen/delivery-artifacts.ts +0 -154
  35. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  36. package/src/codegen/dictionary-spec-contract.ts +0 -138
  37. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  38. package/src/codegen/emit/instance-file.test.ts +0 -71
  39. package/src/codegen/emit/instance-file.ts +0 -257
  40. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  41. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  42. package/src/codegen/emit/types-file.test.ts +0 -96
  43. package/src/codegen/emit/types-file.ts +0 -142
  44. package/src/codegen/fixtures/billing-namespace.json +0 -6
  45. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  46. package/src/codegen/generate-i18n-types.ts +0 -20
  47. package/src/codegen/icu-analysis.ts +0 -110
  48. package/src/codegen/locale-fallback.ts +0 -73
  49. package/src/codegen/locale-policy.test.ts +0 -35
  50. package/src/codegen/locale-policy.ts +0 -30
  51. package/src/codegen/paths.ts +0 -44
  52. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  53. package/src/codegen/read-dictionary.test.ts +0 -594
  54. package/src/codegen/read-dictionary.ts +0 -260
  55. package/src/codegen/regenerate-namespaces.ts +0 -180
  56. package/src/codegen/run-codegen.test.ts +0 -230
  57. package/src/codegen/run-codegen.ts +0 -252
  58. package/src/codegen/types.ts +0 -10
  59. package/src/codegen/write-file-if-changed.test.ts +0 -42
  60. package/src/codegen/write-file-if-changed.ts +0 -20
  61. package/src/codegen-config/build-config.ts +0 -15
  62. package/src/codegen-config/codegen-config.test.ts +0 -36
  63. package/src/codegen-config/index.ts +0 -37
  64. package/src/codegen-config/type-names.ts +0 -20
  65. package/src/codegen-config/write-config.ts +0 -8
  66. package/src/engine.ts +0 -47
  67. package/src/fetch-artifact.ts +0 -16
  68. package/src/format-core.ts +0 -91
  69. package/src/i18n-handle.ts +0 -202
  70. package/src/icu/extract-variables.test.ts +0 -199
  71. package/src/icu/extract-variables.ts +0 -227
  72. package/src/icu/parse-template.ts +0 -31
  73. package/src/index.ts +0 -58
  74. package/src/project-locales.test.ts +0 -280
  75. package/src/project-locales.ts +0 -211
  76. package/src/resolve-locale.test.ts +0 -91
  77. package/src/resolve-locale.ts +0 -88
  78. package/src/scope-multi.ts +0 -105
  79. package/src/scope-types.ts +0 -35
  80. package/src/scope.test.ts +0 -172
  81. package/src/serialized-state.test.ts +0 -114
  82. package/src/serialized-state.ts +0 -28
  83. package/src/setup/setup-i18n.test.ts +0 -85
  84. package/src/setup/setup-i18n.ts +0 -156
  85. package/src/types.ts +0 -65
  86. package/src/validation/create-args-schema.ts +0 -76
  87. package/src/validation/create-normalized-schema.ts +0 -52
  88. package/src/validation/errors.ts +0 -43
  89. package/src/validation/index.ts +0 -202
  90. package/src/validation/normalize.ts +0 -299
  91. package/src/validation/to-dictionary.ts +0 -67
  92. package/src/validation/types.ts +0 -82
  93. package/src/validation/validate-normalized.ts +0 -133
  94. package/src/validation/validation.test.ts +0 -562
@@ -1,1137 +0,0 @@
1
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { describe, expect, it, afterEach } from "vitest";
7
-
8
- const codegenScript = fileURLToPath(new URL("./generate-i18n-types.ts", import.meta.url));
9
-
10
- function runCodegen(cwd: string, configFile = "i18n.codegen.json") {
11
- return spawnSync("tsx", [codegenScript, "--config", configFile], {
12
- cwd,
13
- encoding: "utf8",
14
- env: process.env,
15
- });
16
- }
17
-
18
- describe("generate-i18n-types", () => {
19
- let tempDir: string;
20
-
21
- afterEach(() => {
22
- if (tempDir) {
23
- rmSync(tempDir, { recursive: true, force: true });
24
- }
25
- });
26
-
27
- it("generates nested types for multi-namespace config", () => {
28
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
29
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
30
-
31
- writeFileSync(
32
- join(tempDir, "src/i18n/translations/default.json"),
33
- JSON.stringify({
34
- login_button: { en: "Login" },
35
- welcome: { en: "Welcome {name}!" },
36
- inbox_owner: {
37
- en: "{gender, select, female {{name} owns her inbox} other {{name} owns their inbox}}",
38
- },
39
- account_balance: { en: "Balance: {amount, number, ::currency/EUR}" },
40
- appointment_summary: {
41
- en: "Due {dueDate, date, short} at {startTime, time, short}",
42
- },
43
- })
44
- );
45
- writeFileSync(
46
- join(tempDir, "src/i18n/translations/billing.json"),
47
- JSON.stringify({
48
- invoice_summary: {
49
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
50
- },
51
- })
52
- );
53
- writeFileSync(
54
- join(tempDir, "i18n.codegen.json"),
55
- JSON.stringify({
56
- namespaces: {
57
- default: "src/i18n/translations/default.json",
58
- billing: "src/i18n/translations/billing.json",
59
- },
60
- projectName: "App",
61
- codegenPath: "src/i18n",
62
- })
63
- );
64
-
65
- const result = runCodegen(tempDir);
66
- expect(result.status).toBe(0);
67
-
68
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
69
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
70
- expect(types).toContain("export const I18N_MODE = 'multi' as const");
71
- expect(types).toContain('export const AppLocales = ["en"] as const;');
72
- expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
73
- expect(factory).toContain("export function createI18n(");
74
- expect(factory).toContain("onMissing?: OnMissingTranslation");
75
- expect(factory).toContain("state?: { dictionary: InitialSchema");
76
- expect(factory).toContain("IcuTranslationProviderMulti");
77
- expect(types).toContain("login_button: never");
78
- expect(types).toContain("welcome: { name: string }");
79
- expect(types).toContain("inbox_owner: { gender: string; name: string }");
80
- expect(types).toContain("account_balance: { amount: number }");
81
- expect(types).toContain(
82
- "appointment_summary: { dueDate: Date | number; startTime: Date | number }"
83
- );
84
- expect(types).toContain("invoice_summary: { count: number }");
85
- expect(types).toContain("export type AppParams");
86
- expect(types).toContain("export type AppSchema");
87
- });
88
-
89
- it("does not rewrite unchanged generated files on rerun", () => {
90
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
91
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
92
-
93
- writeFileSync(
94
- join(tempDir, "src/i18n/translations/translations.json"),
95
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
96
- );
97
- writeFileSync(
98
- join(tempDir, "i18n.codegen.json"),
99
- JSON.stringify({
100
- namespaces: {
101
- default: "src/i18n/translations/translations.json",
102
- },
103
- projectName: "App",
104
- codegenPath: "src/i18n",
105
- })
106
- );
107
-
108
- expect(runCodegen(tempDir).status).toBe(0);
109
-
110
- const generatedPaths = [
111
- join(tempDir, "src/i18n/i18n-types.generated.ts"),
112
- join(tempDir, "src/i18n/instance.generated.ts"),
113
- join(tempDir, "src/i18n/namespace-loaders.generated.ts"),
114
- ];
115
- const mtimesBefore = generatedPaths.map((filePath) => statSync(filePath).mtimeMs);
116
-
117
- expect(runCodegen(tempDir).status).toBe(0);
118
-
119
- const mtimesAfter = generatedPaths.map((filePath) => statSync(filePath).mtimeMs);
120
- expect(mtimesAfter).toEqual(mtimesBefore);
121
- });
122
-
123
- it("always generates dictionary schema file under codegenPath", () => {
124
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
125
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
126
-
127
- writeFileSync(
128
- join(tempDir, "src/i18n/translations/default.json"),
129
- JSON.stringify({
130
- welcome: { en: "Welcome {name}!" },
131
- })
132
- );
133
- writeFileSync(
134
- join(tempDir, "src/i18n/translations/billing.json"),
135
- JSON.stringify({
136
- invoice_summary: {
137
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
138
- },
139
- })
140
- );
141
- writeFileSync(
142
- join(tempDir, "i18n.codegen.json"),
143
- JSON.stringify({
144
- namespaces: {
145
- default: "src/i18n/translations/default.json",
146
- billing: "src/i18n/translations/billing.json",
147
- },
148
- projectName: "App",
149
- codegenPath: "src/i18n",
150
- })
151
- );
152
-
153
- const result = runCodegen(tempDir);
154
- expect(result.status).toBe(0);
155
-
156
- const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
157
- expect(schema).toContain("export const DICTIONARY_SPEC");
158
- expect(schema).toContain("mode: 'multi' as const");
159
- expect(schema).toContain("validateExternalDictionaryPartial");
160
- expect(schema).toContain("validateExternalNamespacePartial");
161
- expect(schema).toContain("validateExternalKey");
162
- expect(schema).toContain('"name": "string"');
163
- });
164
-
165
- it("generates multi-mode dictionary schema with namespace validator", () => {
166
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
167
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
168
-
169
- writeFileSync(
170
- join(tempDir, "src/i18n/translations/default.json"),
171
- JSON.stringify({
172
- welcome: { en: "Welcome {name}!" },
173
- })
174
- );
175
- writeFileSync(
176
- join(tempDir, "i18n.codegen.json"),
177
- JSON.stringify({
178
- namespaces: {
179
- default: "src/i18n/translations/default.json",
180
- },
181
- projectName: "App",
182
- codegenPath: "src/i18n",
183
- })
184
- );
185
-
186
- const result = runCodegen(tempDir);
187
- expect(result.status).toBe(0);
188
-
189
- const schema = readFileSync(join(tempDir, "src/i18n/dictionary-schema.generated.ts"), "utf8");
190
- expect(schema).toContain("mode: 'multi' as const");
191
- expect(schema).toContain("validateExternalDictionaryPartial");
192
- expect(schema).toContain("validateExternalNamespacePartial");
193
- expect(schema).toContain("validateExternalKey");
194
- });
195
-
196
- it("fails with a non-zero exit code on malformed ICU", () => {
197
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
198
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
199
-
200
- writeFileSync(
201
- join(tempDir, "src/i18n/translations/translations.json"),
202
- JSON.stringify({
203
- broken: { en: "Hello {{name}}" },
204
- })
205
- );
206
- writeFileSync(
207
- join(tempDir, "i18n.codegen.json"),
208
- JSON.stringify({
209
- namespaces: {
210
- default: "src/i18n/translations/translations.json",
211
- },
212
- projectName: "App",
213
- codegenPath: "src/i18n",
214
- })
215
- );
216
-
217
- const result = runCodegen(tempDir);
218
- expect(result.status).not.toBe(0);
219
- expect(result.stderr).toContain("MALFORMED_ARGUMENT");
220
- });
221
-
222
- it("generates locale fallback constants and wires them into the factory", () => {
223
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
224
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
225
-
226
- writeFileSync(
227
- join(tempDir, "src/i18n/translations/translations.json"),
228
- JSON.stringify({
229
- login_button: { en: "Login" },
230
- })
231
- );
232
- writeFileSync(
233
- join(tempDir, "i18n.codegen.json"),
234
- JSON.stringify({
235
- namespaces: {
236
- default: "src/i18n/translations/translations.json",
237
- },
238
- projectName: "App",
239
- codegenPath: "src/i18n",
240
- localeFallback: {
241
- en: null,
242
- "de-CH": "en",
243
- },
244
- })
245
- );
246
-
247
- const result = runCodegen(tempDir);
248
- expect(result.status).toBe(0);
249
-
250
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
251
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
252
- expect(types).toContain("export const LOCALE_FALLBACK");
253
- expect(types).toContain('"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];");
256
- expect(factory).toContain("localeFallback: LOCALE_FALLBACK");
257
- expect(factory).toContain("...providerOptions");
258
- expect(factory).toContain(
259
- "IcuTranslationProviderMulti<AppSchema, AppParams, AppLocale, typeof LOCALE_FALLBACK>"
260
- );
261
- });
262
-
263
- it("enriches LOCALE_FALLBACK with null for dictionary locales missing from config", () => {
264
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
265
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
266
-
267
- writeFileSync(
268
- join(tempDir, "src/i18n/translations/translations.json"),
269
- JSON.stringify({
270
- login_button: { en: "Login", fr: "Connexion" },
271
- })
272
- );
273
- writeFileSync(
274
- join(tempDir, "i18n.codegen.json"),
275
- JSON.stringify({
276
- namespaces: {
277
- default: "src/i18n/translations/translations.json",
278
- },
279
- projectName: "App",
280
- codegenPath: "src/i18n",
281
- localeFallback: {
282
- en: null,
283
- it: "en",
284
- },
285
- })
286
- );
287
-
288
- const result = runCodegen(tempDir);
289
- expect(result.status).toBe(0);
290
-
291
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
292
- expect(types).toContain('"fr": null');
293
- expect(types).toContain('"it": "en"');
294
- expect(types).toContain('export const AppLocales = ["en", "fr", "it"] as const;');
295
- expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
296
- });
297
-
298
- it("does not emit LOCALE_FALLBACK when localeFallback is absent from config", () => {
299
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
300
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
301
-
302
- writeFileSync(
303
- join(tempDir, "src/i18n/translations/translations.json"),
304
- JSON.stringify({
305
- login_button: { en: "Login", it: "Accedi" },
306
- })
307
- );
308
- writeFileSync(
309
- join(tempDir, "i18n.codegen.json"),
310
- JSON.stringify({
311
- namespaces: {
312
- default: "src/i18n/translations/translations.json",
313
- },
314
- projectName: "App",
315
- codegenPath: "src/i18n",
316
- })
317
- );
318
-
319
- const result = runCodegen(tempDir);
320
- expect(result.status).toBe(0);
321
-
322
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
323
- expect(types).toContain('export const AppLocales = ["en", "it"] as const;');
324
- expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
325
- });
326
-
327
- it("fails on circular locale fallback in config", () => {
328
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
329
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
330
-
331
- writeFileSync(
332
- join(tempDir, "src/i18n/translations/translations.json"),
333
- JSON.stringify({
334
- login_button: { en: "Login" },
335
- })
336
- );
337
- writeFileSync(
338
- join(tempDir, "i18n.codegen.json"),
339
- JSON.stringify({
340
- namespaces: {
341
- default: "src/i18n/translations/translations.json",
342
- },
343
- projectName: "App",
344
- codegenPath: "src/i18n",
345
- localeFallback: {
346
- a: "b",
347
- b: "a",
348
- },
349
- })
350
- );
351
-
352
- const result = runCodegen(tempDir);
353
- expect(result.status).not.toBe(0);
354
- expect(result.stderr).toContain("Circular locale fallback");
355
- });
356
-
357
- it("generates namespace loaders for all namespaces under split-by-locale", () => {
358
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
359
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
360
-
361
- writeFileSync(
362
- join(tempDir, "src/i18n/translations/default.json"),
363
- JSON.stringify({
364
- welcome: { en: "Welcome {name}!" },
365
- })
366
- );
367
- writeFileSync(
368
- join(tempDir, "src/i18n/translations/billing.json"),
369
- JSON.stringify({
370
- invoice_summary: {
371
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
372
- },
373
- })
374
- );
375
- writeFileSync(
376
- join(tempDir, "i18n.codegen.json"),
377
- JSON.stringify({
378
- namespaces: {
379
- default: "src/i18n/translations/default.json",
380
- billing: "src/i18n/translations/billing.json",
381
- },
382
- projectName: "App",
383
- codegenPath: "src/i18n",
384
- })
385
- );
386
-
387
- const result = runCodegen(tempDir);
388
- expect(result.status).toBe(0);
389
-
390
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
391
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
392
- const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
393
-
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])[] }"
398
- );
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]>;"
404
- );
405
- expect(loaders).toContain(
406
- "return import('./translations/billing.en.json').then((m) => m.default);"
407
- );
408
- });
409
-
410
- it("infers number when English plural and Italian simple interpolation share a key", () => {
411
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
412
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
413
-
414
- writeFileSync(
415
- join(tempDir, "src/i18n/translations/translations.json"),
416
- JSON.stringify({
417
- invoice_count: {
418
- en: "You have {count, plural, one {1 invoice} other {# invoices}}",
419
- it: "Hai {count} fatture",
420
- },
421
- })
422
- );
423
- writeFileSync(
424
- join(tempDir, "i18n.codegen.json"),
425
- JSON.stringify({
426
- namespaces: {
427
- default: "src/i18n/translations/translations.json",
428
- },
429
- projectName: "App",
430
- codegenPath: "src/i18n",
431
- })
432
- );
433
-
434
- const result = runCodegen(tempDir);
435
- expect(result.status).toBe(0);
436
-
437
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
438
- expect(types).toContain("invoice_count: { count: number }");
439
- });
440
-
441
- it("fails when plural and select disagree on the same variable across locales", () => {
442
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
443
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
444
-
445
- writeFileSync(
446
- join(tempDir, "src/i18n/translations/translations.json"),
447
- JSON.stringify({
448
- broken: {
449
- en: "{myVar, select, other {x}}",
450
- it: "{myVar, plural, one {1} other {#}}",
451
- },
452
- })
453
- );
454
- writeFileSync(
455
- join(tempDir, "i18n.codegen.json"),
456
- JSON.stringify({
457
- namespaces: {
458
- default: "src/i18n/translations/translations.json",
459
- },
460
- projectName: "App",
461
- codegenPath: "src/i18n",
462
- })
463
- );
464
-
465
- const result = runCodegen(tempDir);
466
- expect(result.status).not.toBe(0);
467
- expect(result.stderr).toContain('Incompatible ICU variable "myVar"');
468
- });
469
-
470
- it("fails when plural and selectordinal disagree on the same variable across locales", () => {
471
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
472
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
473
-
474
- writeFileSync(
475
- join(tempDir, "src/i18n/translations/translations.json"),
476
- JSON.stringify({
477
- broken: {
478
- en: "{rank, plural, one {1} other {#}}",
479
- it: "{rank, selectordinal, one {#°} other {#°}}",
480
- },
481
- })
482
- );
483
- writeFileSync(
484
- join(tempDir, "i18n.codegen.json"),
485
- JSON.stringify({
486
- namespaces: {
487
- default: "src/i18n/translations/translations.json",
488
- },
489
- projectName: "App",
490
- codegenPath: "src/i18n",
491
- })
492
- );
493
-
494
- const result = runCodegen(tempDir);
495
- expect(result.status).not.toBe(0);
496
- expect(result.stderr).toContain('Incompatible ICU variable "rank"');
497
- });
498
-
499
- it("fails when select and number format disagree on the same variable across locales", () => {
500
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
501
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
502
-
503
- writeFileSync(
504
- join(tempDir, "src/i18n/translations/translations.json"),
505
- JSON.stringify({
506
- broken: {
507
- en: "{value, select, other {x}}",
508
- it: "{value, number}",
509
- },
510
- })
511
- );
512
- writeFileSync(
513
- join(tempDir, "i18n.codegen.json"),
514
- JSON.stringify({
515
- namespaces: {
516
- default: "src/i18n/translations/translations.json",
517
- },
518
- projectName: "App",
519
- codegenPath: "src/i18n",
520
- })
521
- );
522
-
523
- const result = runCodegen(tempDir);
524
- expect(result.status).not.toBe(0);
525
- expect(result.stderr).toContain('Incompatible ICU variable "value"');
526
- });
527
-
528
- it("keeps multi mode when namespaces has a single entry", () => {
529
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
530
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
531
-
532
- writeFileSync(
533
- join(tempDir, "src/i18n/translations/default.json"),
534
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
535
- );
536
- writeFileSync(
537
- join(tempDir, "i18n.codegen.json"),
538
- JSON.stringify({
539
- namespaces: {
540
- default: "src/i18n/translations/default.json",
541
- },
542
- projectName: "App",
543
- codegenPath: "src/i18n",
544
- })
545
- );
546
-
547
- const result = runCodegen(tempDir);
548
- expect(result.status).toBe(0);
549
-
550
- const types = readFileSync(join(tempDir, "src/i18n/i18n-types.generated.ts"), "utf8");
551
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
552
- expect(types).toContain("export const I18N_MODE = 'multi' as const");
553
- expect(types).toContain("default: {");
554
- expect(factory).toContain("IcuTranslationProviderMulti");
555
- });
556
-
557
- it("omits import extension by default between generated modules", () => {
558
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
559
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
560
-
561
- writeFileSync(
562
- join(tempDir, "src/i18n/translations/translations.json"),
563
- JSON.stringify({ welcome: { en: "Welcome {name}!" } })
564
- );
565
- writeFileSync(
566
- join(tempDir, "i18n.codegen.json"),
567
- JSON.stringify({
568
- namespaces: {
569
- default: "src/i18n/translations/translations.json",
570
- },
571
- projectName: "App",
572
- codegenPath: "src/i18n",
573
- })
574
- );
575
-
576
- const result = runCodegen(tempDir);
577
- expect(result.status).toBe(0);
578
-
579
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
580
- const loaders = readFileSync(join(tempDir, "src/i18n/namespace-loaders.generated.ts"), "utf8");
581
- expect(factory).toContain("from './i18n-types.generated'");
582
- expect(factory).toContain("dictionary: InitialSchema");
583
- expect(loaders).toContain("from './i18n-types.generated'");
584
- });
585
-
586
- it("compiles yaml dictionaries to json and generates json imports", () => {
587
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
588
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
589
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
590
-
591
- writeFileSync(
592
- join(tempDir, "src/i18n/translations/translations.yaml"),
593
- `welcome:
594
- en: |
595
- Line one
596
- Line two
597
- it: |
598
- Riga uno
599
- Riga due
600
- `
601
- );
602
- writeFileSync(
603
- join(tempDir, "i18n.codegen.json"),
604
- JSON.stringify({
605
- namespaces: {
606
- default: "src/i18n/translations/translations.yaml",
607
- },
608
- projectName: "App",
609
- codegenPath: "src/i18n/generated",
610
- })
611
- );
612
-
613
- const result = runCodegen(tempDir);
614
- expect(result.status).toBe(0);
615
- expect(result.stdout).toContain(
616
- "Compiled: src/i18n/translations/translations.yaml → src/i18n/generated/translations/translations.en.json"
617
- );
618
-
619
- const compiled = JSON.parse(
620
- readFileSync(join(tempDir, "src/i18n/generated/translations/translations.en.json"), "utf8")
621
- ) as Record<string, Record<string, string>>;
622
- expect(compiled.welcome?.en).toBe("Line one\nLine two\n");
623
-
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"),
627
- "utf8"
628
- );
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
- );
633
- });
634
-
635
- it("supports mixed json and yaml namespaces", () => {
636
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
637
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
638
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
639
-
640
- writeFileSync(
641
- join(tempDir, "src/i18n/translations/default.json"),
642
- JSON.stringify({
643
- login_button: { en: "Login", it: "Accedi" },
644
- })
645
- );
646
- writeFileSync(
647
- join(tempDir, "src/i18n/translations/billing.yaml"),
648
- `invoice_summary:
649
- en: You have {count} invoices
650
- `
651
- );
652
- writeFileSync(
653
- join(tempDir, "i18n.codegen.json"),
654
- JSON.stringify({
655
- namespaces: {
656
- default: "src/i18n/translations/default.json",
657
- billing: "src/i18n/translations/billing.yaml",
658
- },
659
- projectName: "App",
660
- codegenPath: "src/i18n/generated",
661
- })
662
- );
663
-
664
- const result = runCodegen(tempDir);
665
- expect(result.status).toBe(0);
666
-
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"),
670
- "utf8"
671
- );
672
- expect(types).toContain("login_button: Partial<Record<AppLocale, string>>;");
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
- );
680
- });
681
-
682
- it("generates lazy loaders that import compiled json for yaml namespaces", () => {
683
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
684
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
685
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
686
-
687
- writeFileSync(
688
- join(tempDir, "src/i18n/translations/default.json"),
689
- JSON.stringify({
690
- login_button: { en: "Login" },
691
- })
692
- );
693
- writeFileSync(
694
- join(tempDir, "src/i18n/translations/billing.yaml"),
695
- `invoice_summary:
696
- en: You have {count} invoices
697
- `
698
- );
699
- writeFileSync(
700
- join(tempDir, "i18n.codegen.json"),
701
- JSON.stringify({
702
- namespaces: {
703
- default: "src/i18n/translations/default.json",
704
- billing: "src/i18n/translations/billing.yaml",
705
- },
706
- projectName: "App",
707
- codegenPath: "src/i18n/generated",
708
- })
709
- );
710
-
711
- const result = runCodegen(tempDir);
712
- expect(result.status).toBe(0);
713
-
714
- const loaders = readFileSync(
715
- join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
716
- "utf8"
717
- );
718
- expect(loaders).toContain(
719
- "return import('./translations/billing.en.json').then((m) => m.default);"
720
- );
721
- });
722
-
723
- it("fails when dictionary extension is unsupported", () => {
724
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
725
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
726
-
727
- writeFileSync(join(tempDir, "src/i18n/translations/translations.toml"), "welcome = {}");
728
- writeFileSync(
729
- join(tempDir, "i18n.codegen.json"),
730
- JSON.stringify({
731
- namespaces: {
732
- default: "src/i18n/translations/translations.toml",
733
- },
734
- projectName: "App",
735
- codegenPath: "src/i18n",
736
- })
737
- );
738
-
739
- const result = runCodegen(tempDir);
740
- expect(result.status).not.toBe(0);
741
- expect(result.stderr).toMatch(/unsupported dictionary (extension|format)/i);
742
- });
743
-
744
- it("generates split-by-locale delivery with per-locale files and namespace loaders", () => {
745
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
746
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
747
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
748
-
749
- writeFileSync(
750
- join(tempDir, "src/i18n/translations/default.json"),
751
- JSON.stringify({
752
- welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
753
- })
754
- );
755
- writeFileSync(
756
- join(tempDir, "src/i18n/translations/user.json"),
757
- JSON.stringify({
758
- profile_title: { en: "Your profile", it: "Il tuo profilo" },
759
- })
760
- );
761
- writeFileSync(
762
- join(tempDir, "src/i18n/translations/billing.json"),
763
- JSON.stringify({
764
- invoice_summary: {
765
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
766
- it: "Hai {count} fatture",
767
- },
768
- })
769
- );
770
- writeFileSync(
771
- join(tempDir, "i18n.codegen.json"),
772
- JSON.stringify({
773
- namespaces: {
774
- default: "src/i18n/translations/default.json",
775
- user: "src/i18n/translations/user.json",
776
- billing: "src/i18n/translations/billing.json",
777
- },
778
- delivery: "split-by-locale",
779
- projectName: "App",
780
- codegenPath: "src/i18n/generated",
781
- })
782
- );
783
-
784
- const result = runCodegen(tempDir);
785
- expect(result.status).toBe(0);
786
- expect(result.stdout).toContain("default.en.json");
787
- expect(result.stdout).toContain("user.it.json");
788
- expect(result.stdout).toContain("billing.en.json");
789
-
790
- const userEn = JSON.parse(
791
- readFileSync(join(tempDir, "src/i18n/generated/translations/user.en.json"), "utf8")
792
- ) as Record<string, Record<string, string>>;
793
- expect(userEn.profile_title).toEqual({ en: "Your profile" });
794
-
795
- const billingIt = JSON.parse(
796
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.it.json"), "utf8")
797
- ) as Record<string, Record<string, string>>;
798
- expect(billingIt.invoice_summary).toEqual({ it: "Hai {count} fatture" });
799
-
800
- expect(() =>
801
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
802
- ).toThrow();
803
-
804
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
805
- const loaders = readFileSync(
806
- join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
807
- "utf8"
808
- );
809
-
810
- expect(types).toContain("welcome: Partial<Record<AppLocale, string>>;");
811
- expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
812
- expect(types).toContain('export const AppLocales = ["en", "it"] as const;');
813
- expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
814
- expect(types).toContain("invoice_summary: { count: number }");
815
-
816
- expect(types).toContain("export type LazyNamespace = 'default' | 'user' | 'billing';");
817
-
818
- expect(loaders).toContain(
819
- "[K in LazyNamespace]: (locale: AppLocale) => Promise<AppSchema[K]>;"
820
- );
821
- expect(loaders).toContain("export const defaultLazyNamespaces");
822
- expect(loaders).toContain("default: (locale) => {");
823
- expect(loaders).toContain("billing: (locale) => {");
824
- expect(loaders).toContain('case "it":');
825
- expect(loaders).toContain(
826
- "return import('./translations/billing.it.json').then((m) => m.default);"
827
- );
828
- expect(loaders).toContain('case "en":');
829
- expect(loaders).toContain(
830
- "return import('./translations/billing.en.json').then((m) => m.default);"
831
- );
832
- expect(loaders).toContain("user: (locale) => {");
833
- });
834
-
835
- it("writes delivery json under artifactsPath when it differs from codegenPath", () => {
836
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
837
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
838
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
839
-
840
- writeFileSync(
841
- join(tempDir, "src/i18n/translations/default.json"),
842
- JSON.stringify({
843
- welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
844
- })
845
- );
846
- writeFileSync(
847
- join(tempDir, "src/i18n/translations/billing.yaml"),
848
- `invoice_summary:
849
- en: You have {count} invoices
850
- it: Hai {count} fatture
851
- `
852
- );
853
- writeFileSync(
854
- join(tempDir, "i18n.codegen.json"),
855
- JSON.stringify({
856
- namespaces: {
857
- default: "src/i18n/translations/default.json",
858
- billing: "src/i18n/translations/billing.yaml",
859
- },
860
- delivery: "split-by-locale",
861
- artifactsPath: "public/i18n",
862
- projectName: "App",
863
- codegenPath: "src/i18n/generated",
864
- })
865
- );
866
-
867
- const result = runCodegen(tempDir);
868
- expect(result.status).toBe(0);
869
- expect(result.stdout).toContain("public/i18n/translations/billing.en.json");
870
- expect(result.stdout).toContain("public/i18n/translations/billing.it.json");
871
-
872
- expect(() =>
873
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
874
- ).toThrow();
875
-
876
- const billingEn = JSON.parse(
877
- readFileSync(join(tempDir, "public/i18n/translations/billing.en.json"), "utf8")
878
- ) as Record<string, Record<string, string>>;
879
- expect(billingEn.invoice_summary).toEqual({ en: "You have {count} invoices" });
880
-
881
- const loaders = readFileSync(
882
- join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
883
- "utf8"
884
- );
885
- expect(loaders).toContain("export const defaultLazyNamespaces");
886
- expect(loaders).toContain(
887
- "return import('../../../public/i18n/translations/default.en.json').then((m) => m.default);"
888
- );
889
- expect(loaders).toContain(
890
- "return import('../../../public/i18n/translations/billing.en.json').then((m) => m.default);"
891
- );
892
- expect(loaders).toContain(
893
- "return import('../../../public/i18n/translations/billing.it.json').then((m) => m.default);"
894
- );
895
- });
896
-
897
- it("applies localeFallback when generating split-by-locale files", () => {
898
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
899
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
900
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
901
-
902
- writeFileSync(
903
- join(tempDir, "src/i18n/translations/default.json"),
904
- JSON.stringify({
905
- login_button: { en: "Login", it: "Accedi" },
906
- })
907
- );
908
- writeFileSync(
909
- join(tempDir, "i18n.codegen.json"),
910
- JSON.stringify({
911
- namespaces: {
912
- default: "src/i18n/translations/default.json",
913
- },
914
- delivery: "split-by-locale",
915
- localeFallback: {
916
- en: null,
917
- "de-DE": "en",
918
- "de-CH": "de-DE",
919
- it: "en",
920
- },
921
- projectName: "App",
922
- codegenPath: "src/i18n/generated",
923
- })
924
- );
925
-
926
- const result = runCodegen(tempDir);
927
- expect(result.status).toBe(0);
928
- expect(result.stdout).toContain("default.de-CH.json");
929
-
930
- const deChSplit = JSON.parse(
931
- readFileSync(join(tempDir, "src/i18n/generated/translations/default.de-CH.json"), "utf8")
932
- ) as Record<string, Record<string, string>>;
933
- expect(deChSplit).toEqual({
934
- login_button: { "de-CH": "Login" },
935
- });
936
-
937
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
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];");
940
- });
941
-
942
- it("generates custom delivery with per-area json files", () => {
943
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
944
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
945
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
946
-
947
- writeFileSync(
948
- join(tempDir, "src/i18n/translations/default.json"),
949
- JSON.stringify({
950
- some_key: { it: "Ciao", fr: "Hallo", "en-US": "Hello" },
951
- some_other_key: { "en-US": "Computer", fr: "Ordinateur" },
952
- })
953
- );
954
- writeFileSync(
955
- join(tempDir, "src/i18n/translations/billing.json"),
956
- JSON.stringify({
957
- invoice_summary: {
958
- "en-US": "You have {count, plural, one {1 invoice} other {{count} invoices}}",
959
- it: "Hai {count} fatture",
960
- },
961
- })
962
- );
963
- writeFileSync(
964
- join(tempDir, "i18n.codegen.json"),
965
- JSON.stringify({
966
- namespaces: {
967
- default: "src/i18n/translations/default.json",
968
- billing: "src/i18n/translations/billing.json",
969
- },
970
- delivery: "custom",
971
- deliveryArtifacts: {
972
- eu: ["it", "fr"],
973
- us: ["en-US"],
974
- },
975
- localeFallback: {
976
- "en-US": null,
977
- it: "en-US",
978
- fr: null,
979
- },
980
- projectName: "App",
981
- codegenPath: "src/i18n/generated",
982
- })
983
- );
984
-
985
- const result = runCodegen(tempDir);
986
- expect(result.status).toBe(0);
987
- expect(result.stdout).toContain("default.eu.json");
988
- expect(result.stdout).toContain("default.us.json");
989
- expect(result.stdout).toContain("billing.eu.json");
990
- expect(result.stdout).toContain("billing.us.json");
991
-
992
- const defaultEu = JSON.parse(
993
- readFileSync(join(tempDir, "src/i18n/generated/translations/default.eu.json"), "utf8")
994
- ) as Record<string, Record<string, string>>;
995
- expect(defaultEu).toEqual({
996
- some_key: { it: "Ciao", fr: "Hallo" },
997
- some_other_key: { it: "Computer", fr: "Ordinateur" },
998
- });
999
-
1000
- const defaultUs = JSON.parse(
1001
- readFileSync(join(tempDir, "src/i18n/generated/translations/default.us.json"), "utf8")
1002
- ) as Record<string, Record<string, string>>;
1003
- expect(defaultUs).toEqual({
1004
- some_key: { "en-US": "Hello" },
1005
- some_other_key: { "en-US": "Computer" },
1006
- });
1007
-
1008
- expect(() =>
1009
- readFileSync(join(tempDir, "src/i18n/generated/translations/default.json"), "utf8")
1010
- ).toThrow();
1011
-
1012
- const types = readFileSync(join(tempDir, "src/i18n/generated/i18n-types.generated.ts"), "utf8");
1013
- const loaders = readFileSync(
1014
- join(tempDir, "src/i18n/generated/namespace-loaders.generated.ts"),
1015
- "utf8"
1016
- );
1017
- const factory = readFileSync(join(tempDir, "src/i18n/generated/instance.generated.ts"), "utf8");
1018
-
1019
- const billingEu = JSON.parse(
1020
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.eu.json"), "utf8")
1021
- ) as Record<string, Record<string, string>>;
1022
- expect(billingEu.invoice_summary).toEqual({ it: "Hai {count} fatture" });
1023
-
1024
- const billingUs = JSON.parse(
1025
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.us.json"), "utf8")
1026
- ) as Record<string, Record<string, string>>;
1027
- expect(billingUs.invoice_summary).toEqual({
1028
- "en-US": "You have {count, plural, one {1 invoice} other {{count} invoices}}",
1029
- });
1030
-
1031
- expect(() =>
1032
- readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
1033
- ).toThrow();
1034
-
1035
- expect(types).toContain('export const AppDeliveryAreas = ["eu", "us"] as const;');
1036
- expect(types).toContain("export type AppDeliveryArea = (typeof AppDeliveryAreas)[number];");
1037
- expect(types).toContain("export const DELIVERY_ARTIFACTS = {");
1038
- expect(types).toContain('"eu": ["fr", "it"] as const');
1039
- expect(types).toContain('"us": ["en-US"] as const');
1040
- expect(types).toContain("} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;");
1041
- expect(types).toContain("export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;");
1042
- expect(types).toContain("export const LOCALE_DELIVERY_AREA = {");
1043
- expect(types).toContain('"it": "eu"');
1044
- expect(types).toContain('"fr": "eu"');
1045
- expect(types).toContain('"en-US": "us"');
1046
- expect(types).toContain("} as const satisfies Record<AppLocale, AppDeliveryArea>;");
1047
- expect(types).toContain('export const AppLocales = ["en-US", "fr", "it"] as const;');
1048
- expect(types).toContain("export type AppLocale = (typeof AppLocales)[number];");
1049
- expect(types).toContain("export const LOCALE_FALLBACK");
1050
- expect(types).toContain('"it": "en-US"');
1051
- expect(types).toContain("some_key: Partial<Record<AppLocale, string>>;");
1052
- expect(types).toContain("invoice_summary: Partial<Record<AppLocale, string>>;");
1053
- expect(types).toContain("invoice_summary: { count: number }");
1054
-
1055
- expect(types).toContain("export type LazyNamespace = 'default' | 'billing';");
1056
-
1057
- expect(loaders).toContain(
1058
- "[K in LazyNamespace]: (area: AppDeliveryArea) => Promise<AppSchema[K]>;"
1059
- );
1060
- expect(loaders).toContain("export const defaultLazyNamespaces");
1061
- expect(loaders).toContain("default: (area) => {");
1062
- expect(loaders).toContain("billing: (area) => {");
1063
- expect(loaders).toContain('case "eu":');
1064
- expect(loaders).toContain('case "us":');
1065
- expect(loaders).toContain(
1066
- "return import('./translations/billing.eu.json').then((m) => m.default);"
1067
- );
1068
- expect(loaders).toContain(
1069
- "return import('./translations/billing.us.json').then((m) => m.default);"
1070
- );
1071
-
1072
- expect(factory).toContain("partitionForLocale: (locale) => LOCALE_DELIVERY_AREA[locale]");
1073
- });
1074
-
1075
- it("fails when deliveryArtifacts does not partition request locales", () => {
1076
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1077
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1078
- mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
1079
-
1080
- writeFileSync(
1081
- join(tempDir, "src/i18n/translations/default.json"),
1082
- JSON.stringify({
1083
- some_key: { it: "Ciao", fr: "Hallo", "en-US": "Hello" },
1084
- })
1085
- );
1086
- writeFileSync(
1087
- join(tempDir, "i18n.codegen.json"),
1088
- JSON.stringify({
1089
- namespaces: {
1090
- default: "src/i18n/translations/default.json",
1091
- },
1092
- delivery: "custom",
1093
- deliveryArtifacts: {
1094
- eu: ["it", "fr"],
1095
- },
1096
- localeFallback: {
1097
- "en-US": null,
1098
- it: "en-US",
1099
- fr: null,
1100
- },
1101
- projectName: "App",
1102
- codegenPath: "src/i18n/generated",
1103
- })
1104
- );
1105
-
1106
- const result = runCodegen(tempDir);
1107
- expect(result.status).not.toBe(0);
1108
- expect(result.stderr).toContain("missing locales");
1109
- expect(result.stderr).toContain("en-US");
1110
- });
1111
-
1112
- it("fails when yaml dictionary has invalid shape", () => {
1113
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-"));
1114
- mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
1115
-
1116
- writeFileSync(
1117
- join(tempDir, "src/i18n/translations/translations.yaml"),
1118
- `welcome:
1119
- en: 123
1120
- `
1121
- );
1122
- writeFileSync(
1123
- join(tempDir, "i18n.codegen.json"),
1124
- JSON.stringify({
1125
- namespaces: {
1126
- default: "src/i18n/translations/translations.yaml",
1127
- },
1128
- projectName: "App",
1129
- codegenPath: "src/i18n",
1130
- })
1131
- );
1132
-
1133
- const result = runCodegen(tempDir);
1134
- expect(result.status).not.toBe(0);
1135
- expect(result.stderr).toContain("must be a string");
1136
- });
1137
- });