@xndrjs/i18n 0.7.0 → 0.8.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 (69) hide show
  1. package/README.md +67 -872
  2. package/dist/codegen/index.d.ts +117 -51
  3. package/dist/codegen/index.js +1585 -93
  4. package/dist/codegen/index.js.map +1 -1
  5. package/dist/index.d.ts +100 -234
  6. package/dist/index.js +74 -638
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +5 -0
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/IcuTranslationProviderMulti.test.ts +18 -40
  12. package/src/IcuTranslationProviderMulti.ts +27 -37
  13. package/src/audit/audit-dictionaries.ts +1 -1
  14. package/src/audit/run-audit.test.ts +6 -7
  15. package/src/builder-load-registry.ts +26 -5
  16. package/src/builder-loaders.ts +8 -11
  17. package/src/builder-types.test.ts +11 -42
  18. package/src/builder.test.ts +82 -243
  19. package/src/builder.ts +8 -110
  20. package/src/codegen/codegen-config-schema.ts +82 -77
  21. package/src/codegen/config.test.ts +55 -143
  22. package/src/codegen/config.ts +10 -67
  23. package/src/codegen/dictionary-spec-contract.test.ts +28 -0
  24. package/src/codegen/dictionary-spec-contract.ts +138 -0
  25. package/src/codegen/emit/dictionary-schema-file.ts +7 -43
  26. package/src/codegen/emit/instance-file.test.ts +35 -52
  27. package/src/codegen/emit/instance-file.ts +170 -268
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
  29. package/src/codegen/emit/namespace-loaders-file.ts +147 -90
  30. package/src/codegen/emit/types-file.test.ts +20 -41
  31. package/src/codegen/emit/types-file.ts +48 -74
  32. package/src/codegen/generate-i18n-types.test.ts +163 -492
  33. package/src/codegen/generate-i18n-types.ts +7 -269
  34. package/src/codegen/paths.ts +0 -14
  35. package/src/codegen/project-locales-set-namespace.test.ts +44 -86
  36. package/src/codegen/read-dictionary.test.ts +27 -9
  37. package/src/codegen/read-dictionary.ts +18 -40
  38. package/src/codegen/regenerate-namespaces.ts +180 -0
  39. package/src/codegen/run-codegen.test.ts +230 -0
  40. package/src/codegen/run-codegen.ts +252 -0
  41. package/src/codegen/types.ts +0 -6
  42. package/src/codegen-config/build-config.ts +4 -23
  43. package/src/codegen-config/codegen-config.test.ts +10 -6
  44. package/src/codegen-config/index.ts +18 -3
  45. package/src/engine.ts +3 -27
  46. package/src/fetch-artifact.ts +16 -0
  47. package/src/i18n-handle.ts +202 -0
  48. package/src/index.ts +15 -34
  49. package/src/project-locales.ts +2 -2
  50. package/src/scope-multi.ts +1 -20
  51. package/src/scope-types.ts +14 -6
  52. package/src/scope.test.ts +3 -312
  53. package/src/serialized-state.test.ts +114 -0
  54. package/src/serialized-state.ts +28 -0
  55. package/src/setup/setup-i18n.test.ts +22 -33
  56. package/src/setup/setup-i18n.ts +22 -27
  57. package/src/types.ts +3 -3
  58. package/src/validation/index.ts +4 -0
  59. package/src/IcuTranslationProviderSingle.test.ts +0 -177
  60. package/src/IcuTranslationProviderSingle.ts +0 -114
  61. package/src/builder-multi.ts +0 -481
  62. package/src/codegen/emit/dictionary-file.test.ts +0 -215
  63. package/src/codegen/emit/dictionary-file.ts +0 -244
  64. package/src/deep-freeze.test.ts +0 -40
  65. package/src/deep-freeze.ts +0 -22
  66. package/src/patch-key.test.ts +0 -186
  67. package/src/patch-key.ts +0 -140
  68. package/src/scope-single.ts +0 -85
  69. package/src/single-builder.ts +0 -153
@@ -1,8 +1,9 @@
1
1
  import { describe, expectTypeOf, it } from "vitest";
2
- import { createI18nMultiBuilder } from "./builder.js";
3
- import type { I18nBuilderMultiInitialImpl, I18nBuilderMultiReadyImpl } from "./builder-multi.js";
2
+ import { createI18nHandle } from "./builder.js";
3
+ import type { I18nHandle } from "./i18n-handle.js";
4
4
  import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
