@xndrjs/i18n 0.8.1 → 0.8.2-alpha.1

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,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
- });