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,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.