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 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-ATde). 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.