# 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 ``) 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 `:`. - 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 ```tsx import { TranslationProvider, useTranslation } from "@suaveplan/react-i18n"; import { createI18n } from "@suaveplan/i18n/plugin"; function LoginButton() { const { t } = useTranslation("auth"); return ; } const i18n = createI18n({ defaultLocale: "en-GB", messages: { "en-GB": { auth: { login: "Sign in" } } }, }); export function App() { return ( ); } ``` ## 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 `` 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`. ```tsx import { Suspense } from "react"; import { TranslationProvider, useTranslation } from "@suaveplan/react-i18n"; import { ErrorBoundary } from "react-error-boundary"; function Profile() { const { t } = useTranslation("profile"); return (

{t("title")}

{t("subtitle", { values: { name: "Alice" } })}

); } export function ProfilePage(props: { i18n: import("@suaveplan/i18n/plugin").I18nInstance }) { return (
{error.code}
}> Loading…}>
); } ``` ## 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 `` and `process.env.NODE_ENV !== "production"`. ## Implementation Notes - The Suspense throw uses a per-(instance, locale, namespace) entry stored in a `WeakMap>`. 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.