Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# useI18n
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`useI18n()` is the foundation hook of `@suaveplan/react-i18n`. It returns the
active `I18nInstance` carried by the nearest `<TranslationProvider>` ancestor.
Every other hook in the package (`useTranslation`, `useLocale`, `useDate`,
`useNumber`, `useCurrency`, `useRelativeTime`, `useList`, `usePlural`,
`useDirection`, `useErrorMessage`) destructures from this base so the rules
about missing-provider behaviour live in one place.
The hook intentionally splits its behaviour by environment so the
configuration error has different escalation paths in the developer's
day-to-day loop and in production traffic. In dev (and tests) the hook
throws `ReactI18nError(REACT_I18N.NO_PROVIDER)` so the bug is visible
immediately. In production the hook returns `null` and emits a single
`warn-once-per-process` log entry — never throwing while real users are
trying to read your UI.
## Features
- Returns the active `I18nInstance` from `TranslationContext`.
- Dev-mode (NODE_ENV !== "production"): throws `ReactI18nError(REACT_I18N.NO_PROVIDER)` outside a Provider.
- Production-mode: returns `null` plus a one-shot `console.warn`.
- The Provider re-renders subscribers on `languageChanged` / `loaded` via `useSyncExternalStore`, so hooks reading `i18n.locale` see the update on the same React commit cycle as the bus emission.
- Zero allocations per render in the common (in-Provider) path.
## Usage: Basic Example
```tsx
import { useI18n, TranslationProvider } from "@suaveplan/react-i18n";
import { createI18n } from "@suaveplan/i18n/plugin";
function HelloLocale() {
const i18n = useI18n();
return <span>{i18n?.locale ?? "no-provider"}</span>;
}
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": { greeting: "Hello" } },
});
export function App() {
return (
<TranslationProvider i18n={i18n}>
<HelloLocale />
</TranslationProvider>
);
}
```
## Usage: Advanced Example
`useI18n` returns the live instance, so a downstream hook can access plugin
slots, subscribe to bus events directly, or call `i18n.changeLanguage`
without going through `useLocale`. This is the recommended composition path
for custom hooks layered on top of the package.
```tsx
import { useI18n } from "@suaveplan/react-i18n";
import { useEffect, useState } from "react";
export function useNamespaceLoadCount(namespace: string): number {
const i18n = useI18n();
const [count, setCount] = useState(0);
useEffect(() => {
if (i18n === null) return;
return i18n.events.on("loaded", (payload) => {
if (payload.namespace === namespace) {
setCount((prev) => prev + 1);
}
});
}, [i18n, namespace]);
return count;
}
```
## API Reference
### `useI18n(): I18nInstance | null`
Returns the active `I18nInstance` from `TranslationContext`.
- **Returns**: `I18nInstance` when a `<TranslationProvider>` ancestor is in scope; `null` outside a Provider in production.
- **Throws**: `ReactI18nError(REACT_I18N.NO_PROVIDER)` outside a Provider when `process.env.NODE_ENV !== "production"`.
## Implementation Notes
- The `null` return in production is intentional. Higher-level hooks (`useTranslation`, `useLocale`, …) destructure this and switch to identity / no-op fallbacks so end-users never see a runtime crash from a missing Provider.
- The dedup key for the warn-once-per-process call is `"react-i18n.use-i18n.no-provider"` — module-level so even multiple call sites and multiple components share the same one-shot.
- The hook intentionally does NOT subscribe to the bus itself. The Provider holds the only `useSyncExternalStore` subscription; descendants re-render via that subscription's change-notification cascade. This keeps the hook's per-render cost equal to a single `useContext` read.
- `process.env.NODE_ENV` is read at call-time (via `isProductionEnv()`) so test suites that swap the env between cases see the swap. Bundler-stamped literals also work because `globalThis.process.env.NODE_ENV` is the lookup target.