React
@xndrjs/i18n-react adds client bindings on top of @xndrjs/i18n: a shared root, colocated namespace gates, and codegen that emits typed I18nRoot / withI18n / I18n for your project. There is no Suspense for translation loads — readiness is explicit via gate / HOC fallbacks.
Install
Section titled “Install”pnpm add @xndrjs/i18n @xndrjs/i18n-react zodpnpm add -D tsxPeers: @xndrjs/i18n ^0.8, react ≥ 19, zod, tsx (codegen CLI).
Codegen
Section titled “Codegen”Uses the same i18n.codegen.json as core. Run after xndrjs-i18n-codegen:
xndrjs-i18n-react-codegen --config i18n/i18n.codegen.jsonOptional i18n-react.codegen.json next to the core config can override the bindings output path (output). Default: {codegenPath}/react-bindings.generated.tsx.
Prefer one script that runs core first, then React — see Overview — Codegen script.
Generated exports typically include:
I18nRoot— provider over handle + load coordinator + localewithI18n— HOC gateI18n— render-prop gateuseI18nRoot— access the handle from under the root
With loaderStrategy: "fetch", generated I18nRoot requires fetchImpl (same DI as createI18n({ fetchImpl })).
SSR → CSR
Section titled “SSR → CSR”On the server, load what the page needs and serialize:
const i18n = createI18n();const { t } = await i18n.load({ namespaces: ["default", "billing"], locale,});const state = i18n.serialize();Pass state into the client root so hydration reuses loaded namespaces (peek) without a second fetch or a loading flash for warm resources:
<I18nRoot locale={locale} state={state}> <BillingPanel invoiceCount={3} /></I18nRoot>withI18n (HOC)
Section titled “withI18n (HOC)”Does not mount your component until the requested namespaces are ready. Prefer this when you need t before the return (branching, derived labels, building children).
const BillingPanel = withI18n<{ invoiceCount: number }>( { namespaces: ["billing"], fallback: <p>Loading…</p> }, function BillingPanel({ invoiceCount }, { t }) { return <p>{t("billing", "invoice_summary", { count: invoiceCount })}</p>; });Own props stay yours; t / locale are injected as the second argument once ready. Wrong keys or params fail at compile time.
<I18n> (gate)
Section titled “<I18n> (gate)”Mounts immediately and only delays the children callback. Prefer this when the shell can render and translation is a leaf inside JSX.
function BillingPanel({ invoiceCount }: { invoiceCount: number }) { return ( <I18n namespaces={["billing"]} fallback={<p>Loading…</p>}> {({ t }) => <p>{t("billing", "invoice_summary", { count: invoiceCount })}</p>} </I18n> );}Missing namespaces show your fallback UI for that gate only while the load is pending — not a blank app. If a load fails and you omit renderError, the gate throws so a React error boundary (e.g. Next.js error.tsx) can handle it; fallback is not used for errors.
Day-to-day loop
Section titled “Day-to-day loop”configure → codegen (core then React) → load on the server → serialize → I18nRoot → gate where you need more namespaces.
See also Runtime, Lazy loading, and the blog post.