@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 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
- * 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 } : {},