Localization
Make a theme multilingual — translate interface strings with the t filter, and build locale-aware links and a language switcher
A site can publish its content in more than one language. The active language is set by the first segment of the URL: /cy/whats-on renders the Welsh version of /whats-on, /fr/whats-on the French version. The site's default language has no prefix — /whats-on is the default language.
A theme needs to do two things to support this:
- Translate the interface strings the theme itself supplies — button labels, headings, the "Read more" you hardcoded. Content typed into the admin is already returned in the active language; the theme only has to translate its own text.
- Keep links in the active language, so a visitor browsing in Welsh stays in Welsh as they move around the site.
Most of the second point is automatic. The first uses the t filter and a set of translation files.
Language switching and locale-prefixed links do not apply in preview. Preview always renders the default language, and locale-aware links won't carry the language prefix there. Check multilingual behaviour on the published site, not through a preview link.
Translating interface strings
Wrap each piece of theme-supplied text in the t filter and give it a key:
<a href="{{ event.url }}">{{ "event.read_more" | t }}</a>
<h2>{{ "home.upcoming_heading" | t }}</h2>Provide the translations in a locales/ folder at the root of the theme, one JSON file per language code. Each file is a flat map of key to translated string.
my-theme/
├── locales/
│ ├── en.json
│ ├── cy.json
│ └── fr.json
├── blocks/
├── layouts/
└── templates/locales/en.json:
{
"event.read_more": "Read more",
"home.upcoming_heading": "What's on"
}locales/cy.json:
{
"event.read_more": "Darllen mwy",
"home.upcoming_heading": "Beth sydd ymlaen"
}When a page renders, t looks up each key in the file for the active language. If that language has no file, or the file is missing the key, it falls back to the default language, then to any other configured language. If no translation is found anywhere, t returns the key unchanged — so an untranslated string shows up as event.read_more on the page, which makes missing keys easy to spot.
Key names are yours to choose — group them however suits the theme (nav.home, footer.copyright, event.book_now). Use the same keys across every language file.
Locale-aware links
Internal links stay in the active language on their own. Every record exposes a url (and relativePath) that already carries the language prefix when one applies:
{% for item in navigation %}
<a href="{{ item.relativePath }}">{{ item.title }}</a>
{% endfor %}
<a href="{{ event.url }}">{{ event.title }}</a>Viewing the site at /cy/..., those links render as /cy/whats-on, /cy/events/hamlet, and so on. On the default language they render with no prefix. As long as links come from a record's url or relativePath, or from navigation, nothing extra is needed.
For links you build by hand — a hardcoded path, or a URL assembled from parts — prefix them with locale_prefix:
<a href="{{ locale_prefix }}/contact">{{ "nav.contact" | t }}</a>locale_prefix returns /cy (or /fr, etc.) on a non-default language and an empty string on the default language, so the same template works in every language.
A language switcher
Use localization to list the available languages and identify the active one, and relativePath to point each link at the current page in another language:
{% assign l10n = localization %}
{% if l10n.available_languages.size > 1 %}
<nav class="language-switcher" aria-label="Language">
{% for lang in l10n.available_languages %}
{% if lang == l10n.default_language %}
{% assign href = page.relativePath %}
{% else %}
{% assign href = "/" | append: lang | append: page.relativePath %}
{% endif %}
<a href="{{ href }}"{% if lang == l10n.current_language %} aria-current="true"{% endif %}>
{{ lang | upcase }}
</a>
{% endfor %}
</nav>
{% endif %}page.relativePath here is the unprefixed path of the current page, so prepending each language code links to the same page in that language. Give the default language no prefix.
The languages a site offers are configured per site by Basker, not in the theme. localization.available_languages reflects that configuration — a theme that hardcodes a fixed list of languages will drift out of sync.
Reference
The functions and filter used here:
| Name | Returns |
|---|---|
{{ "key" | t }} | The translation for key in the active language, or the key itself if untranslated. See Liquid filters. |
locale | The active language code, e.g. cy. |
locale_prefix | /cy on a non-default language, empty string on the default. |
localization | { available_languages, current_language, default_language }. |
routes | { root_url } — the site root, prefixed for the active language. |
See Template context for the full list of global functions.
Related
- Liquid filters — the
tfilter reference. - Template context — every global function and the data records expose.
- Writing layouts — where the
<html lang>attribute and language switcher usually live.