@xndrjs/i18n-react 0.8.0-alpha.0 → 0.8.1

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 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
- * Kick-off is idempotent inside the snapshot getter (safe with useSyncExternalStore).
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
- /** When set, called for error with no keep-previous display. */
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
- const entrySnapshot = {
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(() => void 0);
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.ensure(request);
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,6 +314,15 @@ function resolveNamespaces(namespaces) {
303
314
  }
304
315
  return namespaces;
305
316
  }
317
+ function toThrownError(error) {
318
+ if (error instanceof Error) {
319
+ return error;
320
+ }
321
+ if (typeof error === "string") {
322
+ return new Error(error);
323
+ }
324
+ return new Error("i18n namespace load failed");
325
+ }
306
326
  function renderDisplayEntry(entry, options) {
307
327
  if (entry.status === "ready") {
308
328
  return options.children({ t: entry.t, locale: entry.locale });
@@ -311,7 +331,7 @@ function renderDisplayEntry(entry, options) {
311
331
  if (options.renderError) {
312
332
  return options.renderError({ error: entry.error, retry: entry.retry });
313
333
  }
314
- return options.fallback ?? null;
334
+ throw toThrownError(entry.error);
315
335
  }
316
336
  if (entry.t !== null && entry.display) {
317
337
  const value = {
@@ -358,7 +378,7 @@ function createI18nLoadGate(options) {
358
378
  const { fallback, renderError } = hocOptions;
359
379
  const Outer = forwardRef(function I18nHocOuter(props, ref) {
360
380
  const entry = useGateEntry(hocOptions.namespaces);
361
- const needsFallback = entry.status !== "ready" && entry.t === null;
381
+ const needsFallback = entry.status === "pending" && entry.t === null;
362
382
  const resolvedFallback = !needsFallback || fallback === void 0 ? void 0 : typeof fallback === "function" ? fallback(props) : fallback;
363
383
  return renderDisplayEntry(entry, {
364
384
  ...resolvedFallback !== void 0 ? { fallback: resolvedFallback } : {},
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\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 throw toThrownError(entry.error);\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 === \"pending\" && 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xndrjs/i18n-react",
3
- "version": "0.8.0-alpha.0",
3
+ "version": "0.8.1",
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.0-alpha.0",
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.7.0"
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
- const entrySnapshot: LoadCoordinatorEntry<Scope> = {
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
- // Mark rejection handled when only getEntry consumers attach (no getPromise).
153
- void promise.catch(() => void null);
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?: string;
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: 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("renders fallback on load error (no remount storm)", async () => {
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
- <I18n namespaces={["default"]} fallback={<span data-testid="fallback">err-or-pending</span>}>
192
- {({ t }) => <span data-testid="greeting">{t("default", "greeting")}</span>}
193
- </I18n>
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("fallback").textContent).toBe("err-or-pending");
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,7 +442,7 @@ 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
 
@@ -411,7 +489,7 @@ describe("createI18nLoadGate", () => {
411
489
 
412
490
  const scope = await handle.load({ namespaces: ["default"], locale: "en" });
413
491
  await act(async () => {
414
- pending.resolve(scope);
492
+ pending.resolve(asScopedScope(scope));
415
493
  await pending.promise;
416
494
  });
417
495
 
@@ -424,8 +502,8 @@ describe("createI18nLoadGate ready path", () => {
424
502
  it("renders immediately when the coordinator entry is already resolved", async () => {
425
503
  const handle = createTestHandle();
426
504
  const coordinator = createLoadCoordinator<ScopedScopeLike>();
427
- const locale = "it";
428
- const scope = await handle.load({ namespaces: ["default"], locale: locale });
505
+ const locale: TestLocale = "it";
506
+ const scope = asScopedScope(await handle.load({ namespaces: ["default"], locale }));
429
507
 
430
508
  coordinator.request({
431
509
  engineRef: handle,
@@ -461,7 +539,7 @@ describe("createI18nLoadGate keep-then-switch", () => {
461
539
  it("keeps previous t across locale change until new partition resolves", async () => {
462
540
  const handle = createTestHandle();
463
541
  const coordinator = createLoadCoordinator<ScopedScopeLike>();
464
- let activeLocale = "en";
542
+ let activeLocale: TestLocale = "en";
465
543
  const pendingIt = deferred<ScopedScopeLike>();
466
544
 
467
545
  const { I18n } = createI18nLoadGate({
@@ -475,12 +553,14 @@ describe("createI18nLoadGate keep-then-switch", () => {
475
553
  if (activeLocale === "it") {
476
554
  return pendingIt.promise;
477
555
  }
478
- return handle.load({ namespaces: [...namespaces] as ["default"], locale: activeLocale });
556
+ return handle
557
+ .load({ namespaces: [...namespaces] as ["default"], locale: activeLocale })
558
+ .then(asScopedScope);
479
559
  },
480
560
  }),
481
561
  });
482
562
 
483
- function App({ locale }: { locale: string }) {
563
+ function App({ locale }: { locale: TestLocale }) {
484
564
  activeLocale = locale;
485
565
  return (
486
566
  <I18n namespaces={["default"]} fallback={<span data-testid="fallback">loading</span>}>
@@ -500,7 +580,7 @@ describe("createI18nLoadGate keep-then-switch", () => {
500
580
  expect(screen.queryByTestId("fallback")).toBeNull();
501
581
  expect(screen.getByTestId("row").textContent).toBe("en:Hello:it");
502
582
 
503
- const itScope = await handle.load({ namespaces: ["default"], locale: "it" });
583
+ const itScope = asScopedScope(await handle.load({ namespaces: ["default"], locale: "it" }));
504
584
  await act(async () => {
505
585
  pendingIt.resolve(itScope);
506
586
  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
- * Kick-off is idempotent inside the snapshot getter (safe with useSyncExternalStore).
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.ensure(request);
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
- /** When set, called for error with no keep-previous display. */
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,6 +142,16 @@ function resolveNamespaces(namespaces: readonly string[] | undefined): readonly
145
142
  return namespaces;
146
143
  }
147
144
 
145
+ function toThrownError(error: unknown): Error {
146
+ if (error instanceof Error) {
147
+ return error;
148
+ }
149
+ if (typeof error === "string") {
150
+ return new Error(error);
151
+ }
152
+ return new Error("i18n namespace load failed");
153
+ }
154
+
148
155
  function renderDisplayEntry(
149
156
  entry: LoadDisplayEntry<ScopedScopeLike>,
150
157
  options: {
@@ -159,9 +166,9 @@ function renderDisplayEntry(
159
166
 
160
167
  if (entry.status === "error" && entry.t === null) {
161
168
  if (options.renderError) {
162
- return options.renderError({ error: entry.error, retry: entry.retry });
169
+ return options.renderError({ error: entry.error, retry: entry.retry! });
163
170
  }
164
- return options.fallback ?? null;
171
+ throw toThrownError(entry.error);
165
172
  }
166
173
 
167
174
  if (entry.t !== null && entry.display) {
@@ -230,7 +237,7 @@ export function createI18nLoadGate(options: CreateI18nLoadGateOptions): {
230
237
  const Outer = forwardRef<R, P>(function I18nHocOuter(props, ref) {
231
238
  const entry = useGateEntry(hocOptions.namespaces);
232
239
 
233
- const needsFallback = entry.status !== "ready" && entry.t === null;
240
+ const needsFallback = entry.status === "pending" && entry.t === null;
234
241
  const resolvedFallback =
235
242
  !needsFallback || fallback === undefined
236
243
  ? undefined
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. */