@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.
- package/README.md +1 -2
- package/dist/cli/audit.js +739 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/codegen.js +1471 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/codegen/index.js +43 -40
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +56 -10
- package/dist/index.js.map +1 -1
- package/package.json +6 -9
- package/bin/audit.mjs +0 -14
- package/bin/codegen.mjs +0 -17
- package/bin/setup.mjs +0 -14
- package/src/IcuTranslationProviderMulti.test.ts +0 -215
- package/src/IcuTranslationProviderMulti.ts +0 -176
- package/src/audit/audit-dictionaries.test.ts +0 -134
- package/src/audit/audit-dictionaries.ts +0 -178
- package/src/audit/run-audit.test.ts +0 -169
- package/src/audit/run-audit.ts +0 -94
- package/src/builder-load-registry.ts +0 -39
- package/src/builder-loaders.ts +0 -15
- package/src/builder-types.test.ts +0 -49
- package/src/builder-types.ts +0 -24
- package/src/builder.test.ts +0 -260
- package/src/builder.ts +0 -10
- package/src/codegen/codegen-config-schema.ts +0 -156
- package/src/codegen/config.test.ts +0 -231
- package/src/codegen/config.ts +0 -42
- package/src/codegen/constants.ts +0 -9
- package/src/codegen/delivery-artifacts.test.ts +0 -167
- package/src/codegen/delivery-artifacts.ts +0 -154
- package/src/codegen/dictionary-spec-contract.test.ts +0 -28
- package/src/codegen/dictionary-spec-contract.ts +0 -138
- package/src/codegen/emit/dictionary-schema-file.ts +0 -113
- package/src/codegen/emit/instance-file.test.ts +0 -71
- package/src/codegen/emit/instance-file.ts +0 -257
- package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
- package/src/codegen/emit/namespace-loaders-file.ts +0 -229
- package/src/codegen/emit/types-file.test.ts +0 -96
- package/src/codegen/emit/types-file.ts +0 -142
- package/src/codegen/fixtures/billing-namespace.json +0 -6
- package/src/codegen/generate-i18n-types.test.ts +0 -1137
- package/src/codegen/generate-i18n-types.ts +0 -20
- package/src/codegen/icu-analysis.ts +0 -110
- package/src/codegen/locale-fallback.ts +0 -73
- package/src/codegen/locale-policy.test.ts +0 -35
- package/src/codegen/locale-policy.ts +0 -30
- package/src/codegen/paths.ts +0 -44
- package/src/codegen/project-locales-set-namespace.test.ts +0 -134
- package/src/codegen/read-dictionary.test.ts +0 -594
- package/src/codegen/read-dictionary.ts +0 -260
- package/src/codegen/regenerate-namespaces.ts +0 -180
- package/src/codegen/run-codegen.test.ts +0 -230
- package/src/codegen/run-codegen.ts +0 -252
- package/src/codegen/types.ts +0 -10
- package/src/codegen/write-file-if-changed.test.ts +0 -42
- package/src/codegen/write-file-if-changed.ts +0 -20
- package/src/codegen-config/build-config.ts +0 -15
- package/src/codegen-config/codegen-config.test.ts +0 -36
- package/src/codegen-config/index.ts +0 -37
- package/src/codegen-config/type-names.ts +0 -20
- package/src/codegen-config/write-config.ts +0 -8
- package/src/engine.ts +0 -47
- package/src/fetch-artifact.ts +0 -16
- package/src/format-core.ts +0 -91
- package/src/i18n-handle.ts +0 -202
- package/src/icu/extract-variables.test.ts +0 -199
- package/src/icu/extract-variables.ts +0 -227
- package/src/icu/parse-template.ts +0 -31
- package/src/index.ts +0 -58
- package/src/project-locales.test.ts +0 -280
- package/src/project-locales.ts +0 -211
- package/src/resolve-locale.test.ts +0 -91
- package/src/resolve-locale.ts +0 -88
- package/src/scope-multi.ts +0 -105
- package/src/scope-types.ts +0 -35
- package/src/scope.test.ts +0 -172
- package/src/serialized-state.test.ts +0 -114
- package/src/serialized-state.ts +0 -28
- package/src/setup/setup-i18n.test.ts +0 -85
- package/src/setup/setup-i18n.ts +0 -156
- package/src/types.ts +0 -65
- package/src/validation/create-args-schema.ts +0 -76
- package/src/validation/create-normalized-schema.ts +0 -52
- package/src/validation/errors.ts +0 -43
- package/src/validation/index.ts +0 -202
- package/src/validation/normalize.ts +0 -299
- package/src/validation/to-dictionary.ts +0 -67
- package/src/validation/types.ts +0 -82
- package/src/validation/validate-normalized.ts +0 -133
- 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
|
-
});
|