Languages

One web_page serves every language. Its l10n list gives each language a URL or a domain of its own, a request on one of them renders in that language, and the hreflang links and og:locale tags are written for you. Translations and numbers in the template follow along.

The l10n list gives each language its URL

The list is a setting on the page, next to event. Each URL is a route like event, so it can capture the same :name segments. The canvas has no localization button on a web_page yet, so set it in the Studio Code View:

One page in three languages
blueprint "Shop"

flow page = web_page with {
  event: "shop/:category",
  scopes: ["*public"],
  l10n: [
    {language: "en", url: "en/shop/:category"},
    {language: "nl", url: "nl/winkel/:category"},
    {language: "de", url: "de/laden/:category"}
  ]
} {
  let {category} = page.params
  web_render(page.connection,
    title: category,
    template = "<h1>{{t \"shop.title\"}}</h1>"
  )
}

The params pin holds category on all four URLs, and web_action blocks work on all of them. An action posts to the URL it was clicked on, so it runs in the same language. The page's language out-pin tells the flow which one it got.

A localized URL always renders in its own language

A request on an l10n URL forces that entry's language for the whole render and every function it calls. The URL says what the visitor asked for, so it wins over everything else.

On any other URL, the language is worked out in this order, each later step overriding the one before when it has an answer:

  1. language_default, or en if none is set.
  2. The browser's Accept-Language, if language_browser_enabled is on. Its first language counts, without the region.
  3. The signed-in user's own language, if language_user_enabled is on.
  4. A ?language=nl in the URL or a language cookie, if language_query_enabled is on.

These are system settings. Because an anonymous visitor's language can come from a header or a cookie, a publicly cached page varies on both.

Every entry becomes an hreflang link

With the page above, a request on /nl/winkel/tea gets these tags in its head:

Head of /nl/winkel/tea
<link rel="canonical" href="https://shop.example.com/nl/winkel/tea">
<link rel="alternate" hreflang="en" href="https://shop.example.com/en/shop/tea">
<link rel="alternate" hreflang="nl" href="https://shop.example.com/nl/winkel/tea">
<link rel="alternate" hreflang="de" href="https://shop.example.com/de/laden/tea">
<link rel="alternate" hreflang="x-default" href="https://shop.example.com/shop/tea">
<meta property="og:locale" content="nl_NL">
<meta property="og:locale:alternate" content="en_US">
<meta property="og:locale:alternate" content="de_DE">
  • The event URL is x-default, the page for a visitor no language matches. It gets no language of its own, so give the default language an entry of its own if you want it listed by name. That entry may have the same URL as event, but then that URL always renders in that language.
  • The current request's :name values are filled in, so each link points at the same product or category. An entry whose required value is missing is left out, and an optional :name? is dropped from the URL.
  • A page with no l10n gets no hreflang links.
  • If your own head already has hreflang links, none are written.

A route can name its own host

For a language on a domain of its own, start the route with the hostname. The link is written on that host, and the page answers there. The domain must be pointed at your cluster: see Manage Domains in RUAL Studio.

A site per language
blueprint "Shop"

flow page = web_page with {
  event: "shop.example.com/en/:slug",
  scopes: ["*public"],
  l10n: [
    {language: "en", url: "shop.example.com/en/:slug"},
    {language: "nl", url: "shop.example.com/nl/:slug"},
    {language: "de", url: "shop.example.de/:slug"}
  ]
} {
  web_render(page.connection, template = "<h1>{{t \"shop.title\"}}</h1>")
}

A hostname on its own is that site's home page: shop.example.de and shop.example.de/ both mean https://shop.example.de/.

On a domain other than your main one, only routes that name that host answer. A bare route such as about belongs to the main domain. When a host-named route and a bare route both fit on the main domain, the host-named one wins.

How a host is told from a path

