@xndrjs/i18n-react 0.8.1 → 0.8.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/cli/codegen.js +305 -0
- package/dist/cli/codegen.js.map +1 -0
- package/package.json +6 -9
- package/bin/codegen.mjs +0 -17
- package/src/codegen/emit/react-bindings-file.test.ts +0 -44
- package/src/codegen/emit/react-bindings-file.ts +0 -223
- package/src/codegen/generate-react-bindings.ts +0 -86
- package/src/codegen/react-codegen-config.test.ts +0 -39
- package/src/codegen/react-codegen-config.ts +0 -70
- package/src/codegen/write-file-if-changed.ts +0 -16
- package/src/create-load-coordinator.test.ts +0 -421
- package/src/create-load-coordinator.ts +0 -340
- package/src/create-scoped-t.ts +0 -50
- package/src/index.ts +0 -18
- package/src/namespace-load-gate.test.tsx +0 -608
- package/src/namespace-load-gate.tsx +0 -265
- package/src/root-context.tsx +0 -21
- package/src/root-provider.tsx +0 -65
- package/src/runtime.test.tsx +0 -88
- package/src/types.ts +0 -138
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manual (non-Suspense) i18n load gate: ensure + subscribe via the load
|
|
3
|
-
* coordinator, then render fallback / keep-previous / error UI / throw.
|
|
4
|
-
*/
|
|
5
|
-
import {
|
|
6
|
-
forwardRef,
|
|
7
|
-
useSyncExternalStore,
|
|
8
|
-
type ForwardedRef,
|
|
9
|
-
type ForwardRefExoticComponent,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
type RefAttributes,
|
|
12
|
-
} from "react";
|
|
13
|
-
import type {
|
|
14
|
-
LoadCoordinator,
|
|
15
|
-
LoadCoordinatorRequest,
|
|
16
|
-
LoadDisplayEntry,
|
|
17
|
-
LoadPartition,
|
|
18
|
-
ScopedScopeLike,
|
|
19
|
-
ScopedTranslateFn,
|
|
20
|
-
} from "./types.js";
|
|
21
|
-
|
|
22
|
-
/** Inputs for {@link useNamespaceLoad}. */
|
|
23
|
-
export interface UseNamespaceLoadInput<Scope> {
|
|
24
|
-
coordinator: LoadCoordinator<Scope>;
|
|
25
|
-
engineRef: unknown;
|
|
26
|
-
partition: LoadPartition;
|
|
27
|
-
namespaces: readonly string[];
|
|
28
|
-
locale: string;
|
|
29
|
-
load: () => Promise<Scope>;
|
|
30
|
-
tryResolveSync?: () => Scope | null;
|
|
31
|
-
keepPrevious?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Starts (or reuses) a coordinator load and re-renders when that entry settles.
|
|
36
|
-
* Client getSnapshot may kick `load()`; SSR getServerSnapshot only peeks sync
|
|
37
|
-
* (hydrated `state`) and never starts a client fetch during prerender.
|
|
38
|
-
*/
|
|
39
|
-
export function useNamespaceLoad<Scope>(
|
|
40
|
-
input: UseNamespaceLoadInput<Scope>
|
|
41
|
-
): LoadDisplayEntry<Scope> {
|
|
42
|
-
const {
|
|
43
|
-
coordinator,
|
|
44
|
-
engineRef,
|
|
45
|
-
partition,
|
|
46
|
-
namespaces,
|
|
47
|
-
locale,
|
|
48
|
-
load,
|
|
49
|
-
tryResolveSync,
|
|
50
|
-
keepPrevious,
|
|
51
|
-
} = input;
|
|
52
|
-
|
|
53
|
-
const request: LoadCoordinatorRequest<Scope> = {
|
|
54
|
-
engineRef,
|
|
55
|
-
partition,
|
|
56
|
-
namespaces,
|
|
57
|
-
load,
|
|
58
|
-
...(tryResolveSync !== undefined ? { tryResolveSync } : {}),
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const displayOptions = {
|
|
62
|
-
locale,
|
|
63
|
-
namespaces,
|
|
64
|
-
...(keepPrevious !== undefined ? { keepPrevious } : {}),
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return useSyncExternalStore(
|
|
68
|
-
coordinator.subscribe,
|
|
69
|
-
() => {
|
|
70
|
-
coordinator.ensure(request);
|
|
71
|
-
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
72
|
-
},
|
|
73
|
-
() => {
|
|
74
|
-
coordinator.ensureSync(request);
|
|
75
|
-
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
76
|
-
}
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Scoped `{ t, locale }` injected into gate children / HOC when ready (or kept). */
|
|
81
|
-
export interface I18nLoadGateValue {
|
|
82
|
-
t: ScopedTranslateFn;
|
|
83
|
-
locale: string;
|
|
84
|
-
/** Set during keep-then-switch while a new partition is loading. */
|
|
85
|
-
pendingLocale?: string;
|
|
86
|
-
error?: unknown;
|
|
87
|
-
retry?: () => void;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Load wiring from React context (coordinator, engine, partition, load). */
|
|
91
|
-
export interface I18nLoadArgs {
|
|
92
|
-
coordinator: LoadCoordinator<ScopedScopeLike>;
|
|
93
|
-
engineRef: unknown;
|
|
94
|
-
partition: LoadPartition;
|
|
95
|
-
locale: string;
|
|
96
|
-
load: (namespaces: readonly string[]) => Promise<ScopedScopeLike>;
|
|
97
|
-
tryResolveSync?: (namespaces: readonly string[]) => ScopedScopeLike | null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface CreateI18nLoadGateOptions {
|
|
101
|
-
/**
|
|
102
|
-
* When true (default), keep last resolved `{ t, locale }` while a new
|
|
103
|
-
* partition is pending instead of showing fallback.
|
|
104
|
-
*/
|
|
105
|
-
keepPreviousOnPartitionChange?: boolean;
|
|
106
|
-
/** Hook that resolves coordinator + load inputs from React context. */
|
|
107
|
-
useLoadArgs: () => I18nLoadArgs;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface I18nGateProps {
|
|
111
|
-
namespaces: readonly string[];
|
|
112
|
-
/** Shown only while the load is pending (not on error). */
|
|
113
|
-
fallback?: ReactNode;
|
|
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
|
-
*/
|
|
118
|
-
renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;
|
|
119
|
-
children: (value: I18nLoadGateValue) => ReactNode;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Pending UI for `withI18n` — static node or derived from own props. */
|
|
123
|
-
export type WithI18nFallback<P extends object> = ReactNode | ((props: P) => ReactNode);
|
|
124
|
-
|
|
125
|
-
export interface I18nHocOptions<P extends object = object> {
|
|
126
|
-
namespaces: readonly string[];
|
|
127
|
-
fallback?: WithI18nFallback<P>;
|
|
128
|
-
renderError?: (args: { error: unknown; retry: () => void; props: P }) => ReactNode;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Render fn for `withI18n`: own props, i18n bag, optional forwarded ref. */
|
|
132
|
-
export type WithI18nRender<P extends object, R = never> = (
|
|
133
|
-
props: P,
|
|
134
|
-
i18n: I18nLoadGateValue,
|
|
135
|
-
ref?: ForwardedRef<R>
|
|
136
|
-
) => ReactNode;
|
|
137
|
-
|
|
138
|
-
function resolveNamespaces(namespaces: readonly string[] | undefined): readonly string[] {
|
|
139
|
-
if (namespaces === undefined || namespaces.length === 0) {
|
|
140
|
-
throw new Error("withI18n / I18n: requires a non-empty namespaces list");
|
|
141
|
-
}
|
|
142
|
-
return namespaces;
|
|
143
|
-
}
|
|
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
|
-
|
|
155
|
-
function renderDisplayEntry(
|
|
156
|
-
entry: LoadDisplayEntry<ScopedScopeLike>,
|
|
157
|
-
options: {
|
|
158
|
-
fallback?: ReactNode;
|
|
159
|
-
renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;
|
|
160
|
-
children: (value: I18nLoadGateValue) => ReactNode;
|
|
161
|
-
}
|
|
162
|
-
): ReactNode {
|
|
163
|
-
if (entry.status === "ready") {
|
|
164
|
-
return options.children({ t: entry.t, locale: entry.locale });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (entry.status === "error" && entry.t === null) {
|
|
168
|
-
if (options.renderError) {
|
|
169
|
-
return options.renderError({ error: entry.error, retry: entry.retry! });
|
|
170
|
-
}
|
|
171
|
-
throw toThrownError(entry.error);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (entry.t !== null && entry.display) {
|
|
175
|
-
const value: I18nLoadGateValue = {
|
|
176
|
-
t: entry.t,
|
|
177
|
-
locale: entry.display.locale,
|
|
178
|
-
};
|
|
179
|
-
if (entry.pendingLocale !== undefined) {
|
|
180
|
-
value.pendingLocale = entry.pendingLocale;
|
|
181
|
-
}
|
|
182
|
-
if (entry.status === "error") {
|
|
183
|
-
value.error = entry.error;
|
|
184
|
-
value.retry = entry.retry;
|
|
185
|
-
}
|
|
186
|
-
return options.children(value);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return options.fallback ?? null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Factory for generic `I18n` gate + `withI18n` HOC over the same Outer.
|
|
194
|
-
* Codegen emits typed wrappers; tests use this directly.
|
|
195
|
-
*/
|
|
196
|
-
export function createI18nLoadGate(options: CreateI18nLoadGateOptions): {
|
|
197
|
-
I18n: (props: I18nGateProps) => ReactNode;
|
|
198
|
-
withI18n: <P extends object = object, R = never>(
|
|
199
|
-
hocOptions: I18nHocOptions<P>,
|
|
200
|
-
render: WithI18nRender<P, R>
|
|
201
|
-
) => ForwardRefExoticComponent<P & RefAttributes<R>>;
|
|
202
|
-
} {
|
|
203
|
-
const { useLoadArgs } = options;
|
|
204
|
-
const keepPrevious = options.keepPreviousOnPartitionChange !== false;
|
|
205
|
-
|
|
206
|
-
function useGateEntry(namespaces: readonly string[]): LoadDisplayEntry<ScopedScopeLike> {
|
|
207
|
-
const resolved = resolveNamespaces(namespaces);
|
|
208
|
-
const args = useLoadArgs();
|
|
209
|
-
return useNamespaceLoad({
|
|
210
|
-
coordinator: args.coordinator,
|
|
211
|
-
engineRef: args.engineRef,
|
|
212
|
-
partition: args.partition,
|
|
213
|
-
namespaces: resolved,
|
|
214
|
-
locale: args.locale,
|
|
215
|
-
load: () => args.load(resolved),
|
|
216
|
-
...(args.tryResolveSync !== undefined
|
|
217
|
-
? { tryResolveSync: () => args.tryResolveSync!(resolved) }
|
|
218
|
-
: {}),
|
|
219
|
-
keepPrevious,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function I18n({ namespaces, fallback, renderError, children }: I18nGateProps): ReactNode {
|
|
224
|
-
const entry = useGateEntry(namespaces);
|
|
225
|
-
return renderDisplayEntry(entry, {
|
|
226
|
-
...(fallback !== undefined ? { fallback } : {}),
|
|
227
|
-
...(renderError !== undefined ? { renderError } : {}),
|
|
228
|
-
children,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function withI18n<P extends object = object, R = never>(
|
|
233
|
-
hocOptions: I18nHocOptions<P>,
|
|
234
|
-
render: WithI18nRender<P, R>
|
|
235
|
-
): ForwardRefExoticComponent<P & RefAttributes<R>> {
|
|
236
|
-
const { fallback, renderError } = hocOptions;
|
|
237
|
-
const Outer = forwardRef<R, P>(function I18nHocOuter(props, ref) {
|
|
238
|
-
const entry = useGateEntry(hocOptions.namespaces);
|
|
239
|
-
|
|
240
|
-
const needsFallback = entry.status === "pending" && entry.t === null;
|
|
241
|
-
const resolvedFallback =
|
|
242
|
-
!needsFallback || fallback === undefined
|
|
243
|
-
? undefined
|
|
244
|
-
: typeof fallback === "function"
|
|
245
|
-
? fallback(props as P)
|
|
246
|
-
: fallback;
|
|
247
|
-
|
|
248
|
-
return renderDisplayEntry(entry, {
|
|
249
|
-
...(resolvedFallback !== undefined ? { fallback: resolvedFallback } : {}),
|
|
250
|
-
...(renderError !== undefined
|
|
251
|
-
? {
|
|
252
|
-
renderError: ({ error, retry }) => renderError({ error, retry, props: props as P }),
|
|
253
|
-
}
|
|
254
|
-
: {}),
|
|
255
|
-
children: (value) => render(props as P, value, ref),
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const displayName = render.name || "Component";
|
|
260
|
-
Outer.displayName = `withI18n(${displayName})`;
|
|
261
|
-
return Outer as ForwardRefExoticComponent<P & RefAttributes<R>>;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return { I18n, withI18n };
|
|
265
|
-
}
|
package/src/root-context.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Holds the shared i18n root: handle + coordinator + active locale.
|
|
3
|
-
*
|
|
4
|
-
* Generated {@link useI18nRoot} casts the handle to the project's typed factory return.
|
|
5
|
-
*/
|
|
6
|
-
import { createContext, useContext } from "react";
|
|
7
|
-
import type { I18nRootContextValue } from "./types.js";
|
|
8
|
-
|
|
9
|
-
const I18nRootContext = createContext<I18nRootContextValue | null>(null);
|
|
10
|
-
|
|
11
|
-
/** Resolves the shared root from an ancestor {@link I18nRootProvider}. */
|
|
12
|
-
export function useI18nRootContext(): I18nRootContextValue {
|
|
13
|
-
const value = useContext(I18nRootContext);
|
|
14
|
-
if (value === null) {
|
|
15
|
-
throw new Error("useI18nRoot must be used within I18nRoot / I18nRootProvider");
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Internal — provider wiring. */
|
|
21
|
-
export { I18nRootContext };
|
package/src/root-provider.tsx
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single provider that shares handle + coordinator + locale across a lazy subtree.
|
|
3
|
-
*/
|
|
4
|
-
import { useRef, type ReactNode } from "react";
|
|
5
|
-
import type { FetchArtifact, I18nCreateInput, OnMissingTranslation } from "@xndrjs/i18n";
|
|
6
|
-
import { createLoadCoordinator } from "./create-load-coordinator.js";
|
|
7
|
-
import { I18nRootContext } from "./root-context.js";
|
|
8
|
-
import type {
|
|
9
|
-
CreateI18nFactory,
|
|
10
|
-
I18nHandleLike,
|
|
11
|
-
I18nRootContextValue,
|
|
12
|
-
ScopedScopeLike,
|
|
13
|
-
} from "./types.js";
|
|
14
|
-
|
|
15
|
-
export interface I18nRootProviderProps {
|
|
16
|
-
createI18n: CreateI18nFactory;
|
|
17
|
-
locale: string;
|
|
18
|
-
/**
|
|
19
|
-
* Cold-start / eager dictionary seed (defaults to `{}` when neither prop is set).
|
|
20
|
-
* Mutually exclusive with {@link state}.
|
|
21
|
-
*/
|
|
22
|
-
dictionary?: Record<string, unknown>;
|
|
23
|
-
/** Full create/hydrate input from `handle.serialize()` — mutually exclusive with {@link dictionary}. */
|
|
24
|
-
state?: I18nCreateInput | undefined;
|
|
25
|
-
onMissing?: OnMissingTranslation;
|
|
26
|
-
/** Custom JSON loader for `loaderStrategy: "fetch"` projects (ignored by import loaders). */
|
|
27
|
-
fetchImpl?: FetchArtifact;
|
|
28
|
-
children: ReactNode;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Creates and shares one handle + load coordinator for descendant gates. */
|
|
32
|
-
export function I18nRootProvider({
|
|
33
|
-
createI18n,
|
|
34
|
-
locale,
|
|
35
|
-
dictionary,
|
|
36
|
-
state,
|
|
37
|
-
onMissing,
|
|
38
|
-
fetchImpl,
|
|
39
|
-
children,
|
|
40
|
-
}: I18nRootProviderProps): ReactNode {
|
|
41
|
-
if (state !== undefined && dictionary !== undefined) {
|
|
42
|
-
throw new Error("I18nRootProvider: pass either `state` or `dictionary`, not both.");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const valueRef = useRef<I18nRootContextValue | null>(null);
|
|
46
|
-
|
|
47
|
-
if (valueRef.current === null) {
|
|
48
|
-
const seed: I18nCreateInput =
|
|
49
|
-
state ?? ({ dictionary: dictionary ?? {} } satisfies I18nCreateInput);
|
|
50
|
-
const handle = createI18n({
|
|
51
|
-
state: seed,
|
|
52
|
-
...(onMissing !== undefined ? { onMissing } : {}),
|
|
53
|
-
...(fetchImpl !== undefined ? { fetchImpl } : {}),
|
|
54
|
-
});
|
|
55
|
-
valueRef.current = {
|
|
56
|
-
handle: handle as I18nHandleLike,
|
|
57
|
-
coordinator: createLoadCoordinator<ScopedScopeLike>(),
|
|
58
|
-
locale,
|
|
59
|
-
};
|
|
60
|
-
} else {
|
|
61
|
-
valueRef.current.locale = locale;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return <I18nRootContext.Provider value={valueRef.current}>{children}</I18nRootContext.Provider>;
|
|
65
|
-
}
|
package/src/runtime.test.tsx
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { act } from "react";
|
|
2
|
-
import { createI18nHandle, IcuTranslationProviderMulti } from "@xndrjs/i18n";
|
|
3
|
-
import { render, screen } from "@testing-library/react";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { createI18nLoadGate, I18nRootProvider, useI18nRootContext } from "./index.js";
|
|
6
|
-
import type { I18nLoadArgs } from "./namespace-load-gate.js";
|
|
7
|
-
|
|
8
|
-
type MultiSchema = {
|
|
9
|
-
default: { greeting: { en: string; it: string } };
|
|
10
|
-
};
|
|
11
|
-
type MultiParams = { default: { greeting: never } };
|
|
12
|
-
|
|
13
|
-
function createTestI18n() {
|
|
14
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, MultiParams>({});
|
|
15
|
-
return createI18nHandle(engine, {
|
|
16
|
-
namespaceLoaders: {
|
|
17
|
-
default: async (locale: string) => ({
|
|
18
|
-
greeting: { [locale]: locale === "it" ? "Ciao" : "Hello" },
|
|
19
|
-
}),
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Mirrors codegen: gate factory wired to `I18nRootProvider`. */
|
|
25
|
-
function createDefaultLoadGate() {
|
|
26
|
-
return createI18nLoadGate({
|
|
27
|
-
keepPreviousOnPartitionChange: false,
|
|
28
|
-
useLoadArgs: (): I18nLoadArgs => {
|
|
29
|
-
const root = useI18nRootContext();
|
|
30
|
-
const { handle, coordinator, locale } = root;
|
|
31
|
-
return {
|
|
32
|
-
coordinator,
|
|
33
|
-
engineRef: handle,
|
|
34
|
-
partition: locale,
|
|
35
|
-
locale,
|
|
36
|
-
load: (namespaces) =>
|
|
37
|
-
handle.load({
|
|
38
|
-
namespaces: namespaces as ["default"],
|
|
39
|
-
locale,
|
|
40
|
-
}),
|
|
41
|
-
tryResolveSync: (namespaces) =>
|
|
42
|
-
handle.peek({
|
|
43
|
-
namespaces: namespaces as ["default"],
|
|
44
|
-
locale,
|
|
45
|
-
}),
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("runtime primitives", () => {
|
|
52
|
-
it("per-namespace load via gate then translate", async () => {
|
|
53
|
-
const { I18n } = createDefaultLoadGate();
|
|
54
|
-
|
|
55
|
-
await act(async () => {
|
|
56
|
-
render(
|
|
57
|
-
<I18nRootProvider createI18n={createTestI18n} locale="it">
|
|
58
|
-
<I18n namespaces={["default"]} fallback={<span data-testid="fallback">loading</span>}>
|
|
59
|
-
{({ t, locale }) => (
|
|
60
|
-
<span data-testid="greeting">
|
|
61
|
-
{locale}:{t("default", "greeting")}
|
|
62
|
-
</span>
|
|
63
|
-
)}
|
|
64
|
-
</I18n>
|
|
65
|
-
</I18nRootProvider>
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const greeting = await screen.findByTestId("greeting");
|
|
70
|
-
expect(greeting.textContent).toBe("it:Ciao");
|
|
71
|
-
expect(screen.queryByTestId("fallback")).toBeNull();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("I18nRootProvider rejects dictionary + state together", () => {
|
|
75
|
-
expect(() =>
|
|
76
|
-
render(
|
|
77
|
-
<I18nRootProvider
|
|
78
|
-
createI18n={createTestI18n}
|
|
79
|
-
locale="en"
|
|
80
|
-
dictionary={{}}
|
|
81
|
-
state={{ dictionary: {} }}
|
|
82
|
-
>
|
|
83
|
-
<span />
|
|
84
|
-
</I18nRootProvider>
|
|
85
|
-
)
|
|
86
|
-
).toThrow(/pass either `state` or `dictionary`/);
|
|
87
|
-
});
|
|
88
|
-
});
|
package/src/types.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal runtime types for `@xndrjs/i18n-react` implementation modules.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/** Locale-bound multi-namespace `t(ns, key, …)`. */
|
|
6
|
-
export type ScopedTranslateFn = (
|
|
7
|
-
namespace: string,
|
|
8
|
-
key: string,
|
|
9
|
-
params?: Record<string, unknown>
|
|
10
|
-
) => string;
|
|
11
|
-
|
|
12
|
-
/** Partition key for load deduplication — locale (or delivery area when custom). */
|
|
13
|
-
export type LoadPartition = string | undefined;
|
|
14
|
-
|
|
15
|
-
/** Key inputs for load deduplication. */
|
|
16
|
-
export interface LoadCoordinatorKey {
|
|
17
|
-
engineRef: unknown;
|
|
18
|
-
partition: LoadPartition;
|
|
19
|
-
namespaces: readonly string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Request payload for {@link createLoadCoordinator.ensure}. */
|
|
23
|
-
export interface LoadCoordinatorRequest<Scope> extends LoadCoordinatorKey {
|
|
24
|
-
load: () => Promise<Scope>;
|
|
25
|
-
/**
|
|
26
|
-
* Optional sync resolve for already-seeded resources. When it returns a scope,
|
|
27
|
-
* the entry is marked resolved on the same snapshot (no pending → fallback flash).
|
|
28
|
-
*/
|
|
29
|
-
tryResolveSync?: () => Scope | null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Snapshot of a coordinator cache entry. */
|
|
33
|
-
export type LoadCoordinatorEntry<Scope> =
|
|
34
|
-
| { status: "pending" }
|
|
35
|
-
| { status: "resolved"; scope: Scope }
|
|
36
|
-
| { status: "error"; error: unknown };
|
|
37
|
-
|
|
38
|
-
/** Display entry for gates — includes keep-previous and stable bound `t`. */
|
|
39
|
-
export type LoadDisplayEntry<Scope> =
|
|
40
|
-
| {
|
|
41
|
-
status: "pending";
|
|
42
|
-
display: { scope: Scope; locale: string } | null;
|
|
43
|
-
pendingLocale: string | undefined;
|
|
44
|
-
t: ScopedTranslateFn | null;
|
|
45
|
-
}
|
|
46
|
-
| {
|
|
47
|
-
status: "ready";
|
|
48
|
-
scope: Scope;
|
|
49
|
-
locale: string;
|
|
50
|
-
pendingLocale?: undefined;
|
|
51
|
-
t: ScopedTranslateFn;
|
|
52
|
-
}
|
|
53
|
-
| {
|
|
54
|
-
status: "error";
|
|
55
|
-
error: unknown;
|
|
56
|
-
display: { scope: Scope; locale: string } | null;
|
|
57
|
-
pendingLocale: string | undefined;
|
|
58
|
-
t: ScopedTranslateFn | null;
|
|
59
|
-
retry: () => void;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Options for building a display entry (namespace-bound `t`). */
|
|
63
|
-
export interface GetDisplayEntryOptions {
|
|
64
|
-
locale: string;
|
|
65
|
-
namespaces: readonly string[];
|
|
66
|
-
/** When false, do not keep previous scope across partition changes (default true). */
|
|
67
|
-
keepPrevious?: boolean;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Mutable load coordinator returned by {@link createLoadCoordinator}. */
|
|
71
|
-
export interface LoadCoordinator<Scope> {
|
|
72
|
-
readonly revision: number;
|
|
73
|
-
/** @deprecated Prefer {@link ensure}. */
|
|
74
|
-
request(input: LoadCoordinatorRequest<Scope>): void;
|
|
75
|
-
/** Idempotent: start load if missing. Safe from client getSnapshot. */
|
|
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;
|
|
82
|
-
getEntry(key: LoadCoordinatorKey): LoadCoordinatorEntry<Scope>;
|
|
83
|
-
getDisplayEntry(
|
|
84
|
-
key: LoadCoordinatorKey,
|
|
85
|
-
options: GetDisplayEntryOptions
|
|
86
|
-
): LoadDisplayEntry<Scope>;
|
|
87
|
-
/** Evict entry so the next ensure() re-fetches. */
|
|
88
|
-
retry(key: LoadCoordinatorKey): void;
|
|
89
|
-
subscribe(listener: () => void): () => void;
|
|
90
|
-
getPromise(): Promise<Scope>;
|
|
91
|
-
getResolvedScope(): Scope | null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Options for {@link createScopedT}. */
|
|
95
|
-
export interface CreateScopedTOptions {
|
|
96
|
-
namespaces: readonly string[];
|
|
97
|
-
locale: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Minimal scope shape used by {@link createScopedT}. */
|
|
101
|
-
export interface ScopedScopeLike {
|
|
102
|
-
t: (...args: unknown[]) => string;
|
|
103
|
-
forLocale?: (locale: string) => ScopedScopeLike;
|
|
104
|
-
locale?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Factory passed to {@link I18nRootProvider} — typically codegen `createI18n`. */
|
|
108
|
-
export type CreateI18nFactory = (
|
|
109
|
-
// Project factories type options per loaderStrategy (fetch requires fetchImpl).
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridge codegen-specific option shapes
|
|
111
|
-
options?: any
|
|
112
|
-
// Return is project-typed; runtime casts to {@link I18nHandleLike}.
|
|
113
|
-
) => unknown;
|
|
114
|
-
|
|
115
|
-
/** Minimal handle shape from `@xndrjs/i18n` createI18n(). */
|
|
116
|
-
export interface I18nHandleLike {
|
|
117
|
-
load: (input: { namespaces: readonly string[]; locale: string }) => Promise<ScopedScopeLike>;
|
|
118
|
-
peek: (input: { namespaces: readonly string[]; locale: string }) => ScopedScopeLike | null;
|
|
119
|
-
serialize: () => {
|
|
120
|
-
dictionary: unknown;
|
|
121
|
-
resources: readonly (readonly [string, string])[];
|
|
122
|
-
};
|
|
123
|
-
getLoadState: () => {
|
|
124
|
-
resources: readonly {
|
|
125
|
-
namespace: string;
|
|
126
|
-
partition: string;
|
|
127
|
-
status: "pending" | "loaded" | "error";
|
|
128
|
-
error?: unknown;
|
|
129
|
-
}[];
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Single React context value for the lazy i18n tree. */
|
|
134
|
-
export interface I18nRootContextValue {
|
|
135
|
-
handle: I18nHandleLike;
|
|
136
|
-
coordinator: LoadCoordinator<ScopedScopeLike>;
|
|
137
|
-
locale: string;
|
|
138
|
-
}
|