@xndrjs/i18n-react 0.8.0 → 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/dist/index.d.ts +22 -4
- package/dist/index.js +52 -32
- package/dist/index.js.map +1 -1
- 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 -357
- package/src/create-load-coordinator.ts +0 -312
- package/src/create-scoped-t.ts +0 -50
- package/src/index.ts +0 -18
- package/src/namespace-load-gate.test.tsx +0 -528
- package/src/namespace-load-gate.tsx +0 -258
- 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 -125
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @xndrjs/i18n-react
|
|
2
|
+
|
|
3
|
+
React bindings for [`@xndrjs/i18n`](../i18n): a shared root, colocated namespace gates, and codegen that emits typed `I18nRoot` / `withI18n` / `I18n` for your project.
|
|
4
|
+
|
|
5
|
+
- **No Suspense** for translation loads — readiness is explicit via gate / HOC fallbacks.
|
|
6
|
+
- **SSR → CSR** hydrate with `state` from `serialize()` so warm namespaces resolve via `peek` without a flash.
|
|
7
|
+
- **Apps import generated bindings**; this package’s public surface is mainly what codegen and advanced wiring need.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @xndrjs/i18n @xndrjs/i18n-react zod
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peers: `@xndrjs/i18n` ^0.8, `react` ≥ 19, `zod`.
|
|
16
|
+
|
|
17
|
+
## Codegen
|
|
18
|
+
|
|
19
|
+
Uses the same `i18n.codegen.json` as core. Run **after** `xndrjs-i18n-codegen`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx xndrjs-i18n-react-codegen --config i18n/i18n.codegen.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Optional `i18n-react.codegen.json` next to the core config can override the bindings output path (`output`). Default: `{codegenPath}/react-bindings.generated.tsx`.
|
|
26
|
+
|
|
27
|
+
Prefer one script that runs core first, then React:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"scripts": {
|
|
32
|
+
"i18n:codegen": "xndrjs-i18n-codegen --config i18n/i18n.codegen.json",
|
|
33
|
+
"i18n:react-codegen": "xndrjs-i18n-react-codegen --config i18n/i18n.codegen.json",
|
|
34
|
+
"i18n:generate": "pnpm i18n:codegen && pnpm i18n:react-codegen"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Generated exports typically include:
|
|
40
|
+
|
|
41
|
+
| Export | Role |
|
|
42
|
+
| ------------- | -------------------------------------------------------------- |
|
|
43
|
+
| `I18nRoot` | Provider over handle + load coordinator + locale |
|
|
44
|
+
| `withI18n` | HOC gate — injects `{ t, locale }` when namespaces are ready |
|
|
45
|
+
| `I18n` | Render-prop gate — same readiness model, shell can mount first |
|
|
46
|
+
| `useI18nRoot` | Access the typed handle from under the root |
|
|
47
|
+
|
|
48
|
+
With `loaderStrategy: "fetch"`, generated `I18nRoot` requires `fetchImpl` (same DI as `createI18n({ fetchImpl })`).
|
|
49
|
+
|
|
50
|
+
## SSR → CSR
|
|
51
|
+
|
|
52
|
+
On the server, load what the page needs and serialize:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { createI18n } from "./generated/instance.generated.js";
|
|
56
|
+
|
|
57
|
+
const i18n = createI18n();
|
|
58
|
+
const { t } = await i18n.load({
|
|
59
|
+
namespaces: ["default", "billing"],
|
|
60
|
+
locale,
|
|
61
|
+
});
|
|
62
|
+
const state = i18n.serialize();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Pass `state` into the client root so hydration reuses loaded namespaces:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { I18nRoot, withI18n } from "./generated/react-bindings.generated";
|
|
69
|
+
|
|
70
|
+
<I18nRoot locale={locale} state={state}>
|
|
71
|
+
<BillingPanel invoiceCount={3} />
|
|
72
|
+
</I18nRoot>;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Omit `state` only when you intentionally cold-start on the client (gates show `fallback` until load settles).
|
|
76
|
+
|
|
77
|
+
## `withI18n` (HOC)
|
|
78
|
+
|
|
79
|
+
Prefer when you need `t` before the `return` (branching, derived labels, building children):
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const BillingPanel = withI18n<{ invoiceCount: number }>(
|
|
83
|
+
{ namespaces: ["billing"], fallback: <p>Loading…</p> },
|
|
84
|
+
function BillingPanel({ invoiceCount }, { t }) {
|
|
85
|
+
return <p>{t("billing", "invoice_summary", { count: invoiceCount })}</p>;
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Own props stay yours; `t` / `locale` are the second argument. Wrong keys or params fail at compile time.
|
|
91
|
+
|
|
92
|
+
Hooks inside the render function are supported across `pending` → `ready` (including when `I18nRoot` has no hydrated `state`). The Outer always invokes `render` so hook order stays stable; until namespaces are ready it still returns your `fallback` (with a no-op `t` during that discarded render).
|
|
93
|
+
|
|
94
|
+
## `<I18n>` (gate)
|
|
95
|
+
|
|
96
|
+
Prefer when the shell can render and translation is a leaf inside JSX:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
function BillingPanel({ invoiceCount }: { invoiceCount: number }) {
|
|
100
|
+
return (
|
|
101
|
+
<I18n namespaces={["billing"]} fallback={<p>Loading…</p>}>
|
|
102
|
+
{({ t }) => <p>{t("billing", "invoice_summary", { count: invoiceCount })}</p>}
|
|
103
|
+
</I18n>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Missing namespaces show your fallback for **that gate only**. If a load fails and you omit `renderError`, the gate **throws** so a React error boundary can handle it; `fallback` is not used for errors.
|
|
109
|
+
|
|
110
|
+
## Day-to-day loop
|
|
111
|
+
|
|
112
|
+
**configure → codegen (core then React) → `load` on the server → `serialize` → `I18nRoot` → gate where you need more namespaces.**
|
|
113
|
+
|
|
114
|
+
## Low-level exports
|
|
115
|
+
|
|
116
|
+
Apps normally use generated bindings. Advanced / library wiring can import from `@xndrjs/i18n-react` directly:
|
|
117
|
+
|
|
118
|
+
- `I18nRootProvider` / `useI18nRootContext`
|
|
119
|
+
- `createI18nLoadGate` / `useNamespaceLoad`
|
|
120
|
+
- `createLoadCoordinator`
|
|
121
|
+
- `createScopedT` / `bindNamespaceTranslate`
|
|
122
|
+
|
|
123
|
+
## Docs
|
|
124
|
+
|
|
125
|
+
Full guide: [xndrjs i18n — React](https://xndrjs.dev/v0/infrastructure/i18n/react/) (or the docs app in this monorepo).
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs2 from 'fs';
|
|
3
|
+
import path2 from 'path';
|
|
4
|
+
import { loadConfig, resolveCodegenPaths } from '@xndrjs/i18n/codegen';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
var GENERATED_FILE_BANNER = "// Automatically generated code. Do not edit manually.\n";
|
|
8
|
+
function importExtensionSuffix(importExtension) {
|
|
9
|
+
return importExtension === "none" ? "" : importExtension;
|
|
10
|
+
}
|
|
11
|
+
function toModuleBasename(filePath) {
|
|
12
|
+
return path2.basename(filePath).replace(/\.tsx?$/, "");
|
|
13
|
+
}
|
|
14
|
+
function toRelativeModuleImport(moduleBasename, importExtension) {
|
|
15
|
+
return `./${moduleBasename}${importExtensionSuffix(importExtension)}`;
|
|
16
|
+
}
|
|
17
|
+
function formatMultiScopedTType(schemaTypeName, paramsTypeName, localeTypeName) {
|
|
18
|
+
const emptyBranch = `(namespace: never, key: never, ...args: never[]) => string`;
|
|
19
|
+
return `type ScopedT<Ns extends readonly AppNamespace[]> = Ns extends readonly []
|
|
20
|
+
? ${emptyBranch}
|
|
21
|
+
: I18nScopeMultiForLocale<
|
|
22
|
+
SchemaForNamespaces<${schemaTypeName}, Ns>,
|
|
23
|
+
ParamsForNamespaces<${schemaTypeName}, ${paramsTypeName}, Ns>,
|
|
24
|
+
${localeTypeName},
|
|
25
|
+
${localeTypeName}
|
|
26
|
+
>["t"];
|
|
27
|
+
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
function formatI18nRoot(options) {
|
|
31
|
+
const { factoryName, localeTypeName, loaderStrategy } = options;
|
|
32
|
+
if (loaderStrategy === "fetch") {
|
|
33
|
+
return `export function I18nRoot({
|
|
34
|
+
locale,
|
|
35
|
+
children,
|
|
36
|
+
state,
|
|
37
|
+
dictionary,
|
|
38
|
+
fetchImpl,
|
|
39
|
+
}: {
|
|
40
|
+
locale: ${localeTypeName};
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
state?: I18nCreateInput | undefined;
|
|
43
|
+
dictionary?: Record<string, unknown> | undefined;
|
|
44
|
+
fetchImpl: FetchArtifact;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<I18nRootProvider
|
|
48
|
+
createI18n={${factoryName}}
|
|
49
|
+
locale={locale}
|
|
50
|
+
fetchImpl={fetchImpl}
|
|
51
|
+
{...(state !== undefined ? { state } : {})}
|
|
52
|
+
{...(dictionary !== undefined ? { dictionary } : {})}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</I18nRootProvider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
return `export function I18nRoot({
|
|
61
|
+
locale,
|
|
62
|
+
children,
|
|
63
|
+
state,
|
|
64
|
+
dictionary,
|
|
65
|
+
}: {
|
|
66
|
+
locale: ${localeTypeName};
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
state?: I18nCreateInput | undefined;
|
|
69
|
+
dictionary?: Record<string, unknown> | undefined;
|
|
70
|
+
}) {
|
|
71
|
+
return (
|
|
72
|
+
<I18nRootProvider
|
|
73
|
+
createI18n={${factoryName}}
|
|
74
|
+
locale={locale}
|
|
75
|
+
{...(state !== undefined ? { state } : {})}
|
|
76
|
+
{...(dictionary !== undefined ? { dictionary } : {})}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</I18nRootProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
function formatReactBindingsFile(options) {
|
|
85
|
+
const {
|
|
86
|
+
factoryName,
|
|
87
|
+
localeTypeName,
|
|
88
|
+
instanceImport,
|
|
89
|
+
paramsTypeName,
|
|
90
|
+
schemaTypeName,
|
|
91
|
+
typesImport,
|
|
92
|
+
loaderStrategy = "import"
|
|
93
|
+
} = options;
|
|
94
|
+
const createInputImport = loaderStrategy === "fetch" ? `import type { FetchArtifact, I18nCreateInput } from "@xndrjs/i18n";
|
|
95
|
+
` : `import type { I18nCreateInput } from "@xndrjs/i18n";
|
|
96
|
+
`;
|
|
97
|
+
return `${GENERATED_FILE_BANNER}"use client";
|
|
98
|
+
|
|
99
|
+
import {
|
|
100
|
+
type ForwardedRef,
|
|
101
|
+
type ForwardRefExoticComponent,
|
|
102
|
+
type ReactNode,
|
|
103
|
+
type RefAttributes,
|
|
104
|
+
} from "react";
|
|
105
|
+
import {
|
|
106
|
+
createI18nLoadGate,
|
|
107
|
+
I18nRootProvider,
|
|
108
|
+
useI18nRootContext,
|
|
109
|
+
} from "@xndrjs/i18n-react";
|
|
110
|
+
` + createInputImport + `import type {
|
|
111
|
+
I18nScopeMultiForLocale,
|
|
112
|
+
ParamsForNamespaces,
|
|
113
|
+
SchemaForNamespaces,
|
|
114
|
+
} from "@xndrjs/i18n";
|
|
115
|
+
import { ${factoryName} } from "${instanceImport}";
|
|
116
|
+
import type { ${paramsTypeName}, ${schemaTypeName}, ${localeTypeName} } from "${typesImport}";
|
|
117
|
+
|
|
118
|
+
export function useI18nRoot(): ReturnType<typeof ${factoryName}> {
|
|
119
|
+
return useI18nRootContext().handle as ReturnType<typeof ${factoryName}>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type AppNamespace = keyof ${schemaTypeName} & string;
|
|
123
|
+
|
|
124
|
+
` + formatMultiScopedTType(schemaTypeName, paramsTypeName, localeTypeName) + `type I18nInjected<Ns extends readonly AppNamespace[]> = {
|
|
125
|
+
t: ScopedT<Ns>;
|
|
126
|
+
locale: ${localeTypeName};
|
|
127
|
+
pendingLocale?: ${localeTypeName};
|
|
128
|
+
error?: unknown;
|
|
129
|
+
retry?: () => void;
|
|
130
|
+
};
|
|
131
|
+
export type I18nProps<Ns extends readonly AppNamespace[]> = I18nInjected<Ns>;
|
|
132
|
+
|
|
133
|
+
export type WithI18nFallback<P extends object> = ReactNode | ((props: P) => ReactNode);
|
|
134
|
+
|
|
135
|
+
const gate = createI18nLoadGate({
|
|
136
|
+
useLoadArgs: () => {
|
|
137
|
+
const root = useI18nRootContext();
|
|
138
|
+
const { handle, coordinator, locale } = root;
|
|
139
|
+
return {
|
|
140
|
+
coordinator,
|
|
141
|
+
engineRef: handle,
|
|
142
|
+
partition: locale,
|
|
143
|
+
locale,
|
|
144
|
+
load: (namespaces: readonly string[]) =>
|
|
145
|
+
handle.load({
|
|
146
|
+
namespaces: namespaces as [AppNamespace, ...AppNamespace[]],
|
|
147
|
+
locale,
|
|
148
|
+
}),
|
|
149
|
+
tryResolveSync: (namespaces: readonly string[]) =>
|
|
150
|
+
handle.peek({
|
|
151
|
+
namespaces: namespaces as [AppNamespace, ...AppNamespace[]],
|
|
152
|
+
locale,
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export function withI18n<
|
|
159
|
+
P extends object = object,
|
|
160
|
+
R = never,
|
|
161
|
+
const Ns extends readonly AppNamespace[] = readonly AppNamespace[],
|
|
162
|
+
>(
|
|
163
|
+
options: {
|
|
164
|
+
namespaces: Ns;
|
|
165
|
+
fallback?: WithI18nFallback<P>;
|
|
166
|
+
renderError?: (args: { error: unknown; retry: () => void; props: P }) => ReactNode;
|
|
167
|
+
},
|
|
168
|
+
render: (props: P, i18n: I18nProps<Ns>, ref?: ForwardedRef<R>) => ReactNode
|
|
169
|
+
): ForwardRefExoticComponent<P & RefAttributes<R>> {
|
|
170
|
+
return gate.withI18n(options, render as never) as ForwardRefExoticComponent<
|
|
171
|
+
P & RefAttributes<R>
|
|
172
|
+
>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function I18n<
|
|
176
|
+
const Ns extends readonly AppNamespace[],
|
|
177
|
+
>(props: {
|
|
178
|
+
namespaces: Ns;
|
|
179
|
+
fallback?: ReactNode;
|
|
180
|
+
renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;
|
|
181
|
+
children: (value: I18nProps<Ns>) => ReactNode;
|
|
182
|
+
}) {
|
|
183
|
+
return gate.I18n(props as never);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
` + formatI18nRoot({ factoryName, localeTypeName, loaderStrategy });
|
|
187
|
+
}
|
|
188
|
+
var DEFAULT_REACT_BINDINGS_BASENAME = "react-bindings.generated.tsx";
|
|
189
|
+
var reactCodegenConfigSchema = z.object({
|
|
190
|
+
output: z.string().min(1).optional()
|
|
191
|
+
}).strict();
|
|
192
|
+
function defaultReactBindingsOutput(instanceOutput) {
|
|
193
|
+
return path2.join(path2.dirname(instanceOutput), DEFAULT_REACT_BINDINGS_BASENAME);
|
|
194
|
+
}
|
|
195
|
+
function loadReactCodegenConfig(configPath) {
|
|
196
|
+
if (!fs2.existsSync(configPath)) {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
let raw;
|
|
200
|
+
try {
|
|
201
|
+
raw = JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
throw new Error(
|
|
205
|
+
`[i18n-react Codegen Error] Failed to parse react config JSON (${configPath}): ${message}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const result = reactCodegenConfigSchema.safeParse(raw);
|
|
209
|
+
if (!result.success) {
|
|
210
|
+
const issueLines = result.error.issues.map((issue) => {
|
|
211
|
+
const issuePath = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
212
|
+
return ` - ${issuePath}: ${issue.message}`;
|
|
213
|
+
});
|
|
214
|
+
throw new Error(
|
|
215
|
+
["[i18n-react Codegen Error] Invalid i18n-react.codegen.json:", ...issueLines].join("\n")
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return result.data;
|
|
219
|
+
}
|
|
220
|
+
function resolveReactBindingsOutputPath(options) {
|
|
221
|
+
if (options.cliOut !== void 0) {
|
|
222
|
+
return options.cliOut;
|
|
223
|
+
}
|
|
224
|
+
if (options.reactConfig?.output !== void 0) {
|
|
225
|
+
return options.reactConfig.output;
|
|
226
|
+
}
|
|
227
|
+
return defaultReactBindingsOutput(options.instanceOutput);
|
|
228
|
+
}
|
|
229
|
+
function defaultReactConfigPath(i18nConfigPath) {
|
|
230
|
+
const dir = path2.dirname(i18nConfigPath);
|
|
231
|
+
const base = path2.basename(i18nConfigPath, path2.extname(i18nConfigPath));
|
|
232
|
+
if (base === "i18n.codegen") {
|
|
233
|
+
return path2.join(dir, "i18n-react.codegen.json");
|
|
234
|
+
}
|
|
235
|
+
return path2.join(dir, `${base}.react.codegen.json`);
|
|
236
|
+
}
|
|
237
|
+
function writeFileIfChanged(absolutePath, content) {
|
|
238
|
+
if (fs2.existsSync(absolutePath)) {
|
|
239
|
+
const currentContent = fs2.readFileSync(absolutePath, "utf8");
|
|
240
|
+
if (currentContent === content) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
fs2.mkdirSync(path2.dirname(absolutePath), { recursive: true });
|
|
245
|
+
fs2.writeFileSync(absolutePath, content);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/codegen/generate-react-bindings.ts
|
|
250
|
+
function readCliFlag(flag) {
|
|
251
|
+
const index = process.argv.indexOf(flag);
|
|
252
|
+
if (index < 0) {
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
const value = process.argv[index + 1];
|
|
256
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
257
|
+
throw new Error(`[i18n-react Codegen Error] Missing value for ${flag}`);
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
function main() {
|
|
262
|
+
const configPath = path2.resolve(
|
|
263
|
+
process.cwd(),
|
|
264
|
+
readCliFlag("--config") ?? "i18n/i18n.codegen.json"
|
|
265
|
+
);
|
|
266
|
+
const projectRoot = path2.dirname(configPath);
|
|
267
|
+
if (!fs2.existsSync(configPath)) {
|
|
268
|
+
throw new Error(`[i18n-react Codegen Error] Config file not found: ${configPath}`);
|
|
269
|
+
}
|
|
270
|
+
const config = loadConfig(configPath);
|
|
271
|
+
const paths = resolveCodegenPaths(config);
|
|
272
|
+
const instanceImport = toRelativeModuleImport(toModuleBasename(paths.instanceOutput), "none");
|
|
273
|
+
const typesImport = toRelativeModuleImport(toModuleBasename(paths.typesOutput), "none");
|
|
274
|
+
const reactConfigPath = path2.resolve(
|
|
275
|
+
projectRoot,
|
|
276
|
+
readCliFlag("--react-config") ?? defaultReactConfigPath(configPath)
|
|
277
|
+
);
|
|
278
|
+
const reactConfig = loadReactCodegenConfig(reactConfigPath);
|
|
279
|
+
const cliOut = readCliFlag("--out");
|
|
280
|
+
const reactOutputRelative = resolveReactBindingsOutputPath({
|
|
281
|
+
instanceOutput: paths.instanceOutput,
|
|
282
|
+
...cliOut ? { cliOut } : {},
|
|
283
|
+
reactConfig
|
|
284
|
+
});
|
|
285
|
+
const reactOutputPath = path2.resolve(projectRoot, reactOutputRelative);
|
|
286
|
+
const content = formatReactBindingsFile({
|
|
287
|
+
factoryName: paths.factoryName,
|
|
288
|
+
paramsTypeName: paths.paramsTypeName,
|
|
289
|
+
schemaTypeName: paths.schemaTypeName,
|
|
290
|
+
localeTypeName: paths.localeTypeName,
|
|
291
|
+
instanceImport,
|
|
292
|
+
typesImport,
|
|
293
|
+
loaderStrategy: config.loaderStrategy ?? "import"
|
|
294
|
+
});
|
|
295
|
+
const written = writeFileIfChanged(reactOutputPath, content);
|
|
296
|
+
const displayPath = path2.relative(projectRoot, reactOutputPath);
|
|
297
|
+
if (written) {
|
|
298
|
+
console.log(`[i18n-react Codegen] Wrote ${displayPath}`);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`[i18n-react Codegen] Unchanged ${displayPath}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
main();
|
|
304
|
+
//# sourceMappingURL=codegen.js.map
|
|
305
|
+
//# sourceMappingURL=codegen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/codegen/emit/react-bindings-file.ts","../../src/codegen/react-codegen-config.ts","../../src/codegen/write-file-if-changed.ts","../../src/codegen/generate-react-bindings.ts"],"names":["path","fs"],"mappings":"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","file":"codegen.js","sourcesContent":["import path from \"node:path\";\n\nexport const GENERATED_FILE_BANNER = \"// Automatically generated code. Do not edit manually.\\n\";\n\nexport type ImportExtension = \"none\" | \".ts\" | \".js\";\n\nexport function importExtensionSuffix(importExtension: ImportExtension): string {\n return importExtension === \"none\" ? \"\" : importExtension;\n}\n\nexport function toModuleBasename(filePath: string): string {\n return path.basename(filePath).replace(/\\.tsx?$/, \"\");\n}\n\nexport function toRelativeModuleImport(\n moduleBasename: string,\n importExtension: ImportExtension\n): string {\n return `./${moduleBasename}${importExtensionSuffix(importExtension)}`;\n}\n\nexport interface ReactBindingsFileOptions {\n factoryName: string;\n paramsTypeName: string;\n schemaTypeName: string;\n localeTypeName: string;\n instanceImport: string;\n typesImport: string;\n /** When `\"fetch\"`, {@link I18nRoot} requires `fetchImpl`. */\n loaderStrategy?: \"import\" | \"fetch\";\n}\n\n/** When `Ns` is `[]`, reject every `t(...)` call so refactoring can clear namespaces and re-add only what is used. */\nfunction formatMultiScopedTType(\n schemaTypeName: string,\n paramsTypeName: string,\n localeTypeName: string\n): string {\n const emptyBranch = `(namespace: never, key: never, ...args: never[]) => string`;\n return (\n `type ScopedT<Ns extends readonly AppNamespace[]> = Ns extends readonly []\\n` +\n ` ? ${emptyBranch}\\n` +\n ` : I18nScopeMultiForLocale<\\n` +\n ` SchemaForNamespaces<${schemaTypeName}, Ns>,\\n` +\n ` ParamsForNamespaces<${schemaTypeName}, ${paramsTypeName}, Ns>,\\n` +\n ` ${localeTypeName},\\n` +\n ` ${localeTypeName}\\n` +\n `>[\"t\"];\\n\\n`\n );\n}\n\nfunction formatI18nRoot(options: {\n factoryName: string;\n localeTypeName: string;\n loaderStrategy: \"import\" | \"fetch\";\n}): string {\n const { factoryName, localeTypeName, loaderStrategy } = options;\n\n if (loaderStrategy === \"fetch\") {\n return (\n `export function I18nRoot({\\n` +\n ` locale,\\n` +\n ` children,\\n` +\n ` state,\\n` +\n ` dictionary,\\n` +\n ` fetchImpl,\\n` +\n `}: {\\n` +\n ` locale: ${localeTypeName};\\n` +\n ` children: ReactNode;\\n` +\n ` state?: I18nCreateInput | undefined;\\n` +\n ` dictionary?: Record<string, unknown> | undefined;\\n` +\n ` fetchImpl: FetchArtifact;\\n` +\n `}) {\\n` +\n ` return (\\n` +\n ` <I18nRootProvider\\n` +\n ` createI18n={${factoryName}}\\n` +\n ` locale={locale}\\n` +\n ` fetchImpl={fetchImpl}\\n` +\n ` {...(state !== undefined ? { state } : {})}\\n` +\n ` {...(dictionary !== undefined ? { dictionary } : {})}\\n` +\n ` >\\n` +\n ` {children}\\n` +\n ` </I18nRootProvider>\\n` +\n ` );\\n` +\n `}\\n`\n );\n }\n\n return (\n `export function I18nRoot({\\n` +\n ` locale,\\n` +\n ` children,\\n` +\n ` state,\\n` +\n ` dictionary,\\n` +\n `}: {\\n` +\n ` locale: ${localeTypeName};\\n` +\n ` children: ReactNode;\\n` +\n ` state?: I18nCreateInput | undefined;\\n` +\n ` dictionary?: Record<string, unknown> | undefined;\\n` +\n `}) {\\n` +\n ` return (\\n` +\n ` <I18nRootProvider\\n` +\n ` createI18n={${factoryName}}\\n` +\n ` locale={locale}\\n` +\n ` {...(state !== undefined ? { state } : {})}\\n` +\n ` {...(dictionary !== undefined ? { dictionary } : {})}\\n` +\n ` >\\n` +\n ` {children}\\n` +\n ` </I18nRootProvider>\\n` +\n ` );\\n` +\n `}\\n`\n );\n}\n\n/**\n * Emits typed root + withI18n / I18n for split-by-locale or custom delivery.\n * Every namespace loads through the handle + namespaceLoaders.\n * Partition→area mapping for custom delivery is injected into the handle by core codegen.\n */\nexport function formatReactBindingsFile(options: ReactBindingsFileOptions): string {\n const {\n factoryName,\n localeTypeName,\n instanceImport,\n paramsTypeName,\n schemaTypeName,\n typesImport,\n loaderStrategy = \"import\",\n } = options;\n\n const createInputImport =\n loaderStrategy === \"fetch\"\n ? `import type { FetchArtifact, I18nCreateInput } from \"@xndrjs/i18n\";\\n`\n : `import type { I18nCreateInput } from \"@xndrjs/i18n\";\\n`;\n\n return (\n `${GENERATED_FILE_BANNER}` +\n `\"use client\";\\n\\n` +\n `import {\\n` +\n ` type ForwardedRef,\\n` +\n ` type ForwardRefExoticComponent,\\n` +\n ` type ReactNode,\\n` +\n ` type RefAttributes,\\n` +\n `} from \"react\";\\n` +\n `import {\\n` +\n ` createI18nLoadGate,\\n` +\n ` I18nRootProvider,\\n` +\n ` useI18nRootContext,\\n` +\n `} from \"@xndrjs/i18n-react\";\\n` +\n createInputImport +\n `import type {\\n` +\n ` I18nScopeMultiForLocale,\\n` +\n ` ParamsForNamespaces,\\n` +\n ` SchemaForNamespaces,\\n` +\n `} from \"@xndrjs/i18n\";\\n` +\n `import { ${factoryName} } from \"${instanceImport}\";\\n` +\n `import type { ${paramsTypeName}, ${schemaTypeName}, ${localeTypeName} } from \"${typesImport}\";\\n` +\n `\\n` +\n `export function useI18nRoot(): ReturnType<typeof ${factoryName}> {\\n` +\n ` return useI18nRootContext().handle as ReturnType<typeof ${factoryName}>;\\n` +\n `}\\n\\n` +\n `type AppNamespace = keyof ${schemaTypeName} & string;\\n\\n` +\n formatMultiScopedTType(schemaTypeName, paramsTypeName, localeTypeName) +\n `type I18nInjected<Ns extends readonly AppNamespace[]> = {\\n` +\n ` t: ScopedT<Ns>;\\n` +\n ` locale: ${localeTypeName};\\n` +\n ` pendingLocale?: ${localeTypeName};\\n` +\n ` error?: unknown;\\n` +\n ` retry?: () => void;\\n` +\n `};\\n` +\n `export type I18nProps<Ns extends readonly AppNamespace[]> = I18nInjected<Ns>;\\n\\n` +\n `export type WithI18nFallback<P extends object> = ReactNode | ((props: P) => ReactNode);\\n\\n` +\n `const gate = createI18nLoadGate({\\n` +\n ` useLoadArgs: () => {\\n` +\n ` const root = useI18nRootContext();\\n` +\n ` const { handle, coordinator, locale } = root;\\n` +\n ` return {\\n` +\n ` coordinator,\\n` +\n ` engineRef: handle,\\n` +\n ` partition: locale,\\n` +\n ` locale,\\n` +\n ` load: (namespaces: readonly string[]) =>\\n` +\n ` handle.load({\\n` +\n ` namespaces: namespaces as [AppNamespace, ...AppNamespace[]],\\n` +\n ` locale,\\n` +\n ` }),\\n` +\n ` tryResolveSync: (namespaces: readonly string[]) =>\\n` +\n ` handle.peek({\\n` +\n ` namespaces: namespaces as [AppNamespace, ...AppNamespace[]],\\n` +\n ` locale,\\n` +\n ` }),\\n` +\n ` };\\n` +\n ` },\\n` +\n `});\\n\\n` +\n `export function withI18n<\\n` +\n ` P extends object = object,\\n` +\n ` R = never,\\n` +\n ` const Ns extends readonly AppNamespace[] = readonly AppNamespace[],\\n` +\n `>(\\n` +\n ` options: {\\n` +\n ` namespaces: Ns;\\n` +\n ` fallback?: WithI18nFallback<P>;\\n` +\n ` renderError?: (args: { error: unknown; retry: () => void; props: P }) => ReactNode;\\n` +\n ` },\\n` +\n ` render: (props: P, i18n: I18nProps<Ns>, ref?: ForwardedRef<R>) => ReactNode\\n` +\n `): ForwardRefExoticComponent<P & RefAttributes<R>> {\\n` +\n ` return gate.withI18n(options, render as never) as ForwardRefExoticComponent<\\n` +\n ` P & RefAttributes<R>\\n` +\n ` >;\\n` +\n `}\\n\\n` +\n `export function I18n<\\n` +\n ` const Ns extends readonly AppNamespace[],\\n` +\n `>(props: {\\n` +\n ` namespaces: Ns;\\n` +\n ` fallback?: ReactNode;\\n` +\n ` renderError?: (args: { error: unknown; retry: () => void }) => ReactNode;\\n` +\n ` children: (value: I18nProps<Ns>) => ReactNode;\\n` +\n `}) {\\n` +\n ` return gate.I18n(props as never);\\n` +\n `}\\n\\n` +\n formatI18nRoot({ factoryName, localeTypeName, loaderStrategy })\n );\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_REACT_BINDINGS_BASENAME = \"react-bindings.generated.tsx\";\n\nconst reactCodegenConfigSchema = z\n .object({\n output: z.string().min(1).optional(),\n })\n .strict();\n\nexport type ReactCodegenConfig = z.infer<typeof reactCodegenConfigSchema>;\n\nexport function defaultReactBindingsOutput(instanceOutput: string): string {\n return path.join(path.dirname(instanceOutput), DEFAULT_REACT_BINDINGS_BASENAME);\n}\n\n/** Loads optional `i18n-react.codegen.json` when present. */\nexport function loadReactCodegenConfig(configPath: string): ReactCodegenConfig {\n if (!fs.existsSync(configPath)) {\n return {};\n }\n\n let raw: unknown;\n try {\n raw = JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `[i18n-react Codegen Error] Failed to parse react config JSON (${configPath}): ${message}`\n );\n }\n\n const result = reactCodegenConfigSchema.safeParse(raw);\n if (!result.success) {\n const issueLines = result.error.issues.map((issue) => {\n const issuePath = issue.path.length > 0 ? issue.path.join(\".\") : \"(root)\";\n return ` - ${issuePath}: ${issue.message}`;\n });\n throw new Error(\n [\"[i18n-react Codegen Error] Invalid i18n-react.codegen.json:\", ...issueLines].join(\"\\n\")\n );\n }\n\n return result.data;\n}\n\nexport function resolveReactBindingsOutputPath(options: {\n instanceOutput: string;\n cliOut?: string;\n reactConfig?: ReactCodegenConfig;\n}): string {\n if (options.cliOut !== undefined) {\n return options.cliOut;\n }\n if (options.reactConfig?.output !== undefined) {\n return options.reactConfig.output;\n }\n return defaultReactBindingsOutput(options.instanceOutput);\n}\n\nexport function defaultReactConfigPath(i18nConfigPath: string): string {\n const dir = path.dirname(i18nConfigPath);\n const base = path.basename(i18nConfigPath, path.extname(i18nConfigPath));\n if (base === \"i18n.codegen\") {\n return path.join(dir, \"i18n-react.codegen.json\");\n }\n return path.join(dir, `${base}.react.codegen.json`);\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** Writes only when content changed — stable mtimes for watchers. */\nexport function writeFileIfChanged(absolutePath: string, content: string): boolean {\n if (fs.existsSync(absolutePath)) {\n const currentContent = fs.readFileSync(absolutePath, \"utf8\");\n if (currentContent === content) {\n return false;\n }\n }\n\n fs.mkdirSync(path.dirname(absolutePath), { recursive: true });\n fs.writeFileSync(absolutePath, content);\n return true;\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { loadConfig } from \"@xndrjs/i18n/codegen\";\nimport type { CodegenConfig } from \"@xndrjs/i18n/codegen\";\nimport { resolveCodegenPaths } from \"@xndrjs/i18n/codegen\";\nimport {\n formatReactBindingsFile,\n toModuleBasename,\n toRelativeModuleImport,\n} from \"./emit/react-bindings-file.js\";\nimport {\n defaultReactConfigPath,\n loadReactCodegenConfig,\n resolveReactBindingsOutputPath,\n} from \"./react-codegen-config.js\";\nimport { writeFileIfChanged } from \"./write-file-if-changed.js\";\n\nfunction readCliFlag(flag: string): string | undefined {\n const index = process.argv.indexOf(flag);\n if (index < 0) {\n return undefined;\n }\n const value = process.argv[index + 1];\n if (value === undefined || value.startsWith(\"-\")) {\n throw new Error(`[i18n-react Codegen Error] Missing value for ${flag}`);\n }\n return value;\n}\n\n/**\n * React bindings codegen CLI.\n *\n * Reads `i18n.codegen.json` from `@xndrjs/i18n` and optionally `i18n-react.codegen.json`.\n * Output path: `--out` > react config `output` > default next to instance under `output/`.\n */\nfunction main(): void {\n const configPath = path.resolve(\n process.cwd(),\n readCliFlag(\"--config\") ?? \"i18n/i18n.codegen.json\"\n );\n const projectRoot = path.dirname(configPath);\n\n if (!fs.existsSync(configPath)) {\n throw new Error(`[i18n-react Codegen Error] Config file not found: ${configPath}`);\n }\n\n const config: CodegenConfig = loadConfig(configPath);\n const paths = resolveCodegenPaths(config);\n\n const instanceImport = toRelativeModuleImport(toModuleBasename(paths.instanceOutput), \"none\");\n const typesImport = toRelativeModuleImport(toModuleBasename(paths.typesOutput), \"none\");\n\n const reactConfigPath = path.resolve(\n projectRoot,\n readCliFlag(\"--react-config\") ?? defaultReactConfigPath(configPath)\n );\n const reactConfig = loadReactCodegenConfig(reactConfigPath);\n const cliOut = readCliFlag(\"--out\");\n const reactOutputRelative = resolveReactBindingsOutputPath({\n instanceOutput: paths.instanceOutput,\n ...(cliOut ? { cliOut } : {}),\n reactConfig,\n });\n const reactOutputPath = path.resolve(projectRoot, reactOutputRelative);\n\n const content = formatReactBindingsFile({\n factoryName: paths.factoryName,\n paramsTypeName: paths.paramsTypeName,\n schemaTypeName: paths.schemaTypeName,\n localeTypeName: paths.localeTypeName,\n instanceImport,\n typesImport,\n loaderStrategy: config.loaderStrategy ?? \"import\",\n });\n\n const written = writeFileIfChanged(reactOutputPath, content);\n const displayPath = path.relative(projectRoot, reactOutputPath);\n\n if (written) {\n console.log(`[i18n-react Codegen] Wrote ${displayPath}`);\n } else {\n console.log(`[i18n-react Codegen] Unchanged ${displayPath}`);\n }\n}\n\nmain();\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -71,8 +71,13 @@ interface LoadCoordinator<Scope> {
|
|
|
71
71
|
readonly revision: number;
|
|
72
72
|
/** @deprecated Prefer {@link ensure}. */
|
|
73
73
|
request(input: LoadCoordinatorRequest<Scope>): void;
|
|
74
|
-
/** Idempotent: start load if missing. Safe from getSnapshot. */
|
|
74
|
+
/** Idempotent: start load if missing. Safe from client getSnapshot. */
|
|
75
75
|
ensure(input: LoadCoordinatorRequest<Scope>): void;
|
|
76
|
+
/**
|
|
77
|
+
* Idempotent: resolve from {@link LoadCoordinatorRequest.tryResolveSync} only.
|
|
78
|
+
* Never starts `load()` — use from SSR `getServerSnapshot`.
|
|
79
|
+
*/
|
|
80
|
+
ensureSync(input: LoadCoordinatorRequest<Scope>): void;
|
|
76
81
|
getEntry(key: LoadCoordinatorKey): LoadCoordinatorEntry<Scope>;
|
|
77
82
|
getDisplayEntry(key: LoadCoordinatorKey, options: GetDisplayEntryOptions): LoadDisplayEntry<Scope>;
|
|
78
83
|
/** Evict entry so the next ensure() re-fetches. */
|
|
@@ -108,6 +113,14 @@ interface I18nHandleLike {
|
|
|
108
113
|
dictionary: unknown;
|
|
109
114
|
resources: readonly (readonly [string, string])[];
|
|
110
115
|
};
|
|
116
|
+
getLoadState: () => {
|
|
117
|
+
resources: readonly {
|
|
118
|
+
namespace: string;
|
|
119
|
+
partition: string;
|
|
120
|
+
status: "pending" | "loaded" | "error";
|
|
121
|
+
error?: unknown;
|
|
122
|
+
}[];
|
|
123
|
+
};
|
|
111
124
|
}
|
|
112
125
|
/** Single React context value for the lazy i18n tree. */
|
|
113
126
|
interface I18nRootContextValue {
|
|
@@ -141,7 +154,7 @@ declare function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinat
|
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
156
|
* Manual (non-Suspense) i18n load gate: ensure + subscribe via the load
|
|
144
|
-
* coordinator, then render fallback / keep-previous / error UI.
|
|
157
|
+
* coordinator, then render fallback / keep-previous / error UI / throw.
|
|
145
158
|
*/
|
|
146
159
|
|
|
147
160
|
/** Inputs for {@link useNamespaceLoad}. */
|
|
@@ -157,7 +170,8 @@ interface UseNamespaceLoadInput<Scope> {
|
|
|
157
170
|
}
|
|
158
171
|
/**
|
|
159
172
|
* Starts (or reuses) a coordinator load and re-renders when that entry settles.
|
|
160
|
-
*
|
|
173
|
+
* Client getSnapshot may kick `load()`; SSR getServerSnapshot only peeks sync
|
|
174
|
+
* (hydrated `state`) and never starts a client fetch during prerender.
|
|
161
175
|
*/
|
|
162
176
|
declare function useNamespaceLoad<Scope>(input: UseNamespaceLoadInput<Scope>): LoadDisplayEntry<Scope>;
|
|
163
177
|
/** Scoped `{ t, locale }` injected into gate children / HOC when ready (or kept). */
|
|
@@ -189,8 +203,12 @@ interface CreateI18nLoadGateOptions {
|
|
|
189
203
|
}
|
|
190
204
|
interface I18nGateProps {
|
|
191
205
|
namespaces: readonly string[];
|
|
206
|
+
/** Shown only while the load is pending (not on error). */
|
|
192
207
|
fallback?: ReactNode;
|
|
193
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* When set, called for error with no keep-previous display.
|
|
210
|
+
* Otherwise the load error is thrown for a React error boundary.
|
|
211
|
+
*/
|
|
194
212
|
renderError?: (args: {
|
|
195
213
|
error: unknown;
|
|
196
214
|
retry: () => void;
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,34 @@ function createLoadCoordinator() {
|
|
|
69
69
|
}
|
|
70
70
|
notify();
|
|
71
71
|
}
|
|
72
|
+
function markResolvedSync(key, syncScope) {
|
|
73
|
+
const entrySnapshot = {
|
|
74
|
+
status: "resolved",
|
|
75
|
+
scope: syncScope
|
|
76
|
+
};
|
|
77
|
+
entries.set(key, {
|
|
78
|
+
status: "resolved",
|
|
79
|
+
promise: Promise.resolve(syncScope),
|
|
80
|
+
resolvedScope: syncScope,
|
|
81
|
+
error: null,
|
|
82
|
+
requestId: ++nextRequestId,
|
|
83
|
+
entrySnapshot
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function ensureSync(input) {
|
|
87
|
+
const key = cacheKey(input.engineRef, input.partition, input.namespaces);
|
|
88
|
+
lastKey = key;
|
|
89
|
+
if (entries.has(key)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (input.tryResolveSync === void 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const syncScope = input.tryResolveSync();
|
|
96
|
+
if (syncScope !== null) {
|
|
97
|
+
markResolvedSync(key, syncScope);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
72
100
|
function ensure(input) {
|
|
73
101
|
const key = cacheKey(input.engineRef, input.partition, input.namespaces);
|
|
74
102
|
lastKey = key;
|
|
@@ -79,18 +107,7 @@ function createLoadCoordinator() {
|
|
|
79
107
|
if (input.tryResolveSync !== void 0) {
|
|
80
108
|
const syncScope = input.tryResolveSync();
|
|
81
109
|
if (syncScope !== null) {
|
|
82
|
-
|
|
83
|
-
status: "resolved",
|
|
84
|
-
scope: syncScope
|
|
85
|
-
};
|
|
86
|
-
entries.set(key, {
|
|
87
|
-
status: "resolved",
|
|
88
|
-
promise: Promise.resolve(syncScope),
|
|
89
|
-
resolvedScope: syncScope,
|
|
90
|
-
error: null,
|
|
91
|
-
requestId: ++nextRequestId,
|
|
92
|
-
entrySnapshot
|
|
93
|
-
});
|
|
110
|
+
markResolvedSync(key, syncScope);
|
|
94
111
|
return;
|
|
95
112
|
}
|
|
96
113
|
}
|
|
@@ -120,7 +137,9 @@ function createLoadCoordinator() {
|
|
|
120
137
|
throw error;
|
|
121
138
|
}
|
|
122
139
|
);
|
|
123
|
-
void promise.catch(() =>
|
|
140
|
+
void promise.catch((error) => {
|
|
141
|
+
console.error("[i18n-react] namespace load failed:", error);
|
|
142
|
+
});
|
|
124
143
|
entries.set(key, {
|
|
125
144
|
status: "pending",
|
|
126
145
|
promise,
|
|
@@ -245,6 +264,7 @@ function createLoadCoordinator() {
|
|
|
245
264
|
},
|
|
246
265
|
request: ensure,
|
|
247
266
|
ensure,
|
|
267
|
+
ensureSync,
|
|
248
268
|
getEntry,
|
|
249
269
|
getDisplayEntry,
|
|
250
270
|
retry: retryKey,
|
|
@@ -271,29 +291,20 @@ function useNamespaceLoad(input) {
|
|
|
271
291
|
load,
|
|
272
292
|
...tryResolveSync !== void 0 ? { tryResolveSync } : {}
|
|
273
293
|
};
|
|
294
|
+
const displayOptions = {
|
|
295
|
+
locale,
|
|
296
|
+
namespaces,
|
|
297
|
+
...keepPrevious !== void 0 ? { keepPrevious } : {}
|
|
298
|
+
};
|
|
274
299
|
return useSyncExternalStore(
|
|
275
300
|
coordinator.subscribe,
|
|
276
301
|
() => {
|
|
277
302
|
coordinator.ensure(request);
|
|
278
|
-
return coordinator.getDisplayEntry(
|
|
279
|
-
{ engineRef, partition, namespaces },
|
|
280
|
-
{
|
|
281
|
-
locale,
|
|
282
|
-
namespaces,
|
|
283
|
-
...keepPrevious !== void 0 ? { keepPrevious } : {}
|
|
284
|
-
}
|
|
285
|
-
);
|
|
303
|
+
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
286
304
|
},
|
|
287
305
|
() => {
|
|
288
|
-
coordinator.
|
|
289
|
-
return coordinator.getDisplayEntry(
|
|
290
|
-
{ engineRef, partition, namespaces },
|
|
291
|
-
{
|
|
292
|
-
locale,
|
|
293
|
-
namespaces,
|
|
294
|
-
...keepPrevious !== void 0 ? { keepPrevious } : {}
|
|
295
|
-
}
|
|
296
|
-
);
|
|
306
|
+
coordinator.ensureSync(request);
|
|
307
|
+
return coordinator.getDisplayEntry({ engineRef, partition, namespaces }, displayOptions);
|
|
297
308
|
}
|
|
298
309
|
);
|
|
299
310
|
}
|
|
@@ -303,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
|
-
|
|
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
|
|
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 } : {},
|