5.6 KiB
useTranslation
(no submodules)
Purpose
useTranslation(namespace?) is the primary translation hook of
@suaveplan/react-i18n. It returns a { t, i18n, ready } triple bound
to the active I18nInstance (from the nearest <TranslationProvider>)
and orchestrates Suspense-aware namespace loading when a backend is
registered. Components subscribe to locale changes through the Provider's
internal useSyncExternalStore, so a setLocale('fr-FR') call triggers
re-renders on the same React commit cycle as the bus emission.
The hook splits cleanly into three control-flow modes — synchronous-ready
(no namespace work needed), Suspense-throw (preload in flight), and
error-boundary-throw (preload rejected). All three are testable without
mocking the i18n core: the tests use a real I18nInstance plus a tiny
in-test Backend that exposes resolve / reject controls.
Features
- Returns
{ t, i18n, ready }wheretis the ergonomic options-bag translator. - Optional
namespaceargument auto-prefixes everyt(key)call with<namespace>:. - Suspense-aware: throws
i18n.catalog.preload([locale], [namespace])when the namespace is not loaded and a backend is registered. - Error boundary aware: a rejected preload Promise surfaces as
ReactI18nError(REACT_I18N.NAMESPACE_LOAD_FAILED)witherror.causeset to the originalI18nError. - Production no-Provider soft-fail returns the identity translator (
t(key) === keyordefaultValuewhen supplied). - Per-(instance, locale, namespace) preload registry keeps the Suspense throw stable across re-renders so the boundary actually resolves.
Usage: Basic Example
import { TranslationProvider, useTranslation } from "@suaveplan/react-i18n";
import { createI18n } from "@suaveplan/i18n/plugin";
function LoginButton() {
const { t } = useTranslation("auth");
return <button type="submit">{t("login")}</button>;
}
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": { auth: { login: "Sign in" } } },
});
export function App() {
return (
<TranslationProvider i18n={i18n}>
<LoginButton />
</TranslationProvider>
);
}
Usage: Advanced Example
When a backend is registered on the instance and the namespace has not
yet been loaded, the hook participates in React Suspense. Wrap the
subtree in <Suspense> and the boundary handles the loading state.
A rejected backend read surfaces through the nearest error boundary as
ReactI18nError(REACT_I18N.NAMESPACE_LOAD_FAILED) with the underlying
I18nError available via .cause.
import { Suspense } from "react";
import { TranslationProvider, useTranslation } from "@suaveplan/react-i18n";
import { ErrorBoundary } from "react-error-boundary";
function Profile() {
const { t } = useTranslation("profile");
return (
<section>
<h1>{t("title")}</h1>
<p>{t("subtitle", { values: { name: "Alice" } })}</p>
</section>
);
}
export function ProfilePage(props: { i18n: import("@suaveplan/i18n/plugin").I18nInstance }) {
return (
<TranslationProvider i18n={props.i18n}>
<ErrorBoundary fallbackRender={({ error }) => <pre>{error.code}</pre>}>
<Suspense fallback={<span>Loading…</span>}>
<Profile />
</Suspense>
</ErrorBoundary>
</TranslationProvider>
);
}
API Reference
useTranslation(namespace?: string): UseTranslationReturn
- Parameters:
namespace?— optional namespace prefix applied to everyt(key)call.
- Returns:
{ t, i18n, ready }.t(key, options?)— bound translator.optionsacceptscount,context,defaultValue,values.i18n— the activeI18nInstance, orundefinedin the production no-Provider fallback.ready—truewhen the namespace is loaded (or no namespace was requested),falseonly on the production no-Provider path.
- Throws:
- Promise when a backend is registered, the namespace is not loaded, and Suspense semantics should kick in. The Promise resolves to
voidonce the catalog finishes loading. ReactI18nError(REACT_I18N.NAMESPACE_LOAD_FAILED)when the preload Promise rejects.error.causecarries the originalI18nError(e.g.BACKEND_READ_FAILED).ReactI18nError(REACT_I18N.NO_PROVIDER)when called outside a<TranslationProvider>andprocess.env.NODE_ENV !== "production".
- Promise when a backend is registered, the namespace is not loaded, and Suspense semantics should kick in. The Promise resolves to
Implementation Notes
- The Suspense throw uses a per-(instance, locale, namespace) entry stored in a
WeakMap<I18nInstance, Map<string, PreloadEntry>>. React renders the same component multiple times during a suspension cascade — without this stability the boundary would never resolve. applyNamespacedoes NOT prepend the namespace when the caller-supplied key already contains":". This matches the upstream catalog convention so a key likeprofile:titlealways means namespaceprofile, keytitle.- The hook intentionally does NOT call
useSyncExternalStoreitself. The Provider holds the only subscription and re-renders descendants onlanguageChanged/loadedso per-render cost stays at oneuseContextread. - The bound translator is rebuilt on every render (via
buildBoundTranslator) — it captures the currentinstance.localeimplicitly throughinstance.t, so a locale change re-renders subscribers AND yields a fresh closure. - Production no-Provider fallback uses a lightweight identity translator built inside the hook (NOT
IDENTITY_TRANSLATION_RETURN.t) so it understands theoptions.defaultValueprecedence exactly the same way the live translator does.