A route looks the same to the router either way, so the first part of it is read like this, in order:

  1. A wildcard host (*.example.fr/x) has no single URL, so it is left out of the links. The page still answers there.
  2. The host of the current request is always a host.
  3. One of your entity's own domains is always a host, however it is spelled.
  4. Anything else goes by its spelling: it has a dot and ends in two or more letters. With a slash after it, that makes it a host. Standing alone, it is a host unless it names a file a site serves at its root.
In l10nOn a request to shop.example.com, the link isBecause
nl/winkelhttps://shop.example.com/nl/winkelA path.
shop.example.com/nlhttps://shop.example.com/nlThe request's own host.
shop.example.dehttps://shop.example.de/Spelled like a host, standing alone: that site's home page.
shop.example.de/nlhttps://shop.example.de/nlSpelled like a host, with a slash after it.
shop.example.md/https://shop.example.md/The slash makes it a host, though .md is a file ending too.
winkel.example.mdhttps://winkel.example.md/One of your entity's own domains, so never a file.
sitemap.xml, robots.txt, en.mdhttps://shop.example.com/sitemap.xmlA root file standing alone is a path.
v1.2/noteshttps://shop.example.com/v1.2/notesA dot does not make a host: 2 is not two letters.
*.example.frLeft outA wildcard host has no single URL.

The file endings counted as root files are xml, txt, json, html, htm, md, js, mjs, css, ico, png, jpg, jpeg, gif, svg, webp, avif, webmanifest, rss, atom, pdf, csv, map, woff, woff2 and wasm. For a domain on such an ending that is not your entity's own, write the slash.

og:locale is written as language and territory

Facebook reads nl_NL, not a bare nl, so the page's language is spelled out:

Page languageog:localeRule
en-GB, pt_PTen_GB, pt_PTA language with a region keeps it.
nl, en, ptnl_NL, en_US, pt_BRA bare language takes the territory it most likely means.
A language with no known territoryNo tagBetter no tag than one that is not read.

The page's other languages each get one og:locale:alternate, never its own language and never x-default. Set og:locale in the meta pin to override it.

Translations follow the page's language

Keep the text in Manage Translations in RUAL Studio and print it with {{t}}:

A translation with a value in it
<h1>{{t "shop.title"}}</h1>
<p>{{t "cart.items" count=cart.count}}</p>
<input placeholder="{{t "shop.search"}}">
  • Fallback: the page's language, then the entity's default language, then the key itself.
  • Keys are lower case and trimmed: {{t "Cart.Title"}} is cart.title.
  • Values: {count} in the text is filled from count=..., and the value is escaped.
  • The text itself is yours, so in page text it may hold markup: Read our <a href="/terms">terms</a>. Inside an attribute everything is escaped.
  • A key read from data ({{t key}} or {{key | t}}) is escaped if nobody translated it, as it did not come from you.

Numbers and money follow it too

The same template writes each language's own way:

Language{{1234.5 | money "EUR"}}{{1234.5 | number 2}}
en€1,234.501,234.50
nl€ 1.234,501.234,50
de1.234,50 €1.234,50
fr1 234,50 €1 234,50

A language the platform has no number style for reads as English. date writes digits in the pattern you give it and in the entity's time zone, so the pattern is yours to vary: {{if language == 'en'}}{{d | date "M/D/YYYY"}}{{else}}{{d | date "D-M-YYYY"}}{{/if}}.

The current language is {{language}} in a page or layout and {{web.language}} everywhere, which is also what to give <html lang>.

Next steps

SEO and Sharing The rest of the head tags, and how to override them. Template Syntax The forms of the translation tag and every filter. System Settings Reference The language settings that decide the language on other URLs.

Frequently asked

How do I publish a RUAL web page in more than one language?

List its language versions in the page's l10n field as language and url pairs. A request on such a URL renders in that language, and web_render lists every version as an hreflang link with the main URL as x-default, and writes og:locale and og:locale:alternate.

Can a language version of a RUAL web page live on its own domain?

Yes. Name the host in the l10n URL, as in shop.example.com/nl, or give the hostname alone for that site's home page. A wildcard host has no single URL, so it is left out of the hreflang links.