@xndrjs/i18n-react 0.8.0 → 0.8.2
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/dist/index.d.ts +22 -4
- package/dist/index.js +87 -51
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/create-load-coordinator.test.ts +64 -0
- package/src/create-load-coordinator.ts +42 -14
- package/src/namespace-load-gate.test.tsx +134 -22
- package/src/namespace-load-gate.tsx +81 -54
- package/src/runtime.test.tsx +32 -1
- package/src/types.ts +14 -1
package/dist/index.d.ts
CHANGED
|
@@ -71,8 +71,13 @@ interface LoadCoordinator<Scope> {
|
|
|
71
71
|
readonly revision: number;
|
|
72
72
|
/** @deprecated Prefer {@link ensure}. */
|
|
73
73
|
request(input: LoadCoordinatorRequest<Scope>): void;
|
|
74
|
-
/** Idempotent: start load if missing. Safe from getSnapshot. */
|
|
74
|
+
/** Idempotent: start load if missing. Safe from client getSnapshot. */
|
|
75
75
|
ensure(input: LoadCoordinatorRequest<Scope>): void;
|
|
76
|
+
/**
|
|
77
|
+
* Idempotent: resolve from {@link LoadCoordinatorRequest.tryResolveSync} only.
|
|
78
|
+
* Never starts `load()` — use from SSR `getServerSnapshot`.
|
|
79
|
+
*/
|
|
80
|
+
ensureSync(input: LoadCoordinatorRequest<Scope>): void;
|
|
76
81
|
getEntry(key: LoadCoordinatorKey): LoadCoordinatorEntry<Scope>;
|
|
77
82
|
getDisplayEntry(key: LoadCoordinatorKey, options: GetDisplayEntryOptions): LoadDisplayEntry<Scope>;
|
|
78
83
|
/** Evict entry so the next ensure() re-fetches. */
|
|
@@ -108,6 +113,14 @@ interface I18nHandleLike {
|
|
|
108
113
|
dictionary: unknown;
|
|
109
114
|
resources: readonly (readonly [string, string])[];
|
|
110
115
|
};
|
|
116
|
+
getLoadState: () => {
|
|
117
|
+
resources: readonly {
|
|
118
|
+
namespace: string;
|
|
119
|
+
partition: string;
|
|
120
|
+
status: "pending" | "loaded" | "error";
|
|
121
|
+
error?: unknown;
|
|
122
|
+
}[];
|
|
123
|
+
};
|
|
111
124
|
}
|
|
112
125
|
/** Single React context value for the lazy i18n tree. */
|
|
113
126
|
interface I18nRootContextValue {
|
|
@@ -141,7 +154,7 @@ declare function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinat
|
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
156
|
* Manual (non-Suspense) i18n load gate: ensure + subscribe via the load
|
|
144
|
-
* coordinator, then render fallback / keep-previous / error UI.
|
|
157
|
+
* coordinator, then render fallback / keep-previous / error UI / throw.
|
|
145
158
|
*/
|
|
146
159
|
|
|
147
160
|
/** Inputs for {@link useNamespaceLoad}. */
|
|
@@ -157,7 +170,8 @@ interface UseNamespaceLoadInput<Scope> {
|
|
|
157
170
|
}
|
|
158
171
|
/**
|
|
159
172
|
* Starts (or reuses) a coordinator load and re-renders when that entry settles.
|
|
160
|
-
*
|
|
173
|
+
* Client getSnapshot may kick `load()`; SSR getServerSnapshot only peeks sync
|
|
174
|
+
* (hydrated `state`) and never starts a client fetch during prerender.
|
|
161
175
|
*/
|
|
162
176
|
declare function useNamespaceLoad<Scope>(input: UseNamespaceLoadInput<Scope>): LoadDisplayEntry<Scope>;
|
|
163
177
|
/** Scoped `{ t, locale }` injected into gate children / HOC when ready (or kept). */
|
|
@@ -189,8 +203,12 @@ interface CreateI18nLoadGateOptions {
|
|
|
189
203
|
}
|
|
190
204
|
interface I18nGateProps {
|
|
191
205
|
namespaces: readonly string[];
|
|
206
|
+
/** Shown only while the load is pending (not on error). */
|
|
192
207
|
fallback?: ReactNode;
|
|
193
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* When set, called for error with no keep-previous display.
|
|
210
|
+
* Otherwise the load error is thrown for a React error boundary.
|
|
211
|
+
*/
|
|
194
212
|
renderError?: (args: {
|
|
195
213
|
error: unknown;
|
|
196
214
|
retry: () => void;
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,34 @@ function createLoadCoordinator() {
|
|
|
69
69
|
}
|
|
70
70
|
notify();
|
|
71
71
|
}
|
|
72
|
+
function markResolvedSync(key, syncScope) {
|
|
73
|
+
const entrySnapshot = {
|
|
74
|
+
status: "resolved",
|
|
75
|
+
scope: syncScope
|
|
76
|
+
};
|
|
77
|
+
entries.set(key, {
|
|
78
|
+
status: "resolved",
|
|
79
|
+
promise: Promise.resolve(syncScope),
|
|
80
|
+
resolvedScope: syncScope,
|
|
81
|
+
error: null,
|
|
82
|
+
requestId: ++nextRequestId,
|
|
83
|
+
entrySnapshot
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function ensureSync(input) {
|
|
87
|
+
const key = cacheKey(input.engineRef, input.partition, input.namespaces);
|
|
88
|
+
lastKey = key;
|
|
89
|
+
if (entries.has(key)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (input.tryResolveSync === void 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const syncScope = input.tryResolveSync();
|
|
96
|
+
if (syncScope !== null) {
|
|
97
|
+
markResolvedSync(key, syncScope);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
72
100
|
function ensure(input) {
|
|
73
101
|
const key = cacheKey(input.engineRef, input.partition, input.namespaces);
|
|
74
102
|
lastKey = key;
|
|
@@ -79,18 +107,7 @@ function createLoadCoordinator() {
|
|
|
79
107
|
if (input.tryResolveSync !== void 0) {
|
|
80
108
|
const syncScope = input.tryResolveSync();
|
|
81
109
|
if (syncScope !== null) {
|
|
82
|
-
|
|
83
|
-
status: "resolved",
|
|
84
|
-
scope: syncScope
|
|
85
|
-
};
|
|
86
|
-
entries.set(key, {
|
|
87
|
-
status: "resolved",
|
|
88
|
-
promise: Promise.resolve(syncScope),
|
|
89
|
-
resolvedScope: syncScope,
|
|
90
|
-
error: null,
|
|
91
|
-
requestId: ++nextRequestId,
|
|
92
|
-
entrySnapshot
|
|
93
|
-
});
|
|
110
|
+
markResolvedSync(key, syncScope);
|
|
94
111
|
return;
|
|
95
112
|
}
|
|
96
113
|
}
|
|
@@ -120,7 +137,9 @@ function createLoadCoordinator() {
|
|
|
120
137
|
throw error;
|
|
121
138
|
}
|
|
122
139
|
);
|
|
123
|
-
void promise.catch(() =>
|
|
140
|
+
void promise.catch((error) => {
|
|
141
|
+
console.error("[i18n-react] namespace load failed:", error);
|
|
142
|
+
});
|
|
124
143
|
entries.set(key, {
|
|
125
144
|
status: "pending",
|
|
126
145
|
promise,
|
|
@@ -245,6 +264,7 @@ function createLoadCoordinator() {
|
|
|
245
264
|
},
|
|
246
265
|
request: ensure,
|
|
247
266
|
ensure,
|
|
267
|
+
ensureSync,
|
|
248
268
|
getEntry,
|
|
249
269
|
getDisplayEntry,
|
|
250
270
|
retry: retryKey,
|
|
@@ -271,29 +291,20 @@ function useNamespaceLoad(input) {
|
|
|
271
291
|
load,
|
|
272
292
|
...tryResolveSync !== void 0 ? { tryResolveSync } : {}
|
|
273
293
|
};
|
|
294
|
+
const displayOptions = {
|
|
295
|
+
locale,
|
|
296
|
+
namespaces,
|
|
297
|
+
...keepPrevious !== void 0 ? { keepPrevious } : {}
|
|
298
|
+
};
|
|
274
299
|
return useSyncExternalStore(
|
|
275
300
|
coordinator.subscribe,
|
|
276
301
|
() => {
|
|
277
302
|
coordinator.ensure(request);
|
|
278
|
-
return coordinator.getDisplayEntry(
|
|
279
|
-
{ engineRef, partition, namespaces },
|
|
280
|
-
{
|
|
281
|
-
locale,
|
|
282
|
-
namespaces,
|
|
283
|
-
...keepPrevious !== void 0 ? { keepPrevious } : {}
|
|
284
|
-
}
|
|
285
|
-
);
|
|
303
|
+
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
286
304
|
},
|
|
287
305
|
() => {
|
|
288
|
-
coordinator.
|
|
289
|
-
return coordinator.getDisplayEntry(
|
|
290
|
-
{ engineRef, partition, namespaces },
|
|
291
|
-
{
|
|
292
|
-
locale,
|
|
293
|
-
namespaces,
|
|
294
|
-
...keepPrevious !== void 0 ? { keepPrevious } : {}
|
|
295
|
-
}
|
|
296
|
-
);
|
|
306
|
+
coordinator.ensureSync(request);
|
|
307
|
+
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
297
308
|
}
|
|
298
309
|
);
|
|
299
310
|
}
|
|
@@ -303,15 +314,19 @@ function resolveNamespaces(namespaces) {
|
|
|
303
314
|
}
|
|
304
315
|
return namespaces;
|
|
305
316
|
}
|
|
306
|
-
function
|
|
307
|
-
if (
|
|
308
|
-
return
|
|
317
|
+
function toThrownError(error) {
|
|
318
|
+
if (error instanceof Error) {
|
|
319
|
+
return error;
|
|
309
320
|
}
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
if (typeof error === "string") {
|
|
322
|
+
return new Error(error);
|
|
323
|
+
}
|
|
324
|
+
return new Error("i18n namespace load failed");
|
|
325
|
+
}
|
|
326
|
+
var noopT = () => "";
|
|
327
|
+
function gateValueFromEntry(entry) {
|
|
328
|
+
if (entry.status === "ready") {
|
|
329
|
+
return { t: entry.t, locale: entry.locale };
|
|
315
330
|
}
|
|
316
331
|
if (entry.t !== null && entry.display) {
|
|
317
332
|
const value = {
|
|
@@ -325,6 +340,19 @@ function renderDisplayEntry(entry, options) {
|
|
|
325
340
|
value.error = entry.error;
|
|
326
341
|
value.retry = entry.retry;
|
|
327
342
|
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
function renderDisplayEntry(entry, options) {
|
|
348
|
+
if (entry.status === "error" && entry.t === null) {
|
|
349
|
+
if (options.renderError) {
|
|
350
|
+
return options.renderError({ error: entry.error, retry: entry.retry });
|
|
351
|
+
}
|
|
352
|
+
throw toThrownError(entry.error);
|
|
353
|
+
}
|
|
354
|
+
const value = gateValueFromEntry(entry);
|
|
355
|
+
if (value !== null) {
|
|
328
356
|
return options.children(value);
|
|
329
357
|
}
|
|
330
358
|
return options.fallback ?? null;
|
|
@@ -332,10 +360,10 @@ function renderDisplayEntry(entry, options) {
|
|
|
332
360
|
function createI18nLoadGate(options) {
|
|
333
361
|
const { useLoadArgs } = options;
|
|
334
362
|
const keepPrevious = options.keepPreviousOnPartitionChange !== false;
|
|
335
|
-
function
|
|
363
|
+
function useGateLoad(namespaces) {
|
|
336
364
|
const resolved = resolveNamespaces(namespaces);
|
|
337
365
|
const args = useLoadArgs();
|
|
338
|
-
|
|
366
|
+
const entry = useNamespaceLoad({
|
|
339
367
|
coordinator: args.coordinator,
|
|
340
368
|
engineRef: args.engineRef,
|
|
341
369
|
partition: args.partition,
|
|
@@ -345,9 +373,10 @@ function createI18nLoadGate(options) {
|
|
|
345
373
|
...args.tryResolveSync !== void 0 ? { tryResolveSync: () => args.tryResolveSync(resolved) } : {},
|
|
346
374
|
keepPrevious
|
|
347
375
|
});
|
|
376
|
+
return { entry, locale: args.locale };
|
|
348
377
|
}
|
|
349
378
|
function I18n({ namespaces, fallback, renderError, children }) {
|
|
350
|
-
const entry =
|
|
379
|
+
const { entry } = useGateLoad(namespaces);
|
|
351
380
|
return renderDisplayEntry(entry, {
|
|
352
381
|
...fallback !== void 0 ? { fallback } : {},
|
|
353
382
|
...renderError !== void 0 ? { renderError } : {},
|
|
@@ -357,16 +386,23 @@ function createI18nLoadGate(options) {
|
|
|
357
386
|
function withI18n(hocOptions, render) {
|
|
358
387
|
const { fallback, renderError } = hocOptions;
|
|
359
388
|
const Outer = forwardRef(function I18nHocOuter(props, ref) {
|
|
360
|
-
const entry =
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
renderError
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
}
|
|
389
|
+
const { entry, locale } = useGateLoad(hocOptions.namespaces);
|
|
390
|
+
const displayValue = gateValueFromEntry(entry);
|
|
391
|
+
const value = displayValue ?? { t: noopT, locale };
|
|
392
|
+
const rendered = render(props, value, ref);
|
|
393
|
+
if (entry.status === "error" && entry.t === null) {
|
|
394
|
+
if (renderError) {
|
|
395
|
+
return renderError({ error: entry.error, retry: entry.retry, props });
|
|
396
|
+
}
|
|
397
|
+
throw toThrownError(entry.error);
|
|
398
|
+
}
|
|
399
|
+
if (displayValue !== null) {
|
|
400
|
+
return rendered;
|
|
401
|
+
}
|
|
402
|
+
if (fallback === void 0) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return typeof fallback === "function" ? fallback(props) : fallback;
|
|
370
406
|
});
|
|
371
407
|
const displayName = render.name || "Component";
|
|
372
408
|
Outer.displayName = `withI18n(${displayName})`;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/create-scoped-t.ts","../src/create-load-coordinator.ts","../src/namespace-load-gate.tsx","../src/root-context.tsx","../src/root-provider.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Builds the scoped `t` function used by providers and load gates.\n *\n * Takes a loaded `@xndrjs/i18n` scope and returns a capability-safe translator:\n * namespaces outside the provider list throw at runtime. Locale is always bound.\n */\nimport type { CreateScopedTOptions, ScopedScopeLike, ScopedTranslateFn } from \"./types.js\";\n\nfunction assertNamespace(namespace: string, namespaces: readonly string[]): void {\n if (!namespaces.includes(namespace)) {\n throw new Error(\n `Namespace \"${namespace}\" is not loaded by this provider. Allowed: [${namespaces.join(\", \")}]`\n );\n }\n}\n\nfunction resolveActiveScope(scope: ScopedScopeLike, locale: string): ScopedScopeLike {\n if (typeof scope.forLocale === \"function\" && scope.locale !== locale) {\n return scope.forLocale(locale);\n }\n return scope;\n}\n\n/**\n * Wraps a loaded scope `t` with namespace guards and locale binding.\n */\nexport function createScopedT(\n scope: ScopedScopeLike,\n options: CreateScopedTOptions\n): ScopedTranslateFn {\n const activeScope = resolveActiveScope(scope, options.locale);\n const { namespaces } = options;\n\n return ((namespace: string, key: string, ...rest: unknown[]) => {\n assertNamespace(namespace, namespaces);\n return activeScope.t(namespace, key, ...rest);\n }) as ScopedTranslateFn;\n}\n\n/**\n * Curries a multi-namespace context `t(ns, key, …)` into a namespace-bound `t(key, …)`.\n * Used when composing gate values without introducing a separate hooks API.\n */\nexport function bindNamespaceTranslate(\n t: ScopedTranslateFn,\n namespace: string\n): (key: string, params?: Record<string, unknown>) => string {\n return (key: string, params?: Record<string, unknown>) =>\n params === undefined ? t(namespace, key) : t(namespace, key, params);\n}\n","/**\n * Load deduplication for lazy i18n namespace gates / HOCs.\n *\n * Supports multiple concurrent keys so parallel per-namespace gates can share\n * one coordinator. Each `(engine, partition, namespaces)` entry keeps its own\n * promise and status. Display state (keep-previous / pendingLocale / stable `t`)\n * lives here so gate components stay pure observers of `useSyncExternalStore`.\n */\nimport { createScopedT } from \"./create-scoped-t.js\";\nimport type {\n GetDisplayEntryOptions,\n LoadCoordinator,\n LoadCoordinatorEntry,\n LoadCoordinatorKey,\n LoadCoordinatorRequest,\n LoadDisplayEntry,\n ScopedScopeLike,\n ScopedTranslateFn,\n} from \"./types.js\";\n\nconst engineIds = new WeakMap<object, number>();\nlet nextEngineId = 1;\n\nfunction engineId(ref: unknown): number {\n if (ref === null || (typeof ref !== \"object\" && typeof ref !== \"function\")) {\n return 0;\n }\n const obj = ref as object;\n let id = engineIds.get(obj);\n if (id === undefined) {\n id = nextEngineId++;\n engineIds.set(obj, id);\n }\n return id;\n}\n\nfunction cacheKey(\n engineRef: unknown,\n partition: string | undefined,\n namespaces: readonly string[]\n): string {\n return `${engineId(engineRef)}\\0${partition ?? \"\"}\\0${namespaces.join(\"\\0\")}`;\n}\n\n/** Gate identity for keep-previous (namespaces only — partition/locale can change). */\nfunction gateId(engineRef: unknown, namespaces: readonly string[]): string {\n return `${engineId(engineRef)}\\0${namespaces.join(\"\\0\")}`;\n}\n\ninterface CacheEntry<Scope> {\n status: \"pending\" | \"resolved\" | \"error\";\n promise: Promise<Scope>;\n resolvedScope: Scope | null;\n error: unknown;\n requestId: number;\n /** Stable LoadCoordinatorEntry snapshot for getEntry(). */\n entrySnapshot: LoadCoordinatorEntry<Scope>;\n}\n\ninterface DisplayCache {\n lastReady: { scope: ScopedScopeLike; locale: string } | null;\n /** Stable display entry object until inputs change. */\n displaySnapshot: LoadDisplayEntry<ScopedScopeLike> | null;\n /** Identity key for the last displaySnapshot. */\n displayKey: string | null;\n boundT: ScopedTranslateFn | null;\n boundTKey: string | null;\n}\n\n/**\n * Dedupes in-flight loads by `(engineRef, partition, namespaces)` and exposes\n * display snapshots + retry for manual pending/resolved/error rendering.\n */\nexport function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinator<Scope> {\n let revision = 0;\n let nextRequestId = 0;\n const entries = new Map<string, CacheEntry<Scope>>();\n const displayByGate = new Map<string, DisplayCache>();\n const listeners = new Set<() => void>();\n let lastKey: string | null = null;\n\n function notify(): void {\n for (const listener of listeners) {\n listener();\n }\n }\n\n function bumpAndNotify(): void {\n revision += 1;\n // Invalidate display snapshots that depend on entry status.\n for (const display of displayByGate.values()) {\n display.displaySnapshot = null;\n display.displayKey = null;\n }\n notify();\n }\n\n function ensure(input: LoadCoordinatorRequest<Scope>): void {\n const key = cacheKey(input.engineRef, input.partition, input.namespaces);\n lastKey = key;\n\n const existing = entries.get(key);\n if (existing !== undefined) {\n return;\n }\n\n if (input.tryResolveSync !== undefined) {\n const syncScope = input.tryResolveSync();\n if (syncScope !== null) {\n const entrySnapshot: LoadCoordinatorEntry<Scope> = {\n status: \"resolved\",\n scope: syncScope,\n };\n entries.set(key, {\n status: \"resolved\",\n promise: Promise.resolve(syncScope),\n resolvedScope: syncScope,\n error: null,\n requestId: ++nextRequestId,\n entrySnapshot,\n });\n return;\n }\n }\n\n const requestId = ++nextRequestId;\n const promise = input.load().then(\n (scope) => {\n const entry = entries.get(key);\n if (entry === undefined || entry.requestId !== requestId) {\n return scope;\n }\n entry.status = \"resolved\";\n entry.resolvedScope = scope;\n entry.error = null;\n entry.entrySnapshot = { status: \"resolved\", scope };\n bumpAndNotify();\n return scope;\n },\n (error: unknown) => {\n const entry = entries.get(key);\n if (entry !== undefined && entry.requestId === requestId) {\n entry.status = \"error\";\n entry.error = error;\n entry.resolvedScope = null;\n entry.entrySnapshot = { status: \"error\", error };\n bumpAndNotify();\n }\n throw error;\n }\n );\n // Mark rejection handled when only getEntry consumers attach (no getPromise).\n void promise.catch(() => void null);\n\n entries.set(key, {\n status: \"pending\",\n promise,\n resolvedScope: null,\n error: null,\n requestId,\n entrySnapshot: { status: \"pending\" },\n });\n }\n\n function getEntry(keyInput: LoadCoordinatorKey): LoadCoordinatorEntry<Scope> {\n const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);\n const entry = entries.get(key);\n if (entry === undefined) {\n return { status: \"pending\" };\n }\n return entry.entrySnapshot;\n }\n\n function getOrCreateDisplay(engineRef: unknown, namespaces: readonly string[]): DisplayCache {\n const id = gateId(engineRef, namespaces);\n let display = displayByGate.get(id);\n if (display === undefined) {\n display = {\n lastReady: null,\n displaySnapshot: null,\n displayKey: null,\n boundT: null,\n boundTKey: null,\n };\n displayByGate.set(id, display);\n }\n return display;\n }\n\n function bindT(\n display: DisplayCache,\n scope: ScopedScopeLike,\n locale: string,\n namespaces: readonly string[],\n scopeToken: string\n ): ScopedTranslateFn {\n const tKey = `${scopeToken}\\0${locale}\\0${namespaces.join(\"\\0\")}`;\n if (display.boundT !== null && display.boundTKey === tKey) {\n return display.boundT;\n }\n const t = createScopedT(scope, { namespaces, locale });\n display.boundT = t;\n display.boundTKey = tKey;\n return t;\n }\n\n function getDisplayEntry(\n keyInput: LoadCoordinatorKey,\n options: GetDisplayEntryOptions\n ): LoadDisplayEntry<Scope> {\n const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);\n const entry = entries.get(key);\n const display = getOrCreateDisplay(keyInput.engineRef, options.namespaces);\n const keepPrevious = options.keepPrevious !== false;\n\n const retry = () => {\n retryKey(keyInput);\n };\n\n let result: LoadDisplayEntry<Scope>;\n\n if (entry === undefined || entry.status === \"pending\") {\n const kept = keepPrevious ? display.lastReady : null;\n result = {\n status: \"pending\",\n display: kept as { scope: Scope & ScopedScopeLike; locale: string } | null,\n pendingLocale: kept ? options.locale : undefined,\n t: kept\n ? bindT(display, kept.scope, kept.locale, options.namespaces, `kept:${kept.locale}`)\n : null,\n } as LoadDisplayEntry<Scope>;\n } else if (entry.status === \"resolved\") {\n const scope = entry.resolvedScope as Scope & ScopedScopeLike;\n display.lastReady = { scope, locale: options.locale };\n result = {\n status: \"ready\",\n scope,\n locale: options.locale,\n t: bindT(display, scope, options.locale, options.namespaces, `ready:${key}`),\n } as LoadDisplayEntry<Scope>;\n } else {\n const kept = keepPrevious ? display.lastReady : null;\n result = {\n status: \"error\",\n error: entry.error,\n display: kept as { scope: Scope & ScopedScopeLike; locale: string } | null,\n pendingLocale: kept ? options.locale : undefined,\n t: kept\n ? bindT(display, kept.scope, kept.locale, options.namespaces, `kept:${kept.locale}`)\n : null,\n retry,\n } as LoadDisplayEntry<Scope>;\n }\n\n const displayKey = `${key}\\0${result.status}\\0${options.locale}\\0${String(!!result.t)}`;\n if (display.displaySnapshot !== null && display.displayKey === displayKey) {\n return display.displaySnapshot as LoadDisplayEntry<Scope>;\n }\n display.displaySnapshot = result as LoadDisplayEntry<ScopedScopeLike>;\n display.displayKey = displayKey;\n return result;\n }\n\n function retryKey(keyInput: LoadCoordinatorKey): void {\n const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);\n if (!entries.has(key)) {\n return;\n }\n entries.delete(key);\n bumpAndNotify();\n }\n\n function subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n }\n\n function getPromise(): Promise<Scope> {\n if (lastKey === null) {\n throw new Error(\"createLoadCoordinator: call ensure()/request() before getPromise()\");\n }\n const entry = entries.get(lastKey);\n if (entry === undefined) {\n throw new Error(\"createLoadCoordinator: call ensure()/request() before getPromise()\");\n }\n return entry.promise;\n }\n\n function getResolvedScope(): Scope | null {\n if (lastKey === null) {\n return null;\n }\n const entry = entries.get(lastKey);\n return entry?.status === \"resolved\" ? entry.resolvedScope : null;\n }\n\n return {\n get revision() {\n return revision;\n },\n request: ensure,\n ensure,\n getEntry,\n getDisplayEntry,\n retry: retryKey,\n subscribe,\n getPromise,\n getResolvedScope,\n };\n}\n","/**\n * Manual (non-Suspense) i18n load gate: ensure + subscribe via the load\n * coordinator, then render fallback / keep-previous / error UI.\n */\nimport {\n forwardRef,\n useSyncExternalStore,\n type ForwardedRef,\n type ForwardRefExoticComponent,\n type ReactNode,\n type RefAttributes,\n} from \"react\";\nimport type {\n LoadCoordinator,\n LoadCoordinatorRequest,\n LoadDisplayEntry,\n LoadPartition,\n ScopedScopeLike,\n ScopedTranslateFn,\n} from \"./types.js\";\n\n/** Inputs for {@link useNamespaceLoad}. */\nexport interface UseNamespaceLoadInput<Scope> {\n coordinator: LoadCoordinator<Scope>;\n engineRef: unknown;\n partition: LoadPartition;\n namespaces: readonly string[];\n locale: string;\n load: () => Promise<Scope>;\n tryResolveSync?: () => Scope | null;\n keepPrevious?: boolean;\n}\n\n/**\n * Starts (or reuses) a coordinator load and re-renders when that entry settles.\n * Kick-off is idempotent inside the snapshot getter (safe with useSyncExternalStore).\n */\nexport function useNamespaceLoad<Scope>(\n input: UseNamespaceLoadInput<Scope>\n): LoadDisplayEntry<Scope> {\n const {\n coordinator,\n engineRef,\n partition,\n namespaces,\n locale,\n load,\n tryResolveSync,\n keepPrevious,\n } = input;\n\n const request: LoadCoordinatorRequest<Scope> = {\n engineRef,\n partition,\n namespaces,\n load,\n ...(tryResolveSync !== undefined ? { tryResolveSync } : {}),\n };\n\n return useSyncExternalStore(\n coordinator.subscribe,\n () => {\n coordinator.ensure(request);\n return coordinator.getDisplayEntry(\n { engineRef, partition, namespaces },\n {\n locale,\n namespaces,\n ...(keepPrevious !== undefined ? { keepPrevious } : {}),\n }\n );\n },\n () => {\n coordinator.ensure(request);\n return coordinator.getDisplayEntry(\n { engineRef, partition, namespaces },\n {\n locale,\n namespaces,\n ...(keepPrevious !== undefined ? { keepPrevious } : {}),\n }\n );\n }\n );\n}\n\n/** Scoped `{ t, locale }` injected into gate children / HOC when ready (or kept). */\nexport interface I18nLoadGateValue {\n t: ScopedTranslateFn;\n locale: string;\n /** Set during keep-then-switch while a new partition is loading. */\n pendingLocale?: string;\n error?: unknown;\n retry?: () => void;\n}\n\n/** Load wiring from React context (coordinator, engine, partition, load). */\nexport interface I18nLoadArgs {\n coordinator: LoadCoordinator<ScopedScopeLike>;\n engineRef: unknown;\n partition: LoadPartition;\n locale: string;\n load: (namespaces: readonly string[]) => Promise<ScopedScopeLike>;\n tryResolveSync?: (namespaces: readonly string[]) => ScopedScopeLike | null;\n}\n\nexport interface CreateI18nLoadGateOptions {\n /**\n * When true (default), keep last resolved `{ t, locale }` while a new\n * partition is pending instead of showing fallback.\n */\n keepPreviousOnPartitionChange?: boolean;\n /** Hook that resolves coordinator + load inputs from React context. */\n useLoadArgs: () => I18nLoadArgs;\n}\n\nexport interface I18nGateProps {\n namespaces: readonly string[];\n fallback?: ReactNode;\n /** When set, called for error with no keep-previous display. */\n renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;\n children: (value: I18nLoadGateValue) => ReactNode;\n}\n\n/** Pending UI for `withI18n` — static node or derived from own props. */\nexport type WithI18nFallback<P extends object> = ReactNode | ((props: P) => ReactNode);\n\nexport interface I18nHocOptions<P extends object = object> {\n namespaces: readonly string[];\n fallback?: WithI18nFallback<P>;\n renderError?: (args: { error: unknown; retry: () => void; props: P }) => ReactNode;\n}\n\n/** Render fn for `withI18n`: own props, i18n bag, optional forwarded ref. */\nexport type WithI18nRender<P extends object, R = never> = (\n props: P,\n i18n: I18nLoadGateValue,\n ref?: ForwardedRef<R>\n) => ReactNode;\n\nfunction resolveNamespaces(namespaces: readonly string[] | undefined): readonly string[] {\n if (namespaces === undefined || namespaces.length === 0) {\n throw new Error(\"withI18n / I18n: requires a non-empty namespaces list\");\n }\n return namespaces;\n}\n\nfunction renderDisplayEntry(\n entry: LoadDisplayEntry<ScopedScopeLike>,\n options: {\n fallback?: ReactNode;\n renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;\n children: (value: I18nLoadGateValue) => ReactNode;\n }\n): ReactNode {\n if (entry.status === \"ready\") {\n return options.children({ t: entry.t, locale: entry.locale });\n }\n\n if (entry.status === \"error\" && entry.t === null) {\n if (options.renderError) {\n return options.renderError({ error: entry.error, retry: entry.retry });\n }\n return options.fallback ?? null;\n }\n\n if (entry.t !== null && entry.display) {\n const value: I18nLoadGateValue = {\n t: entry.t,\n locale: entry.display.locale,\n };\n if (entry.pendingLocale !== undefined) {\n value.pendingLocale = entry.pendingLocale;\n }\n if (entry.status === \"error\") {\n value.error = entry.error;\n value.retry = entry.retry;\n }\n return options.children(value);\n }\n\n return options.fallback ?? null;\n}\n\n/**\n * Factory for generic `I18n` gate + `withI18n` HOC over the same Outer.\n * Codegen emits typed wrappers; tests use this directly.\n */\nexport function createI18nLoadGate(options: CreateI18nLoadGateOptions): {\n I18n: (props: I18nGateProps) => ReactNode;\n withI18n: <P extends object = object, R = never>(\n hocOptions: I18nHocOptions<P>,\n render: WithI18nRender<P, R>\n ) => ForwardRefExoticComponent<P & RefAttributes<R>>;\n} {\n const { useLoadArgs } = options;\n const keepPrevious = options.keepPreviousOnPartitionChange !== false;\n\n function useGateEntry(namespaces: readonly string[]): LoadDisplayEntry<ScopedScopeLike> {\n const resolved = resolveNamespaces(namespaces);\n const args = useLoadArgs();\n return useNamespaceLoad({\n coordinator: args.coordinator,\n engineRef: args.engineRef,\n partition: args.partition,\n namespaces: resolved,\n locale: args.locale,\n load: () => args.load(resolved),\n ...(args.tryResolveSync !== undefined\n ? { tryResolveSync: () => args.tryResolveSync!(resolved) }\n : {}),\n keepPrevious,\n });\n }\n\n function I18n({ namespaces, fallback, renderError, children }: I18nGateProps): ReactNode {\n const entry = useGateEntry(namespaces);\n return renderDisplayEntry(entry, {\n ...(fallback !== undefined ? { fallback } : {}),\n ...(renderError !== undefined ? { renderError } : {}),\n children,\n });\n }\n\n function withI18n<P extends object = object, R = never>(\n hocOptions: I18nHocOptions<P>,\n render: WithI18nRender<P, R>\n ): ForwardRefExoticComponent<P & RefAttributes<R>> {\n const { fallback, renderError } = hocOptions;\n const Outer = forwardRef<R, P>(function I18nHocOuter(props, ref) {\n const entry = useGateEntry(hocOptions.namespaces);\n\n const needsFallback = entry.status !== \"ready\" && entry.t === null;\n const resolvedFallback =\n !needsFallback || fallback === undefined\n ? undefined\n : typeof fallback === \"function\"\n ? fallback(props as P)\n : fallback;\n\n return renderDisplayEntry(entry, {\n ...(resolvedFallback !== undefined ? { fallback: resolvedFallback } : {}),\n ...(renderError !== undefined\n ? {\n renderError: ({ error, retry }) => renderError({ error, retry, props: props as P }),\n }\n : {}),\n children: (value) => render(props as P, value, ref),\n });\n });\n\n const displayName = render.name || \"Component\";\n Outer.displayName = `withI18n(${displayName})`;\n return Outer as ForwardRefExoticComponent<P & RefAttributes<R>>;\n }\n\n return { I18n, withI18n };\n}\n","/**\n * Holds the shared i18n root: handle + coordinator + active locale.\n *\n * Generated {@link useI18nRoot} casts the handle to the project's typed factory return.\n */\nimport { createContext, useContext } from \"react\";\nimport type { I18nRootContextValue } from \"./types.js\";\n\nconst I18nRootContext = createContext<I18nRootContextValue | null>(null);\n\n/** Resolves the shared root from an ancestor {@link I18nRootProvider}. */\nexport function useI18nRootContext(): I18nRootContextValue {\n const value = useContext(I18nRootContext);\n if (value === null) {\n throw new Error(\"useI18nRoot must be used within I18nRoot / I18nRootProvider\");\n }\n return value;\n}\n\n/** Internal — provider wiring. */\nexport { I18nRootContext };\n","/**\n * Single provider that shares handle + coordinator + locale across a lazy subtree.\n */\nimport { useRef, type ReactNode } from \"react\";\nimport type { FetchArtifact, I18nCreateInput, OnMissingTranslation } from \"@xndrjs/i18n\";\nimport { createLoadCoordinator } from \"./create-load-coordinator.js\";\nimport { I18nRootContext } from \"./root-context.js\";\nimport type {\n CreateI18nFactory,\n I18nHandleLike,\n I18nRootContextValue,\n ScopedScopeLike,\n} from \"./types.js\";\n\nexport interface I18nRootProviderProps {\n createI18n: CreateI18nFactory;\n locale: string;\n /**\n * Cold-start / eager dictionary seed (defaults to `{}` when neither prop is set).\n * Mutually exclusive with {@link state}.\n */\n dictionary?: Record<string, unknown>;\n /** Full create/hydrate input from `handle.serialize()` — mutually exclusive with {@link dictionary}. */\n state?: I18nCreateInput | undefined;\n onMissing?: OnMissingTranslation;\n /** Custom JSON loader for `loaderStrategy: \"fetch\"` projects (ignored by import loaders). */\n fetchImpl?: FetchArtifact;\n children: ReactNode;\n}\n\n/** Creates and shares one handle + load coordinator for descendant gates. */\nexport function I18nRootProvider({\n createI18n,\n locale,\n dictionary,\n state,\n onMissing,\n fetchImpl,\n children,\n}: I18nRootProviderProps): ReactNode {\n if (state !== undefined && dictionary !== undefined) {\n throw new Error(\"I18nRootProvider: pass either `state` or `dictionary`, not both.\");\n }\n\n const valueRef = useRef<I18nRootContextValue | null>(null);\n\n if (valueRef.current === null) {\n const seed: I18nCreateInput =\n state ?? ({ dictionary: dictionary ?? {} } satisfies I18nCreateInput);\n const handle = createI18n({\n state: seed,\n ...(onMissing !== undefined ? { onMissing } : {}),\n ...(fetchImpl !== undefined ? { fetchImpl } : {}),\n });\n valueRef.current = {\n handle: handle as I18nHandleLike,\n coordinator: createLoadCoordinator<ScopedScopeLike>(),\n locale,\n };\n } else {\n valueRef.current.locale = locale;\n }\n\n return <I18nRootContext.Provider value={valueRef.current}>{children}</I18nRootContext.Provider>;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/create-scoped-t.ts","../src/create-load-coordinator.ts","../src/namespace-load-gate.tsx","../src/root-context.tsx","../src/root-provider.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Builds the scoped `t` function used by providers and load gates.\n *\n * Takes a loaded `@xndrjs/i18n` scope and returns a capability-safe translator:\n * namespaces outside the provider list throw at runtime. Locale is always bound.\n */\nimport type { CreateScopedTOptions, ScopedScopeLike, ScopedTranslateFn } from \"./types.js\";\n\nfunction assertNamespace(namespace: string, namespaces: readonly string[]): void {\n if (!namespaces.includes(namespace)) {\n throw new Error(\n `Namespace \"${namespace}\" is not loaded by this provider. Allowed: [${namespaces.join(\", \")}]`\n );\n }\n}\n\nfunction resolveActiveScope(scope: ScopedScopeLike, locale: string): ScopedScopeLike {\n if (typeof scope.forLocale === \"function\" && scope.locale !== locale) {\n return scope.forLocale(locale);\n }\n return scope;\n}\n\n/**\n * Wraps a loaded scope `t` with namespace guards and locale binding.\n */\nexport function createScopedT(\n scope: ScopedScopeLike,\n options: CreateScopedTOptions\n): ScopedTranslateFn {\n const activeScope = resolveActiveScope(scope, options.locale);\n const { namespaces } = options;\n\n return ((namespace: string, key: string, ...rest: unknown[]) => {\n assertNamespace(namespace, namespaces);\n return activeScope.t(namespace, key, ...rest);\n }) as ScopedTranslateFn;\n}\n\n/**\n * Curries a multi-namespace context `t(ns, key, …)` into a namespace-bound `t(key, …)`.\n * Used when composing gate values without introducing a separate hooks API.\n */\nexport function bindNamespaceTranslate(\n t: ScopedTranslateFn,\n namespace: string\n): (key: string, params?: Record<string, unknown>) => string {\n return (key: string, params?: Record<string, unknown>) =>\n params === undefined ? t(namespace, key) : t(namespace, key, params);\n}\n","/**\n * Load deduplication for lazy i18n namespace gates / HOCs.\n *\n * Supports multiple concurrent keys so parallel per-namespace gates can share\n * one coordinator. Each `(engine, partition, namespaces)` entry keeps its own\n * promise and status. Display state (keep-previous / pendingLocale / stable `t`)\n * lives here so gate components stay pure observers of `useSyncExternalStore`.\n */\nimport { createScopedT } from \"./create-scoped-t.js\";\nimport type {\n GetDisplayEntryOptions,\n LoadCoordinator,\n LoadCoordinatorEntry,\n LoadCoordinatorKey,\n LoadCoordinatorRequest,\n LoadDisplayEntry,\n ScopedScopeLike,\n ScopedTranslateFn,\n} from \"./types.js\";\n\nconst engineIds = new WeakMap<object, number>();\nlet nextEngineId = 1;\n\nfunction engineId(ref: unknown): number {\n if (ref === null || (typeof ref !== \"object\" && typeof ref !== \"function\")) {\n return 0;\n }\n const obj = ref as object;\n let id = engineIds.get(obj);\n if (id === undefined) {\n id = nextEngineId++;\n engineIds.set(obj, id);\n }\n return id;\n}\n\nfunction cacheKey(\n engineRef: unknown,\n partition: string | undefined,\n namespaces: readonly string[]\n): string {\n return `${engineId(engineRef)}\\0${partition ?? \"\"}\\0${namespaces.join(\"\\0\")}`;\n}\n\n/** Gate identity for keep-previous (namespaces only — partition/locale can change). */\nfunction gateId(engineRef: unknown, namespaces: readonly string[]): string {\n return `${engineId(engineRef)}\\0${namespaces.join(\"\\0\")}`;\n}\n\ninterface CacheEntry<Scope> {\n status: \"pending\" | \"resolved\" | \"error\";\n promise: Promise<Scope>;\n resolvedScope: Scope | null;\n error: unknown;\n requestId: number;\n /** Stable LoadCoordinatorEntry snapshot for getEntry(). */\n entrySnapshot: LoadCoordinatorEntry<Scope>;\n}\n\ninterface DisplayCache {\n lastReady: { scope: ScopedScopeLike; locale: string } | null;\n /** Stable display entry object until inputs change. */\n displaySnapshot: LoadDisplayEntry<ScopedScopeLike> | null;\n /** Identity key for the last displaySnapshot. */\n displayKey: string | null;\n boundT: ScopedTranslateFn | null;\n boundTKey: string | null;\n}\n\n/**\n * Dedupes in-flight loads by `(engineRef, partition, namespaces)` and exposes\n * display snapshots + retry for manual pending/resolved/error rendering.\n */\nexport function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinator<Scope> {\n let revision = 0;\n let nextRequestId = 0;\n const entries = new Map<string, CacheEntry<Scope>>();\n const displayByGate = new Map<string, DisplayCache>();\n const listeners = new Set<() => void>();\n let lastKey: string | null = null;\n\n function notify(): void {\n for (const listener of listeners) {\n listener();\n }\n }\n\n function bumpAndNotify(): void {\n revision += 1;\n // Invalidate display snapshots that depend on entry status.\n for (const display of displayByGate.values()) {\n display.displaySnapshot = null;\n display.displayKey = null;\n }\n notify();\n }\n\n function markResolvedSync(key: string, syncScope: Scope): void {\n const entrySnapshot: LoadCoordinatorEntry<Scope> = {\n status: \"resolved\",\n scope: syncScope,\n };\n entries.set(key, {\n status: \"resolved\",\n promise: Promise.resolve(syncScope),\n resolvedScope: syncScope,\n error: null,\n requestId: ++nextRequestId,\n entrySnapshot,\n });\n }\n\n /**\n * SSR / getServerSnapshot: hydrate from peek/state only — never kick `load()`.\n */\n function ensureSync(input: LoadCoordinatorRequest<Scope>): void {\n const key = cacheKey(input.engineRef, input.partition, input.namespaces);\n lastKey = key;\n\n if (entries.has(key)) {\n return;\n }\n\n if (input.tryResolveSync === undefined) {\n return;\n }\n\n const syncScope = input.tryResolveSync();\n if (syncScope !== null) {\n markResolvedSync(key, syncScope);\n }\n }\n\n function ensure(input: LoadCoordinatorRequest<Scope>): void {\n const key = cacheKey(input.engineRef, input.partition, input.namespaces);\n lastKey = key;\n\n const existing = entries.get(key);\n if (existing !== undefined) {\n return;\n }\n\n if (input.tryResolveSync !== undefined) {\n const syncScope = input.tryResolveSync();\n if (syncScope !== null) {\n markResolvedSync(key, syncScope);\n return;\n }\n }\n\n const requestId = ++nextRequestId;\n const promise = input.load().then(\n (scope) => {\n const entry = entries.get(key);\n if (entry === undefined || entry.requestId !== requestId) {\n return scope;\n }\n entry.status = \"resolved\";\n entry.resolvedScope = scope;\n entry.error = null;\n entry.entrySnapshot = { status: \"resolved\", scope };\n bumpAndNotify();\n return scope;\n },\n (error: unknown) => {\n const entry = entries.get(key);\n if (entry !== undefined && entry.requestId === requestId) {\n entry.status = \"error\";\n entry.error = error;\n entry.resolvedScope = null;\n entry.entrySnapshot = { status: \"error\", error };\n bumpAndNotify();\n }\n throw error;\n }\n );\n // Avoid unhandled rejection when only gate subscribers attach (no getPromise).\n void promise.catch((error: unknown) => {\n console.error(\"[i18n-react] namespace load failed:\", error);\n });\n\n entries.set(key, {\n status: \"pending\",\n promise,\n resolvedScope: null,\n error: null,\n requestId,\n entrySnapshot: { status: \"pending\" },\n });\n }\n\n function getEntry(keyInput: LoadCoordinatorKey): LoadCoordinatorEntry<Scope> {\n const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);\n const entry = entries.get(key);\n if (entry === undefined) {\n return { status: \"pending\" };\n }\n return entry.entrySnapshot;\n }\n\n function getOrCreateDisplay(engineRef: unknown, namespaces: readonly string[]): DisplayCache {\n const id = gateId(engineRef, namespaces);\n let display = displayByGate.get(id);\n if (display === undefined) {\n display = {\n lastReady: null,\n displaySnapshot: null,\n displayKey: null,\n boundT: null,\n boundTKey: null,\n };\n displayByGate.set(id, display);\n }\n return display;\n }\n\n function bindT(\n display: DisplayCache,\n scope: ScopedScopeLike,\n locale: string,\n namespaces: readonly string[],\n scopeToken: string\n ): ScopedTranslateFn {\n const tKey = `${scopeToken}\\0${locale}\\0${namespaces.join(\"\\0\")}`;\n if (display.boundT !== null && display.boundTKey === tKey) {\n return display.boundT;\n }\n const t = createScopedT(scope, { namespaces, locale });\n display.boundT = t;\n display.boundTKey = tKey;\n return t;\n }\n\n function getDisplayEntry(\n keyInput: LoadCoordinatorKey,\n options: GetDisplayEntryOptions\n ): LoadDisplayEntry<Scope> {\n const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);\n const entry = entries.get(key);\n const display = getOrCreateDisplay(keyInput.engineRef, options.namespaces);\n const keepPrevious = options.keepPrevious !== false;\n\n const retry = () => {\n retryKey(keyInput);\n };\n\n let result: LoadDisplayEntry<Scope>;\n\n if (entry === undefined || entry.status === \"pending\") {\n const kept = keepPrevious ? display.lastReady : null;\n result = {\n status: \"pending\",\n display: kept as { scope: Scope & ScopedScopeLike; locale: string } | null,\n pendingLocale: kept ? options.locale : undefined,\n t: kept\n ? bindT(display, kept.scope, kept.locale, options.namespaces, `kept:${kept.locale}`)\n : null,\n } as LoadDisplayEntry<Scope>;\n } else if (entry.status === \"resolved\") {\n const scope = entry.resolvedScope as Scope & ScopedScopeLike;\n display.lastReady = { scope, locale: options.locale };\n result = {\n status: \"ready\",\n scope,\n locale: options.locale,\n t: bindT(display, scope, options.locale, options.namespaces, `ready:${key}`),\n } as LoadDisplayEntry<Scope>;\n } else {\n const kept = keepPrevious ? display.lastReady : null;\n result = {\n status: \"error\",\n error: entry.error,\n display: kept as { scope: Scope & ScopedScopeLike; locale: string } | null,\n pendingLocale: kept ? options.locale : undefined,\n t: kept\n ? bindT(display, kept.scope, kept.locale, options.namespaces, `kept:${kept.locale}`)\n : null,\n retry,\n } as LoadDisplayEntry<Scope>;\n }\n\n const displayKey = `${key}\\0${result.status}\\0${options.locale}\\0${String(!!result.t)}`;\n if (display.displaySnapshot !== null && display.displayKey === displayKey) {\n return display.displaySnapshot as LoadDisplayEntry<Scope>;\n }\n display.displaySnapshot = result as LoadDisplayEntry<ScopedScopeLike>;\n display.displayKey = displayKey;\n return result;\n }\n\n function retryKey(keyInput: LoadCoordinatorKey): void {\n const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);\n if (!entries.has(key)) {\n return;\n }\n entries.delete(key);\n bumpAndNotify();\n }\n\n function subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n }\n\n function getPromise(): Promise<Scope> {\n if (lastKey === null) {\n throw new Error(\"createLoadCoordinator: call ensure()/request() before getPromise()\");\n }\n const entry = entries.get(lastKey);\n if (entry === undefined) {\n throw new Error(\"createLoadCoordinator: call ensure()/request() before getPromise()\");\n }\n return entry.promise;\n }\n\n function getResolvedScope(): Scope | null {\n if (lastKey === null) {\n return null;\n }\n const entry = entries.get(lastKey);\n return entry?.status === \"resolved\" ? entry.resolvedScope : null;\n }\n\n return {\n get revision() {\n return revision;\n },\n request: ensure,\n ensure,\n ensureSync,\n getEntry,\n getDisplayEntry,\n retry: retryKey,\n subscribe,\n getPromise,\n getResolvedScope,\n };\n}\n","/**\n * Manual (non-Suspense) i18n load gate: ensure + subscribe via the load\n * coordinator, then render fallback / keep-previous / error UI / throw.\n */\nimport {\n forwardRef,\n useSyncExternalStore,\n type ForwardedRef,\n type ForwardRefExoticComponent,\n type ReactNode,\n type RefAttributes,\n} from \"react\";\nimport type {\n LoadCoordinator,\n LoadCoordinatorRequest,\n LoadDisplayEntry,\n LoadPartition,\n ScopedScopeLike,\n ScopedTranslateFn,\n} from \"./types.js\";\n\n/** Inputs for {@link useNamespaceLoad}. */\nexport interface UseNamespaceLoadInput<Scope> {\n coordinator: LoadCoordinator<Scope>;\n engineRef: unknown;\n partition: LoadPartition;\n namespaces: readonly string[];\n locale: string;\n load: () => Promise<Scope>;\n tryResolveSync?: () => Scope | null;\n keepPrevious?: boolean;\n}\n\n/**\n * Starts (or reuses) a coordinator load and re-renders when that entry settles.\n * Client getSnapshot may kick `load()`; SSR getServerSnapshot only peeks sync\n * (hydrated `state`) and never starts a client fetch during prerender.\n */\nexport function useNamespaceLoad<Scope>(\n input: UseNamespaceLoadInput<Scope>\n): LoadDisplayEntry<Scope> {\n const {\n coordinator,\n engineRef,\n partition,\n namespaces,\n locale,\n load,\n tryResolveSync,\n keepPrevious,\n } = input;\n\n const request: LoadCoordinatorRequest<Scope> = {\n engineRef,\n partition,\n namespaces,\n load,\n ...(tryResolveSync !== undefined ? { tryResolveSync } : {}),\n };\n\n const displayOptions = {\n locale,\n namespaces,\n ...(keepPrevious !== undefined ? { keepPrevious } : {}),\n };\n\n return useSyncExternalStore(\n coordinator.subscribe,\n () => {\n coordinator.ensure(request);\n return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);\n },\n () => {\n coordinator.ensureSync(request);\n return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);\n }\n );\n}\n\n/** Scoped `{ t, locale }` injected into gate children / HOC when ready (or kept). */\nexport interface I18nLoadGateValue {\n t: ScopedTranslateFn;\n locale: string;\n /** Set during keep-then-switch while a new partition is loading. */\n pendingLocale?: string;\n error?: unknown;\n retry?: () => void;\n}\n\n/** Load wiring from React context (coordinator, engine, partition, load). */\nexport interface I18nLoadArgs {\n coordinator: LoadCoordinator<ScopedScopeLike>;\n engineRef: unknown;\n partition: LoadPartition;\n locale: string;\n load: (namespaces: readonly string[]) => Promise<ScopedScopeLike>;\n tryResolveSync?: (namespaces: readonly string[]) => ScopedScopeLike | null;\n}\n\nexport interface CreateI18nLoadGateOptions {\n /**\n * When true (default), keep last resolved `{ t, locale }` while a new\n * partition is pending instead of showing fallback.\n */\n keepPreviousOnPartitionChange?: boolean;\n /** Hook that resolves coordinator + load inputs from React context. */\n useLoadArgs: () => I18nLoadArgs;\n}\n\nexport interface I18nGateProps {\n namespaces: readonly string[];\n /** Shown only while the load is pending (not on error). */\n fallback?: ReactNode;\n /**\n * When set, called for error with no keep-previous display.\n * Otherwise the load error is thrown for a React error boundary.\n */\n renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;\n children: (value: I18nLoadGateValue) => ReactNode;\n}\n\n/** Pending UI for `withI18n` — static node or derived from own props. */\nexport type WithI18nFallback<P extends object> = ReactNode | ((props: P) => ReactNode);\n\nexport interface I18nHocOptions<P extends object = object> {\n namespaces: readonly string[];\n fallback?: WithI18nFallback<P>;\n renderError?: (args: { error: unknown; retry: () => void; props: P }) => ReactNode;\n}\n\n/** Render fn for `withI18n`: own props, i18n bag, optional forwarded ref. */\nexport type WithI18nRender<P extends object, R = never> = (\n props: P,\n i18n: I18nLoadGateValue,\n ref?: ForwardedRef<R>\n) => ReactNode;\n\nfunction resolveNamespaces(namespaces: readonly string[] | undefined): readonly string[] {\n if (namespaces === undefined || namespaces.length === 0) {\n throw new Error(\"withI18n / I18n: requires a non-empty namespaces list\");\n }\n return namespaces;\n}\n\nfunction toThrownError(error: unknown): Error {\n if (error instanceof Error) {\n return error;\n }\n if (typeof error === \"string\") {\n return new Error(error);\n }\n return new Error(\"i18n namespace load failed\");\n}\n\nconst noopT: ScopedTranslateFn = () => \"\";\n\n/** Gate value for display, or `null` when only fallback / error UI should show. */\nfunction gateValueFromEntry(entry: LoadDisplayEntry<ScopedScopeLike>): I18nLoadGateValue | null {\n if (entry.status === \"ready\") {\n return { t: entry.t, locale: entry.locale };\n }\n\n if (entry.t !== null && entry.display) {\n const value: I18nLoadGateValue = {\n t: entry.t,\n locale: entry.display.locale,\n };\n if (entry.pendingLocale !== undefined) {\n value.pendingLocale = entry.pendingLocale;\n }\n if (entry.status === \"error\") {\n value.error = entry.error;\n value.retry = entry.retry;\n }\n return value;\n }\n\n return null;\n}\n\nfunction renderDisplayEntry(\n entry: LoadDisplayEntry<ScopedScopeLike>,\n options: {\n fallback?: ReactNode;\n renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;\n children: (value: I18nLoadGateValue) => ReactNode;\n }\n): ReactNode {\n if (entry.status === \"error\" && entry.t === null) {\n if (options.renderError) {\n return options.renderError({ error: entry.error, retry: entry.retry! });\n }\n throw toThrownError(entry.error);\n }\n\n const value = gateValueFromEntry(entry);\n if (value !== null) {\n return options.children(value);\n }\n\n return options.fallback ?? null;\n}\n\n/**\n * Factory for generic `I18n` gate + `withI18n` HOC over the same Outer.\n * Codegen emits typed wrappers; tests use this directly.\n */\nexport function createI18nLoadGate(options: CreateI18nLoadGateOptions): {\n I18n: (props: I18nGateProps) => ReactNode;\n withI18n: <P extends object = object, R = never>(\n hocOptions: I18nHocOptions<P>,\n render: WithI18nRender<P, R>\n ) => ForwardRefExoticComponent<P & RefAttributes<R>>;\n} {\n const { useLoadArgs } = options;\n const keepPrevious = options.keepPreviousOnPartitionChange !== false;\n\n function useGateLoad(namespaces: readonly string[]): {\n entry: LoadDisplayEntry<ScopedScopeLike>;\n locale: string;\n } {\n const resolved = resolveNamespaces(namespaces);\n const args = useLoadArgs();\n const entry = useNamespaceLoad({\n coordinator: args.coordinator,\n engineRef: args.engineRef,\n partition: args.partition,\n namespaces: resolved,\n locale: args.locale,\n load: () => args.load(resolved),\n ...(args.tryResolveSync !== undefined\n ? { tryResolveSync: () => args.tryResolveSync!(resolved) }\n : {}),\n keepPrevious,\n });\n return { entry, locale: args.locale };\n }\n\n function I18n({ namespaces, fallback, renderError, children }: I18nGateProps): ReactNode {\n const { entry } = useGateLoad(namespaces);\n return renderDisplayEntry(entry, {\n ...(fallback !== undefined ? { fallback } : {}),\n ...(renderError !== undefined ? { renderError } : {}),\n children,\n });\n }\n\n function withI18n<P extends object = object, R = never>(\n hocOptions: I18nHocOptions<P>,\n render: WithI18nRender<P, R>\n ): ForwardRefExoticComponent<P & RefAttributes<R>> {\n const { fallback, renderError } = hocOptions;\n const Outer = forwardRef<R, P>(function I18nHocOuter(props, ref) {\n const { entry, locale } = useGateLoad(hocOptions.namespaces);\n\n // Always invoke `render` so hooks inside it are Outer hooks and stay\n // unconditional across pending → ready (e.g. I18nRoot without hydrated state).\n const displayValue = gateValueFromEntry(entry);\n const value = displayValue ?? ({ t: noopT, locale } satisfies I18nLoadGateValue);\n const rendered = render(props as P, value, ref);\n\n if (entry.status === \"error\" && entry.t === null) {\n if (renderError) {\n return renderError({ error: entry.error, retry: entry.retry!, props: props as P });\n }\n throw toThrownError(entry.error);\n }\n\n if (displayValue !== null) {\n return rendered;\n }\n\n if (fallback === undefined) {\n return null;\n }\n return typeof fallback === \"function\" ? fallback(props as P) : fallback;\n });\n\n const displayName = render.name || \"Component\";\n Outer.displayName = `withI18n(${displayName})`;\n return Outer as ForwardRefExoticComponent<P & RefAttributes<R>>;\n }\n\n return { I18n, withI18n };\n}\n","/**\n * Holds the shared i18n root: handle + coordinator + active locale.\n *\n * Generated {@link useI18nRoot} casts the handle to the project's typed factory return.\n */\nimport { createContext, useContext } from \"react\";\nimport type { I18nRootContextValue } from \"./types.js\";\n\nconst I18nRootContext = createContext<I18nRootContextValue | null>(null);\n\n/** Resolves the shared root from an ancestor {@link I18nRootProvider}. */\nexport function useI18nRootContext(): I18nRootContextValue {\n const value = useContext(I18nRootContext);\n if (value === null) {\n throw new Error(\"useI18nRoot must be used within I18nRoot / I18nRootProvider\");\n }\n return value;\n}\n\n/** Internal — provider wiring. */\nexport { I18nRootContext };\n","/**\n * Single provider that shares handle + coordinator + locale across a lazy subtree.\n */\nimport { useRef, type ReactNode } from \"react\";\nimport type { FetchArtifact, I18nCreateInput, OnMissingTranslation } from \"@xndrjs/i18n\";\nimport { createLoadCoordinator } from \"./create-load-coordinator.js\";\nimport { I18nRootContext } from \"./root-context.js\";\nimport type {\n CreateI18nFactory,\n I18nHandleLike,\n I18nRootContextValue,\n ScopedScopeLike,\n} from \"./types.js\";\n\nexport interface I18nRootProviderProps {\n createI18n: CreateI18nFactory;\n locale: string;\n /**\n * Cold-start / eager dictionary seed (defaults to `{}` when neither prop is set).\n * Mutually exclusive with {@link state}.\n */\n dictionary?: Record<string, unknown>;\n /** Full create/hydrate input from `handle.serialize()` — mutually exclusive with {@link dictionary}. */\n state?: I18nCreateInput | undefined;\n onMissing?: OnMissingTranslation;\n /** Custom JSON loader for `loaderStrategy: \"fetch\"` projects (ignored by import loaders). */\n fetchImpl?: FetchArtifact;\n children: ReactNode;\n}\n\n/** Creates and shares one handle + load coordinator for descendant gates. */\nexport function I18nRootProvider({\n createI18n,\n locale,\n dictionary,\n state,\n onMissing,\n fetchImpl,\n children,\n}: I18nRootProviderProps): ReactNode {\n if (state !== undefined && dictionary !== undefined) {\n throw new Error(\"I18nRootProvider: pass either `state` or `dictionary`, not both.\");\n }\n\n const valueRef = useRef<I18nRootContextValue | null>(null);\n\n if (valueRef.current === null) {\n const seed: I18nCreateInput =\n state ?? ({ dictionary: dictionary ?? {} } satisfies I18nCreateInput);\n const handle = createI18n({\n state: seed,\n ...(onMissing !== undefined ? { onMissing } : {}),\n ...(fetchImpl !== undefined ? { fetchImpl } : {}),\n });\n valueRef.current = {\n handle: handle as I18nHandleLike,\n coordinator: createLoadCoordinator<ScopedScopeLike>(),\n locale,\n };\n } else {\n valueRef.current.locale = locale;\n }\n\n return <I18nRootContext.Provider value={valueRef.current}>{children}</I18nRootContext.Provider>;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xndrjs/i18n-react",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "React runtime primitives and codegen for @xndrjs/i18n client bindings.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=18"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@xndrjs/i18n": "^0.8.
|
|
33
|
+
"@xndrjs/i18n": "^0.8.1",
|
|
34
34
|
"react": ">=19",
|
|
35
35
|
"tsx": ">=4",
|
|
36
36
|
"zod": "^4.0.0"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"tsx": "^4.22.4",
|
|
47
47
|
"vitest": "^4.1.0",
|
|
48
48
|
"zod": "^4.3.6",
|
|
49
|
-
"@xndrjs/i18n": "^0.8.
|
|
49
|
+
"@xndrjs/i18n": "^0.8.1"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsup",
|
|
@@ -78,6 +78,44 @@ describe("createLoadCoordinator", () => {
|
|
|
78
78
|
expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: 42 });
|
|
79
79
|
});
|
|
80
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
|
+
|
|
81
119
|
it("resolves synchronously when tryResolveSync returns a scope", () => {
|
|
82
120
|
const coordinator = createLoadCoordinator<string>();
|
|
83
121
|
const load = vi.fn(() => Promise.resolve("async"));
|
|
@@ -224,6 +262,32 @@ describe("createLoadCoordinator", () => {
|
|
|
224
262
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
225
263
|
});
|
|
226
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
|
+
|
|
227
291
|
it("keeps an error entry and notifies subscribers on reject", async () => {
|
|
228
292
|
const coordinator = createLoadCoordinator<string>();
|
|
229
293
|
const pending = deferred<string>();
|
|
@@ -95,6 +95,42 @@ export function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinato
|
|
|
95
95
|
notify();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function markResolvedSync(key: string, syncScope: Scope): void {
|
|
99
|
+
const entrySnapshot: LoadCoordinatorEntry<Scope> = {
|
|
100
|
+
status: "resolved",
|
|
101
|
+
scope: syncScope,
|
|
102
|
+
};
|
|
103
|
+
entries.set(key, {
|
|
104
|
+
status: "resolved",
|
|
105
|
+
promise: Promise.resolve(syncScope),
|
|
106
|
+
resolvedScope: syncScope,
|
|
107
|
+
error: null,
|
|
108
|
+
requestId: ++nextRequestId,
|
|
109
|
+
entrySnapshot,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* SSR / getServerSnapshot: hydrate from peek/state only — never kick `load()`.
|
|
115
|
+
*/
|
|
116
|
+
function ensureSync(input: LoadCoordinatorRequest<Scope>): void {
|
|
117
|
+
const key = cacheKey(input.engineRef, input.partition, input.namespaces);
|
|
118
|
+
lastKey = key;
|
|
119
|
+
|
|
120
|
+
if (entries.has(key)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (input.tryResolveSync === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const syncScope = input.tryResolveSync();
|
|
129
|
+
if (syncScope !== null) {
|
|
130
|
+
markResolvedSync(key, syncScope);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
function ensure(input: LoadCoordinatorRequest<Scope>): void {
|
|
99
135
|
const key = cacheKey(input.engineRef, input.partition, input.namespaces);
|
|
100
136
|
lastKey = key;
|
|
@@ -107,18 +143,7 @@ export function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinato
|
|
|
107
143
|
if (input.tryResolveSync !== undefined) {
|
|
108
144
|
const syncScope = input.tryResolveSync();
|
|
109
145
|
if (syncScope !== null) {
|
|
110
|
-
|
|
111
|
-
status: "resolved",
|
|
112
|
-
scope: syncScope,
|
|
113
|
-
};
|
|
114
|
-
entries.set(key, {
|
|
115
|
-
status: "resolved",
|
|
116
|
-
promise: Promise.resolve(syncScope),
|
|
117
|
-
resolvedScope: syncScope,
|
|
118
|
-
error: null,
|
|
119
|
-
requestId: ++nextRequestId,
|
|
120
|
-
entrySnapshot,
|
|
121
|
-
});
|
|
146
|
+
markResolvedSync(key, syncScope);
|
|
122
147
|
return;
|
|
123
148
|
}
|
|
124
149
|
}
|
|
@@ -149,8 +174,10 @@ export function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinato
|
|
|
149
174
|
throw error;
|
|
150
175
|
}
|
|
151
176
|
);
|
|
152
|
-
//
|
|
153
|
-
void promise.catch(() =>
|
|
177
|
+
// Avoid unhandled rejection when only gate subscribers attach (no getPromise).
|
|
178
|
+
void promise.catch((error: unknown) => {
|
|
179
|
+
console.error("[i18n-react] namespace load failed:", error);
|
|
180
|
+
});
|
|
154
181
|
|
|
155
182
|
entries.set(key, {
|
|
156
183
|
status: "pending",
|
|
@@ -302,6 +329,7 @@ export function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinato
|
|
|
302
329
|
},
|
|
303
330
|
request: ensure,
|
|
304
331
|
ensure,
|
|
332
|
+
ensureSync,
|
|
305
333
|
getEntry,
|
|
306
334
|
getDisplayEntry,
|
|
307
335
|
retry: retryKey,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { act, useState } from "react";
|
|
1
|
+
import { act, Component, type ReactNode, useState } from "react";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
2
3
|
import { createI18nHandle, IcuTranslationProviderMulti } from "@xndrjs/i18n";
|
|
3
4
|
import { render, screen } from "@testing-library/react";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
6
|
import { createLoadCoordinator } from "./create-load-coordinator.js";
|
|
6
7
|
import { createI18nLoadGate, useNamespaceLoad, type I18nLoadArgs } from "./namespace-load-gate.js";
|
|
7
8
|
import type { ScopedScopeLike } from "./types.js";
|
|
@@ -14,6 +15,11 @@ type MultiParams = {
|
|
|
14
15
|
default: { greeting: never };
|
|
15
16
|
billing: { invoice: never };
|
|
16
17
|
};
|
|
18
|
+
type TestLocale = "en" | "it";
|
|
19
|
+
|
|
20
|
+
function asScopedScope(scope: unknown): ScopedScopeLike {
|
|
21
|
+
return scope as ScopedScopeLike;
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
function deferred<T>() {
|
|
19
25
|
let resolve!: (value: T) => void;
|
|
@@ -25,6 +31,24 @@ function deferred<T>() {
|
|
|
25
31
|
return { promise, resolve, reject };
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
class TestErrorBoundary extends Component<
|
|
35
|
+
{ children: ReactNode; fallback: ReactNode },
|
|
36
|
+
{ error: Error | null }
|
|
37
|
+
> {
|
|
38
|
+
override state = { error: null as Error | null };
|
|
39
|
+
|
|
40
|
+
static getDerivedStateFromError(error: Error) {
|
|
41
|
+
return { error };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override render() {
|
|
45
|
+
if (this.state.error !== null) {
|
|
46
|
+
return this.props.fallback;
|
|
47
|
+
}
|
|
48
|
+
return this.props.children;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
function createTestHandle() {
|
|
29
53
|
const engine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>({});
|
|
30
54
|
return createI18nHandle(engine, {
|
|
@@ -41,7 +65,7 @@ function createTestHandle() {
|
|
|
41
65
|
|
|
42
66
|
function createGateFixture(options?: {
|
|
43
67
|
load?: (namespaces: readonly string[]) => Promise<ScopedScopeLike>;
|
|
44
|
-
locale?:
|
|
68
|
+
locale?: TestLocale;
|
|
45
69
|
keepPreviousOnPartitionChange?: boolean;
|
|
46
70
|
}) {
|
|
47
71
|
const handle = createTestHandle();
|
|
@@ -50,7 +74,7 @@ function createGateFixture(options?: {
|
|
|
50
74
|
const load =
|
|
51
75
|
options?.load ??
|
|
52
76
|
((namespaces: readonly string[]) =>
|
|
53
|
-
handle.load({ namespaces: [...namespaces] as ["default"], locale
|
|
77
|
+
handle.load({ namespaces: [...namespaces] as ["default"], locale }).then(asScopedScope));
|
|
54
78
|
|
|
55
79
|
const { I18n, withI18n } = createI18nLoadGate({
|
|
56
80
|
...(options?.keepPreviousOnPartitionChange !== undefined
|
|
@@ -91,12 +115,58 @@ describe("useNamespaceLoad", () => {
|
|
|
91
115
|
|
|
92
116
|
const scope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
93
117
|
await act(async () => {
|
|
94
|
-
pending.resolve(scope);
|
|
118
|
+
pending.resolve(asScopedScope(scope));
|
|
95
119
|
await pending.promise;
|
|
96
120
|
});
|
|
97
121
|
|
|
98
122
|
expect(screen.getByTestId("status").textContent).toBe("ready");
|
|
99
123
|
});
|
|
124
|
+
|
|
125
|
+
it("SSR getServerSnapshot does not call load without sync seed", () => {
|
|
126
|
+
const load = vi.fn(() => Promise.resolve(asScopedScope({ t: () => "" })));
|
|
127
|
+
const handle = createTestHandle();
|
|
128
|
+
const coordinator = createLoadCoordinator<ScopedScopeLike>();
|
|
129
|
+
|
|
130
|
+
function Probe() {
|
|
131
|
+
const entry = useNamespaceLoad({
|
|
132
|
+
coordinator,
|
|
133
|
+
engineRef: handle,
|
|
134
|
+
partition: "it",
|
|
135
|
+
namespaces: ["default"],
|
|
136
|
+
locale: "it",
|
|
137
|
+
load,
|
|
138
|
+
});
|
|
139
|
+
return <span>{entry.status}</span>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const html = renderToString(<Probe />);
|
|
143
|
+
expect(html).toContain("pending");
|
|
144
|
+
expect(load).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("SSR getServerSnapshot uses tryResolveSync without calling load", async () => {
|
|
148
|
+
const handle = createTestHandle();
|
|
149
|
+
await handle.load({ namespaces: ["default"], locale: "it" });
|
|
150
|
+
const load = vi.fn(() => Promise.reject(new Error("should not load")));
|
|
151
|
+
const coordinator = createLoadCoordinator<ScopedScopeLike>();
|
|
152
|
+
|
|
153
|
+
function Probe() {
|
|
154
|
+
const entry = useNamespaceLoad({
|
|
155
|
+
coordinator,
|
|
156
|
+
engineRef: handle,
|
|
157
|
+
partition: "it",
|
|
158
|
+
namespaces: ["default"],
|
|
159
|
+
locale: "it",
|
|
160
|
+
load,
|
|
161
|
+
tryResolveSync: () => asScopedScope(handle.peek({ namespaces: ["default"], locale: "it" })),
|
|
162
|
+
});
|
|
163
|
+
return <span>{entry.status}</span>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const html = renderToString(<Probe />);
|
|
167
|
+
expect(html).toContain("ready");
|
|
168
|
+
expect(load).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
100
170
|
});
|
|
101
171
|
|
|
102
172
|
describe("createI18nLoadGate", () => {
|
|
@@ -132,7 +202,7 @@ describe("createI18nLoadGate", () => {
|
|
|
132
202
|
const handle = createTestHandle();
|
|
133
203
|
const scope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
134
204
|
await act(async () => {
|
|
135
|
-
pending.resolve(scope);
|
|
205
|
+
pending.resolve(asScopedScope(scope));
|
|
136
206
|
await pending.promise;
|
|
137
207
|
});
|
|
138
208
|
|
|
@@ -159,7 +229,7 @@ describe("createI18nLoadGate", () => {
|
|
|
159
229
|
const handle = createTestHandle();
|
|
160
230
|
const scope = await handle.load({ namespaces: ["default", "billing"], locale: "it" });
|
|
161
231
|
await act(async () => {
|
|
162
|
-
pending.resolve(scope);
|
|
232
|
+
pending.resolve(asScopedScope(scope));
|
|
163
233
|
await pending.promise;
|
|
164
234
|
});
|
|
165
235
|
|
|
@@ -181,24 +251,30 @@ describe("createI18nLoadGate", () => {
|
|
|
181
251
|
expect(container.textContent).toBe("");
|
|
182
252
|
});
|
|
183
253
|
|
|
184
|
-
it("
|
|
254
|
+
it("throws to an error boundary when load fails without renderError", async () => {
|
|
185
255
|
const pending = deferred<ScopedScopeLike>();
|
|
186
256
|
const { I18n, coordinator, handle, locale } = createGateFixture({
|
|
187
257
|
load: () => pending.promise,
|
|
188
258
|
});
|
|
259
|
+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
189
260
|
|
|
190
261
|
render(
|
|
191
|
-
<
|
|
192
|
-
{
|
|
193
|
-
|
|
262
|
+
<TestErrorBoundary fallback={<span data-testid="caught">caught</span>}>
|
|
263
|
+
<I18n namespaces={["default"]} fallback={<span data-testid="fallback">loading</span>}>
|
|
264
|
+
{({ t }) => <span data-testid="greeting">{t("default", "greeting")}</span>}
|
|
265
|
+
</I18n>
|
|
266
|
+
</TestErrorBoundary>
|
|
194
267
|
);
|
|
195
268
|
|
|
269
|
+
expect(screen.getByTestId("fallback").textContent).toBe("loading");
|
|
270
|
+
|
|
196
271
|
await act(async () => {
|
|
197
272
|
pending.reject(new Error("load failed"));
|
|
198
273
|
await pending.promise.catch(() => undefined);
|
|
199
274
|
});
|
|
200
275
|
|
|
201
|
-
expect(screen.getByTestId("
|
|
276
|
+
expect(screen.getByTestId("caught").textContent).toBe("caught");
|
|
277
|
+
expect(screen.queryByTestId("fallback")).toBeNull();
|
|
202
278
|
expect(screen.queryByTestId("greeting")).toBeNull();
|
|
203
279
|
expect(
|
|
204
280
|
coordinator.getEntry({
|
|
@@ -207,6 +283,8 @@ describe("createI18nLoadGate", () => {
|
|
|
207
283
|
namespaces: ["default"],
|
|
208
284
|
})
|
|
209
285
|
).toEqual({ status: "error", error: expect.any(Error) });
|
|
286
|
+
|
|
287
|
+
consoleError.mockRestore();
|
|
210
288
|
});
|
|
211
289
|
|
|
212
290
|
it("renderError exposes retry for failed loads", async () => {
|
|
@@ -258,7 +336,7 @@ describe("createI18nLoadGate", () => {
|
|
|
258
336
|
|
|
259
337
|
const scope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
260
338
|
await act(async () => {
|
|
261
|
-
currentPromise.resolve(scope);
|
|
339
|
+
currentPromise.resolve(asScopedScope(scope));
|
|
262
340
|
await currentPromise.promise;
|
|
263
341
|
});
|
|
264
342
|
|
|
@@ -308,7 +386,7 @@ describe("createI18nLoadGate", () => {
|
|
|
308
386
|
const handle = createTestHandle();
|
|
309
387
|
const scope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
310
388
|
await act(async () => {
|
|
311
|
-
pending.resolve(scope);
|
|
389
|
+
pending.resolve(asScopedScope(scope));
|
|
312
390
|
await pending.promise;
|
|
313
391
|
});
|
|
314
392
|
|
|
@@ -364,13 +442,45 @@ describe("createI18nLoadGate", () => {
|
|
|
364
442
|
const handle = createTestHandle();
|
|
365
443
|
const scope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
366
444
|
await act(async () => {
|
|
367
|
-
pending.resolve(scope);
|
|
445
|
+
pending.resolve(asScopedScope(scope));
|
|
368
446
|
await pending.promise;
|
|
369
447
|
});
|
|
370
448
|
|
|
371
449
|
expect(screen.getByTestId("child").textContent).toBe("x:Ciao");
|
|
372
450
|
});
|
|
373
451
|
|
|
452
|
+
it("HOC render may use hooks across pending → ready (no hydrated state)", async () => {
|
|
453
|
+
const pending = deferred<ScopedScopeLike>();
|
|
454
|
+
const { withI18n } = createGateFixture({ load: () => pending.promise });
|
|
455
|
+
|
|
456
|
+
const Child = withI18n<{ label: string }>(
|
|
457
|
+
{
|
|
458
|
+
namespaces: ["default"],
|
|
459
|
+
fallback: <span data-testid="fallback">loading</span>,
|
|
460
|
+
},
|
|
461
|
+
function Child({ label }, { t }) {
|
|
462
|
+
const [n] = useState(0);
|
|
463
|
+
return (
|
|
464
|
+
<span data-testid="child">
|
|
465
|
+
{label}:{t("default", "greeting")}:{n}
|
|
466
|
+
</span>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
render(<Child label="x" />);
|
|
472
|
+
expect(screen.getByTestId("fallback").textContent).toBe("loading");
|
|
473
|
+
|
|
474
|
+
const handle = createTestHandle();
|
|
475
|
+
const scope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
476
|
+
await act(async () => {
|
|
477
|
+
pending.resolve(asScopedScope(scope));
|
|
478
|
+
await pending.promise;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(screen.getByTestId("child").textContent).toBe("x:Ciao:0");
|
|
482
|
+
});
|
|
483
|
+
|
|
374
484
|
it("dedupes loads across two gates sharing one coordinator", async () => {
|
|
375
485
|
const handle = createTestHandle();
|
|
376
486
|
const coordinator = createLoadCoordinator<ScopedScopeLike>();
|
|
@@ -411,7 +521,7 @@ describe("createI18nLoadGate", () => {
|
|
|
411
521
|
|
|
412
522
|
const scope = await handle.load({ namespaces: ["default"], locale: "en" });
|
|
413
523
|
await act(async () => {
|
|
414
|
-
pending.resolve(scope);
|
|
524
|
+
pending.resolve(asScopedScope(scope));
|
|
415
525
|
await pending.promise;
|
|
416
526
|
});
|
|
417
527
|
|
|
@@ -424,8 +534,8 @@ describe("createI18nLoadGate ready path", () => {
|
|
|
424
534
|
it("renders immediately when the coordinator entry is already resolved", async () => {
|
|
425
535
|
const handle = createTestHandle();
|
|
426
536
|
const coordinator = createLoadCoordinator<ScopedScopeLike>();
|
|
427
|
-
const locale = "it";
|
|
428
|
-
const scope = await handle.load({ namespaces: ["default"], locale
|
|
537
|
+
const locale: TestLocale = "it";
|
|
538
|
+
const scope = asScopedScope(await handle.load({ namespaces: ["default"], locale }));
|
|
429
539
|
|
|
430
540
|
coordinator.request({
|
|
431
541
|
engineRef: handle,
|
|
@@ -461,7 +571,7 @@ describe("createI18nLoadGate keep-then-switch", () => {
|
|
|
461
571
|
it("keeps previous t across locale change until new partition resolves", async () => {
|
|
462
572
|
const handle = createTestHandle();
|
|
463
573
|
const coordinator = createLoadCoordinator<ScopedScopeLike>();
|
|
464
|
-
let activeLocale = "en";
|
|
574
|
+
let activeLocale: TestLocale = "en";
|
|
465
575
|
const pendingIt = deferred<ScopedScopeLike>();
|
|
466
576
|
|
|
467
577
|
const { I18n } = createI18nLoadGate({
|
|
@@ -475,12 +585,14 @@ describe("createI18nLoadGate keep-then-switch", () => {
|
|
|
475
585
|
if (activeLocale === "it") {
|
|
476
586
|
return pendingIt.promise;
|
|
477
587
|
}
|
|
478
|
-
return handle
|
|
588
|
+
return handle
|
|
589
|
+
.load({ namespaces: [...namespaces] as ["default"], locale: activeLocale })
|
|
590
|
+
.then(asScopedScope);
|
|
479
591
|
},
|
|
480
592
|
}),
|
|
481
593
|
});
|
|
482
594
|
|
|
483
|
-
function App({ locale }: { locale:
|
|
595
|
+
function App({ locale }: { locale: TestLocale }) {
|
|
484
596
|
activeLocale = locale;
|
|
485
597
|
return (
|
|
486
598
|
<I18n namespaces={["default"]} fallback={<span data-testid="fallback">loading</span>}>
|
|
@@ -500,7 +612,7 @@ describe("createI18nLoadGate keep-then-switch", () => {
|
|
|
500
612
|
expect(screen.queryByTestId("fallback")).toBeNull();
|
|
501
613
|
expect(screen.getByTestId("row").textContent).toBe("en:Hello:it");
|
|
502
614
|
|
|
503
|
-
const itScope = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
615
|
+
const itScope = asScopedScope(await handle.load({ namespaces: ["default"], locale: "it" }));
|
|
504
616
|
await act(async () => {
|
|
505
617
|
pendingIt.resolve(itScope);
|
|
506
618
|
await pendingIt.promise;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manual (non-Suspense) i18n load gate: ensure + subscribe via the load
|
|
3
|
-
* coordinator, then render fallback / keep-previous / error UI.
|
|
3
|
+
* coordinator, then render fallback / keep-previous / error UI / throw.
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
6
|
forwardRef,
|
|
@@ -33,7 +33,8 @@ export interface UseNamespaceLoadInput<Scope> {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Starts (or reuses) a coordinator load and re-renders when that entry settles.
|
|
36
|
-
*
|
|
36
|
+
* Client getSnapshot may kick `load()`; SSR getServerSnapshot only peeks sync
|
|
37
|
+
* (hydrated `state`) and never starts a client fetch during prerender.
|
|
37
38
|
*/
|
|
38
39
|
export function useNamespaceLoad<Scope>(
|
|
39
40
|
input: UseNamespaceLoadInput<Scope>
|
|
@@ -57,29 +58,21 @@ export function useNamespaceLoad<Scope>(
|
|
|
57
58
|
...(tryResolveSync !== undefined ? { tryResolveSync } : {}),
|
|
58
59
|
};
|
|
59
60
|
|
|
61
|
+
const displayOptions = {
|
|
62
|
+
locale,
|
|
63
|
+
namespaces,
|
|
64
|
+
...(keepPrevious !== undefined ? { keepPrevious } : {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
60
67
|
return useSyncExternalStore(
|
|
61
68
|
coordinator.subscribe,
|
|
62
69
|
() => {
|
|
63
70
|
coordinator.ensure(request);
|
|
64
|
-
return coordinator.getDisplayEntry(
|
|
65
|
-
{ engineRef, partition, namespaces },
|
|
66
|
-
{
|
|
67
|
-
locale,
|
|
68
|
-
namespaces,
|
|
69
|
-
...(keepPrevious !== undefined ? { keepPrevious } : {}),
|
|
70
|
-
}
|
|
71
|
-
);
|
|
71
|
+
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
72
72
|
},
|
|
73
73
|
() => {
|
|
74
|
-
coordinator.
|
|
75
|
-
return coordinator.getDisplayEntry(
|
|
76
|
-
{ engineRef, partition, namespaces },
|
|
77
|
-
{
|
|
78
|
-
locale,
|
|
79
|
-
namespaces,
|
|
80
|
-
...(keepPrevious !== undefined ? { keepPrevious } : {}),
|
|
81
|
-
}
|
|
82
|
-
);
|
|
74
|
+
coordinator.ensureSync(request);
|
|
75
|
+
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
83
76
|
}
|
|
84
77
|
);
|
|
85
78
|
}
|
|
@@ -116,8 +109,12 @@ export interface CreateI18nLoadGateOptions {
|
|
|
116
109
|
|
|
117
110
|
export interface I18nGateProps {
|
|
118
111
|
namespaces: readonly string[];
|
|
112
|
+
/** Shown only while the load is pending (not on error). */
|
|
119
113
|
fallback?: ReactNode;
|
|
120
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* When set, called for error with no keep-previous display.
|
|
116
|
+
* Otherwise the load error is thrown for a React error boundary.
|
|
117
|
+
*/
|
|
121
118
|
renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;
|
|
122
119
|
children: (value: I18nLoadGateValue) => ReactNode;
|
|
123
120
|
}
|
|
@@ -145,23 +142,22 @@ function resolveNamespaces(namespaces: readonly string[] | undefined): readonly
|
|
|
145
142
|
return namespaces;
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
fallback?: ReactNode;
|
|
152
|
-
renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;
|
|
153
|
-
children: (value: I18nLoadGateValue) => ReactNode;
|
|
145
|
+
function toThrownError(error: unknown): Error {
|
|
146
|
+
if (error instanceof Error) {
|
|
147
|
+
return error;
|
|
154
148
|
}
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
return options.children({ t: entry.t, locale: entry.locale });
|
|
149
|
+
if (typeof error === "string") {
|
|
150
|
+
return new Error(error);
|
|
158
151
|
}
|
|
152
|
+
return new Error("i18n namespace load failed");
|
|
153
|
+
}
|
|
159
154
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
const noopT: ScopedTranslateFn = () => "";
|
|
156
|
+
|
|
157
|
+
/** Gate value for display, or `null` when only fallback / error UI should show. */
|
|
158
|
+
function gateValueFromEntry(entry: LoadDisplayEntry<ScopedScopeLike>): I18nLoadGateValue | null {
|
|
159
|
+
if (entry.status === "ready") {
|
|
160
|
+
return { t: entry.t, locale: entry.locale };
|
|
165
161
|
}
|
|
166
162
|
|
|
167
163
|
if (entry.t !== null && entry.display) {
|
|
@@ -176,6 +172,29 @@ function renderDisplayEntry(
|
|
|
176
172
|
value.error = entry.error;
|
|
177
173
|
value.retry = entry.retry;
|
|
178
174
|
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderDisplayEntry(
|
|
182
|
+
entry: LoadDisplayEntry<ScopedScopeLike>,
|
|
183
|
+
options: {
|
|
184
|
+
fallback?: ReactNode;
|
|
185
|
+
renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;
|
|
186
|
+
children: (value: I18nLoadGateValue) => ReactNode;
|
|
187
|
+
}
|
|
188
|
+
): ReactNode {
|
|
189
|
+
if (entry.status === "error" && entry.t === null) {
|
|
190
|
+
if (options.renderError) {
|
|
191
|
+
return options.renderError({ error: entry.error, retry: entry.retry! });
|
|
192
|
+
}
|
|
193
|
+
throw toThrownError(entry.error);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const value = gateValueFromEntry(entry);
|
|
197
|
+
if (value !== null) {
|
|
179
198
|
return options.children(value);
|
|
180
199
|
}
|
|
181
200
|
|
|
@@ -196,10 +215,13 @@ export function createI18nLoadGate(options: CreateI18nLoadGateOptions): {
|
|
|
196
215
|
const { useLoadArgs } = options;
|
|
197
216
|
const keepPrevious = options.keepPreviousOnPartitionChange !== false;
|
|
198
217
|
|
|
199
|
-
function
|
|
218
|
+
function useGateLoad(namespaces: readonly string[]): {
|
|
219
|
+
entry: LoadDisplayEntry<ScopedScopeLike>;
|
|
220
|
+
locale: string;
|
|
221
|
+
} {
|
|
200
222
|
const resolved = resolveNamespaces(namespaces);
|
|
201
223
|
const args = useLoadArgs();
|
|
202
|
-
|
|
224
|
+
const entry = useNamespaceLoad({
|
|
203
225
|
coordinator: args.coordinator,
|
|
204
226
|
engineRef: args.engineRef,
|
|
205
227
|
partition: args.partition,
|
|
@@ -211,10 +233,11 @@ export function createI18nLoadGate(options: CreateI18nLoadGateOptions): {
|
|
|
211
233
|
: {}),
|
|
212
234
|
keepPrevious,
|
|
213
235
|
});
|
|
236
|
+
return { entry, locale: args.locale };
|
|
214
237
|
}
|
|
215
238
|
|
|
216
239
|
function I18n({ namespaces, fallback, renderError, children }: I18nGateProps): ReactNode {
|
|
217
|
-
const entry =
|
|
240
|
+
const { entry } = useGateLoad(namespaces);
|
|
218
241
|
return renderDisplayEntry(entry, {
|
|
219
242
|
...(fallback !== undefined ? { fallback } : {}),
|
|
220
243
|
...(renderError !== undefined ? { renderError } : {}),
|
|
@@ -228,25 +251,29 @@ export function createI18nLoadGate(options: CreateI18nLoadGateOptions): {
|
|
|
228
251
|
): ForwardRefExoticComponent<P & RefAttributes<R>> {
|
|
229
252
|
const { fallback, renderError } = hocOptions;
|
|
230
253
|
const Outer = forwardRef<R, P>(function I18nHocOuter(props, ref) {
|
|
231
|
-
const entry =
|
|
254
|
+
const { entry, locale } = useGateLoad(hocOptions.namespaces);
|
|
255
|
+
|
|
256
|
+
// Always invoke `render` so hooks inside it are Outer hooks and stay
|
|
257
|
+
// unconditional across pending → ready (e.g. I18nRoot without hydrated state).
|
|
258
|
+
const displayValue = gateValueFromEntry(entry);
|
|
259
|
+
const value = displayValue ?? ({ t: noopT, locale } satisfies I18nLoadGateValue);
|
|
260
|
+
const rendered = render(props as P, value, ref);
|
|
261
|
+
|
|
262
|
+
if (entry.status === "error" && entry.t === null) {
|
|
263
|
+
if (renderError) {
|
|
264
|
+
return renderError({ error: entry.error, retry: entry.retry!, props: props as P });
|
|
265
|
+
}
|
|
266
|
+
throw toThrownError(entry.error);
|
|
267
|
+
}
|
|
232
268
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
? undefined
|
|
237
|
-
: typeof fallback === "function"
|
|
238
|
-
? fallback(props as P)
|
|
239
|
-
: fallback;
|
|
269
|
+
if (displayValue !== null) {
|
|
270
|
+
return rendered;
|
|
271
|
+
}
|
|
240
272
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
renderError: ({ error, retry }) => renderError({ error, retry, props: props as P }),
|
|
246
|
-
}
|
|
247
|
-
: {}),
|
|
248
|
-
children: (value) => render(props as P, value, ref),
|
|
249
|
-
});
|
|
273
|
+
if (fallback === undefined) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return typeof fallback === "function" ? fallback(props as P) : fallback;
|
|
250
277
|
});
|
|
251
278
|
|
|
252
279
|
const displayName = render.name || "Component";
|
package/src/runtime.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { act } from "react";
|
|
1
|
+
import { act, useState } from "react";
|
|
2
2
|
import { createI18nHandle, IcuTranslationProviderMulti } from "@xndrjs/i18n";
|
|
3
3
|
import { render, screen } from "@testing-library/react";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
@@ -85,4 +85,35 @@ describe("runtime primitives", () => {
|
|
|
85
85
|
)
|
|
86
86
|
).toThrow(/pass either `state` or `dictionary`/);
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
it("withI18n allows hooks in render when I18nRoot has no hydrated state", async () => {
|
|
90
|
+
const { withI18n } = createDefaultLoadGate();
|
|
91
|
+
|
|
92
|
+
const Child = withI18n<{ label: string }>(
|
|
93
|
+
{
|
|
94
|
+
namespaces: ["default"],
|
|
95
|
+
fallback: <span data-testid="fallback">loading</span>,
|
|
96
|
+
},
|
|
97
|
+
function Child({ label }, { t }) {
|
|
98
|
+
const [n] = useState(0);
|
|
99
|
+
return (
|
|
100
|
+
<span data-testid="child">
|
|
101
|
+
{label}:{t("default", "greeting")}:{n}
|
|
102
|
+
</span>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await act(async () => {
|
|
108
|
+
render(
|
|
109
|
+
<I18nRootProvider createI18n={createTestI18n} locale="it">
|
|
110
|
+
<Child label="x" />
|
|
111
|
+
</I18nRootProvider>
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(await screen.findByTestId("child")).toBeTruthy();
|
|
116
|
+
expect(screen.getByTestId("child").textContent).toBe("x:Ciao:0");
|
|
117
|
+
expect(screen.queryByTestId("fallback")).toBeNull();
|
|
118
|
+
});
|
|
88
119
|
});
|
package/src/types.ts
CHANGED
|
@@ -72,8 +72,13 @@ export interface LoadCoordinator<Scope> {
|
|
|
72
72
|
readonly revision: number;
|
|
73
73
|
/** @deprecated Prefer {@link ensure}. */
|
|
74
74
|
request(input: LoadCoordinatorRequest<Scope>): void;
|
|
75
|
-
/** Idempotent: start load if missing. Safe from getSnapshot. */
|
|
75
|
+
/** Idempotent: start load if missing. Safe from client getSnapshot. */
|
|
76
76
|
ensure(input: LoadCoordinatorRequest<Scope>): void;
|
|
77
|
+
/**
|
|
78
|
+
* Idempotent: resolve from {@link LoadCoordinatorRequest.tryResolveSync} only.
|
|
79
|
+
* Never starts `load()` — use from SSR `getServerSnapshot`.
|
|
80
|
+
*/
|
|
81
|
+
ensureSync(input: LoadCoordinatorRequest<Scope>): void;
|
|
77
82
|
getEntry(key: LoadCoordinatorKey): LoadCoordinatorEntry<Scope>;
|
|
78
83
|
getDisplayEntry(
|
|
79
84
|
key: LoadCoordinatorKey,
|
|
@@ -115,6 +120,14 @@ export interface I18nHandleLike {
|
|
|
115
120
|
dictionary: unknown;
|
|
116
121
|
resources: readonly (readonly [string, string])[];
|
|
117
122
|
};
|
|
123
|
+
getLoadState: () => {
|
|
124
|
+
resources: readonly {
|
|
125
|
+
namespace: string;
|
|
126
|
+
partition: string;
|
|
127
|
+
status: "pending" | "loaded" | "error";
|
|
128
|
+
error?: unknown;
|
|
129
|
+
}[];
|
|
130
|
+
};
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
/** Single React context value for the lazy i18n tree. */
|