@xndrjs/i18n 0.4.0 → 0.5.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 (57) hide show
  1. package/README.md +156 -29
  2. package/dist/codegen/index.d.ts +85 -0
  3. package/dist/codegen/index.js +181 -0
  4. package/dist/codegen/index.js.map +1 -0
  5. package/dist/index.d.ts +34 -5
  6. package/dist/index.js +175 -74
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +4 -0
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +6 -1
  11. package/src/IcuTranslationProviderMulti.test.ts +47 -0
  12. package/src/IcuTranslationProviderMulti.ts +41 -61
  13. package/src/IcuTranslationProviderSingle.test.ts +67 -0
  14. package/src/IcuTranslationProviderSingle.ts +27 -51
  15. package/src/audit/audit-dictionaries.ts +4 -1
  16. package/src/audit/run-audit.test.ts +35 -1
  17. package/src/audit/run-audit.ts +15 -7
  18. package/src/codegen/codegen-config-schema.ts +68 -1
  19. package/src/codegen/config.test.ts +174 -39
  20. package/src/codegen/config.ts +14 -5
  21. package/src/codegen/constants.ts +8 -0
  22. package/src/codegen/delivery-artifacts.test.ts +142 -0
  23. package/src/codegen/delivery-artifacts.ts +124 -0
  24. package/src/codegen/emit/dictionary-file.test.ts +190 -0
  25. package/src/codegen/emit/dictionary-file.ts +163 -0
  26. package/src/codegen/emit/dictionary-schema-file.ts +5 -0
  27. package/src/codegen/emit/instance-file.ts +114 -28
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +176 -0
  29. package/src/codegen/emit/namespace-loaders-file.ts +114 -6
  30. package/src/codegen/emit/types-file.test.ts +114 -0
  31. package/src/codegen/emit/types-file.ts +48 -11
  32. package/src/codegen/generate-i18n-types.test.ts +754 -16
  33. package/src/codegen/generate-i18n-types.ts +111 -47
  34. package/src/codegen/icu-analysis.ts +9 -2
  35. package/src/codegen/locale-fallback.ts +19 -10
  36. package/src/codegen/locale-policy.ts +4 -0
  37. package/src/codegen/paths.ts +9 -5
  38. package/src/codegen/project-locales-set-namespace.test.ts +5 -5
  39. package/src/codegen/read-dictionary.test.ts +474 -2
  40. package/src/codegen/read-dictionary.ts +164 -15
  41. package/src/codegen/write-file-if-changed.test.ts +42 -0
  42. package/src/codegen/write-file-if-changed.ts +20 -0
  43. package/src/codegen-config/build-config.ts +34 -0
  44. package/src/codegen-config/codegen-config.test.ts +32 -0
  45. package/src/codegen-config/index.ts +21 -0
  46. package/src/codegen-config/write-config.ts +8 -0
  47. package/src/deep-freeze.test.ts +13 -0
  48. package/src/deep-freeze.ts +5 -0
  49. package/src/format-core.ts +91 -0
  50. package/src/index.ts +8 -2
  51. package/src/project-locales.test.ts +129 -10
  52. package/src/project-locales.ts +90 -3
  53. package/src/setup/setup-i18n.test.ts +1 -1
  54. package/src/setup/setup-i18n.ts +6 -30
  55. package/src/types.ts +19 -4
  56. package/src/validation/normalize.ts +4 -0
  57. /package/src/{setup → codegen-config}/type-names.ts +0 -0
@@ -6,7 +6,11 @@ import {
6
6
  getDictionaryFormat,
7
7
  prepareDictionaryEntries,
8
8
  readDictionaryFile,
9
+ resolveAreaJsonPath,
9
10
  resolveCompiledJsonPath,
11
+ resolveSplitJsonPath,
12
+ splitDictionaryByDeliveryArea,
13
+ splitDictionaryByLocale,
10
14
  } from "./read-dictionary.js";
11
15
 
