-
Notifications
You must be signed in to change notification settings - Fork 0
Multi language and RTL
Atelier is multi-language, and the number of languages is yours to pick. One is fine. So are four. Nothing in the package requires a second locale, and nothing requires any particular language.
What it does give you, once you declare more than one, is a single page carrying every locale. Translatable fields hold a map keyed by locale inside the same block tree:
{
"id": "b_8f3a",
"type": "hero",
"attributes": {
"heading": { "en": "Welcome", "fr": "Bienvenue" },
"align": "center"
}
}Not one tree per language, not one page per language. The structure is shared and only the text differs.
The accepted cost: a translation cannot have a different section order from the default locale. For a marketing site that is the right trade, and it stops the languages drifting into different pages that nobody notices have diverged.
'locales' => [
'en' => ['label' => 'English', 'dir' => 'ltr'],
],That is a valid config. A single locale works fine, the language switcher hides itself, and every URL is unprefixed.
Add as many as the site needs:
'locales' => [
'en' => ['label' => 'English', 'dir' => 'ltr'],
'fr' => ['label' => 'Français', '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. dir is either
ltr or rtl, and it drives the dir attribute on the html element plus the RTL font swap.
Locale codes are yours. Anything you put in the array becomes a tab in page settings, an entry in the editor's language switcher and a set of URLs. The package ships English and Arabic as a starting point because a bilingual site is the case that proves the model works in both directions; delete either line, or both, and replace them with whatever the site actually speaks.
Reorder the array. That is the whole change:
'locales' => [
'fr' => ['label' => 'Français', 'dir' => 'ltr'],
'en' => ['label' => 'English', 'dir' => 'ltr'],
],Now French lives at /{slug} and English at /en/{slug}:
| Locale | URL |
|---|---|
fr (first) |
/a-propos |
en |
/en/about |
Everything that reads "the default locale" follows the order, so this one edit also means the editor opens on French, a missing translation falls back to French, page settings show the French slug without a prefix, and the canonical URL of the French 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.
/about is now at /en/about, and /about 404s unless a French 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.
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, every language.
A translatable repeater holds the whole list per locale, so one language can have a different number of FAQ items than another.
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.
Each locale has its own slug, edited in its own tab. With English first:
| Locale | URL |
|---|---|
en (first, so no prefix) |
/about |
fr |
/fr/a-propos |
ar |
/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 and independent, so a translation's URL can be a translation, a
transliteration, a non-Latin script, or the same string as the default. Nested slugs work: a
page at services/web-design resolves, and its translation can be services/conception-web.
-
<html lang="fr" dir="ltr">, withlanganddirfrom the locale being rendered -
hreflanglinks between every locale, so none competes with itself - Per-locale meta title, description, canonical and share image
- On an RTL locale,
dir="rtl"and the RTL font stack
hreflang="x-default" is not emitted yet.
RTL is a property of a locale, not a language the package knows about. Set 'dir' => 'rtl'
and that locale mirrors. Arabic, Hebrew, Persian and Urdu all work the same way, and a site
with no RTL locale never pays for the support.
Mirroring is done by dir="rtl" and CSS logical properties, not by different markup or a
second stylesheet.
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. Written this way a block costs nothing extra in an LTR-only site and needs no second version for RTL.
Things that still need thought in RTL:
-
Arrows and chevrons pointing at "next" need mirroring.
rtl:-scale-x-100on 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.
The font swap rides on [dir="rtl"] rather than a locale code, so every RTL locale picks it
up without further configuration. See Design tokens for changing which
font that is.
Check both directions in the editor's language switcher before shipping. The preview renders RTL exactly as the public page will.
The toolbar has a language switcher, hidden when there is only one locale. 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 every locale lives in the same tree and only the view of it changes.
The page settings screen has a tab per locale for slug and SEO.
- Add it to
atelier.locales. - 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.
- Translate the content. Until then, the new locale renders the default locale's text.
- 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.
Delete it from atelier.locales. Every read of the locale list happens at request time, so
the locale drops out of the sitemap, the hreflang links, the editor's switcher and the page
settings tabs at once, and its URLs start returning 404.
Its translations stay in the block trees and its slug rows stay in the table, unread. That is
deliberate: putting the locale back restores the content rather than losing it. If a removed
locale still appears anywhere, the running config is not the file you edited. Run
php artisan config:clear, and check whether your app has its own published
config/atelier.php, which takes precedence over the package's copy.
Building
Running a site
Reference