@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,562 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { DictionarySpec } from "./types.js";
3
- import { normalizeDictionary, normalizeKeyDictionaryPartial } from "./normalize.js";
4
- import { validateNormalizedDictionary } from "./validate-normalized.js";
5
- import { toDictionary } from "./to-dictionary.js";
6
- import {
7
- validateExternalDictionaryPartial,
8
- validateExternalKey,
9
- validateExternalNamespacePartial,
10
- } from "./index.js";
11
-
12
- const spec: DictionarySpec = {
13
- mode: "single",
14
- requiredKeys: ["login_button", "welcome", "invoice_count"],
15
- argsByKey: {
16
- login_button: {},
17
- welcome: { name: "string" },
18
- invoice_count: { count: "number" },
19
- },
20
- };
21
-
22
- const validInput = {
23
- login_button: { en: "Login", it: "Accedi" },
24
- welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
25
- invoice_count: {
26
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
27
- },
28
- };
29
-
30
- describe("normalizeDictionary", () => {
31
- it("rejects non-object input", () => {
32
- const result = normalizeDictionary(null, spec);
33
- expect(result.ok).toBe(false);
34
- if (!result.ok) {
35
- expect(result.issues[0]?.kind).toBe("invalid_input");
36
- }
37
- });
38
-
39
- it("rejects missing required keys", () => {
40
- const result = normalizeDictionary({ login_button: { en: "Login" } }, spec);
41
- expect(result.ok).toBe(false);
42
- if (!result.ok) {
43
- expect(result.issues.some((i) => i.kind === "missing_key")).toBe(true);
44
- }
45
- });
46
-
47
- it("ignores extra keys", () => {
48
- const result = normalizeDictionary({ ...validInput, extra_key: { en: "Extra" } }, spec);
49
- expect(result.ok).toBe(true);
50
- });
51
-
52
- it("accepts partial locales", () => {
53
- const result = normalizeDictionary(
54
- {
55
- login_button: { en: "Login" },
56
- welcome: { en: "Welcome {name}!" },
57
- invoice_count: { en: "You have {count, plural, one {1} other {{count}}}" },
58
- },
59
- spec
60
- );
61
- expect(result.ok).toBe(true);
62
- if (result.ok && result.data.mode === "single") {
63
- expect(Object.keys(result.data.keys.login_button!.locales)).toEqual(["en"]);
64
- }
65
- });
66
-
67
- it("rejects ICU syntax errors", () => {
68
- const result = normalizeDictionary(
69
- {
70
- ...validInput,
71
- welcome: { en: "Hi {name" },
72
- },
73
- spec
74
- );
75
- expect(result.ok).toBe(false);
76
- if (!result.ok) {
77
- expect(result.issues[0]?.kind).toBe("icu_syntax_error");
78
- }
79
- });
80
-
81
- it("rejects inconsistent args across locales", () => {
82
- const result = normalizeDictionary(
83
- {
84
- ...validInput,
85
- welcome: { en: "Welcome {name}!", it: "Benvenuto {nome}!" },
86
- },
87
- spec
88
- );
89
- expect(result.ok).toBe(false);
90
- if (!result.ok) {
91
- expect(result.issues[0]?.kind).toBe("locale_args_mismatch");
92
- }
93
- });
94
-
95
- it("accepts plural in one locale and simple interpolation in another for the same variable", () => {
96
- const result = normalizeDictionary(
97
- {
98
- login_button: { en: "Login" },
99
- welcome: { en: "Welcome {name}!" },
100
- invoice_count: {
101
- en: "You have {count, plural, one {1 invoice} other {# invoices}}",
102
- it: "Hai {count} fatture",
103
- },
104
- },
105
- spec
106
- );
107
-
108
- expect(result.ok).toBe(true);
109
- if (result.ok && result.data.mode === "single") {
110
- expect(result.data.keys.invoice_count?.mergedArgs).toEqual({ count: "number" });
111
- }
112
- });
113
-
114
- it("rejects plural and select on the same variable across locales", () => {
115
- const result = normalizeDictionary(
116
- {
117
- login_button: { en: "Login" },
118
- welcome: { en: "Welcome {name}!" },
119
- invoice_count: {
120
- en: "{count, select, other {{count}}}",
121
- it: "{count, plural, one {1} other {#}}",
122
- },
123
- },
124
- spec
125
- );
126
-
127
- expect(result.ok).toBe(false);
128
- if (!result.ok) {
129
- expect(result.issues.some((issue) => issue.kind === "locale_args_mismatch")).toBe(true);
130
- }
131
- });
132
-
133
- it("rejects plural and selectordinal on the same variable across locales", () => {
134
- const result = normalizeDictionary(
135
- {
136
- login_button: { en: "Login" },
137
- welcome: { en: "Welcome {name}!" },
138
- invoice_count: {
139
- en: "{count, plural, one {1} other {#}}",
140
- it: "{count, selectordinal, one {#°} other {#°}}",
141
- },
142
- },
143
- spec
144
- );
145
-
146
- expect(result.ok).toBe(false);
147
- if (!result.ok) {
148
- expect(result.issues.some((issue) => issue.kind === "locale_args_mismatch")).toBe(true);
149
- }
150
- });
151
-
152
- it("rejects select and number format on the same variable across locales", () => {
153
- const result = normalizeDictionary(
154
- {
155
- login_button: { en: "Login" },
156
- welcome: { en: "Welcome {name}!" },
157
- invoice_count: {
158
- en: "{count, select, other {x}}",
159
- it: "{count, number}",
160
- },
161
- },
162
- spec
163
- );
164
-
165
- expect(result.ok).toBe(false);
166
- if (!result.ok) {
167
- expect(result.issues.some((issue) => issue.kind === "locale_args_mismatch")).toBe(true);
168
- }
169
- });
170
-
171
- it("accepts select in one locale and simple interpolation in another", () => {
172
- const result = normalizeDictionary(
173
- {
174
- login_button: { en: "Login" },
175
- welcome: {
176
- en: "{gender, select, female {she} other {they}}",
177
- it: "Pronome: {gender}",
178
- },
179
- invoice_count: {
180
- en: "You have {count, plural, one {1 invoice} other {# invoices}}",
181
- },
182
- },
183
- {
184
- ...spec,
185
- requiredKeys: ["login_button", "welcome", "invoice_count"],
186
- argsByKey: {
187
- ...spec.argsByKey,
188
- welcome: { gender: "string" },
189
- },
190
- }
191
- );
192
-
193
- expect(result.ok).toBe(true);
194
- if (result.ok && result.data.mode === "single") {
195
- expect(result.data.keys.welcome?.mergedArgs).toEqual({ gender: "string" });
196
- }
197
- });
198
- });
199
-
200
- describe("validateNormalizedDictionary", () => {
201
- it("accepts matching merged args", () => {
202
- const normalized = normalizeDictionary(validInput, spec);
203
- expect(normalized.ok).toBe(true);
204
- if (!normalized.ok) return;
205
-
206
- const result = validateNormalizedDictionary(normalized.data, spec);
207
- expect(result.ok).toBe(true);
208
- });
209
-
210
- it("rejects variable mismatch vs spec", () => {
211
- const normalized = normalizeDictionary(
212
- {
213
- login_button: { en: "Login" },
214
- welcome: { en: "Welcome {name}!" },
215
- invoice_count: {
216
- en: "You have {total, plural, one {1} other {{total}}}",
217
- },
218
- },
219
- spec
220
- );
221
- expect(normalized.ok).toBe(true);
222
- if (!normalized.ok) return;
223
-
224
- const result = validateNormalizedDictionary(normalized.data, spec);
225
- expect(result.ok).toBe(false);
226
- if (!result.ok) {
227
- expect(
228
- result.issues.some(
229
- (i) => i.kind === "variable_mismatch" || i.kind === "variable_type_mismatch"
230
- )
231
- ).toBe(true);
232
- }
233
- });
234
- });
235
-
236
- describe("ICU variant argument extraction", () => {
237
- const variantSpec: DictionarySpec = {
238
- mode: "single",
239
- requiredKeys: ["inbox_owner", "ranking_position", "account_balance", "appointment_summary"],
240
- argsByKey: {
241
- inbox_owner: { gender: "string", name: "string" },
242
- ranking_position: { position: "number" },
243
- account_balance: { amount: "number" },
244
- appointment_summary: { dueDate: "date", startTime: "date" },
245
- },
246
- };
247
-
248
- const variantInput = {
249
- inbox_owner: {
250
- en: "{gender, select, female {{name} owns her inbox} male {{name} owns his inbox} other {{name} owns their inbox}}",
251
- },
252
- ranking_position: {
253
- en: "You finished {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}",
254
- },
255
- account_balance: {
256
- en: "Balance: {amount, number, ::currency/EUR}",
257
- },
258
- appointment_summary: {
259
- en: "Due {dueDate, date, short} at {startTime, time, short}",
260
- },
261
- };
262
-
263
- it("normalizes select, selectordinal, number, date, and time arguments", () => {
264
- const normalized = normalizeDictionary(variantInput, variantSpec);
265
- expect(normalized.ok).toBe(true);
266
- if (!normalized.ok || normalized.data.mode !== "single") return;
267
-
268
- expect(normalized.data.keys.inbox_owner?.mergedArgs).toEqual({
269
- gender: "string",
270
- name: "string",
271
- });
272
- expect(normalized.data.keys.ranking_position?.mergedArgs).toEqual({
273
- position: "number",
274
- });
275
- expect(normalized.data.keys.account_balance?.mergedArgs).toEqual({
276
- amount: "number",
277
- });
278
- expect(normalized.data.keys.appointment_summary?.mergedArgs).toEqual({
279
- dueDate: "date",
280
- startTime: "date",
281
- });
282
- });
283
-
284
- it("validates ICU variant args against the generated spec", () => {
285
- const normalized = normalizeDictionary(variantInput, variantSpec);
286
- expect(normalized.ok).toBe(true);
287
- if (!normalized.ok) return;
288
-
289
- const result = validateNormalizedDictionary(normalized.data, variantSpec);
290
- expect(result.ok).toBe(true);
291
- });
292
- });
293
-
294
- describe("toDictionary", () => {
295
- it("reconstructs key-locale-template shape", () => {
296
- const normalized = normalizeDictionary(validInput, spec);
297
- expect(normalized.ok).toBe(true);
298
- if (!normalized.ok) return;
299
-
300
- const dictionary = toDictionary(normalized.data);
301
- expect(dictionary).toEqual({
302
- login_button: { en: "Login", it: "Accedi" },
303
- welcome: { en: "Welcome {name}!", it: "Benvenuto {name}!" },
304
- invoice_count: {
305
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
306
- },
307
- });
308
- });
309
- });
310
-
311
- describe("validateExternalDictionaryPartial", () => {
312
- it("validates end-to-end with full snapshot payload", () => {
313
- const result = validateExternalDictionaryPartial<typeof validInput>(validInput, spec);
314
- expect(result.ok).toBe(true);
315
- if (result.ok) {
316
- expect((result.data as typeof validInput).login_button.en).toBe("Login");
317
- }
318
- });
319
-
320
- it("accepts partial payload with one of N keys", () => {
321
- const result = validateExternalDictionaryPartial({ login_button: { en: "Login" } }, spec);
322
- expect(result.ok).toBe(true);
323
- if (result.ok) {
324
- expect(Object.keys(result.data)).toEqual(["login_button"]);
325
- }
326
- });
327
-
328
- it("accepts empty payload", () => {
329
- const result = validateExternalDictionaryPartial({}, spec);
330
- expect(result.ok).toBe(true);
331
- if (result.ok) {
332
- expect(result.data).toEqual({});
333
- }
334
- });
335
-
336
- it("rejects unknown keys", () => {
337
- const result = validateExternalDictionaryPartial(
338
- { login_button: { en: "Login" }, typo_key: { en: "Oops" } },
339
- spec
340
- );
341
- expect(result.ok).toBe(false);
342
- if (!result.ok) {
343
- expect(result.issues.some((issue) => issue.kind === "unknown_key")).toBe(true);
344
- }
345
- });
346
-
347
- it("rejects ICU errors on partial keys", () => {
348
- const result = validateExternalDictionaryPartial({ welcome: { en: "Hi {name" } }, spec);
349
- expect(result.ok).toBe(false);
350
- if (!result.ok) {
351
- expect(result.issues[0]?.kind).toBe("icu_syntax_error");
352
- }
353
- });
354
- });
355
-
356
- describe("validateExternalKey (single)", () => {
357
- it("validates a single key and returns Pick type data", () => {
358
- const result = validateExternalKey<{ welcome: (typeof validInput)["welcome"] }>(
359
- "welcome",
360
- { en: "Welcome {name}!" },
361
- spec
362
- );
363
- expect(result.ok).toBe(true);
364
- if (result.ok) {
365
- expect(result.data.welcome.en).toBe("Welcome {name}!");
366
- }
367
- });
368
-
369
- it("rejects unknown keys", () => {
370
- const result = validateExternalKey("typo_key", { en: "Oops" }, spec);
371
- expect(result.ok).toBe(false);
372
- if (!result.ok) {
373
- expect(result.issues[0]?.kind).toBe("unknown_key");
374
- }
375
- });
376
- });
377
-
378
- describe("normalizeKeyDictionaryPartial", () => {
379
- it("accepts one of N keys without missing_key errors", () => {
380
- const result = normalizeKeyDictionaryPartial(
381
- { login_button: { en: "Login" } },
382
- spec.requiredKeys,
383
- spec.argsByKey,
384
- []
385
- );
386
- expect(result.ok).toBe(true);
387
- });
388
-
389
- it("rejects keys with zero locales", () => {
390
- const result = normalizeKeyDictionaryPartial(
391
- { login_button: {} },
392
- spec.requiredKeys,
393
- spec.argsByKey,
394
- []
395
- );
396
- expect(result.ok).toBe(false);
397
- if (!result.ok) {
398
- expect(result.issues[0]?.kind).toBe("invalid_input");
399
- }
400
- });
401
- });
402
-
403
- describe("normalizeDictionary multi mode", () => {
404
- const multiSpec: DictionarySpec = {
405
- mode: "multi",
406
- requiredKeys: {
407
- default: ["welcome"],
408
- billing: ["invoice_summary"],
409
- },
410
- argsByKey: {
411
- default: { welcome: { name: "string" } },
412
- billing: { invoice_summary: { count: "number" } },
413
- },
414
- };
415
-
416
- it("validates namespace structure", () => {
417
- const result = normalizeDictionary(
418
- {
419
- default: { welcome: { en: "Welcome {name}!" } },
420
- billing: {
421
- invoice_summary: {
422
- en: "You have {count, plural, one {1} other {{count}}}",
423
- },
424
- },
425
- },
426
- multiSpec
427
- );
428
- expect(result.ok).toBe(true);
429
- });
430
-
431
- it("reports missing keys with namespace path", () => {
432
- const result = normalizeDictionary(
433
- {
434
- default: { welcome: { en: "Welcome {name}!" } },
435
- },
436
- multiSpec
437
- );
438
- expect(result.ok).toBe(false);
439
- if (!result.ok) {
440
- expect(
441
- result.issues.some(
442
- (i) =>
443
- i.kind === "missing_key" &&
444
- "path" in i &&
445
- i.path.join(".") === "billing.invoice_summary"
446
- )
447
- ).toBe(true);
448
- }
449
- });
450
- });
451
-
452
- describe("validateExternalNamespacePartial", () => {
453
- const multiSpec: DictionarySpec = {
454
- mode: "multi",
455
- requiredKeys: {
456
- default: ["welcome"],
457
- billing: ["invoice_summary"],
458
- },
459
- argsByKey: {
460
- default: { welcome: { name: "string" } },
461
- billing: { invoice_summary: { count: "number" } },
462
- },
463
- };
464
-
465
- it("accepts one of N keys in a namespace", () => {
466
- const result = validateExternalNamespacePartial(
467
- "billing",
468
- {
469
- invoice_summary: {
470
- en: "You have {count, plural, one {1} other {{count}}}",
471
- },
472
- },
473
- multiSpec
474
- );
475
- expect(result.ok).toBe(true);
476
- });
477
-
478
- it("does not error when contract keys are absent from payload", () => {
479
- const result = validateExternalNamespacePartial("billing", {}, multiSpec);
480
- expect(result.ok).toBe(true);
481
- });
482
-
483
- it("validates full CMS snapshot when all keys are in payload", () => {
484
- const result = validateExternalNamespacePartial(
485
- "billing",
486
- {
487
- invoice_summary: {
488
- en: "You have {count, plural, one {1} other {{count}}}",
489
- },
490
- },
491
- multiSpec
492
- );
493
- expect(result.ok).toBe(true);
494
- });
495
-
496
- it("rejects unknown keys in namespace payload", () => {
497
- const result = validateExternalNamespacePartial(
498
- "billing",
499
- { typo_key: { en: "Oops" } },
500
- multiSpec
501
- );
502
- expect(result.ok).toBe(false);
503
- if (!result.ok) {
504
- expect(result.issues[0]?.kind).toBe("unknown_key");
505
- }
506
- });
507
- });
508
-
509
- describe("validateExternalKey (multi)", () => {
510
- const multiSpec: DictionarySpec = {
511
- mode: "multi",
512
- requiredKeys: {
513
- billing: ["invoice_summary"],
514
- },
515
- argsByKey: {
516
- billing: { invoice_summary: { count: "number" } },
517
- },
518
- };
519
-
520
- it("validates a single key in a namespace", () => {
521
- const result = validateExternalKey(
522
- "billing",
523
- "invoice_summary",
524
- { en: "You have {count, plural, one {1} other {{count}}}" },
525
- multiSpec
526
- );
527
- expect(result.ok).toBe(true);
528
- if (result.ok) {
529
- expect((result.data as { invoice_summary: { en: string } }).invoice_summary.en).toContain(
530
- "{count"
531
- );
532
- }
533
- });
534
- });
535
-
536
- describe("validateExternalDictionaryPartial (multi)", () => {
537
- const multiSpec: DictionarySpec = {
538
- mode: "multi",
539
- requiredKeys: {
540
- default: ["welcome"],
541
- billing: ["invoice_summary"],
542
- },
543
- argsByKey: {
544
- default: { welcome: { name: "string" } },
545
- billing: { invoice_summary: { count: "number" } },
546
- },
547
- };
548
-
549
- it("accepts partial namespaces", () => {
550
- const result = validateExternalDictionaryPartial(
551
- {
552
- billing: {
553
- invoice_summary: {
554
- en: "You have {count, plural, one {1} other {{count}}}",
555
- },
556
- },
557
- },
558
- multiSpec
559
- );
560
- expect(result.ok).toBe(true);
561
- });
562
- });