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:
// 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 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:
{
"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 orAccept-Languageoutside this set falls back todefaultLocale.
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:
- an explicit
?lang=query override, - the visitor's
Accept-Languageprimary subtag (the same value targeting can match on aslang), - 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 theContent-Languageheader, - emits
hreflangalternate<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">) carriesinLanguage(the rendered language) and, for multilingual docs,availableLanguages. - The Facts API (
/render/<slug>.facts.json) returns the facts in one language with alanguagefield;?lang=<loc>selects it, and theETagvaries 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
localeshonest: it driveshreflangand the language switcher, so list only languages you've actually translated.