Files
skills/docs/localization/i18n/react-i18n/src/hooks/use-translation/use-translation.md
2026-05-29 08:20:56 +01:00

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 } where t is the ergonomic options-bag translator.
  • Optional namespace argument auto-prefixes every t(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) with error.cause set to the original I18nError.
  • Production no-Provider soft-fail returns the identity translator (t(key) === key or defaultValue when 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 every t(key) call.
  • Returns: { t, i18n, ready }.
    • t(key, options?) — bound translator. options accepts count, context, defaultValue, values.
    • i18n — the active I18nInstance, or undefined in the production no-Provider fallback.
    • ready — true when the namespace is loaded (or no namespace was requested), false only 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 void once the catalog finishes loading.
    • ReactI18nError(REACT_I18N.NAMESPACE_LOAD_FAILED) when the preload Promise rejects. error.cause carries the original I18nError (e.g. BACKEND_READ_FAILED).
    • ReactI18nError(REACT_I18N.NO_PROVIDER) when called outside a <TranslationProvider> and process.env.NODE_ENV !== "production".

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.
  • applyNamespace does NOT prepend the namespace when the caller-supplied key already contains ":". This matches the upstream catalog convention so a key like profile:title always means namespace profile, key title.
  • The hook intentionally does NOT call useSyncExternalStore itself. The Provider holds the only subscription and re-renders descendants on languageChanged / loaded so per-render cost stays at one useContext read.
  • The bound translator is rebuilt on every render (via buildBoundTranslator) — it captures the current instance.locale implicitly through instance.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 the options.defaultValue precedence exactly the same way the live translator does.