@xndrjs/i18n-react 0.8.2 → 0.8.3
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 +125 -0
- package/dist/cli/codegen.js +305 -0
- package/dist/cli/codegen.js.map +1 -0
- package/package.json +6 -9
- package/bin/codegen.mjs +0 -17
- package/src/codegen/emit/react-bindings-file.test.ts +0 -44
- package/src/codegen/emit/react-bindings-file.ts +0 -223
- package/src/codegen/generate-react-bindings.ts +0 -86
- package/src/codegen/react-codegen-config.test.ts +0 -39
- package/src/codegen/react-codegen-config.ts +0 -70
- package/src/codegen/write-file-if-changed.ts +0 -16
- package/src/create-load-coordinator.test.ts +0 -421
- package/src/create-load-coordinator.ts +0 -340
- package/src/create-scoped-t.ts +0 -50
- package/src/index.ts +0 -18
- package/src/namespace-load-gate.test.tsx +0 -640
- package/src/namespace-load-gate.tsx +0 -285
- package/src/root-context.tsx +0 -21
- package/src/root-provider.tsx +0 -65
- package/src/runtime.test.tsx +0 -119
- package/src/types.ts +0 -138
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { createI18nHandle } from "@xndrjs/i18n";
|
|
3
|
-
import { IcuTranslationProviderMulti } from "@xndrjs/i18n";
|
|
4
|
-
import { createLoadCoordinator } from "./create-load-coordinator.js";
|
|
5
|
-
|
|
6
|
-
type MultiSchema = {
|
|
7
|
-
default: {
|
|
8
|
-
greeting: { en: string; it: string };
|
|
9
|
-
};
|
|
10
|
-
billing: {
|
|
11
|
-
invoice: { en: string; it: string };
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type TestMultiParams = {
|
|
16
|
-
default: {
|
|
17
|
-
greeting: never;
|
|
18
|
-
};
|
|
19
|
-
billing: {
|
|
20
|
-
invoice: never;
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const defaultEn = {
|
|
25
|
-
greeting: { en: "Hello" },
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const defaultIt = {
|
|
29
|
-
greeting: { it: "Ciao" },
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function deferred<T>() {
|
|
33
|
-
let resolve!: (value: T) => void;
|
|
34
|
-
let reject!: (reason?: unknown) => void;
|
|
35
|
-
const promise = new Promise<T>((res, rej) => {
|
|
36
|
-
resolve = res;
|
|
37
|
-
reject = rej;
|
|
38
|
-
});
|
|
39
|
-
return { promise, resolve, reject };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("createLoadCoordinator", () => {
|
|
43
|
-
it("dedupes in-flight loads for the same key", async () => {
|
|
44
|
-
const coordinator = createLoadCoordinator<string>();
|
|
45
|
-
const load = vi.fn(() => Promise.resolve("loaded"));
|
|
46
|
-
const engine = {};
|
|
47
|
-
const key = { engineRef: engine, partition: "en", namespaces: ["default"] as const };
|
|
48
|
-
|
|
49
|
-
coordinator.request({ ...key, load });
|
|
50
|
-
coordinator.request({ ...key, load });
|
|
51
|
-
|
|
52
|
-
expect(load).toHaveBeenCalledTimes(1);
|
|
53
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
54
|
-
await expect(coordinator.getPromise()).resolves.toBe("loaded");
|
|
55
|
-
expect(coordinator.revision).toBe(1);
|
|
56
|
-
expect(coordinator.getResolvedScope()).toBe("loaded");
|
|
57
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "loaded" });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("skips reload when the same key is already resolved", async () => {
|
|
61
|
-
const coordinator = createLoadCoordinator<number>();
|
|
62
|
-
const load = vi.fn(() => Promise.resolve(42));
|
|
63
|
-
const engine = {};
|
|
64
|
-
const key = {
|
|
65
|
-
engineRef: engine,
|
|
66
|
-
partition: undefined,
|
|
67
|
-
namespaces: ["default"] as const,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
coordinator.request({ ...key, load });
|
|
71
|
-
await coordinator.getPromise();
|
|
72
|
-
|
|
73
|
-
coordinator.request({ ...key, load });
|
|
74
|
-
|
|
75
|
-
expect(load).toHaveBeenCalledTimes(1);
|
|
76
|
-
expect(coordinator.revision).toBe(1);
|
|
77
|
-
await expect(coordinator.getPromise()).resolves.toBe(42);
|
|
78
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: 42 });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("ensureSync resolves from tryResolveSync without calling load", () => {
|
|
82
|
-
const coordinator = createLoadCoordinator<string>();
|
|
83
|
-
const load = vi.fn(() => Promise.resolve("async"));
|
|
84
|
-
const tryResolveSync = vi.fn(() => "sync");
|
|
85
|
-
const engine = {};
|
|
86
|
-
const key = { engineRef: engine, partition: "en", namespaces: ["default"] as const };
|
|
87
|
-
|
|
88
|
-
coordinator.ensureSync({ ...key, load, tryResolveSync });
|
|
89
|
-
|
|
90
|
-
expect(tryResolveSync).toHaveBeenCalledTimes(1);
|
|
91
|
-
expect(load).not.toHaveBeenCalled();
|
|
92
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "sync" });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("ensureSync does nothing when tryResolveSync misses (no async kick)", () => {
|
|
96
|
-
const coordinator = createLoadCoordinator<string>();
|
|
97
|
-
const load = vi.fn(() => Promise.resolve("async"));
|
|
98
|
-
const engine = {};
|
|
99
|
-
const key = { engineRef: engine, partition: "en", namespaces: ["default"] as const };
|
|
100
|
-
|
|
101
|
-
coordinator.ensureSync({ ...key, load, tryResolveSync: () => null });
|
|
102
|
-
|
|
103
|
-
expect(load).not.toHaveBeenCalled();
|
|
104
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("ensureSync without tryResolveSync leaves entry absent", () => {
|
|
108
|
-
const coordinator = createLoadCoordinator<string>();
|
|
109
|
-
const load = vi.fn(() => Promise.resolve("async"));
|
|
110
|
-
const engine = {};
|
|
111
|
-
const key = { engineRef: engine, partition: "en", namespaces: ["default"] as const };
|
|
112
|
-
|
|
113
|
-
coordinator.ensureSync({ ...key, load });
|
|
114
|
-
|
|
115
|
-
expect(load).not.toHaveBeenCalled();
|
|
116
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("resolves synchronously when tryResolveSync returns a scope", () => {
|
|
120
|
-
const coordinator = createLoadCoordinator<string>();
|
|
121
|
-
const load = vi.fn(() => Promise.resolve("async"));
|
|
122
|
-
const tryResolveSync = vi.fn(() => "sync");
|
|
123
|
-
const engine = {};
|
|
124
|
-
const key = {
|
|
125
|
-
engineRef: engine,
|
|
126
|
-
partition: "en",
|
|
127
|
-
namespaces: ["default"] as const,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
coordinator.request({ ...key, load, tryResolveSync });
|
|
131
|
-
|
|
132
|
-
expect(load).not.toHaveBeenCalled();
|
|
133
|
-
expect(tryResolveSync).toHaveBeenCalledTimes(1);
|
|
134
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "sync" });
|
|
135
|
-
expect(coordinator.getResolvedScope()).toBe("sync");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("falls back to async load when tryResolveSync returns null", async () => {
|
|
139
|
-
const coordinator = createLoadCoordinator<string>();
|
|
140
|
-
const load = vi.fn(() => Promise.resolve("async"));
|
|
141
|
-
const tryResolveSync = vi.fn(() => null);
|
|
142
|
-
const engine = {};
|
|
143
|
-
const key = {
|
|
144
|
-
engineRef: engine,
|
|
145
|
-
partition: "en",
|
|
146
|
-
namespaces: ["default"] as const,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
coordinator.request({ ...key, load, tryResolveSync });
|
|
150
|
-
|
|
151
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
152
|
-
await expect(coordinator.getPromise()).resolves.toBe("async");
|
|
153
|
-
expect(load).toHaveBeenCalledTimes(1);
|
|
154
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "async" });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("keeps concurrent namespace loads independent", async () => {
|
|
158
|
-
const coordinator = createLoadCoordinator<string>();
|
|
159
|
-
const billing = deferred<string>();
|
|
160
|
-
const user = deferred<string>();
|
|
161
|
-
const engine = {};
|
|
162
|
-
const billingKey = {
|
|
163
|
-
engineRef: engine,
|
|
164
|
-
partition: "en",
|
|
165
|
-
namespaces: ["billing"] as const,
|
|
166
|
-
};
|
|
167
|
-
const userKey = {
|
|
168
|
-
engineRef: engine,
|
|
169
|
-
partition: "en",
|
|
170
|
-
namespaces: ["user"] as const,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
coordinator.request({ ...billingKey, load: () => billing.promise });
|
|
174
|
-
const billingPromise = coordinator.getPromise();
|
|
175
|
-
|
|
176
|
-
coordinator.request({ ...userKey, load: () => user.promise });
|
|
177
|
-
const userPromise = coordinator.getPromise();
|
|
178
|
-
|
|
179
|
-
expect(coordinator.getEntry(billingKey)).toEqual({ status: "pending" });
|
|
180
|
-
expect(coordinator.getEntry(userKey)).toEqual({ status: "pending" });
|
|
181
|
-
|
|
182
|
-
billing.resolve("billing-scope");
|
|
183
|
-
user.resolve("user-scope");
|
|
184
|
-
|
|
185
|
-
await expect(billingPromise).resolves.toBe("billing-scope");
|
|
186
|
-
await expect(userPromise).resolves.toBe("user-scope");
|
|
187
|
-
expect(coordinator.revision).toBe(2);
|
|
188
|
-
expect(coordinator.getEntry(billingKey)).toEqual({
|
|
189
|
-
status: "resolved",
|
|
190
|
-
scope: "billing-scope",
|
|
191
|
-
});
|
|
192
|
-
expect(coordinator.getEntry(userKey)).toEqual({
|
|
193
|
-
status: "resolved",
|
|
194
|
-
scope: "user-scope",
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("caches distinct partitions without cancelling the earlier one", async () => {
|
|
199
|
-
const coordinator = createLoadCoordinator<string>();
|
|
200
|
-
const first = deferred<string>();
|
|
201
|
-
const second = deferred<string>();
|
|
202
|
-
const engine = {};
|
|
203
|
-
|
|
204
|
-
coordinator.request({
|
|
205
|
-
engineRef: engine,
|
|
206
|
-
partition: "en",
|
|
207
|
-
namespaces: ["default"],
|
|
208
|
-
load: () => first.promise,
|
|
209
|
-
});
|
|
210
|
-
const enPromise = coordinator.getPromise();
|
|
211
|
-
|
|
212
|
-
coordinator.request({
|
|
213
|
-
engineRef: engine,
|
|
214
|
-
partition: "it",
|
|
215
|
-
namespaces: ["default"],
|
|
216
|
-
load: () => second.promise,
|
|
217
|
-
});
|
|
218
|
-
const itPromise = coordinator.getPromise();
|
|
219
|
-
|
|
220
|
-
first.resolve("en-scope");
|
|
221
|
-
second.resolve("it-scope");
|
|
222
|
-
|
|
223
|
-
await expect(enPromise).resolves.toBe("en-scope");
|
|
224
|
-
await expect(itPromise).resolves.toBe("it-scope");
|
|
225
|
-
expect(coordinator.revision).toBe(2);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("notifies subscribers when a load resolves", async () => {
|
|
229
|
-
const coordinator = createLoadCoordinator<string>();
|
|
230
|
-
const pending = deferred<string>();
|
|
231
|
-
const engine = {};
|
|
232
|
-
const key = {
|
|
233
|
-
engineRef: engine,
|
|
234
|
-
partition: "en",
|
|
235
|
-
namespaces: ["default"] as const,
|
|
236
|
-
};
|
|
237
|
-
const listener = vi.fn();
|
|
238
|
-
|
|
239
|
-
const unsubscribe = coordinator.subscribe(listener);
|
|
240
|
-
coordinator.request({ ...key, load: () => pending.promise });
|
|
241
|
-
|
|
242
|
-
expect(listener).not.toHaveBeenCalled();
|
|
243
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
244
|
-
|
|
245
|
-
pending.resolve("ready");
|
|
246
|
-
await expect(coordinator.getPromise()).resolves.toBe("ready");
|
|
247
|
-
|
|
248
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
249
|
-
expect(coordinator.revision).toBe(1);
|
|
250
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "ready" });
|
|
251
|
-
|
|
252
|
-
unsubscribe();
|
|
253
|
-
const other = deferred<string>();
|
|
254
|
-
coordinator.request({
|
|
255
|
-
engineRef: engine,
|
|
256
|
-
partition: "it",
|
|
257
|
-
namespaces: ["default"],
|
|
258
|
-
load: () => other.promise,
|
|
259
|
-
});
|
|
260
|
-
other.resolve("other");
|
|
261
|
-
await vi.waitFor(() => expect(coordinator.revision).toBe(2));
|
|
262
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("logs rejected loads when getPromise is not awaited", async () => {
|
|
266
|
-
const coordinator = createLoadCoordinator<string>();
|
|
267
|
-
const pending = deferred<string>();
|
|
268
|
-
const engine = {};
|
|
269
|
-
const key = {
|
|
270
|
-
engineRef: engine,
|
|
271
|
-
partition: "en",
|
|
272
|
-
namespaces: ["default"] as const,
|
|
273
|
-
};
|
|
274
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => void 0);
|
|
275
|
-
|
|
276
|
-
coordinator.ensure({ ...key, load: () => pending.promise });
|
|
277
|
-
|
|
278
|
-
pending.reject(new Error("load failed"));
|
|
279
|
-
await vi.waitFor(() => {
|
|
280
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "error", error: expect.any(Error) });
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
expect(consoleError).toHaveBeenCalledWith(
|
|
284
|
-
"[i18n-react] namespace load failed:",
|
|
285
|
-
expect.any(Error)
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
consoleError.mockRestore();
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("keeps an error entry and notifies subscribers on reject", async () => {
|
|
292
|
-
const coordinator = createLoadCoordinator<string>();
|
|
293
|
-
const pending = deferred<string>();
|
|
294
|
-
const engine = {};
|
|
295
|
-
const key = {
|
|
296
|
-
engineRef: engine,
|
|
297
|
-
partition: "en",
|
|
298
|
-
namespaces: ["default"] as const,
|
|
299
|
-
};
|
|
300
|
-
const load = vi.fn(() => pending.promise);
|
|
301
|
-
const listener = vi.fn();
|
|
302
|
-
const failure = new Error("load failed");
|
|
303
|
-
|
|
304
|
-
coordinator.subscribe(listener);
|
|
305
|
-
coordinator.request({ ...key, load });
|
|
306
|
-
|
|
307
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
308
|
-
|
|
309
|
-
pending.reject(failure);
|
|
310
|
-
await expect(coordinator.getPromise()).rejects.toThrow("load failed");
|
|
311
|
-
|
|
312
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
313
|
-
expect(coordinator.revision).toBe(1);
|
|
314
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "error", error: failure });
|
|
315
|
-
expect(coordinator.getResolvedScope()).toBeNull();
|
|
316
|
-
|
|
317
|
-
// Deduped: settled error entry is not retried until retry().
|
|
318
|
-
coordinator.request({ ...key, load });
|
|
319
|
-
expect(load).toHaveBeenCalledTimes(1);
|
|
320
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "error", error: failure });
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("retry() evicts an error entry so the next ensure reloads", async () => {
|
|
324
|
-
const coordinator = createLoadCoordinator<string>();
|
|
325
|
-
const engine = {};
|
|
326
|
-
const key = {
|
|
327
|
-
engineRef: engine,
|
|
328
|
-
partition: "en",
|
|
329
|
-
namespaces: ["default"] as const,
|
|
330
|
-
};
|
|
331
|
-
const load = vi
|
|
332
|
-
.fn()
|
|
333
|
-
.mockImplementationOnce(() => Promise.reject(new Error("fail")))
|
|
334
|
-
.mockImplementationOnce(() => Promise.resolve("ok"));
|
|
335
|
-
|
|
336
|
-
coordinator.request({ ...key, load });
|
|
337
|
-
await expect(coordinator.getPromise()).rejects.toThrow("fail");
|
|
338
|
-
expect(coordinator.getEntry(key).status).toBe("error");
|
|
339
|
-
|
|
340
|
-
coordinator.retry(key);
|
|
341
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
|
|
342
|
-
|
|
343
|
-
coordinator.request({ ...key, load });
|
|
344
|
-
await expect(coordinator.getPromise()).resolves.toBe("ok");
|
|
345
|
-
expect(load).toHaveBeenCalledTimes(2);
|
|
346
|
-
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "ok" });
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("distinguishes different engineRef objects via WeakMap ids", async () => {
|
|
350
|
-
const coordinator = createLoadCoordinator<string>();
|
|
351
|
-
const engineA = {};
|
|
352
|
-
const engineB = {};
|
|
353
|
-
const loadA = vi.fn(() => Promise.resolve("a"));
|
|
354
|
-
const loadB = vi.fn(() => Promise.resolve("b"));
|
|
355
|
-
|
|
356
|
-
coordinator.request({
|
|
357
|
-
engineRef: engineA,
|
|
358
|
-
partition: "en",
|
|
359
|
-
namespaces: ["default"],
|
|
360
|
-
load: loadA,
|
|
361
|
-
});
|
|
362
|
-
const promiseA = coordinator.getPromise();
|
|
363
|
-
coordinator.request({
|
|
364
|
-
engineRef: engineB,
|
|
365
|
-
partition: "en",
|
|
366
|
-
namespaces: ["default"],
|
|
367
|
-
load: loadB,
|
|
368
|
-
});
|
|
369
|
-
const promiseB = coordinator.getPromise();
|
|
370
|
-
|
|
371
|
-
expect(loadA).toHaveBeenCalledTimes(1);
|
|
372
|
-
expect(loadB).toHaveBeenCalledTimes(1);
|
|
373
|
-
await expect(promiseA).resolves.toBe("a");
|
|
374
|
-
await expect(promiseB).resolves.toBe("b");
|
|
375
|
-
expect(
|
|
376
|
-
coordinator.getEntry({ engineRef: engineA, partition: "en", namespaces: ["default"] })
|
|
377
|
-
).toEqual({ status: "resolved", scope: "a" });
|
|
378
|
-
expect(
|
|
379
|
-
coordinator.getEntry({ engineRef: engineB, partition: "en", namespaces: ["default"] })
|
|
380
|
-
).toEqual({ status: "resolved", scope: "b" });
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it("returns pending for unknown keys without using React use()", () => {
|
|
384
|
-
const coordinator = createLoadCoordinator<string>();
|
|
385
|
-
expect(
|
|
386
|
-
coordinator.getEntry({
|
|
387
|
-
engineRef: {},
|
|
388
|
-
partition: "en",
|
|
389
|
-
namespaces: ["missing"],
|
|
390
|
-
})
|
|
391
|
-
).toEqual({ status: "pending" });
|
|
392
|
-
expect(coordinator.revision).toBe(0);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it("integrates with handle split-by-locale loads", async () => {
|
|
396
|
-
const defaultLoader = vi.fn(async (locale: string) =>
|
|
397
|
-
locale === "it" ? defaultIt : defaultEn
|
|
398
|
-
);
|
|
399
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
400
|
-
const handle = createI18nHandle(engine, {
|
|
401
|
-
namespaceLoaders: {
|
|
402
|
-
default: defaultLoader,
|
|
403
|
-
},
|
|
404
|
-
});
|
|
405
|
-
const load = () => handle.load({ namespaces: ["default"], locale: "it" });
|
|
406
|
-
const coordinator = createLoadCoordinator<Awaited<ReturnType<typeof load>>>();
|
|
407
|
-
const key = {
|
|
408
|
-
engineRef: engine,
|
|
409
|
-
partition: "it",
|
|
410
|
-
namespaces: ["default"] as const,
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
coordinator.request({ ...key, load });
|
|
414
|
-
|
|
415
|
-
const scope = await coordinator.getPromise();
|
|
416
|
-
expect(defaultLoader).toHaveBeenCalledWith("it", { locale: "it" });
|
|
417
|
-
expect(scope.t("default", "greeting")).toBe("Ciao");
|
|
418
|
-
expect(coordinator.revision).toBe(1);
|
|
419
|
-
expect(coordinator.getEntry(key).status).toBe("resolved");
|
|
420
|
-
});
|
|
421
|
-
});
|