first commit
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# `<LanguageSwitcher>`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`<LanguageSwitcher>` is a headless render-prop component that exposes the
|
||||
language-switching surface to consumer-controlled UI. It renders no DOM
|
||||
markup of its own — the caller controls the visual treatment via the
|
||||
render-prop child. The component bundles the bookkeeping that every
|
||||
language switcher needs: it normalises the supplied locale list into
|
||||
`LocaleDescriptor` objects with derived text-direction, subscribes to
|
||||
`languageChanged` / `loaded` events through the active Provider's
|
||||
`I18nInstance`, wraps the locale-change call in `useTransition` so the
|
||||
namespace load triggered by the switch never blocks urgent updates, and
|
||||
exposes `isPending` so the caller can render a busy state.
|
||||
|
||||
## Features
|
||||
|
||||
- Headless: zero DOM, zero CSS — render any UI in the render prop
|
||||
- Locale normalisation: accepts string codes or `LocaleDescriptor` objects
|
||||
- Auto-derives `direction` (`ltr` / `rtl`) via `getTextDirection` from
|
||||
`@suaveplan/i18n`
|
||||
- `change(locale): Promise<void>` returns when the language change
|
||||
resolves so callers can chain `await change(...)`
|
||||
- `isPending` aggregates the `useTransition` flag and the awaitable
|
||||
in-flight flag so both render-time pendingness and async resolution
|
||||
are surfaced
|
||||
- Throws `ReactI18nError(REACT_I18N.NO_PROVIDER)` outside a Provider
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```tsx
|
||||
import { TranslationProvider } from "@suaveplan/react-i18n";
|
||||
import { LanguageSwitcher } from "@suaveplan/react-i18n/components/language-switcher";
|
||||
|
||||
<TranslationProvider i18n={i18n}>
|
||||
<LanguageSwitcher locales={["en-GB", "fr-FR", "ar-AE"]}>
|
||||
{({ locales, current, change, isPending }) => (
|
||||
<select
|
||||
value={current}
|
||||
disabled={isPending}
|
||||
onChange={(e) => change(e.target.value)}
|
||||
>
|
||||
{locales.map((l) => (
|
||||
<option key={l.code} value={l.code}>
|
||||
{l.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</LanguageSwitcher>
|
||||
</TranslationProvider>;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
A typed dropdown with custom labels, RTL-aware ordering, and loading
|
||||
spinner:
|
||||
|
||||
```tsx
|
||||
import { LanguageSwitcher } from "@suaveplan/react-i18n/components/language-switcher";
|
||||
|
||||
<LanguageSwitcher
|
||||
locales={[
|
||||
{ code: "en-GB", name: "English", nativeName: "English" },
|
||||
{ code: "fr-FR", name: "French", nativeName: "Français" },
|
||||
{ code: "ar-AE", name: "Arabic", nativeName: "العربية", direction: "rtl" },
|
||||
]}
|
||||
>
|
||||
{({ locales, current, change, isPending }) => (
|
||||
<div role="group" aria-label="Language">
|
||||
{locales.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
type="button"
|
||||
aria-pressed={l.code === current}
|
||||
aria-busy={isPending && l.code !== current}
|
||||
dir={l.direction}
|
||||
onClick={() => {
|
||||
void change(l.code);
|
||||
}}
|
||||
>
|
||||
{l.nativeName ?? l.code}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</LanguageSwitcher>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `LanguageSwitcher(props: LanguageSwitcherProps): ReactElement | null`
|
||||
|
||||
Props:
|
||||
|
||||
- `locales: ReadonlyArray<string | LocaleDescriptor>` — locales to
|
||||
surface to the render-prop. Strings are upgraded to `{ code,
|
||||
direction }` descriptors via `getTextDirection`.
|
||||
- `children: (state: LanguageSwitcherState) => ReactNode` — render-prop
|
||||
child. Receives the resolved state object.
|
||||
|
||||
### `LanguageSwitcherState`
|
||||
|
||||
- `locales: readonly LocaleDescriptor[]`
|
||||
- `current: string`
|
||||
- `change: (locale: string) => Promise<void>` — wrapped in
|
||||
`useTransition`; returns when the underlying `i18n.changeLanguage`
|
||||
resolves
|
||||
- `isPending: boolean` — true while a transition or awaitable change is
|
||||
in flight
|
||||
|
||||
### `LocaleDescriptor`
|
||||
|
||||
- `code: string`
|
||||
- `name?: string` — Latin-script display name
|
||||
- `nativeName?: string` — native-script display name
|
||||
- `direction?: "ltr" | "rtl"` — derived when not supplied
|
||||
|
||||
Throws `ReactI18nError(REACT_I18N.NO_PROVIDER)` outside a
|
||||
`TranslationProvider`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The component uses `useSyncExternalStore` so `current` reflects the
|
||||
active locale across concurrent renders.
|
||||
- `useTransition` alone surfaces pendingness only during a render. The
|
||||
component layers an explicit awaiting flag so an `await change(...)`
|
||||
in user code observes the entire async lifecycle (start → load → end)
|
||||
rather than only the React-scheduled portion.
|
||||
- `getTextDirection` is invoked once per render via `useMemo` keyed on
|
||||
`props.locales`. Stable input arrays therefore avoid re-deriving
|
||||
direction on every render.
|
||||
- The render-prop is invoked as a function, not as JSX (`children(state)`),
|
||||
so React does not treat the function as a component instance — no
|
||||
`displayName` warnings, no Strict Mode double-invocation surprise.
|
||||
Reference in New Issue
Block a user