first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
# useTranslation
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```tsx
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`.
```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 (
<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.