Initial Commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# `<I18nSuspense>`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`<I18nSuspense>` is a thin convenience wrapper around React's built-in
|
||||
`<Suspense>` boundary. It gives a translation-aware default fallback
|
||||
that announces loading state to assistive technology via
|
||||
`aria-live="polite"`, increments the
|
||||
`react_i18n.suspense.fallback.total` OTel counter every time the
|
||||
fallback renders, and resolves the fallback label through the active
|
||||
catalog so consumers can localise the loading announcement without
|
||||
forking the component. The wrapper exists so consumers do not need to
|
||||
re-implement the same a11y + telemetry boilerplate around every
|
||||
namespace boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Default fallback with `role="status"`, `aria-live="polite"`, and
|
||||
`aria-busy="true"` for screen-reader-friendly loading state
|
||||
- Visually hidden loading label so the announcement does not push DOM
|
||||
layout around
|
||||
- Counter `react_i18n.suspense.fallback.total{ namespace, locale }`
|
||||
records every fallback render
|
||||
- Loading label resolves from the active catalog with the bundled
|
||||
English string (`react-i18n.suspense.loading`) as the fallback
|
||||
- Custom `fallback` prop bypasses the default entirely (consumer-owned
|
||||
spinner, skeleton, or loading screen)
|
||||
- Zero hardcoded user-facing strings — every visible string flows
|
||||
through a translation key
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
Wrap a subtree that may suspend during a backend-driven namespace load:
|
||||
|
||||
```tsx
|
||||
import { I18nSuspense } from "@suaveplan/react-i18n/components/i18n-suspense";
|
||||
|
||||
<I18nSuspense namespace="auth">
|
||||
<LoginForm />
|
||||
</I18nSuspense>;
|
||||
```
|
||||
|
||||
When `LoginForm` reads from a not-yet-loaded namespace and `useTranslation`
|
||||
throws the preload promise, the wrapper renders the default fallback,
|
||||
records the metric sample, and re-renders the children once the load
|
||||
resolves.
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Custom fallback + per-namespace telemetry segmentation:
|
||||
|
||||
```tsx
|
||||
import { I18nSuspense } from "@suaveplan/react-i18n/components/i18n-suspense";
|
||||
|
||||
<I18nSuspense
|
||||
namespace="checkout"
|
||||
fallback={
|
||||
<div role="status" aria-live="polite" aria-busy="true">
|
||||
<Spinner />
|
||||
<span>{t("checkout.loading")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CheckoutFlow />
|
||||
</I18nSuspense>;
|
||||
```
|
||||
|
||||
Consumers can override the bundled loading label by adding the
|
||||
`react-i18n.suspense.loading` key to their catalog:
|
||||
|
||||
```ts
|
||||
const i18n = createI18n({
|
||||
defaultLocale: "de-DE",
|
||||
messages: {
|
||||
"de-DE": {
|
||||
"react-i18n": {
|
||||
suspense: { loading: "Übersetzungen werden geladen…" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The default fallback will pick up the override automatically.
|
||||
|
||||
## API Reference
|
||||
|
||||
### `I18nSuspense(props: I18nSuspenseProps): ReactElement`
|
||||
|
||||
Props:
|
||||
|
||||
- `fallback?: ReactNode` — replaces the default screen-reader-friendly
|
||||
fallback element. When omitted, the wrapper renders its built-in
|
||||
`aria-live="polite"` div.
|
||||
- `namespace?: string` — namespace label attached to the
|
||||
`react_i18n.suspense.fallback.total` counter sample. Default: `""`.
|
||||
- `children: ReactNode` — subtree wrapped by the underlying
|
||||
`<Suspense>` boundary.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The default-fallback element is a child component (`DefaultFallback`)
|
||||
rather than an inline `<div>` so the `useEffect` that records the
|
||||
metric runs only when the fallback is actually rendered (Suspense
|
||||
defers child rendering, but the fallback always renders fresh on each
|
||||
suspend cycle).
|
||||
- A per-instance `useRef<boolean>` guards against React's Strict-Mode
|
||||
double-invoke so the counter records exactly once per logical
|
||||
fallback render.
|
||||
- The visually-hidden style block uses the canonical `clip:
|
||||
rect(0,0,0,0)` pattern so the label is exposed to assistive tech but
|
||||
invisible to sighted users.
|
||||
- `resolveLoadingLabel` checks `i18n.catalog.has` before `t()` so a
|
||||
missing key does not also fire a `missingKey` event for the fallback
|
||||
bundle path — the bundled string is the silent default.
|
||||
- `aria-busy` on the fallback container hints to assistive tech that the
|
||||
region is updating; combined with `role="status"` + `aria-live="polite"`
|
||||
this is the WAI-ARIA-recommended pattern for transient loading
|
||||
indicators.
|
||||
Reference in New Issue
Block a user