@xndrjs/i18n 0.8.1 → 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 (93) 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/package.json +6 -9
  11. package/bin/audit.mjs +0 -14
  12. package/bin/codegen.mjs +0 -17
  13. package/bin/setup.mjs +0 -14
  14. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  15. package/src/IcuTranslationProviderMulti.ts +0 -188
  16. package/src/audit/audit-dictionaries.test.ts +0 -134
  17. package/src/audit/audit-dictionaries.ts +0 -178
  18. package/src/audit/run-audit.test.ts +0 -169
  19. package/src/audit/run-audit.ts +0 -94
  20. package/src/builder-load-registry.test.ts +0 -44
  21. package/src/builder-load-registry.ts +0 -78
  22. package/src/builder-loaders.ts +0 -15
  23. package/src/builder-types.test.ts +0 -57
  24. package/src/builder-types.ts +0 -24
  25. package/src/builder.test.ts +0 -303
  26. package/src/builder.ts +0 -15
  27. package/src/codegen/codegen-config-schema.ts +0 -156
  28. package/src/codegen/config.test.ts +0 -231
  29. package/src/codegen/config.ts +0 -42
  30. package/src/codegen/constants.ts +0 -9
  31. package/src/codegen/delivery-artifacts.test.ts +0 -167
  32. package/src/codegen/delivery-artifacts.ts +0 -154
  33. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  34. package/src/codegen/dictionary-spec-contract.ts +0 -138
  35. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  36. package/src/codegen/emit/instance-file.test.ts +0 -71
  37. package/src/codegen/emit/instance-file.ts +0 -257
  38. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  39. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  40. package/src/codegen/emit/types-file.test.ts +0 -96
  41. package/src/codegen/emit/types-file.ts +0 -142
  42. package/src/codegen/fixtures/billing-namespace.json +0 -6
  43. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  44. package/src/codegen/generate-i18n-types.ts +0 -20
  45. package/src/codegen/icu-analysis.ts +0 -110
  46. package/src/codegen/locale-fallback.ts +0 -73
  47. package/src/codegen/locale-policy.test.ts +0 -35
  48. package/src/codegen/locale-policy.ts +0 -30
  49. package/src/codegen/paths.ts +0 -44
  50. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  51. package/src/codegen/read-dictionary.test.ts +0 -594
  52. package/src/codegen/read-dictionary.ts +0 -260
  53. package/src/codegen/regenerate-namespaces.ts +0 -180
  54. package/src/codegen/run-codegen.test.ts +0 -230
  55. package/src/codegen/run-codegen.ts +0 -252
  56. package/src/codegen/types.ts +0 -10
  57. package/src/codegen/write-file-if-changed.test.ts +0 -42
  58. package/src/codegen/write-file-if-changed.ts +0 -20
  59. package/src/codegen-config/build-config.ts +0 -15
  60. package/src/codegen-config/codegen-config.test.ts +0 -36
  61. package/src/codegen-config/index.ts +0 -37
  62. package/src/codegen-config/type-names.ts +0 -20
  63. package/src/codegen-config/write-config.ts +0 -8
  64. package/src/engine.ts +0 -47
  65. package/src/fetch-artifact.ts +0 -16
  66. package/src/format-core.ts +0 -91
  67. package/src/i18n-handle.ts +0 -216
  68. package/src/icu/extract-variables.test.ts +0 -199
  69. package/src/icu/extract-variables.ts +0 -227
  70. package/src/icu/parse-template.ts +0 -31
  71. package/src/index.ts +0 -63
  72. package/src/load-state.ts +0 -15
  73. package/src/project-locales.test.ts +0 -280
  74. package/src/project-locales.ts +0 -211
  75. package/src/resolve-locale.test.ts +0 -91
  76. package/src/resolve-locale.ts +0 -88
  77. package/src/scope-multi.ts +0 -105
  78. package/src/scope-types.ts +0 -35
  79. package/src/scope.test.ts +0 -172
  80. package/src/serialized-state.test.ts +0 -114
  81. package/src/serialized-state.ts +0 -28
  82. package/src/setup/setup-i18n.test.ts +0 -85
  83. package/src/setup/setup-i18n.ts +0 -156
  84. package/src/types.ts +0 -65
  85. package/src/validation/create-args-schema.ts +0 -76
  86. package/src/validation/create-normalized-schema.ts +0 -52
  87. package/src/validation/errors.ts +0 -43
  88. package/src/validation/index.ts +0 -202
  89. package/src/validation/normalize.ts +0 -299
  90. package/src/validation/to-dictionary.ts +0 -67
  91. package/src/validation/types.ts +0 -82
  92. package/src/validation/validate-normalized.ts +0 -133
  93. 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
- });