# i18n consumer integration harness

## Pattern 1 (DEFAULT) — per-locale SSR routing via `@platform-modules/i18n` core (framework-free)

Most sites (including pure Astro, no React) should use this. SEO-correct: each locale is a real URL (`/`, `/en/`).

On the server:

```astro
---
import { resolveLocale, t } from '@platform-modules/i18n';
import { dir } from '@platform-modules/i18n/rtl';
import { parseLocalePath, buildLocalePath } from '@platform-modules/i18n/routing';
import { i18n } from '../messages';

const locale = resolveLocale(i18n, Astro.request); // path → cookie → Accept-Language → default
const direction = dir(locale);
const { rest } = parseLocalePath(Astro.url.pathname, i18n.locales);
---
<html lang={locale} dir={direction}>
  <body>
    <p>{t(i18n, locale, 'greeting', { name: 'Dana' })}</p>
    <a href={buildLocalePath('en', rest, i18n.defaultLocale)}>English</a>
    <a href={buildLocalePath('he', rest, i18n.defaultLocale)}>עברית</a>
  </body>
</html>
```

- `resolveLocale(i18n, Astro.request)` — path → cookie → Accept-Language → default.
- Set `<html lang={locale} dir={dir(locale)}>` using `@platform-modules/i18n/rtl`.
- Build locale-aware links with `parseLocalePath` + `buildLocalePath(locale, rest, defaultLocale)` from `@platform-modules/i18n/routing`. Derive `rest` via `parseLocalePath(Astro.url.pathname, i18n.locales)` first — `parseLocalePath` strips the current locale segment; passing the raw pathname would double-prefix (e.g. `/en/en/about`). A language switcher is just `<a href={...}>` to the other locale's URL — a full SSR navigation re-renders everything in the new locale. No React required.
- Render text server-side with `t(i18n, locale, key, vars)`.

**Verified:** `/` renders Hebrew (`שלום, Dana`), `<html dir="rtl">`; `/en/` renders English (`Hello, Dana`), `<html dir="ltr">`.

## Pattern 2 — `@platform-modules/i18n-react` context kit (within-island reactivity ONLY)

Use when a single React island needs to switch locale live without navigation (e.g. a self-contained widget).

Self-wrap `<I18nProvider>` **inside** the island (see `/` page + `Greeting.tsx`). Consumers read via `useTranslations()` / `useLocale()`. One `setLocale` re-renders all context consumers in that island.

**Verified:** test 3 on `/` — one `setLocale` flips greeting text (`שלום, Dana` → `Hello, Dana`), tagline, `data-locale`, and `<html dir>` while the URL stays on `/`.

Do **not** put `<I18nProvider>` in a `.astro` file expecting it to reach island children — Astro serializes slot children to HTML before the parent renders, so context will not cross.

## Limitation (the pilot's finding)

**React context does NOT cross island boundaries.** A `<Switcher>` island and a `<Content>` island are separate React trees; clicking the switcher updates only its own tree.

**Verified:** `/multi` test — clicking `multi-lang-en` in the Switcher island leaves the sibling Content island on Hebrew (`שלום, Dana`, `data-locale="he"`).

For cross-island or page-level locale, use Pattern 1 (per-locale routing) — the recommended default — or a shared cross-island store (e.g. nanostores) read by every island.

## UI primitives a11y gate

`/ui-primitives-a11y` is the consumer harness route for T8. Playwright runs `axe-core` there and on the palette picker under the media matrix (`prefers-reduced-motion`, `forced-colors`, `prefers-contrast`, `200%` zoom).

Screenshot baselines are drift detectors only. They are checked in only after explicit `opus integration review` approval recorded in [tests/visual-baselines.ts](/tmp/wt-ui-ux-kit-T8/apps/consumer/tests/visual-baselines.ts:1); Codex output alone does not auto-approve them.
