# Native i18n (multilingual pages)

MIDL is **multilingual by construction**. Because a page is data, not markup, a single document can
carry every translation inline and the renderer resolves the right language per visitor — no parallel
page trees, no per-language variant duplication, no redeploys.

Localization (which **language**) and personalization (which **variant**) are **orthogonal**: a page
can be both translated and A/B-tested, and the two never multiply each other.

## 1. Localizable text values

Any human-readable text prop is EITHER a plain string (as before) OR a **locale map** keyed by BCP-47
language tag:

```jsonc
// before — still valid, renders identically
{ "id": "hero", "type": "ui:Hero", "title": "Ship faster" }

// localized — same prop, a { bcp47: string } map
{ "id": "hero", "type": "ui:Hero", "title": { "en": "Ship faster", "de": "Schneller liefern" } }
```

Plain strings pass through unchanged, so **every existing document is byte-identical** to before —
localization is backward-compatible by construction. You localize a value only when you want to; an
untranslated string is simply shared across all languages.

The localizable props (per component type) and the resolution order are published in the machine
[vocabulary](/api/agents/vocabulary.json) under its `localization` block. Today they are:

| Component | Localizable prop |
| --- | --- |
| `ui:Hero` | `title` |
| `ui:Heading`, `ui:Text`, `ui:Button`, `ui:Submit`, `ui:NavItem`, `ui:Link`, `ui:Badge`, `ui:Label` | `text` |
| `ui:Accordion` | `summary` |

## 2. Declare the document's languages

Set two props at the **document root**:

```jsonc
{
  "midl": "0.1",
  "ns": { "ui": "https://midl.ai/ui#" },
  "defaultLocale": "en",        // the canonical language; absent → "en"
  "locales": ["en", "de"],      // the languages this document is translated into
  "frames": [ /* … */ ]
}
```

- `defaultLocale` — the fallback language a value resolves to when the visitor's locale isn't present.
- `locales` — the set the runtime is allowed to serve and advertise (`hreflang`, the language switcher).
  A `?lang=` value or `Accept-Language` outside this set falls back to `defaultLocale`.

## 3. Resolution (how a value becomes one string)

For a given visitor locale, a locale map resolves in this order:

```
active → primary(active) → defaultLocale → primary(defaultLocale) → first string → ""
```

Matching is **case-insensitive** and falls back region→primary (`de-AT` → `de`). A plain string always
returns itself. This single primitive (`resolveLocalized` in `@wisepunk/core`) is shared by **both**
renderers (HTML string emitter and the Svelte components) through the common render plan, so the two
can never diverge — the parity gate proves it.

## 4. Which language a visitor sees

On every request the server resolves the active locale, in order:

1. an explicit **`?lang=`** query override,
2. the visitor's **`Accept-Language`** primary subtag (the same value targeting can match on as `lang`),
3. the document's **`defaultLocale`**.

Only locales the document declares in `locales` are honored. The page then:

- renders all text in that language,
- sets **`<html lang>`** and the **`Content-Language`** header,
- emits **`hreflang`** alternate `<link>`s for every declared locale (`?lang=<loc>`),

so search engines and agents discover the translations.

## 5. The machine twin is localized too

A MIDL page is dual-use — the agent/SEO surfaces resolve text the same way:

- **JSON-LD** (`<script type="application/ld+json">`) carries `inLanguage` (the rendered language) and,
  for multilingual docs, `availableLanguages`.
- The **Facts API** (`/render/<slug>.facts.json`) returns the facts in one language with a `language`
  field; `?lang=<loc>` selects it, and the `ETag` varies by language.

Agents never see `[object Object]` — localized values are always resolved to a real string.

## 6. Targeting on language

Because the visitor's language is part of the derived context, a **content variant** can target a
language directly (`lang:de`) — useful when a market needs a different message, not just a translation.
This composes with geo/device/returning targeting and stays independent of the translation layer.

## 7. Authoring tips

- Translate the **whole** value or leave it a plain string — a half-filled locale map is allowed, but
  the validator emits a **coverage warning** for any declared locale a map omits (it never blocks the
  save; partial translation is valid and renderable).
- An empty locale map (`{}`) is rejected as an invalid text value — use a string or at least one entry.
- Keep `locales` honest: it drives `hreflang` and the language switcher, so list only languages you've
  actually translated.
