Skip to content

Bilingual and RTL

Abdulkader Safi edited this page Aug 18, 2026 · 3 revisions

Bilingual and RTL

One page, two languages, one section order. Translatable fields hold a map keyed by locale inside the same block tree:

{
  "id": "b_8f3a",
  "type": "hero",
  "attributes": {
    "heading": { "en": "Welcome", "ar": "أهلا بك" },
    "align": "center"
  }
}

Not two trees, not two pages. The structure is shared and only the text differs.

The accepted cost: Arabic cannot have a different section order from English. For a marketing site that is the right trade, and it stops the two languages drifting into different pages that nobody notices have diverged.

Configuring locales

'locales' => [
    'en' => ['label' => 'English', 'dir' => 'ltr'],
    'ar' => ['label' => 'العربية', 'dir' => 'rtl'],
],

The first locale is the default and lives at /{slug}. Every other locale lives at /{locale}/{slug}. The label is what the editor's language switcher shows, and dir drives dir="rtl" on the html element plus the Arabic font swap.

Making Arabic the default

Reorder the array. That is the whole change:

'locales' => [
    'ar' => ['label' => 'العربية', 'dir' => 'rtl'],
    'en' => ['label' => 'English', 'dir' => 'ltr'],
],

Now Arabic lives at /{slug} and English at /en/{slug}:

Locale URL
ar (first) /man-nahnu
en /en/about

Everything that reads "the default locale" follows the order, so this one edit also means the editor opens on Arabic, a missing translation falls back to Arabic, page settings show the Arabic slug without a prefix, and the canonical URL of the Arabic page is the unprefixed one. There is nothing else to change and no data to migrate: block trees are keyed by locale code, and the codes did not move.

⚠️ On a site that already has pages, this changes every URL and writes no redirects. A page that was at /about is now at /en/about, and /about 404s unless an Arabic page claims that slug. The slugs themselves never changed, so the redirect that normally covers a rename never fires. Reorder before launch, or be ready to add redirects by hand.

Nothing here is Arabic-specific. Put fr first and French becomes the default; add it anywhere else and you get /fr/{slug}, a third tab in page settings and a third language in the editor.

⚠️ Decide the set before you create pages. Adding or removing a locale later means migrating the per-locale maps inside every block tree on every page. If the site is English only, delete the Arabic line now; a single locale works fine and the language switcher hides itself.

What is translated and what is shared

A block declares it:

public static function translatable(): array
{
    return ['heading', 'items'];
}

Everything not listed is shared across every locale. A background colour, an alignment, an image, a link target: one value, both languages.

⚠️ Nothing in the editor marks which is which yet. Editing a shared field while the Arabic tab is open changes English too, and there is no warning. It is on the list.

A translatable repeater holds the whole list per locale, so Arabic can have a different number of FAQ items than English.

Missing translations

A missing value falls back to the default locale rather than rendering a hole. A half-translated page reads as untranslated rather than broken, which is the better failure for something a client is part-way through.

⚠️ The cost of that fallback: an untranslated Arabic section renders English and looks finished. There is no indicator of which sections still need translating, so a page can look complete and not be. Also on the list.

URLs

Each locale has its own slug, edited in its own tab. With the shipped config, where English is first:

Locale URL
en (first, so no prefix) /about
ar /ar/about-ar

Slugs live in their own table with a unique index on (locale, slug), because a JSON map cannot carry a unique index and two pages sharing a slug in one locale is a real bug.

Slugs are per locale, so the Arabic URL can be a transliteration, an Arabic-script slug, or the same string as English. Nested slugs work: a page at services/web-design resolves, and its Arabic counterpart can be khadamat/tasmim-web.

What the public page emits

  • <html lang="ar" dir="rtl">
  • hreflang links between every locale, so neither competes with itself
  • Per-locale meta title, description, canonical and share image
  • The Arabic font stack, swapped in by a [dir="rtl"] rule

hreflang="x-default" is not emitted yet.

RTL in your blocks

Arabic is mirrored by dir="rtl" and CSS logical properties, not by different markup.

Use logical properties, never physical ones:

Instead of Write
ml-4 ms-4
mr-4 me-4
pl-6 ps-6
pr-6 pe-6
text-left text-start
text-right text-end
border-l border-s
rounded-l-lg rounded-s-lg
left-0 start-0

Every shipped block follows this, and there is not a single physical direction utility in any of them.

Things that still need thought in RTL:

  • Arrows and chevrons pointing at "next" need mirroring. rtl:-scale-x-100 on the icon.
  • Logos and photographs must not be mirrored. Only directional glyphs flip.
  • Numbers and code stay LTR inside RTL text, which browsers handle if you leave them alone.

Check both directions in the editor's language switcher before shipping. The preview renders RTL exactly as the public page will.

In the editor

The toolbar has a language switcher. Switching changes which locale's values the settings form edits and which locale the preview renders, including direction. Nothing is lost switching back and forth, because both locales live in the same tree and only the view of it changes.

The page settings screen has a tab per locale for slug and SEO.

Adding a locale to a live site

  1. Add it to atelier.locales.
  2. Every page now has an empty slug for it. Open each page and fill it in, or generate one from the title by saving with the field empty.
  3. Translate the content. Until then, the new locale renders the default locale's text.
  4. Check the sitemap: the new URLs appear as soon as they have slugs, and each locale can be marked noindex on its own while you translate.

Clone this wiki locally