---
title: "Languages · RUAL Documentation"
description: "One page in many languages: l10n URLs and domains of their own, hreflang and og:locale written for you, translations in templates and localized numbers."
canonical: https://docs.rual.nl/web/languages
language: en
---

# Languages

One [`web_page`](https://docs.rual.nl/block-types/web/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.

In short

- `l10n` is a list of `{language, url}` on the page. A request on such a URL always renders in that language.

- Every entry becomes an `hreflang` link, with the `event` URL as `x-default`.

- A route can name its own host: `shop.example.com/nl`, or just `shop.example.de` for that site's home page.

- `{{t}}`, `money` and `number` follow the page's language with no extra work.

## 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`](https://docs.rual.nl/block-types/web/web_page) yet, so set it in the [Studio Code View](https://docs.rual.nl/engine/studio-code-view):

```
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`](https://docs.rual.nl/block-types/web/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:

- `language_default`, or `en` if none is set.

- The browser's `Accept-Language`, if `language_browser_enabled` is on. Its first language counts, without the region.

- The signed-in user's own language, if `language_user_enabled` is on.

- A `?language=nl` in the URL or a `language` cookie, if `language_query_enabled` is on.

These are [system settings](https://docs.rual.nl/blueprints/system-settings-reference). Because an anonymous visitor's language can come from a header or a cookie, a publicly cached page [varies on both](https://docs.rual.nl/web/speed#cache-seconds).

## Every entry becomes an hreflang link

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

```
<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](https://docs.rual.nl/interfaces/rual-studio).

```
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:

- **A wildcard host** (`*.example.fr/x`) has no single URL, so it is left out of the links. The page still answers there.

- **The host of the current request** is always a host.

- **One of your entity's own domains** is always a host, however it is spelled.

- **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 `l10n` | On a request to `shop.example.com`, the link is | Because |
| --- | --- | --- |
| `nl/winkel` | `https://shop.example.com/nl/winkel` | A path. |
| `shop.example.com/nl` | `https://shop.example.com/nl` | The request's own host. |
| `shop.example.de` | `https://shop.example.de/` | Spelled like a host, standing alone: that site's home page. |
| `shop.example.de/nl` | `https://shop.example.de/nl` | Spelled 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.md` | `https://winkel.example.md/` | One of your entity's own domains, so never a file. |
| `sitemap.xml`, `robots.txt`, `en.md` | `https://shop.example.com/sitemap.xml` | A root file standing alone is a path. |
| `v1.2/notes` | `https://shop.example.com/v1.2/notes` | A dot does not make a host: `2` is not two letters. |
| `*.example.fr` | Left out | A 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 language | `og:locale` | Rule |
| --- | --- | --- |
| `en-GB`, `pt_PT` | `en_GB`, `pt_PT` | A language with a region keeps it. |
| `nl`, `en`, `pt` | `nl_NL`, `en_US`, `pt_BR` | A bare language takes the territory it most likely means. |
| A language with no known territory | No tag | Better 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](https://docs.rual.nl/web/seo#meta) to override it.

## Translations follow the page's language

Keep the text in **Manage Translations** in [RUAL Studio](https://docs.rual.nl/interfaces/rual-studio#user-interfaces) and print it with `{{t}}`:

```
<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 [terms](https://docs.rual.nl/terms)`. 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.50` | `1,234.50` |
| `nl` | `€ 1.234,50` | `1.234,50` |
| `de` | `1.234,50 €` | `1.234,50` |
| `fr` | `1 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 ` `.

## Next steps

- [SEO and Sharing](https://docs.rual.nl/web/seo): The rest of the head tags, and how to override them.

- [Template Syntax](https://docs.rual.nl/web/templates#translations): The forms of the translation tag and every filter.

- [System Settings Reference](https://docs.rual.nl/blueprints/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.
