Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user