5
5
  import type { LocalesForDeliveryArea, LocalesForDeliveryAreaOrAll } from "./builder-types.js";
6
+ import type { PartialMultiDictionary } from "./types.js";
6
7
 
7
8
  type TypestateSchema = {
8
9
  billing: { invoice_summary: { en: string; it: string } };
@@ -12,26 +13,6 @@ type TypestateParams = {
12
13
  billing: { invoice_summary: { count: number } };
13
14
  };
14
15
 
15
- function assertBuilderTypestate(): void {
16
- const engine = new IcuTranslationProviderMulti<TypestateSchema, TypestateParams>({});
17
- const initial = createI18nMultiBuilder(engine);
18
-
19
- // @ts-expect-error withLocale requires withNamespaces first
20
- initial.withLocale("en");
21
- // @ts-expect-error load requires withNamespaces first
22
- void initial.load();
23
- // @ts-expect-error withDeliveryArea requires withNamespaces first
24
- initial.withDeliveryArea("eu");
25
- // @ts-expect-error empty namespace list is rejected
26
- initial.withNamespaces([]);
27
-
28
- const ready = initial.withNamespaces(["billing"]);
29
- ready.withLocale("en");
30
- void ready.load();
31
- }
32
-
33
- void assertBuilderTypestate;
34
-
35
16
  describe("builder-types", () => {
36
17
  type ProjectLocale = "en" | "en-US" | "it" | "fr";
37
18
  type DeliveryArea = "eu" | "amer";
@@ -55,26 +36,14 @@ describe("builder-types", () => {
55
36
  });
56
37
  });
57
38
 
58
- describe("I18nBuilderMulti typestate", () => {
59
- it("initial builder only exposes withNamespaces", () => {
60
- const engine = new IcuTranslationProviderMulti<TypestateSchema, TypestateParams>({});
61
- const initial = createI18nMultiBuilder(engine);
62
- expectTypeOf(initial).toEqualTypeOf<
63
- I18nBuilderMultiInitialImpl<TypestateSchema, TypestateParams>
64
- >();
65
- });
66
-
67
- it("ready builder exposes partition and load", () => {
39
+ describe("I18nHandle typestate", () => {
40
+ it("exposes load / peek / serialize", () => {
68
41
  const engine = new IcuTranslationProviderMulti<TypestateSchema, TypestateParams>({});
69
- const ready = createI18nMultiBuilder(engine).withNamespaces(["billing"]);
70
- expectTypeOf(ready).toEqualTypeOf<
71
- I18nBuilderMultiReadyImpl<
72
- TypestateSchema,
73
- TypestateParams,
74
- "en" | "it",
75
- "en" | "it",
76
- readonly ["billing"]
77
- >
78
- >();
42
+ const handle = createI18nHandle(engine);
43
+ expectTypeOf(handle).toMatchTypeOf<I18nHandle<TypestateSchema, TypestateParams>>();
44
+ expectTypeOf(handle.serialize).returns.toEqualTypeOf<{
45
+ dictionary: PartialMultiDictionary<TypestateSchema>;
46
+ resources: readonly (readonly [string, string])[];
47
+ }>();
79
48
  });
80
49
  });
@@ -1,17 +1,6 @@
1
1
  import { describe, expect, expectTypeOf, it, vi } from "vitest";
2
- import { createI18nBuilder, createI18nMultiBuilder } from "./builder.js";
2
+ import { createI18nHandle } from "./builder.js";
3
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
4
 
16
5
  type MultiSchema = {
17
6
  default: {
@@ -33,12 +22,6 @@ type TestMultiParams = {
33
22
  };
34
23
  };
35
24
 
36
- type TestDeliveryArea = "eu" | "us";
37
- type TestDeliveryArtifacts = {
38
- eu: readonly ["it"];
39
- us: readonly ["en"];
40
- };
41
-
42
25
  const billingIt = {
43
26
  invoice_summary: {
44
27
  it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
@@ -56,89 +39,64 @@ const defaultIt = {
56
39
  welcome: { it: "Benvenuto {name}!" },
57
40
  };
58
41
 
59
- describe("I18nBuilder multi", () => {
42
+ describe("I18nHandle", () => {
60
43
  it("load() invokes mock loaders, merges into the engine, and returns a bound view", async () => {
61
44
  const billingLoader = vi.fn(async (locale: string) => {
62
45
  return locale === "it" ? billingIt : billingEn;
63
46
  });
64
47
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
65
- const builder = createI18nBuilder(engine, {
48
+ const handle = createI18nHandle(engine, {
66
49
  namespaceLoaders: {
67
50
  billing: billingLoader,
68
51
  },
69
52
  });
70
53
 
71
- const view = await builder.withNamespaces(["billing"]).withLocale("it").load();
54
+ const view = await handle.load({ namespaces: ["billing"], locale: "it" });
72
55
 
73
- expect(billingLoader).toHaveBeenCalledWith("it");
56
+ expect(billingLoader).toHaveBeenCalledWith("it", { locale: "it" });
74
57
  expect(view.locale).toBe("it");
75
58
  expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
76
59
  "Hai 2 fatture per Ada"
77
60
  );
78
61
  });
79
62
 
80
- it("chains withNamespaces and withLocale before load()", async () => {
63
+ it("loads multiple namespaces in one call", async () => {
81
64
  const billingLoader = vi.fn(async (locale: string) =>
82
65
  locale === "it" ? billingIt : billingEn
83
66
  );
84
67
  const defaultLoader = vi.fn(async () => defaultIt);
85
68
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
86
69
 
87
- const view = await createI18nMultiBuilder(engine, {
70
+ const view = await createI18nHandle(engine, {
88
71
  namespaceLoaders: {
89
72
  billing: billingLoader,
90
73
  default: defaultLoader,
91
74
  },
92
- })
93
- .withNamespaces(["billing", "default"])
94
- .withLocale("it")
95
- .load();
75
+ }).load({ namespaces: ["billing", "default"], locale: "it" });
96
76
 
97
- expect(billingLoader).toHaveBeenCalledWith("it");
98
- expect(defaultLoader).toHaveBeenCalledWith("it");
77
+ expect(billingLoader).toHaveBeenCalledWith("it", { locale: "it" });
78
+ expect(defaultLoader).toHaveBeenCalledWith("it", { locale: "it" });
99
79
  expect(view.t("default", "login_button")).toBe("Accedi");
100
80
  expect(view.t("billing", "invoice_summary", { count: 1, name: "Bob" })).toBe(
101
81
  "Hai 1 fattura per Bob"
102
82
  );
103
83
  });
104
84
 
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 () => {
85
+ it("accumulates engine data across repeated load() calls on the same handle", async () => {
128
86
  const billingLoader = vi.fn(async (locale: string) =>
129
87
  locale === "it" ? billingIt : billingEn
130
88
  );
131
89
  const defaultLoader = vi.fn(async () => defaultIt);
132
90
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
133
- const builder = createI18nBuilder(engine, {
91
+ const handle = createI18nHandle(engine, {
134
92
  namespaceLoaders: {
135
93
  billing: billingLoader,
136
94
  default: defaultLoader,
137
95
  },
138
96
  });
139
97
 
140
- const billingView = await builder.withNamespaces(["billing"]).withLocale("it").load();
141
- const defaultView = await builder.withNamespaces(["default"]).withLocale("it").load();
98
+ const billingView = await handle.load({ namespaces: ["billing"], locale: "it" });
99
+ const defaultView = await handle.load({ namespaces: ["default"], locale: "it" });
142
100
 
143
101
  expect(billingLoader).toHaveBeenCalledTimes(1);
144
102
  expect(defaultLoader).toHaveBeenCalledTimes(1);
@@ -146,8 +104,8 @@ describe("I18nBuilder multi", () => {
146
104
  "Hai 1 fattura per Ada"
147
105
  );
148
106
  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");
107
+ expect(engine.getAll().billing?.invoice_summary?.it).toBe(billingIt.invoice_summary.it);
108
+ expect(engine.getAll().default?.login_button?.it).toBe("Accedi");
151
109
  });
152
110
 
153
111
  it("returns a view scoped to namespaces declared at load time", async () => {
@@ -156,15 +114,15 @@ describe("I18nBuilder multi", () => {
156
114
  );
157
115
  const defaultLoader = vi.fn(async () => defaultIt);
158
116
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
159
- const builder = createI18nBuilder(engine, {
117
+ const handle = createI18nHandle(engine, {
160
118
  namespaceLoaders: {
161
119
  billing: billingLoader,
162
120
  default: defaultLoader,
163
121
  },
164
122
  });
165
123
 
166
- const billingView = await builder.withNamespaces(["billing"]).withLocale("it").load();
167
- await builder.withNamespaces(["default"]).withLocale("it").load();
124
+ const billingView = await handle.load({ namespaces: ["billing"], locale: "it" });
125
+ await handle.load({ namespaces: ["default"], locale: "it" });
168
126
 
169
127
  expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
170
128
  "Hai 1 fattura per Ada"
@@ -172,74 +130,84 @@ describe("I18nBuilder multi", () => {
172
130
  // billingView is typed for the billing namespace only; default is excluded at compile time.
173
131
  });
174
132
 
175
- it("loads via withDeliveryArea for custom delivery partitions", async () => {
133
+ it("maps locale to delivery partition via partitionForLocale", async () => {
176
134
  const billingLoader = vi.fn(async (area: string) => {
177
135
  return area === "eu" ? billingIt : billingEn;
178
136
  });
179
137
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
180
138
 
181
- const euView = await createI18nMultiBuilder<
182
- MultiSchema,
183
- TestMultiParams,
184
- "en" | "it",
185
- TestDeliveryArea,
186
- TestDeliveryArtifacts
187
- >(engine, {
139
+ const view = await createI18nHandle(engine, {
188
140
  namespaceLoaders: {
189
141
  billing: billingLoader,
190
142
  },
191
- })
192
- .withNamespaces(["billing"])
193
- .withDeliveryArea("eu")
194
- .load();
143
+ partitionForLocale: (locale) => (locale === "it" ? "eu" : "us"),
144
+ }).load({ namespaces: ["billing"], locale: "it" });
195
145
 
196
- expect(billingLoader).toHaveBeenCalledWith("eu");
197
- expect(euView.t("billing", "invoice_summary", "it", { count: 2, name: "Ada" })).toBe(
146
+ expect(billingLoader).toHaveBeenCalledWith("eu", { locale: "it" });
147
+ expect(view.locale).toBe("it");
148
+ expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
198
149
  "Hai 2 fatture per Ada"
199
150
  );
200
- expectTypeOf(euView.t).toBeCallableWith("billing", "invoice_summary", "it", {
151
+ expectTypeOf(view.t).toBeCallableWith("billing", "invoice_summary", {
201
152
  count: 2,
202
153
  name: "Ada",
203
154
  });
204
- expectTypeOf(euView.forLocale).toBeCallableWith("it");
205
- expectTypeOf(euView.forLocale).parameters.not.toMatchTypeOf<["en-US"]>();
206
155
  });
207
156
 
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
- }));
157
+ it("loads partitioned namespace loaders with locale", async () => {
158
+ const billingLoader = vi.fn(async (locale: string) =>
159
+ locale === "it"
160
+ ? {
161
+ invoice_summary: {
162
+ it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
163
+ },
164
+ }
165
+ : {
166
+ invoice_summary: {
167
+ en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
168
+ },
169
+ }
170
+ );
215
171
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
216
172
 
217
- const view = await createI18nBuilder(engine, {
173
+ const view = await createI18nHandle(engine, {
218
174
  namespaceLoaders: {
219
175
  billing: billingLoader,
220
176
  },
221
- })
222
- .withNamespaces(["billing"])
223
- .load();
177
+ }).load({ namespaces: ["billing"], locale: "en" });
224
178
 
225
179
  expect(billingLoader).toHaveBeenCalledTimes(1);
226
- expect(billingLoader.mock.calls[0]).toEqual([]);
227
- expect(view.t("billing", "invoice_summary", "en", { count: 1, name: "Ada" })).toBe(
180
+ expect(billingLoader.mock.calls[0]).toEqual(["en", { locale: "en" }]);
181
+ expect(view.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
228
182
  "You have 1 invoice for Ada"
229
183
  );
230
184
  });
231
185
 
186
+ it("rejects an empty namespaces array", async () => {
187
+ const billingLoader = vi.fn(async () => billingEn);
188
+ const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
189
+ const handle = createI18nHandle(engine, {
190
+ namespaceLoaders: {
191
+ billing: billingLoader,
192
+ },
193
+ });
194
+
195
+ await expect(
196
+ handle.load({ namespaces: [] as unknown as ["billing"], locale: "en" })
197
+ ).rejects.toThrow("load() requires a non-empty namespaces array");
198
+ });
199
+
232
200
  it("skips a repeated load for the same namespace partition", async () => {
233
201
  const billingLoader = vi.fn(async (locale: string) =>
234
202
  locale === "it" ? billingIt : billingEn
235
203
  );
236
204
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
237
- const options = { namespaceLoaders: { billing: billingLoader } };
238
- const chain = () =>
239
- createI18nBuilder(engine, options).withNamespaces(["billing"]).withLocale("it");
205
+ const handle = createI18nHandle(engine, {
206
+ namespaceLoaders: { billing: billingLoader },
207
+ });
240
208
 
241
- await chain().load();
242
- await chain().load();
209
+ await handle.load({ namespaces: ["billing"], locale: "it" });
210
+ await handle.load({ namespaces: ["billing"], locale: "it" });
243
211
 
244
212
  expect(billingLoader).toHaveBeenCalledTimes(1);
245
213
  });
@@ -247,175 +215,46 @@ describe("I18nBuilder multi", () => {
247
215
  it("returns a destructuring-safe locale-bound scope from load()", async () => {
248
216
  const billingLoader = vi.fn(async () => billingEn);
249
217
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
250
- const { t } = await createI18nBuilder(engine, {
218
+ const { t } = await createI18nHandle(engine, {
251
219
  namespaceLoaders: { billing: billingLoader },
252
- })
253
- .withNamespaces(["billing"])
254
- .withLocale("en")
255
- .load();
220
+ }).load({ namespaces: ["billing"], locale: "en" });
256
221
 
257
222
  expect(t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
258
223
  "You have 1 invoice for Ada"
259
224
  );
260
225
  });
261
226
 
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
227
  it("still loads a different partition for the same namespace", async () => {
290
228
  const billingLoader = vi.fn(async (locale: string) =>
291
229
  locale === "it" ? billingIt : billingEn
292
230
  );
293
231
  const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
294
- const builder = createI18nBuilder(engine, {
232
+ const handle = createI18nHandle(engine, {
295
233
  namespaceLoaders: { billing: billingLoader },
296
234
  });
297
235
 
298
- await builder.withNamespaces(["billing"]).withLocale("it").load();
299
- await builder.withNamespaces(["billing"]).withLocale("en").load();
236
+ await handle.load({ namespaces: ["billing"], locale: "it" });
237
+ await handle.load({ namespaces: ["billing"], locale: "en" });
300
238
 
301
239
  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!");
240
+ expect(billingLoader).toHaveBeenNthCalledWith(1, "it", { locale: "it" });
241
+ expect(billingLoader).toHaveBeenNthCalledWith(2, "en", { locale: "en" });
353
242
  });
354
243
 
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: "" },
244
+ it("peek() returns null until resources are loaded, then a sync scope", async () => {
245
+ const billingLoader = vi.fn(async () => billingEn);
246
+ const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
247
+ const handle = createI18nHandle(engine, {
248
+ namespaceLoaders: { billing: billingLoader },
365
249
  });
366
250
 
367
- const view = await createI18nBuilder<SingleSchema, SingleParams>(engine, { dictionaryLoader })
368
- .withLocale("en")
369
- .load();
251
+ expect(handle.peek({ namespaces: ["billing"], locale: "en" })).toBeNull();
370
252
 
371
- // @ts-expect-error
372
- expect(() => view.forLocale("it").set("login_button", "Accedi")).toThrow(
373
- "[i18n] Key not preloaded: login_button (it)"
253
+ await handle.load({ namespaces: ["billing"], locale: "en" });
254
+ const sync = handle.peek({ namespaces: ["billing"], locale: "en" });
255
+ expect(sync).not.toBeNull();
256
+ expect(sync!.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
257
+ "You have 1 invoice for Ada"
374
258
  );
375
259
  });
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
260
  });
package/src/builder.ts CHANGED
@@ -1,112 +1,10 @@
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";
1
+ export type { NamespaceLoader, PartitionedLoader } from "./builder-loaders.js";
9
2
  export { invokeNamespaceLoader } from "./builder-loaders.js";
10
3
  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
- }
4
+ I18nHandle,
5
+ I18nHandleOptions,
6
+ LoadNamespacesInput,
7
+ PartitionForLocale,
8
+ ScopeForLocale,
9
+ } from "./i18n-handle.js";
10
+ export { createI18nHandle } from "./i18n-handle.js";