@xndrjs/i18n-react 0.8.2 → 0.8.3
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 -640
- package/src/namespace-load-gate.tsx +0 -285
- package/src/root-context.tsx +0 -21
- package/src/root-provider.tsx +0 -65
- package/src/runtime.test.tsx +0 -119
- package/src/types.ts +0 -138
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xndrjs/i18n-react",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "React runtime primitives and codegen for @xndrjs/i18n client bindings.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"bin": {
|
|
14
|
-
"xndrjs-i18n-react-codegen": "./
|
|
14
|
+
"xndrjs-i18n-react-codegen": "./dist/cli/codegen.js"
|
|
15
15
|
},
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"exports": {
|
|
@@ -22,17 +22,14 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
26
|
-
"bin",
|
|
27
|
-
"src"
|
|
25
|
+
"dist"
|
|
28
26
|
],
|
|
29
27
|
"engines": {
|
|
30
28
|
"node": ">=18"
|
|
31
29
|
},
|
|
32
30
|
"peerDependencies": {
|
|
33
|
-
"@xndrjs/i18n": "^0.8.
|
|
31
|
+
"@xndrjs/i18n": "^0.8.2",
|
|
34
32
|
"react": ">=19",
|
|
35
|
-
"tsx": ">=4",
|
|
36
33
|
"zod": "^4.0.0"
|
|
37
34
|
},
|
|
38
35
|
"devDependencies": {
|
|
@@ -46,11 +43,11 @@
|
|
|
46
43
|
"tsx": "^4.22.4",
|
|
47
44
|
"vitest": "^4.1.0",
|
|
48
45
|
"zod": "^4.3.6",
|
|
49
|
-
"@xndrjs/i18n": "^0.8.
|
|
46
|
+
"@xndrjs/i18n": "^0.8.2"
|
|
50
47
|
},
|
|
51
48
|
"scripts": {
|
|
52
49
|
"build": "tsup",
|
|
53
|
-
"codegen": "
|
|
50
|
+
"codegen": "node dist/cli/codegen.js",
|
|
54
51
|
"pack": "pnpm run build && mkdir -p artifacts && npm pack --pack-destination artifacts",
|
|
55
52
|
"lint": "eslint .",
|
|
56
53
|
"lint:fix": "eslint . --fix",
|
package/bin/codegen.mjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const script = join(
|
|
7
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
8
|
-
"../src/codegen/generate-react-bindings.ts"
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
const result = spawnSync("tsx", [script, ...process.argv.slice(2)], {
|
|
12
|
-
stdio: "inherit",
|
|
13
|
-
cwd: process.cwd(),
|
|
14
|
-
env: process.env,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
process.exit(result.status ?? 1);
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatReactBindingsFile } from "./react-bindings-file.js";
|
|
3
|
-
|
|
4
|
-
const baseOptions = {
|
|
5
|
-
factoryName: "createI18n",
|
|
6
|
-
paramsTypeName: "MyProjectParams",
|
|
7
|
-
schemaTypeName: "MyProjectSchema",
|
|
8
|
-
localeTypeName: "MyProjectLocale",
|
|
9
|
-
instanceImport: "./instance.generated",
|
|
10
|
-
typesImport: "./i18n-types.generated",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
describe("formatReactBindingsFile", () => {
|
|
14
|
-
it("emits I18nRoot + withI18n / I18n over a single context", () => {
|
|
15
|
-
const output = formatReactBindingsFile(baseOptions);
|
|
16
|
-
|
|
17
|
-
expect(output).toContain("I18nRoot");
|
|
18
|
-
expect(output).toContain("I18nRootProvider");
|
|
19
|
-
expect(output).toContain("useI18nRoot");
|
|
20
|
-
expect(output).toContain("export function withI18n");
|
|
21
|
-
expect(output).toContain("export type I18nProps");
|
|
22
|
-
expect(output).toContain("Ns extends readonly []");
|
|
23
|
-
expect(output).toContain("namespace: never, key: never");
|
|
24
|
-
expect(output).toContain("WithI18nFallback");
|
|
25
|
-
expect(output).toContain("ForwardRefExoticComponent");
|
|
26
|
-
expect(output).toContain("RefAttributes");
|
|
27
|
-
expect(output).toContain("handle.load");
|
|
28
|
-
expect(output).toContain("handle.peek");
|
|
29
|
-
expect(output).toContain("useI18nRootContext()");
|
|
30
|
-
expect(output).not.toContain("fetchImpl");
|
|
31
|
-
expect(output).not.toContain("FetchArtifact");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("requires fetchImpl on I18nRoot when loaderStrategy is fetch", () => {
|
|
35
|
-
const output = formatReactBindingsFile({
|
|
36
|
-
...baseOptions,
|
|
37
|
-
loaderStrategy: "fetch",
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(output).toContain("import type { FetchArtifact, I18nCreateInput }");
|
|
41
|
-
expect(output).toContain("fetchImpl: FetchArtifact");
|
|
42
|
-
expect(output).toContain("fetchImpl={fetchImpl}");
|
|
43
|
-
});
|
|
44
|
-
});
|