@xndrjs/i18n 0.6.0 → 0.7.0-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 +154 -134
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +276 -61
- package/dist/index.js +687 -126
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +23 -7
- package/dist/validation/index.js +196 -89
- package/dist/validation/index.js.map +1 -1
- package/package.json +1 -1
- package/src/IcuTranslationProviderMulti.test.ts +115 -215
- package/src/IcuTranslationProviderMulti.ts +107 -96
- package/src/IcuTranslationProviderSingle.test.ts +36 -294
- package/src/IcuTranslationProviderSingle.ts +55 -72
- package/src/builder-load-registry.ts +18 -0
- package/src/builder-loaders.ts +18 -0
- package/src/builder-multi.ts +481 -0
- package/src/builder-types.test.ts +80 -0
- package/src/builder-types.ts +24 -0
- package/src/builder.test.ts +421 -0
- package/src/builder.ts +112 -0
- package/src/codegen/delivery-artifacts.test.ts +2 -1
- package/src/codegen/delivery-artifacts.ts +2 -1
- package/src/codegen/emit/dictionary-file.test.ts +0 -7
- package/src/codegen/emit/dictionary-schema-file.ts +55 -42
- package/src/codegen/emit/instance-file.test.ts +88 -0
- package/src/codegen/emit/instance-file.ts +164 -17
- package/src/codegen/emit/namespace-loaders-file.test.ts +35 -10
- package/src/codegen/emit/namespace-loaders-file.ts +11 -61
- package/src/codegen/emit/types-file.test.ts +0 -2
- package/src/codegen/generate-i18n-types.test.ts +8 -303
- package/src/codegen/generate-i18n-types.ts +16 -1
- package/src/codegen/project-locales-set-namespace.test.ts +25 -32
- package/src/engine.ts +71 -0
- package/src/index.ts +47 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +76 -0
- package/src/project-locales.ts +58 -1
- package/src/scope-multi.ts +124 -0
- package/src/scope-single.ts +85 -0
- package/src/scope-types.ts +27 -0
- package/src/scope.test.ts +481 -0
- package/src/single-builder.ts +153 -0
- package/src/types.ts +16 -0
- package/src/validation/create-normalized-schema.ts +1 -1
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +135 -27
- package/src/validation/normalize.ts +176 -46
- package/src/validation/types.ts +4 -0
- package/src/validation/validate-normalized.ts +43 -1
- package/src/validation/validation.test.ts +200 -7
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
2
|
+
import { createI18nBuilder, createI18nMultiBuilder } from "./builder.js";
|
|
3
|
+
import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
|
|
4
|
+
import { IcuTranslationProviderSingle } from "./IcuTranslationProviderSingle.js";
|
|
5
|
+
|
|
6
|
+
type SingleSchema = {
|
|
7
|
+
login_button: { en: string; it: string };
|
|
8
|
+
welcome: { en: string; it: string };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type SingleParams = {
|
|
12
|
+
login_button: never;
|
|
13
|
+
welcome: { name: string };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type MultiSchema = {
|
|
17
|
+
default: {
|
|
18
|
+
login_button: { en: string; it: string };
|
|
19
|
+
welcome: { en: string };
|
|
20
|
+
};
|
|
21
|
+
billing: {
|
|
22
|
+
invoice_summary: { en: string; it: string };
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type TestMultiParams = {
|
|
27
|
+
default: {
|
|
28
|
+
login_button: never;
|
|
29
|
+
welcome: { name: string };
|
|
30
|
+
};
|
|
31
|
+
billing: {
|
|
32
|
+
invoice_summary: { count: number; name: string };
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TestDeliveryArea = "eu" | "us";
|
|
37
|
+
type TestDeliveryArtifacts = {
|
|
38
|
+
eu: readonly ["it"];
|
|
39
|
+
us: readonly ["en"];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const billingIt = {
|
|
43
|
+
invoice_summary: {
|
|
44
|
+
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const billingEn = {
|
|
49
|
+
invoice_summary: {
|
|
50
|
+
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const defaultIt = {
|
|
55
|
+
login_button: { it: "Accedi" },
|
|
56
|
+
welcome: { it: "Benvenuto {name}!" },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe("I18nBuilder multi", () => {
|
|
60
|
+
it("load() invokes mock loaders, merges into the engine, and returns a bound view", async () => {
|
|
61
|
+
const billingLoader = vi.fn(async (locale: string) => {
|
|
62
|
+
return locale === "it" ? billingIt : billingEn;
|
|
63
|
+
});
|
|
64
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
65
|
+
const builder = createI18nBuilder(engine, {
|
|
66
|
+
namespaceLoaders: {
|
|
67
|
+
billing: billingLoader,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const view = await builder.withNamespaces(["billing"]).withLocale("it").load();
|
|
72
|
+
|
|
73
|
+
expect(billingLoader).toHaveBeenCalledWith("it");
|
|
74
|
+
expect(view.locale).toBe("it");
|
|
75
|
+
expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
|
|
76
|
+
"Hai 2 fatture per Ada"
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("chains withNamespaces and withLocale before load()", async () => {
|
|
81
|
+
const billingLoader = vi.fn(async (locale: string) =>
|
|
82
|
+
locale === "it" ? billingIt : billingEn
|
|
83
|
+
);
|
|
84
|
+
const defaultLoader = vi.fn(async () => defaultIt);
|
|
85
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
86
|
+
|
|
87
|
+
const view = await createI18nMultiBuilder(engine, {
|
|
88
|
+
namespaceLoaders: {
|
|
89
|
+
billing: billingLoader,
|
|
90
|
+
default: defaultLoader,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
.withNamespaces(["billing", "default"])
|
|
94
|
+
.withLocale("it")
|
|
95
|
+
.load();
|
|
96
|
+
|
|
97
|
+
expect(billingLoader).toHaveBeenCalledWith("it");
|
|
98
|
+
expect(defaultLoader).toHaveBeenCalledWith("it");
|
|
99
|
+
expect(view.t("default", "login_button")).toBe("Accedi");
|
|
100
|
+
expect(view.t("billing", "invoice_summary", { count: 1, name: "Bob" })).toBe(
|
|
101
|
+
"Hai 1 fattura per Bob"
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("patches a preloaded key via scope.set() after load()", async () => {
|
|
106
|
+
const billingLoader = vi.fn(async () => billingEn);
|
|
107
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
108
|
+
|
|
109
|
+
const view = await createI18nBuilder(engine, {
|
|
110
|
+
namespaceLoaders: { billing: billingLoader },
|
|
111
|
+
})
|
|
112
|
+
.withNamespaces(["billing"])
|
|
113
|
+
.withLocale("en")
|
|
114
|
+
.load();
|
|
115
|
+
|
|
116
|
+
view.set(
|
|
117
|
+
"billing",
|
|
118
|
+
"invoice_summary",
|
|
119
|
+
"You have {count, plural, one {1 invoice only} other {{count} invoices only}} for {name}"
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(view.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
123
|
+
"You have 1 invoice only for Ada"
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("accumulates engine data across repeated load() calls on the same builder", async () => {
|
|
128
|
+
const billingLoader = vi.fn(async (locale: string) =>
|
|
129
|
+
locale === "it" ? billingIt : billingEn
|
|
130
|
+
);
|
|
131
|
+
const defaultLoader = vi.fn(async () => defaultIt);
|
|
132
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
133
|
+
const builder = createI18nBuilder(engine, {
|
|
134
|
+
namespaceLoaders: {
|
|
135
|
+
billing: billingLoader,
|
|
136
|
+
default: defaultLoader,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const billingView = await builder.withNamespaces(["billing"]).withLocale("it").load();
|
|
141
|
+
const defaultView = await builder.withNamespaces(["default"]).withLocale("it").load();
|
|
142
|
+
|
|
143
|
+
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(defaultLoader).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
146
|
+
"Hai 1 fattura per Ada"
|
|
147
|
+
);
|
|
148
|
+
expect(defaultView.t("default", "login_button")).toBe("Accedi");
|
|
149
|
+
expect(engine.getAll().billing.invoice_summary.it).toBe(billingIt.invoice_summary.it);
|
|
150
|
+
expect(engine.getAll().default.login_button.it).toBe("Accedi");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("returns a view scoped to namespaces declared at load time", async () => {
|
|
154
|
+
const billingLoader = vi.fn(async (locale: string) =>
|
|
155
|
+
locale === "it" ? billingIt : billingEn
|
|
156
|
+
);
|
|
157
|
+
const defaultLoader = vi.fn(async () => defaultIt);
|
|
158
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
159
|
+
const builder = createI18nBuilder(engine, {
|
|
160
|
+
namespaceLoaders: {
|
|
161
|
+
billing: billingLoader,
|
|
162
|
+
default: defaultLoader,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const billingView = await builder.withNamespaces(["billing"]).withLocale("it").load();
|
|
167
|
+
await builder.withNamespaces(["default"]).withLocale("it").load();
|
|
168
|
+
|
|
169
|
+
expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
170
|
+
"Hai 1 fattura per Ada"
|
|
171
|
+
);
|
|
172
|
+
// billingView is typed for the billing namespace only; default is excluded at compile time.
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("loads via withDeliveryArea for custom delivery partitions", async () => {
|
|
176
|
+
const billingLoader = vi.fn(async (area: string) => {
|
|
177
|
+
return area === "eu" ? billingIt : billingEn;
|
|
178
|
+
});
|
|
179
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
180
|
+
|
|
181
|
+
const euView = await createI18nMultiBuilder<
|
|
182
|
+
MultiSchema,
|
|
183
|
+
TestMultiParams,
|
|
184
|
+
"en" | "it",
|
|
185
|
+
TestDeliveryArea,
|
|
186
|
+
TestDeliveryArtifacts
|
|
187
|
+
>(engine, {
|
|
188
|
+
namespaceLoaders: {
|
|
189
|
+
billing: billingLoader,
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
.withNamespaces(["billing"])
|
|
193
|
+
.withDeliveryArea("eu")
|
|
194
|
+
.load();
|
|
195
|
+
|
|
196
|
+
expect(billingLoader).toHaveBeenCalledWith("eu");
|
|
197
|
+
expect(euView.t("billing", "invoice_summary", "it", { count: 2, name: "Ada" })).toBe(
|
|
198
|
+
"Hai 2 fatture per Ada"
|
|
199
|
+
);
|
|
200
|
+
expectTypeOf(euView.t).toBeCallableWith("billing", "invoice_summary", "it", {
|
|
201
|
+
count: 2,
|
|
202
|
+
name: "Ada",
|
|
203
|
+
});
|
|
204
|
+
expectTypeOf(euView.forLocale).toBeCallableWith("it");
|
|
205
|
+
expectTypeOf(euView.forLocale).parameters.not.toMatchTypeOf<["en-US"]>();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("loads canonical namespace loaders without a partition", async () => {
|
|
209
|
+
const billingLoader = vi.fn(async () => ({
|
|
210
|
+
invoice_summary: {
|
|
211
|
+
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
212
|
+
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
216
|
+
|
|
217
|
+
const view = await createI18nBuilder(engine, {
|
|
218
|
+
namespaceLoaders: {
|
|
219
|
+
billing: billingLoader,
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
.withNamespaces(["billing"])
|
|
223
|
+
.load();
|
|
224
|
+
|
|
225
|
+
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(billingLoader.mock.calls[0]).toEqual([]);
|
|
227
|
+
expect(view.t("billing", "invoice_summary", "en", { count: 1, name: "Ada" })).toBe(
|
|
228
|
+
"You have 1 invoice for Ada"
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("skips a repeated load for the same namespace partition", async () => {
|
|
233
|
+
const billingLoader = vi.fn(async (locale: string) =>
|
|
234
|
+
locale === "it" ? billingIt : billingEn
|
|
235
|
+
);
|
|
236
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
237
|
+
const options = { namespaceLoaders: { billing: billingLoader } };
|
|
238
|
+
const chain = () =>
|
|
239
|
+
createI18nBuilder(engine, options).withNamespaces(["billing"]).withLocale("it");
|
|
240
|
+
|
|
241
|
+
await chain().load();
|
|
242
|
+
await chain().load();
|
|
243
|
+
|
|
244
|
+
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("returns a destructuring-safe locale-bound scope from load()", async () => {
|
|
248
|
+
const billingLoader = vi.fn(async () => billingEn);
|
|
249
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
250
|
+
const { t } = await createI18nBuilder(engine, {
|
|
251
|
+
namespaceLoaders: { billing: billingLoader },
|
|
252
|
+
})
|
|
253
|
+
.withNamespaces(["billing"])
|
|
254
|
+
.withLocale("en")
|
|
255
|
+
.load();
|
|
256
|
+
|
|
257
|
+
expect(t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
258
|
+
"You have 1 invoice for Ada"
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("preserves runtime patches when a later builder reloads the same resource", async () => {
|
|
263
|
+
const billingLoader = vi.fn(async () => billingEn);
|
|
264
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
265
|
+
const options = { namespaceLoaders: { billing: billingLoader } };
|
|
266
|
+
|
|
267
|
+
const firstView = await createI18nBuilder(engine, options)
|
|
268
|
+
.withNamespaces(["billing"])
|
|
269
|
+
.withLocale("en")
|
|
270
|
+
.load();
|
|
271
|
+
|
|
272
|
+
firstView.set(
|
|
273
|
+
"billing",
|
|
274
|
+
"invoice_summary",
|
|
275
|
+
"You have {count, plural, one {1 invoice only} other {{count} invoices only}} for {name}"
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const secondView = await createI18nBuilder(engine, options)
|
|
279
|
+
.withNamespaces(["billing"])
|
|
280
|
+
.withLocale("en")
|
|
281
|
+
.load();
|
|
282
|
+
|
|
283
|
+
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(secondView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
285
|
+
"You have 1 invoice only for Ada"
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("still loads a different partition for the same namespace", async () => {
|
|
290
|
+
const billingLoader = vi.fn(async (locale: string) =>
|
|
291
|
+
locale === "it" ? billingIt : billingEn
|
|
292
|
+
);
|
|
293
|
+
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
294
|
+
const builder = createI18nBuilder(engine, {
|
|
295
|
+
namespaceLoaders: { billing: billingLoader },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await builder.withNamespaces(["billing"]).withLocale("it").load();
|
|
299
|
+
await builder.withNamespaces(["billing"]).withLocale("en").load();
|
|
300
|
+
|
|
301
|
+
expect(billingLoader).toHaveBeenCalledTimes(2);
|
|
302
|
+
expect(billingLoader).toHaveBeenNthCalledWith(1, "it");
|
|
303
|
+
expect(billingLoader).toHaveBeenNthCalledWith(2, "en");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("I18nBuilder single", () => {
|
|
308
|
+
it("load() invokes a locale loader and returns a bound view", async () => {
|
|
309
|
+
const dictionaryLoader = vi.fn(async (locale: string) => {
|
|
310
|
+
return locale === "it"
|
|
311
|
+
? {
|
|
312
|
+
login_button: { it: "Accedi" },
|
|
313
|
+
welcome: { it: "Benvenuto {name}!" },
|
|
314
|
+
}
|
|
315
|
+
: {
|
|
316
|
+
login_button: { en: "Login" },
|
|
317
|
+
welcome: { en: "Welcome {name}!" },
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
const engine = new IcuTranslationProviderSingle<SingleSchema, SingleParams>({
|
|
321
|
+
login_button: { en: "", it: "" },
|
|
322
|
+
welcome: { en: "", it: "" },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const view = await createI18nBuilder<SingleSchema, SingleParams>(engine, { dictionaryLoader })
|
|
326
|
+
.withLocale("it")
|
|
327
|
+
.load();
|
|
328
|
+
|
|
329
|
+
expect(dictionaryLoader).toHaveBeenCalledWith("it");
|
|
330
|
+
expect(view.locale).toBe("it");
|
|
331
|
+
expect(view.t("login_button")).toBe("Accedi");
|
|
332
|
+
expect(view.t("welcome", { name: "Ada" })).toBe("Benvenuto Ada!");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("patches a preloaded key via scope.set() after load()", async () => {
|
|
336
|
+
const dictionaryLoader = vi.fn(async () => ({
|
|
337
|
+
login_button: { en: "Login" },
|
|
338
|
+
welcome: { en: "Welcome {name}!" },
|
|
339
|
+
}));
|
|
340
|
+
const engine = new IcuTranslationProviderSingle<SingleSchema, SingleParams>({
|
|
341
|
+
login_button: { en: "", it: "" },
|
|
342
|
+
welcome: { en: "", it: "" },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const view = await createI18nBuilder<SingleSchema, SingleParams>(engine, { dictionaryLoader })
|
|
346
|
+
.withLocale("en")
|
|
347
|
+
.load();
|
|
348
|
+
|
|
349
|
+
view.set("login_button", "Sign in");
|
|
350
|
+
|
|
351
|
+
expect(view.t("login_button")).toBe("Sign in");
|
|
352
|
+
expect(view.t("welcome", { name: "Ada" })).toBe("Welcome Ada!");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("throws when scope.set() targets a key that was not preloaded", async () => {
|
|
356
|
+
const dictionaryLoader = vi.fn(async () => ({
|
|
357
|
+
login_button: { en: "Login" },
|
|
358
|
+
welcome: { en: "Welcome {name}!" },
|
|
359
|
+
}));
|
|
360
|
+
const engine = new IcuTranslationProviderSingle<SingleSchema, SingleParams>({
|
|
361
|
+
// @ts-expect-error
|
|
362
|
+
login_button: { en: "" },
|
|
363
|
+
// @ts-expect-error
|
|
364
|
+
welcome: { en: "" },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const view = await createI18nBuilder<SingleSchema, SingleParams>(engine, { dictionaryLoader })
|
|
368
|
+
.withLocale("en")
|
|
369
|
+
.load();
|
|
370
|
+
|
|
371
|
+
// @ts-expect-error
|
|
372
|
+
expect(() => view.forLocale("it").set("login_button", "Accedi")).toThrow(
|
|
373
|
+
"[i18n] Key not preloaded: login_button (it)"
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("skips a repeated load for the same locale partition", async () => {
|
|
378
|
+
const dictionaryLoader = vi.fn(async () => ({
|
|
379
|
+
login_button: { en: "Login" },
|
|
380
|
+
welcome: { en: "Welcome {name}!" },
|
|
381
|
+
}));
|
|
382
|
+
const engine = new IcuTranslationProviderSingle<SingleSchema, SingleParams>({
|
|
383
|
+
login_button: { en: "", it: "" },
|
|
384
|
+
welcome: { en: "", it: "" },
|
|
385
|
+
});
|
|
386
|
+
const options = { dictionaryLoader };
|
|
387
|
+
const chain = () =>
|
|
388
|
+
createI18nBuilder<SingleSchema, SingleParams>(engine, options).withLocale("en");
|
|
389
|
+
|
|
390
|
+
await chain().load();
|
|
391
|
+
await chain().load();
|
|
392
|
+
|
|
393
|
+
expect(dictionaryLoader).toHaveBeenCalledTimes(1);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("preserves runtime patches when a later builder reloads the same resource", async () => {
|
|
397
|
+
const dictionaryLoader = vi.fn(async () => ({
|
|
398
|
+
login_button: { en: "Login" },
|
|
399
|
+
welcome: { en: "Welcome {name}!" },
|
|
400
|
+
}));
|
|
401
|
+
const engine = new IcuTranslationProviderSingle<SingleSchema, SingleParams>({
|
|
402
|
+
login_button: { en: "", it: "" },
|
|
403
|
+
welcome: { en: "", it: "" },
|
|
404
|
+
});
|
|
405
|
+
const options = { dictionaryLoader };
|
|
406
|
+
|
|
407
|
+
const firstView = await createI18nBuilder<SingleSchema, SingleParams>(engine, options)
|
|
408
|
+
.withLocale("en")
|
|
409
|
+
.load();
|
|
410
|
+
|
|
411
|
+
firstView.set("login_button", "Sign in");
|
|
412
|
+
|
|
413
|
+
const secondView = await createI18nBuilder<SingleSchema, SingleParams>(engine, options)
|
|
414
|
+
.withLocale("en")
|
|
415
|
+
.load();
|
|
416
|
+
|
|
417
|
+
expect(dictionaryLoader).toHaveBeenCalledTimes(1);
|
|
418
|
+
expect(secondView.t("login_button")).toBe("Sign in");
|
|
419
|
+
expect(secondView.t("welcome", { name: "Ada" })).toBe("Welcome Ada!");
|
|
420
|
+
});
|
|
421
|
+
});
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { DeliveryArtifactsMap } from "./builder-types.js";
|
|
2
|
+
import { I18nBuilderMultiInitialImpl, type I18nBuilderMultiOptions } from "./builder-multi.js";
|
|
3
|
+
import { I18nBuilderSingleImpl, type I18nBuilderSingleOptions } from "./single-builder.js";
|
|
4
|
+
import type { I18nEngineMultiImpl, I18nEngineSingleImpl } from "./engine.js";
|
|
5
|
+
import type { KeyDictionary, LocaleOfMulti, LocaleOfSingle, MultiDictionary } from "./types.js";
|
|
6
|
+
import type { MultiParams } from "./scope-types.js";
|
|
7
|
+
|
|
8
|
+
export type { CanonicalLoader, NamespaceLoader, PartitionedLoader } from "./builder-loaders.js";
|
|
9
|
+
export { invokeNamespaceLoader } from "./builder-loaders.js";
|
|
10
|
+
export type {
|
|
11
|
+
I18nBuilderMulti,
|
|
12
|
+
I18nBuilderMultiForLocale,
|
|
13
|
+
I18nBuilderMultiInitial,
|
|
14
|
+
I18nBuilderMultiOptions,
|
|
15
|
+
I18nBuilderMultiPartitioned,
|
|
16
|
+
I18nBuilderMultiReady,
|
|
17
|
+
} from "./builder-multi.js";
|
|
18
|
+
export type {
|
|
19
|
+
I18nBuilderSingle,
|
|
20
|
+
I18nBuilderSingleForLocale,
|
|
21
|
+
I18nBuilderSingleOptions,
|
|
22
|
+
} from "./single-builder.js";
|
|
23
|
+
|
|
24
|
+
type AnySingleEngine = I18nEngineSingleImpl<KeyDictionary, { [K in keyof KeyDictionary]: unknown }>;
|
|
25
|
+
type AnyMultiEngine = I18nEngineMultiImpl<MultiDictionary, MultiParams<MultiDictionary>>;
|
|
26
|
+
|
|
27
|
+
function isMultiEngine(engine: AnySingleEngine | AnyMultiEngine): engine is AnyMultiEngine {
|
|
28
|
+
return engine.__i18nEngineMode === "multi";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createI18nSingleBuilder<
|
|
32
|
+
Schema extends KeyDictionary,
|
|
33
|
+
Params extends { [K in keyof Schema]: unknown },
|
|
34
|
+
RequestLocales extends string = LocaleOfSingle<Schema>,
|
|
35
|
+
>(
|
|
36
|
+
engine: I18nEngineSingleImpl<Schema, Params, RequestLocales>,
|
|
37
|
+
options?: I18nBuilderSingleOptions<Schema, RequestLocales>
|
|
38
|
+
): I18nBuilderSingleImpl<Schema, Params, RequestLocales> {
|
|
39
|
+
return new I18nBuilderSingleImpl(engine, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createI18nMultiBuilder<
|
|
43
|
+
Schema extends MultiDictionary,
|
|
44
|
+
Params extends MultiParams<Schema>,
|
|
45
|
+
RequestLocales extends string = LocaleOfMulti<Schema>,
|
|
46
|
+
DeliveryArea extends string = never,
|
|
47
|
+
DeliveryArtifacts extends DeliveryArtifactsMap<RequestLocales, DeliveryArea> =
|
|
48
|
+
DeliveryArtifactsMap<RequestLocales, DeliveryArea>,
|
|
49
|
+
>(
|
|
50
|
+
engine: I18nEngineMultiImpl<Schema, Params, RequestLocales>,
|
|
51
|
+
options?: I18nBuilderMultiOptions<Schema, RequestLocales>
|
|
52
|
+
): I18nBuilderMultiInitialImpl<
|
|
53
|
+
Schema,
|
|
54
|
+
Params,
|
|
55
|
+
RequestLocales,
|
|
56
|
+
RequestLocales,
|
|
57
|
+
DeliveryArea,
|
|
58
|
+
DeliveryArtifacts
|
|
59
|
+
> {
|
|
60
|
+
return new I18nBuilderMultiInitialImpl<
|
|
61
|
+
Schema,
|
|
62
|
+
Params,
|
|
63
|
+
RequestLocales,
|
|
64
|
+
RequestLocales,
|
|
65
|
+
DeliveryArea,
|
|
66
|
+
DeliveryArtifacts
|
|
67
|
+
>(engine, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createI18nBuilder<
|
|
71
|
+
Schema extends MultiDictionary,
|
|
72
|
+
Params extends MultiParams<Schema>,
|
|
73
|
+
RequestLocales extends string = LocaleOfMulti<Schema>,
|
|
74
|
+
>(
|
|
75
|
+
engine: I18nEngineMultiImpl<Schema, Params, RequestLocales>,
|
|
76
|
+
options?: I18nBuilderMultiOptions<Schema, RequestLocales>
|
|
77
|
+
): I18nBuilderMultiInitialImpl<
|
|
78
|
+
Schema,
|
|
79
|
+
Params,
|
|
80
|
+
RequestLocales,
|
|
81
|
+
RequestLocales,
|
|
82
|
+
never,
|
|
83
|
+
DeliveryArtifactsMap<RequestLocales, never>
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
export function createI18nBuilder<
|
|
87
|
+
Schema extends KeyDictionary,
|
|
88
|
+
Params extends { [K in keyof Schema]: unknown },
|
|
89
|
+
RequestLocales extends string = LocaleOfSingle<Schema>,
|
|
90
|
+
>(
|
|
91
|
+
engine: I18nEngineSingleImpl<Schema, Params, RequestLocales>,
|
|
92
|
+
options?: I18nBuilderSingleOptions<Schema, RequestLocales>
|
|
93
|
+
): I18nBuilderSingleImpl<Schema, Params, RequestLocales>;
|
|
94
|
+
|
|
95
|
+
export function createI18nBuilder(
|
|
96
|
+
engine: AnySingleEngine | AnyMultiEngine,
|
|
97
|
+
options?:
|
|
98
|
+
| I18nBuilderMultiOptions<MultiDictionary, string>
|
|
99
|
+
| I18nBuilderSingleOptions<KeyDictionary, string>
|
|
100
|
+
): never {
|
|
101
|
+
if (isMultiEngine(engine)) {
|
|
102
|
+
return createI18nMultiBuilder(
|
|
103
|
+
engine,
|
|
104
|
+
options as I18nBuilderMultiOptions<MultiDictionary, string> | undefined
|
|
105
|
+
) as never;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return createI18nSingleBuilder(
|
|
109
|
+
engine,
|
|
110
|
+
options as I18nBuilderSingleOptions<KeyDictionary, string> | undefined
|
|
111
|
+
) as never;
|
|
112
|
+
}
|
|
@@ -160,7 +160,8 @@ describe("delivery-artifacts", () => {
|
|
|
160
160
|
' "eu": ["fr", "it"] as const,\n' +
|
|
161
161
|
' "us": ["en-US"] as const,\n' +
|
|
162
162
|
"} as const satisfies Record<AppDeliveryArea, readonly AppLocale[]>;\n\n" +
|
|
163
|
-
"export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;\n\n"
|
|
163
|
+
"export type AppDeliveryArtifacts = typeof DELIVERY_ARTIFACTS;\n\n" +
|
|
164
|
+
"export type LocalesForDeliveryArea<A extends AppDeliveryArea> = AppDeliveryArtifacts[A][number];\n\n"
|
|
164
165
|
);
|
|
165
166
|
});
|
|
166
167
|
});
|
|
@@ -148,6 +148,7 @@ export function formatDeliveryArtifactsBlock(
|
|
|
148
148
|
|
|
149
149
|
return (
|
|
150
150
|
`export const ${constName} = {\n${areaEntries}\n} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;\n\n` +
|
|
151
|
-
`export type ${deliveryArtifactsTypeName} = typeof ${constName};\n\n`
|
|
151
|
+
`export type ${deliveryArtifactsTypeName} = typeof ${constName};\n\n` +
|
|
152
|
+
`export type LocalesForDeliveryArea<A extends ${deliveryAreaTypeName}> = ${deliveryArtifactsTypeName}[A][number];\n\n`
|
|
152
153
|
);
|
|
153
154
|
}
|
|
@@ -27,7 +27,6 @@ describe("formatDictionaryFile", () => {
|
|
|
27
27
|
|
|
28
28
|
expect(output).toContain("export const defaultDictionary: InitialSchema");
|
|
29
29
|
expect(output).toContain("defaultNs");
|
|
30
|
-
expect(output).not.toContain("defaultDictionaryFor");
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
it("emits an empty module when all namespaces are lazy in split mode (multi)", () => {
|
|
@@ -100,8 +99,6 @@ describe("formatDictionaryFile", () => {
|
|
|
100
99
|
"export function defaultDictionaryFor(locale: AppLocale): InitialSchema"
|
|
101
100
|
);
|
|
102
101
|
expect(output).toContain("return {\n default: defaultByLocale[locale],\n };");
|
|
103
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
104
|
-
expect(output).not.toContain("billingEn");
|
|
105
102
|
});
|
|
106
103
|
|
|
107
104
|
it("emits defaultDictionaryFor with per-locale imports in split mode (single)", () => {
|
|
@@ -136,7 +133,6 @@ describe("formatDictionaryFile", () => {
|
|
|
136
133
|
);
|
|
137
134
|
expect(output).toContain("export function defaultDictionaryFor(locale: AppLocale): AppSchema");
|
|
138
135
|
expect(output).toContain("return translationsByLocale[locale];");
|
|
139
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
140
136
|
});
|
|
141
137
|
|
|
142
138
|
it("emits defaultDictionaryFor with per-area imports in custom mode (multi)", () => {
|
|
@@ -178,8 +174,6 @@ describe("formatDictionaryFile", () => {
|
|
|
178
174
|
"export function defaultDictionaryFor(area: AppDeliveryArea): InitialSchema"
|
|
179
175
|
);
|
|
180
176
|
expect(output).toContain("return {\n default: defaultByArea[area],\n };");
|
|
181
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
182
|
-
expect(output).not.toContain("billingEu");
|
|
183
177
|
});
|
|
184
178
|
|
|
185
179
|
it("emits defaultDictionaryFor with per-area imports in custom mode (single)", () => {
|
|
@@ -217,6 +211,5 @@ describe("formatDictionaryFile", () => {
|
|
|
217
211
|
"export function defaultDictionaryFor(area: AppDeliveryArea): AppSchema"
|
|
218
212
|
);
|
|
219
213
|
expect(output).toContain("return translationsByArea[area];");
|
|
220
|
-
expect(output).not.toContain("export const defaultDictionary");
|
|
221
214
|
});
|
|
222
215
|
});
|