@halecraft/i18n-react
v0.1.0
Published
Optional React bindings for @halecraft/i18n: a controlled Provider + useT/useLocale.
Readme
@halecraft/i18n-react
Optional React bindings for @halecraft/i18n: a controlled Provider plus useT/useLocale. The core is framework-agnostic — messages are just functions — so this adapter is thin: it wires the app-owned active locale through React context and derives a locale-bound Translator from your compiled catalogs.
react(>=18) and@halecraft/i18nare peer dependencies. Strings-only — rich-text<Trans>is out of scope (see below).
Quickstart
import { def } from "@halecraft/i18n";
// `halecraft-i18n compile` output — the eager registry of all locales:
import { catalogs } from "./messages/generated";
import { Provider, useLocale, useT } from "@halecraft/i18n-react";
import { useState } from "react";
const greet = def("Hello {name}");
function Greeting() {
const t = useT();
return <p>{t(greet, { name: "Ada" })}</p>;
}
function LocaleSwitcher() {
const [locale, setLocale] = useLocale();
return (
<button type="button" onClick={() => setLocale(locale === "en" ? "es" : "en")}>
{locale}
</button>
);
}
export function App() {
// The app owns the locale (here `useState`; in real apps usually the router/URL).
const [locale, setLocale] = useState("en");
return (
<Provider catalogs={catalogs} locale={locale} onLocaleChange={setLocale}>
<Greeting />
<LocaleSwitcher />
</Provider>
);
}API
<Provider catalogs locale onLocaleChange?>— controlled. You ownlocale(router/URL/state); the Provider holds no locale state, derives the activeTranslatorviacatalogs.use(locale), and re-renders consumers whenlocale(orcatalogs) changes.catalogsis anyCatalogsregistry — typically the eagercatalogsexport fromhalecraft-i18n compile.useT(): Translator— the active, locale-bound translator. Call itt(message, args)(args required iff the message has any).useLocale(): readonly [locale, setLocale]— the current locale and a setter that delegates toonLocaleChange. OmitonLocaleChangefor a read-only Provider (thensetLocaleis a no-op + a dev warning).
Both hooks throw if used outside a <Provider>.
Pass a stable
onLocaleChange(your router's setter, oruseCallback). An inline arrow changes identity every render, which defeats the Provider'suseMemoand re-renders everyuseTconsumer on each parent render.
Controlled, not stateful
There's no uncontrolled mode to learn: to let the Provider "own" the locale, give it useState — const [locale, setLocale] = useState("en") then <Provider … locale={locale} onLocaleChange={setLocale}>. That's exactly what an app router already does, so the locale stays a single source of truth and never fights the URL.
Graceful degradation
A missing translation renders the English source, never a blank or a MISSING_KEY (a core guarantee of @halecraft/i18n). A half-translated locale is always shippable.
Lazy / code-split locales
For apps that load only the active locale's chunk, use compile's lazy loaders. The Provider is synchronous and eager, so the app owns the async load — await the loader, build a registry, update both props (a Suspense boundary covers the in-flight import):
const cat = await loaders[locale](); // a LocaleCatalog
const catalogs = createCatalogs([cat]); // or merge with already-loaded ones
// <Provider catalogs={catalogs} locale={locale} onLocaleChange={setLocale}>…A loaders-aware Provider may land in a later version.
Rich text / <Trans>
Out of scope for v1. Messages return strings — the runtime sets ignoreTag: true, so <b>…</b> is literal text — which keeps the "just functions" property. Component interpolation (Hello <b>{name}</b>) would need a different return type; a future Trans could consume a message's parsed ICU AST + a tag map.
SSR / RSC
Safe by construction: build the Catalogs registry once and pass the request's locale. The registry is immutable and there's no module-global locale, so concurrent requests never bleed. The Provider is a client component (it uses context + hooks); server components can render with a per-request catalogs.use(locale) directly.
Vanilla / any framework
You don't need React to use @halecraft/i18n — see the runtime's Locale & rendering (createTranslator / catalogs.use(locale), and bare-calling a message to render English).
License
MIT
