Localisation
How fields are translated: the translate modes (none, locale, language, group), the shared skeleton and per-locale leaves of repeaters, blocks, links and assets, structure per_locale, and field fallback on the public site.
A document exists once and holds a version of its content per locale. The schema decides, field by field, what is translated and what is shared, so a translator never retypes a phone number and an editor never rebuilds a page body eleven times.
Locales, the source locale, fallback chains and the locales each site enables are configured in config/locales.yaml and each site’s site.yaml (Sites, locales and routes).
Translate modes
translate: sets how many values a field has across locales:
| Mode | One value per | Typical fields |
|---|---|---|
none | document | Coordinates, relations, numbers, dates, opening hours, prices, select choices |
locale | locale | Titles, prose, summaries, slugs, SEO copy, link text, alt text |
language | language, ignoring region | Copy shared by en-GB and en-IE, by fr-FR and fr-CA |
group | locale group | A market-level fact: a regional phone line, a disclaimer per market |
Each field type has a default: text, slug, portable_text, asset, link, video, seo, repeater and blocks are locale; every other type is none (see the table on Fields). translatable: true and translatable: false are shorthand for locale and none.
fields:
- { name: title, type: text } # locale, the default
- { name: phone, type: phone } # none, the default
- { name: legalNotice, type: portable_text, translate: language }
- { name: helpline, type: phone, translate: group } engine-dmo check warns when a field whose type defaults to none is given another mode (a number field is rarely translated), and when a rich text field is given translate: none (cannot be translated at all).
Language takes the language subtag of the locale code: en-GB and en-IE share the en value.
Group takes the locale’s group from config/locales.yaml. A locale may be in one group only:
# config/locales.yaml
locales:
available:
- { code: en-GB, label: English (UK), source: true, default: true, timezone: Europe/London, currency: GBP }
- { code: en-IE, label: English (Ireland), timezone: Europe/Dublin, currency: EUR }
- { code: fr-FR, label: Français, timezone: Europe/Paris, currency: EUR }
groups:
- { key: uk-ie, locales: [en-GB, en-IE] } Values in none, language and group modes are held in the document’s shared revision chain, so they change in every locale at once and an editor can change them from any locale.
The shared skeleton and per-locale leaves
A repeater, a blocks field, a link, an asset and a video are split in two:
- The skeleton is shared by every locale: which items exist, in what order, of which block type, and their untranslated values (a relation, a number, an image choice, a link’s target, a video’s id).
- The leaves are each locale’s own: the translated values inside each item (headings, text, captions, alt text, link text, a video’s title and transcript), keyed by the item’s
_key.
Here is one walk in two locales, with a repeater of stages and a hero image:
flowchart TB
subgraph shared["Skeleton, shared by every locale"]
sk1["stages: s1, s2 in this order"]
sk2["heroImage: asset red-kite.jpg<br/>with its focal point"]
sk3["difficulty: moderate<br/>(translate: none)"]
end
subgraph en["en leaves"]
en1["s1.title, s2.title"]
en2["heroImage alt, caption"]
end
subgraph cy["cy leaves"]
cy1["s1.title, s2.title"]
cy2["heroImage alt, caption"]
end
shared --> enpage["the en page"]
en --> enpage
shared --> cypage["the cy page"]
cy --> cypageSo adding a block in the source locale adds it to every locale’s draft at once, reordering reorders everywhere, and removing removes everywhere. A translator fills in the leaves; they never rebuild the structure. The skeleton and each locale’s leaves have their own revision history.
fields:
- name: body
type: blocks
blocks:
- key: imageBanner
fields:
- { name: image, type: asset } # the image is skeleton; its alt and caption are leaves
- { name: caption, type: text } # a leaf
- key: listingGrid
fields:
- { name: heading, type: text } # a leaf
- { name: listings, type: relation, to: [listing], multiple: true } # skeleton Links, assets and videos
engine-dmo describe type shows a link, asset or video field as translate locale, its default mode. That means the field has per-locale leaves, not that each locale picks its own target:
| Field | Shared by every locale (skeleton) | Each locale’s own (leaves) |
|---|---|---|
asset | the chosen asset, its focal point, hotspot and crop | alt, caption |
link | the kind, the target, new tab, rel, the suffix, the style | text, title, aria_label |
video | the source, its id, the start, the poster, captions confirmation, duration | title, transcript |
A value written in any locale carries both parts, and its skeleton part replaces the shared one:
- A locale that writes a different asset (or link target, or video) does not get its own: every locale shows the new one. A seed file whose
cyblock givesheroImagea different asset changes the English page too. - A locale that writes only the leaves,
heroImage: { alt: Llun }, writes an empty asset into the skeleton, and the document then fails to publish in every locale (Choose a file from the library). A locale that writes the field at all repeats the same asset:heroImage: { asset: red-kite, alt: Barcud coch }.
For images the simplest rule is to put alt text and captions in the asset’s own library record, per locale (a seed asset’s alt: { en: ..., cy: ... }), and write only heroImage: { asset: red-kite } in the source locale. Every locale then shows the library’s text in its language. In a seed file a locale that does not write the field starts from the source locale’s value, per-use alt included, so an English per-use alt would also show on the Welsh page. To let each locale choose its own image, declare the field with structure: per_locale, below.
structure: per_locale
fields:
- name: marketNotes
type: repeater
structure: per_locale
fields:
- { name: note, type: text } structure: per_locale makes the whole value each locale’s own: each locale has its own list of items, in its own order. Use it where the content genuinely differs per market. On a link, asset or video it makes the whole choice each locale’s (a different video per language). It contradicts translate: none, which check warns about.
A single document can hold a field the other way from its schema, for one market-specific page, through an audited per-document override:
engine-dmo content structure --id <document id> --field body --mode per_locale --reason "Japanese edition has its own programme" --mode shared returns it. The override applies to the working revisions and takes effect at the next publish.
Rich text is whole
A portable_text field is translated whole: each locale holds its complete value, because a translator must be free to merge and split sentences. It has no skeleton.
What the public site shows when a translation is missing
fallback: on a field sets what a locale with no value of its own shows on the public site:
fallback | A plain field with no value in this locale | A skeleton item whose leaves are untranslated |
|---|---|---|
empty (the default) | Nothing: the field is left out. | The item is left out. |
inherit | The source locale’s value. | The item is shown with the source locale’s leaves. |
fields:
- name: items
type: repeater
fallback: inherit # show every item, in the source language where untranslated
fields:
- { name: document, type: relation, to: [listing] }
- { name: note, type: text } An item with no translatable leaves (only skeleton values) is never “untranslated”. The admin and previews always show every item and mark what is untranslated, whatever fallback says. The core guide type’s items uses fallback: inherit, so a translated guide shows every item with the source note until its own is written.
Field fallback is separate from the locale fallback chain, which decides which locale’s published document a relation, a link or a navigation item shows when the target is not published in the request locale (Relations).
Slugs and the home page
slug is translated by default, so each locale has its own address: /walks/cherwell-valley and /cy/teithiau/taith-dyffryn-cherwell. The home page is the exception. When a site’s routes.yaml names it by slug (home: { type: page, slug: home }), a document is the home page in a locale only where its slug in that locale is home. Give the home document the slug home in every locale; a Welsh slug such as hafan makes it an ordinary page at /cy/hafan, and /cy/ answers 404. A home page that is a singleton (home: { type: homepage }) is the home page whatever its slug.
Required in which locales
required: true is checked when publishing in the source locale only, unless required_in says otherwise (all, or a list of locale codes). See Validation.
Labels
Every label, help text, option label, block label and validation message in the schema may be a locale map, and engine-dmo check reports the enabled locales that have none (Content types). These are what editors and, for option labels, visitors read.
AI drafting
When an AI provider is configured, an editor can ask for a draft translation. machine_translate: forbid on a field stops drafting for that field in every locale; a locale’s own machine_translation: forbid in config/locales.yaml stops it for every field in that locale.
Changing a field’s mode later
Changing translate: on a field documents already hold moves values between chains and is a destructive change in the schema plan (Schema changes). Choose deliberately at the start.
Tools
| Command | What it shows |
|---|---|
engine-dmo locale list | Codes, the source, groups, fallbacks and coverage. |
engine-dmo locale coverage --site <key> | Translation coverage by type and locale, as data. |
engine-dmo locale stale | What has drifted in a locale since its source changed, by document and field. |
engine-dmo locale add <code> | Seeds drafts of a new locale for the chosen types and creates a translation job. --dry-run counts first. |
All need a database.
Blocks and rich text
Repeaters (a list of items with the same fields), blocks (a flexible page body of typed blocks, each with its own template), shared blocks, and Portable Text rich text with styles, lists, marks, link annotations and object blocks, and how each renders.
Schema changes
How a change to the type files reaches the database: schema plan and schema apply, what is destructive and how to approve it, renames by id with aliases, removed fields kept under _orphaned, retyping and coercion, reprojection, and what apply refuses.