Languages
A translated page is not a copy of the page. It is the same node with a per-language overlay of its translatable fields. The URL tree, the block structure, the images and the links are shared; only the text differs. That is why a German page never drifts from its English original, why adding a block once adds it everywhere, and why a translation can be “stale” — the original changed after it was translated — and the editor can say so.
The story declares the languages
properties:
languages: [en, de, fr]
default_language: en
The default language is the base text stored on the node. Every other language is an overlay. A story with one language has no overlays and no language switch.
Fields carry two flags
For a field a visitor reads, both flags are required; they do different jobs.
# on the archetype or element type — what the EDITOR translates
- $type: TextField
name: headline
translatable: true
# on the node type property — what the SERVER tracks as translated
- name: title
type: String
is_translatable: true
The editor’s translation mode derives its field list from the archetype and element types, so a flag only on the node type is invisible to authors; the server’s staleness tracking reads the node type, so a flag only on the archetype translates but never warns. The standard:* blocks and page archetypes already carry both. Never flag an identifier, a proper noun, a colour, an icon or a price.
Translation mode in the editor
Switching the language in the app bar puts the editor in translation mode: the preview reloads in that language, non-translatable fields lock, and translatable ones show the base text as a placeholder until the author overwrites it. Auto translate collects every translatable field on the page and runs the translation automation in one go; the result is a draft the author reviews before saving, like any other edit.
Translations are versioned and published with the page. Publishing a page publishes all its languages.
The renderer asks for a locale
Two URL shapes, and a site accepts both because the editor’s embed rule sends whichever the story’s locale_mode says:
| Shape | Example | When |
|---|---|---|
| Prefix | /de/about | The public site; locale_mode: prefix |
| Query | /about?lang=de | locale_mode: query; also what the editor appends when it previews a language |
Parse the locale, validate it against the story’s languages, and add a clause to every read that returns translatable text:
SELECT … FROM 'stories' WHERE path = $1 AND locale = 'de'
The overlay is applied by the database, references included: a resolved call-to-action page arrives with its German title. Untranslated fields fall back to the base text, so a half-translated page still renders whole. The chrome (navigation, header, footer) takes the same clause, which is why the Astro example caches it per locale.
Links between pages keep the visitor’s language: hrefOf(node) in the example prefixes the locale unless it is the default. Emit <link rel="alternate" hreflang> for each language from the story’s list; the page is the same node in all of them, so the alternates are the same path with a different prefix.
What is not a translation
Two situations look like a language problem and are not:
- A market with different content — a Swiss site that sells other products than the German one, with its own pages, its own prices and its own legal texts. That is a linked copy of the story, not a language; see Linked sites.
- A block that should only appear in one language — a notice for the French market. That is a variant with a locale rule; see Personalisation.
Pilot documentation. Studio is in a partner pilot. Screens and YAML shown here are current as of the version noted on each page; tell your Maravilla contact when something does not match what you see.