Meridian handbook
For agentsRecipes

Multilingual site

How an agent adds a second locale to a Meridian site with dev locale add, gives routes per-locale paths, translates interface strings and labels, seeds translated documents, adds a language switcher and verifies hreflang and the translated pages.

Each document has one shared structure and per-locale text. Fields that hold text (text, slug, portable_text) differ per locale; values such as numbers, selects, places and relations are shared by every locale and written once. A locale’s URLs carry a prefix (/cy/...) by default, and each route may give each locale its own path words.

An asset field is both: describe type shows it as translate locale because its per-use alt text and caption are per locale, but the chosen image and its focal point are shared. A cy block does not repeat the field, and giving it a different asset there replaces the image for every locale. For a different image per locale, declare the field with structure: per_locale (Localisation).

Adding a locale touches these files. When you forget one, check warns about those under “check warns” and says nothing about those under “check is silent”:

flowchart LR
  add["dev locale add cy"] --> loc["config/locales.yaml"]
  add --> site["sites/main/site.yaml"]
  subgraph warned["check warns"]
    labels["schema labels"]
    strings["strings/cy.yaml<br/>(once the file exists)"]
  end
  subgraph silent["check is silent"]
    ep["endpoints.yaml<br/>input labels"]
    nav["seed/navigation<br/>item labels"]
    mail["email templates"]
    rt["routes.yaml paths"]
  end
  site --> you["You translate"]
  you --> warned
  you --> silent

Add the locale

engine-dmo dev locale add cy --label Cymraeg --fallback en --site main --apply
# config/locales.yaml (written)
    - { code: cy, id: loc_0vb8pv7bqm, label: Cymraeg, fallback: [en] }
# config/sites/main/site.yaml (written)
locales:
  enabled: [en, cy]
  default: en

The site’s locales.strategy decides the addresses: prefix (the default: every locale but the default under /<code>/), prefix-all, or domain with a host per locale in locales.domains. Set the new locale’s timezone and currency in locales.yaml if they differ from the source locale’s.

After this, check warns at every schema label without a cy translation (“the label of field photo has no translation for cy”). Add them: label: { en: Photo, cy: Llun }. Give the site’s name too:

engine-dmo dev config set sites/main/site.yaml site.name.cy "Croeso Banbury"

dev config set rewrites the map in key order ({ cy: ..., en: ... }), which is harmless.

Give routes per-locale paths

# config/sites/main/routes.yaml
routes:
  - type: walk
    id: rte_34agwcmxkh            # keep the id
    uri: { en: "/walks/{slug}", cy: "/teithiau/{slug}" }
    index: { en: /walks, cy: /teithiau }
    paginate: 12
    facets: [difficulty]

special:
  search: { en: /search, cy: /chwilio }

A route given per locale needs a value for every locale the site enables.

Translate the interface strings

Copy config/strings/en.yaml to config/strings/cy.yaml with the same keys. check says nothing about a locale with no strings file at all; it lists missing keys only once cy.yaml exists, so create the file first:

# config/strings/cy.yaml
strings:
  a11y.skip: Neidio i'r prif gynnwys
  nav.main: Prif
  nav.footer: Troedyn
  walks.count:
    zero: "{count} taith"
    one: "{count} daith"
    two: "{count} daith"
    few: "{count} taith"
    many: "{count} taith"
    other: "{count} taith"

Plural strings take the locale’s CLDR categories (zero, one, two, few, many, other); Welsh uses all six, English one and other. check warns with the list of keys a locale is missing (cy is missing 14 of 18 strings: ...); a missing key falls back down the locale’s chain. Translate every key before handing back. Never machine-translate without telling the person.

Seed translated documents

# seed/documents/walk/cherwell-towpath.yaml
locales:
  en:
    title: Cherwell towpath
    slug: cherwell-towpath
    summary: A flat riverside walk along the canal towpath from the town centre.
    distanceKm: 6.5
    difficulty: easy
  cy:
    title: Llwybr tynnu Cherwell
    slug: llwybr-tynnu-cherwell
    summary: Taith wastad ar hyd llwybr tynnu'r gamlas o ganol y dref.
publish: [en, cy]

The cy block gives only the translated text: distanceKm, difficulty and the photo are shared. A locale not in publish stays a draft, and its page does not exist in that locale.

The home page keeps the slug routes.yaml names in home: in every locale. With home: { type: page, slug: home }, the Welsh home page is the cy block with slug: home, served at /cy/; a cy slug such as hafan makes it an ordinary page at /cy/hafan, and /cy/ answers 404.

# seed/documents/page/home.yaml
locales:
  en:
    title: Welcome
    slug: home
  cy:
    title: Croeso
    slug: home
publish: [en, cy]

Add the locale to seed/seed.yaml as well (locales: [en, cy]). That list limits only the analytics seed apply generates; documents load in every locale they give either way.

Translate what check does not

  • Form labels. Each endpoint input’s label in config/sites/<site>/endpoints.yaml, and each message in its validation: label: { en: Your name, cy: Eich enw }. Without one the Welsh form shows the English label.
  • Menu labels. Each item in seed/navigation/*.yaml: label: { en: Walks, cy: Teithiau }.
  • Emails. Email templates have no t(): it prints nothing there. Branch on locale inside each block (Enquiry form).
  • Select options. An option with no label at all prints its value in every locale.

Add a language switcher

{# in templates/_shared/base.html, inside the header #}
{{ localeSwitcher() }}

localeSwitcher() prints a labelled <nav> of the locales the current page is published in, the current one with aria-current="page" and each link with hreflang and lang; it prints nothing on a page that exists in one locale. Label it with the string nav.language. head() writes the hreflang alternates and x-default, and <html lang> comes from locale.code.

Verify

engine-dmo check
engine-dmo schema apply
engine-dmo seed reset --confirm && engine-dmo seed apply
engine-dmo render /cy/teithiau/llwybr-tynnu-cherwell --json
engine-dmo render /teithiau/llwybr-tynnu-cherwell --locale cy --json

Then render the Welsh home page and a form page: engine-dmo render /cy/ --json and engine-dmo render /cy/cysylltu --json, and read the <label>s in html.

The page answers 200 with "locale": "cy", <html lang="cy">, a canonical on /cy/teithiau/..., hreflang alternates for en, cy and x-default, and the Welsh strings. --locale takes the unprefixed path and adds the prefix for you. Run engine-dmo a11y audit --all, which covers every locale’s pages in the sitemap. See Localisation.