12
16
  describe("read-dictionary", () => {
@@ -34,6 +38,24 @@ describe("read-dictionary", () => {
34
38
  );
35
39
  });
36
40
 
41
+ it("resolves split json path per locale under the generated output directory", () => {
42
+ expect(
43
+ resolveSplitJsonPath("src/i18n/translations/user.json", "it", "src/i18n/generated")
44
+ ).toBe("src/i18n/generated/translations/user.it.json");
45
+ expect(resolveSplitJsonPath("translations/billing.yaml", "en", "generated")).toBe(
46
+ "generated/translations/billing.en.json"
47
+ );
48
+ });
49
+
50
+ it("resolves area json path under the generated output directory", () => {
51
+ expect(
52
+ resolveAreaJsonPath("src/i18n/translations/billing.json", "eu", "src/i18n/generated")
53
+ ).toBe("src/i18n/generated/translations/billing.eu.json");
54
+ expect(resolveAreaJsonPath("translations/default.yaml", "us", "generated")).toBe(
55
+ "generated/translations/default.us.json"
56
+ );
57
+ });
58
+
37
59
  it("reads multiline yaml dictionaries", () => {
38
60
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
39
61
  const yamlPath = join(tempDir, "welcome.yaml");
@@ -52,6 +74,37 @@ describe("read-dictionary", () => {
52
74
  expect(dictionary.welcome?.it).toBe("Ciao {name}");
53
75
  });
54
76
 
77
+ it("rejects keys with characters outside letters, digits, and underscore", () => {
78
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
79
+ const jsonPath = join(tempDir, "default.json");
80
+ writeFileSync(jsonPath, JSON.stringify({ "app.title": { en: "My App" } }));
81
+
82
+ expect(() => readDictionaryFile(jsonPath)).toThrow(
83
+ "[Codegen Error] Dictionary file " +
84
+ jsonPath +
85
+ ': invalid key "app.title" (allowed: letters, digits, underscore; must not start with a digit).'
86
+ );
87
+ });
88
+
89
+ it("rejects keys starting with a digit", () => {
90
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
91
+ const jsonPath = join(tempDir, "default.json");
92
+ writeFileSync(jsonPath, JSON.stringify({ "1key": { en: "One" } }));
93
+
94
+ expect(() => readDictionaryFile(jsonPath)).toThrow('invalid key "1key"');
95
+ });
96
+
97
+ it("accepts keys made of letters, digits, and underscore", () => {
98
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
99
+ const jsonPath = join(tempDir, "default.json");
100
+ writeFileSync(
101
+ jsonPath,
102
+ JSON.stringify({ _private: { en: "ok" }, welcome_2: { en: "ok" }, CamelCase: { en: "ok" } })
103
+ );
104
+
105
+ expect(() => readDictionaryFile(jsonPath)).not.toThrow();
106
+ });
107
+
55
108
  it("compiles yaml sources to json under the generated output directory", () => {
56
109
  tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
57
110
  mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
@@ -66,12 +119,18 @@ describe("read-dictionary", () => {
66
119
  const result = prepareDictionaryEntries(
67
120
  tempDir,
68
121
  [{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
69
- "src/i18n/generated"
122
+ "src/i18n/generated",
123
+ {
124
+ dictionariesByNamespace: {
125
+ billing: readDictionaryFile(join(tempDir, "src/i18n/translations/billing.yaml")),
126
+ },
127
+ }
70
128
  );
71
129
 
72
130
  expect(result.resolvedEntries).toEqual([
73
131
  { namespace: "billing", filePath: "src/i18n/generated/translations/billing.json" },
74
132
  ]);
133
+ expect(result.splitPathsByNamespace).toEqual({});
75
134
  expect(result.compiledFiles).toEqual([
76
135
  "src/i18n/translations/billing.yaml → src/i18n/generated/translations/billing.json",
77
136
  ]);
@@ -93,12 +152,425 @@ describe("read-dictionary", () => {
93
152
  const result = prepareDictionaryEntries(
94
153
  tempDir,
95
154
  [{ namespace: "default", filePath: "translations/default.json" }],
96
- "generated"
155
+ "generated",
156
+ {
157
+ dictionariesByNamespace: {
158
+ default: readDictionaryFile(join(tempDir, "translations/default.json")),
159
+ },
160
+ }
97
161
  );
98
162
 
99
163
  expect(result.resolvedEntries).toEqual([
100
164
  { namespace: "default", filePath: "translations/default.json" },
101
165
  ]);
166
+ expect(result.splitPathsByNamespace).toEqual({});
102
167
  expect(result.compiledFiles).toEqual([]);
103
168
  });
169
+
170
+ it("splits multiple namespaces into per-locale files", () => {
171
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
172
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
173
+ mkdirSync(join(tempDir, "generated"), { recursive: true });
174
+ writeFileSync(
175
+ join(tempDir, "translations/user.json"),
176
+ JSON.stringify({
177
+ profile_title: { en: "Your profile", it: "Il tuo profilo" },
178
+ })
179
+ );
180
+ writeFileSync(
181
+ join(tempDir, "translations/billing.json"),
182
+ JSON.stringify({
183
+ invoice_summary: { en: "You have {count} invoices", it: "Hai {count} fatture" },
184
+ })
185
+ );
186
+
187
+ const result = prepareDictionaryEntries(
188
+ tempDir,
189
+ [
190
+ { namespace: "user", filePath: "translations/user.json" },
191
+ { namespace: "billing", filePath: "translations/billing.json" },
192
+ ],
193
+ "generated",
194
+ {
195
+ dictionariesByNamespace: {
196
+ user: readDictionaryFile(join(tempDir, "translations/user.json")),
197
+ billing: readDictionaryFile(join(tempDir, "translations/billing.json")),
198
+ },
199
+ delivery: "split-by-locale",
200
+ requestLocales: ["en", "it"],
201
+ }
202
+ );
203
+
204
+ expect(result.splitPathsByNamespace).toEqual({
205
+ user: {
206
+ en: "generated/translations/user.en.json",
207
+ it: "generated/translations/user.it.json",
208
+ },
209
+ billing: {
210
+ en: "generated/translations/billing.en.json",
211
+ it: "generated/translations/billing.it.json",
212
+ },
213
+ });
214
+
215
+ const userEn = JSON.parse(
216
+ readFileSync(join(tempDir, "generated/translations/user.en.json"), "utf8")
217
+ ) as Record<string, Record<string, string>>;
218
+ expect(userEn.profile_title).toEqual({ en: "Your profile" });
219
+
220
+ const billingIt = JSON.parse(
221
+ readFileSync(join(tempDir, "generated/translations/billing.it.json"), "utf8")
222
+ ) as Record<string, Record<string, string>>;
223
+ expect(billingIt.invoice_summary).toEqual({ it: "Hai {count} fatture" });
224
+ });
225
+
226
+ it("splits json sources into per-locale files under generated without touching the source", () => {
227
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
228
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
229
+ mkdirSync(join(tempDir, "generated"), { recursive: true });
230
+ const sourcePath = join(tempDir, "translations/user.json");
231
+ writeFileSync(
232
+ sourcePath,
233
+ JSON.stringify({
234
+ profile_title: { en: "Your profile", it: "Il tuo profilo" },
235
+ greeting: { en: "Hello {name}!", it: "Ciao {name}!" },
236
+ })
237
+ );
238
+
239
+ const result = prepareDictionaryEntries(
240
+ tempDir,
241
+ [{ namespace: "user", filePath: "translations/user.json" }],
242
+ "generated",
243
+ {
244
+ dictionariesByNamespace: { user: readDictionaryFile(sourcePath) },
245
+ delivery: "split-by-locale",
246
+ requestLocales: ["en", "it"],
247
+ }
248
+ );
249
+
250
+ expect(result.resolvedEntries).toEqual([
251
+ { namespace: "user", filePath: "translations/user.json" },
252
+ ]);
253
+ expect(result.splitPathsByNamespace).toEqual({
254
+ user: {
255
+ en: "generated/translations/user.en.json",
256
+ it: "generated/translations/user.it.json",
257
+ },
258
+ });
259
+ expect(result.compiledFiles).toEqual([
260
+ "translations/user.json → generated/translations/user.en.json",
261
+ "translations/user.json → generated/translations/user.it.json",
262
+ ]);
263
+
264
+ const sourceAfter = JSON.parse(readFileSync(sourcePath, "utf8")) as Record<
265
+ string,
266
+ Record<string, string>
267
+ >;
268
+ expect(sourceAfter.profile_title).toEqual({ en: "Your profile", it: "Il tuo profilo" });
269
+
270
+ const enSplit = JSON.parse(
271
+ readFileSync(join(tempDir, "generated/translations/user.en.json"), "utf8")
272
+ ) as Record<string, Record<string, string>>;
273
+ expect(enSplit).toEqual({
274
+ profile_title: { en: "Your profile" },
275
+ greeting: { en: "Hello {name}!" },
276
+ });
277
+
278
+ const itSplit = JSON.parse(
279
+ readFileSync(join(tempDir, "generated/translations/user.it.json"), "utf8")
280
+ ) as Record<string, Record<string, string>>;
281
+ expect(itSplit).toEqual({
282
+ profile_title: { it: "Il tuo profilo" },
283
+ greeting: { it: "Ciao {name}!" },
284
+ });
285
+ });
286
+
287
+ it("splits yaml sources into per-locale json without emitting a canonical intermediate", () => {
288
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
289
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
290
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
291
+ writeFileSync(
292
+ join(tempDir, "src/i18n/translations/billing.yaml"),
293
+ `invoice_summary:
294
+ en: You have {count} invoices
295
+ it: Hai {count} fatture
296
+ `
297
+ );
298
+
299
+ const result = prepareDictionaryEntries(
300
+ tempDir,
301
+ [{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
302
+ "src/i18n/generated",
303
+ {
304
+ dictionariesByNamespace: {
305
+ billing: readDictionaryFile(join(tempDir, "src/i18n/translations/billing.yaml")),
306
+ },
307
+ delivery: "split-by-locale",
308
+ requestLocales: ["en", "it"],
309
+ }
310
+ );
311
+
312
+ expect(result.resolvedEntries).toEqual([
313
+ { namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
314
+ ]);
315
+ expect(result.splitPathsByNamespace.billing).toEqual({
316
+ en: "src/i18n/generated/translations/billing.en.json",
317
+ it: "src/i18n/generated/translations/billing.it.json",
318
+ });
319
+
320
+ const canonicalPath = join(tempDir, "src/i18n/generated/translations/billing.json");
321
+ expect(() => readFileSync(canonicalPath, "utf8")).toThrow();
322
+
323
+ const enSplit = JSON.parse(
324
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.en.json"), "utf8")
325
+ ) as Record<string, Record<string, string>>;
326
+ expect(enSplit.invoice_summary?.en).toBe("You have {count} invoices");
327
+ });
328
+
329
+ it("applies localeFallback when splitting dictionaries", () => {
330
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
331
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
332
+ mkdirSync(join(tempDir, "generated"), { recursive: true });
333
+ writeFileSync(
334
+ join(tempDir, "translations/default.json"),
335
+ JSON.stringify({
336
+ login_button: { en: "Login", it: "Accedi" },
337
+ })
338
+ );
339
+
340
+ const localeFallback = {
341
+ en: null,
342
+ "de-DE": "en",
343
+ "de-CH": "de-DE",
344
+ it: "en",
345
+ } as const;
346
+
347
+ prepareDictionaryEntries(
348
+ tempDir,
349
+ [{ namespace: "default", filePath: "translations/default.json" }],
350
+ "generated",
351
+ {
352
+ dictionariesByNamespace: {
353
+ default: readDictionaryFile(join(tempDir, "translations/default.json")),
354
+ },
355
+ delivery: "split-by-locale",
356
+ requestLocales: ["de-CH"],
357
+ localeFallback,
358
+ }
359
+ );
360
+
361
+ const deChSplit = JSON.parse(
362
+ readFileSync(join(tempDir, "generated/translations/default.de-CH.json"), "utf8")
363
+ ) as Record<string, Record<string, string>>;
364
+ expect(deChSplit).toEqual({
365
+ login_button: { "de-CH": "Login" },
366
+ });
367
+ });
368
+
369
+ it("uses the provided parsed dictionaries instead of re-reading sources", () => {
370
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
371
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
372
+ mkdirSync(join(tempDir, "generated"), { recursive: true });
373
+ writeFileSync(
374
+ join(tempDir, "translations/default.json"),
375
+ JSON.stringify({ welcome: { en: "On disk" } })
376
+ );
377
+
378
+ prepareDictionaryEntries(
379
+ tempDir,
380
+ [{ namespace: "default", filePath: "translations/default.json" }],
381
+ "generated",
382
+ {
383
+ dictionariesByNamespace: { default: { welcome: { en: "In memory" } } },
384
+ delivery: "split-by-locale",
385
+ requestLocales: ["en"],
386
+ }
387
+ );
388
+
389
+ const enSplit = JSON.parse(
390
+ readFileSync(join(tempDir, "generated/translations/default.en.json"), "utf8")
391
+ ) as Record<string, Record<string, string>>;
392
+ expect(enSplit.welcome).toEqual({ en: "In memory" });
393
+ });
394
+
395
+ it("throws when a parsed dictionary is missing for a namespace", () => {
396
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
397
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
398
+ writeFileSync(
399
+ join(tempDir, "translations/default.json"),
400
+ JSON.stringify({ welcome: { en: "Welcome" } })
401
+ );
402
+
403
+ expect(() =>
404
+ prepareDictionaryEntries(
405
+ tempDir,
406
+ [{ namespace: "default", filePath: "translations/default.json" }],
407
+ "generated",
408
+ { dictionariesByNamespace: {} }
409
+ )
410
+ ).toThrow('[Codegen Error] Missing parsed dictionary for namespace "default".');
411
+ });
412
+
413
+ it("projects each locale independently via splitDictionaryByLocale", () => {
414
+ const dictionary = {
415
+ login_button: { en: "Login", it: "Accedi" },
416
+ welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
417
+ };
418
+ const localeFallback = {
419
+ en: null,
420
+ "de-CH": "en",
421
+ } as const;
422
+
423
+ expect(splitDictionaryByLocale(dictionary, ["en", "it"])).toEqual({
424
+ en: {
425
+ login_button: { en: "Login" },
426
+ welcome: { en: "Welcome {name}!" },
427
+ },
428
+ it: {
429
+ login_button: { it: "Accedi" },
430
+ welcome: { it: "Benvenuto {name}!" },
431
+ },
432
+ });
433
+
434
+ expect(splitDictionaryByLocale(dictionary, ["de-CH"], localeFallback)).toEqual({
435
+ "de-CH": {
436
+ login_button: { "de-CH": "Login" },
437
+ welcome: { "de-CH": "Welcome {name}!" },
438
+ },
439
+ });
440
+ });
441
+
442
+ it("splits dictionaries into per-area files with hybrid projection", () => {
443
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
444
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
445
+ mkdirSync(join(tempDir, "generated"), { recursive: true });
446
+ writeFileSync(
447
+ join(tempDir, "translations/default.json"),
448
+ JSON.stringify({
449
+ some_key: { it: "Ciao", fr: "Hallo", "en-US": "Hello" },
450
+ some_other_key: { "en-US": "Computer", fr: "Ordinateur" },
451
+ })
452
+ );
453
+
454
+ const deliveryArtifacts = {
455
+ eu: ["it", "fr"],
456
+ us: ["en-US"],
457
+ };
458
+ const localeFallback = {
459
+ "en-US": null,
460
+ it: "en-US",
461
+ fr: null,
462
+ } as const;
463
+
464
+ const result = prepareDictionaryEntries(
465
+ tempDir,
466
+ [{ namespace: "default", filePath: "translations/default.json" }],
467
+ "generated",
468
+ {
469
+ dictionariesByNamespace: {
470
+ default: readDictionaryFile(join(tempDir, "translations/default.json")),
471
+ },
472
+ delivery: "custom",
473
+ deliveryArtifacts,
474
+ localeFallback,
475
+ }
476
+ );
477
+
478
+ expect(result.splitPathsByNamespace).toEqual({
479
+ default: {
480
+ eu: "generated/translations/default.eu.json",
481
+ us: "generated/translations/default.us.json",
482
+ },
483
+ });
484
+ expect(result.compiledFiles).toEqual([
485
+ "translations/default.json → generated/translations/default.eu.json",
486
+ "translations/default.json → generated/translations/default.us.json",
487
+ ]);
488
+
489
+ const euArea = JSON.parse(
490
+ readFileSync(join(tempDir, "generated/translations/default.eu.json"), "utf8")
491
+ ) as Record<string, Record<string, string>>;
492
+ expect(euArea).toEqual({
493
+ some_key: { it: "Ciao", fr: "Hallo" },
494
+ some_other_key: { it: "Computer", fr: "Ordinateur" },
495
+ });
496
+
497
+ const usArea = JSON.parse(
498
+ readFileSync(join(tempDir, "generated/translations/default.us.json"), "utf8")
499
+ ) as Record<string, Record<string, string>>;
500
+ expect(usArea).toEqual({
501
+ some_key: { "en-US": "Hello" },
502
+ some_other_key: { "en-US": "Computer" },
503
+ });
504
+ });
505
+
506
+ it("splits yaml sources into per-area json without emitting a canonical intermediate", () => {
507
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
508
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
509
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
510
+ writeFileSync(
511
+ join(tempDir, "src/i18n/translations/billing.yaml"),
512
+ `invoice_summary:
513
+ en-US: You have {count} invoices
514
+ it: Hai {count} fatture
515
+ `
516
+ );
517
+
518
+ const result = prepareDictionaryEntries(
519
+ tempDir,
520
+ [{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
521
+ "src/i18n/generated",
522
+ {
523
+ dictionariesByNamespace: {
524
+ billing: readDictionaryFile(join(tempDir, "src/i18n/translations/billing.yaml")),
525
+ },
526
+ delivery: "custom",
527
+ deliveryArtifacts: { us: ["en-US"], eu: ["it"] },
528
+ localeFallback: { "en-US": null, it: "en-US" },
529
+ }
530
+ );
531
+
532
+ expect(result.resolvedEntries).toEqual([
533
+ { namespace: "billing", filePath: "src/i18n/translations/billing.yaml" },
534
+ ]);
535
+ expect(result.splitPathsByNamespace.billing).toEqual({
536
+ eu: "src/i18n/generated/translations/billing.eu.json",
537
+ us: "src/i18n/generated/translations/billing.us.json",
538
+ });
539
+
540
+ expect(() =>
541
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
542
+ ).toThrow();
543
+
544
+ const euArea = JSON.parse(
545
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.eu.json"), "utf8")
546
+ ) as Record<string, Record<string, string>>;
547
+ expect(euArea.invoice_summary).toEqual({ it: "Hai {count} fatture" });
548
+ });
549
+
550
+ it("projects each delivery area via splitDictionaryByDeliveryArea", () => {
551
+ const dictionary = {
552
+ some_key: { it: "Ciao", fr: "Hallo", "en-US": "Hello" },
553
+ some_other_key: { "en-US": "Computer", fr: "Ordinateur" },
554
+ };
555
+ const deliveryArtifacts = {
556
+ eu: ["it", "fr"],
557
+ us: ["en-US"],
558
+ };
559
+ const localeFallback = {
560
+ "en-US": null,
561
+ it: "en-US",
562
+ fr: null,
563
+ } as const;
564
+
565
+ expect(splitDictionaryByDeliveryArea(dictionary, deliveryArtifacts, localeFallback)).toEqual({
566
+ eu: {
567
+ some_key: { it: "Ciao", fr: "Hallo" },
568
+ some_other_key: { it: "Computer", fr: "Ordinateur" },
569
+ },
570
+ us: {
571
+ some_key: { "en-US": "Hello" },
572
+ some_other_key: { "en-US": "Computer" },
573
+ },
574
+ });
575
+ });
104
576
  });