Core dna renders server-side sites with Smarty or with Twig, one engine per site, and treats the two as peers. This guide is the one place both are documented: what they share, each engine's language and the platform's additions and limits, the two side by side, and translation.
L1–L2
Unit 05 · Translation and internationalisation
Translated content and the language switcher
Translation and internationalisationPage 25 of 25Updated
Content is translated as linked records, not through translation files, and it works identically in Smarty and Twig. How a translated record relates to its source, getOtherLanguageVersions() for hreflang links, a language switcher that finds the equivalent record, and why URLs carry no language of their own.
Who this is for. The developer building a multi-language site's page templates and header, on either engine. Afterwards you can render a translated record, link its versions to each other for search engines, and give visitors a switcher that keeps them on the same page.
The model: a translation is a record
A translated page, post or question is a separate record in its own language, at its own URL, with its own layout and content. Rendering it needs nothing special: it is a normal record, shown by its own layout's template like any other (Which template renders a record). What a template adds is the links between the versions.
Each record has a language. New records take the site's Default content language (Globals.defaultLanguage); the Supported content languages setting lists the others editors may choose.
Records are linked to a source. With Enable localization switched on in the site's Internationalization settings, an editor can mark a record as the translation of another, its source. The source and its translations form one group.
Which records can be linked: pages, blogs and blog posts, book pages, and FAQ pages and questions. A product has one description per language rather than linked records, and custom entity records carry a language without linked versions.
None of this involves the engine. The same records, the same methods and the same links reach a Smarty template and a Twig template.
All the versions of a record
getOtherLanguageVersions() returns every record in the group, the current one included, keyed by language code (the language's locale id, such as en-us or es-es), plus x-default for the version in the site's default content language. That is exactly the shape hreflang links need:
{% for code, version in page.getOtherLanguageVersions() %}
<link rel="alternate" hreflang="{{ code }}" href="{{ _request.getSchemeAndHost() }}{{ version.getFullPath() }}">
{% endfor %}
A record with no translations returns only itself, under its own code and, when it is in the default content language, under x-default as well. That is harmless in hreflang but not worth printing; the switcher below counts the languages other than x-default before showing anything. The site's /sitemap.xml already lists the same alternates for pages and book pages, so the head links and the sitemap agree.
A switcher that keeps the visitor on the same page
Link each language to the equivalent record, never to that language's home page:
{% set current = page.getLanguage() %}
{% set languages = page.getOtherLanguageVersions()|filter((version, code) => code != 'x-default') %}
{% if languages|length > 1 %}
<ul class="language-switcher">
{% for code, version in languages %}
{% set language = version.getLanguage() %}
<li>
<a href="{{ version.getFullPath() }}" hreflang="{{ code }}" lang="{{ language.getCode() }}"
data-lang="{{ language.getCode() }}"{% if current and code == current.getLcid() %} aria-current="true"{% endif %}>
{{ language.getName()|e }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
A language has a name (getName(), such as “English - United States”), a two-letter code (getCode(), en) and a locale id (getLcid(), en-us). The keys of getOtherLanguageVersions() are locale ids; the interface-language cookie takes the two-letter code. Show the names in their own language, or keep a short label per code in your translation files, rather than printing the admin's English names to Spanish visitors.
When the current page has no version in a language, that language is missing from the switcher. Say so where it matters (“This page is not available in Español”) rather than sending the visitor to a home page they did not ask for.
Moving the interface strings with the content
Following a switcher link changes the content, because the visitor lands on the other record. It does not change the interface strings, which follow the visitor's interface language (Which interface language a visitor gets). To move both, set the cdna_locale cookie to the two-letter code when the visitor picks a language; the platform reads the cookie but never sets it:
Template code
<script nonce="{$_inlineScriptNonce}">
document.addEventListener('click', function (event) {
var link = event.target.closest('.language-switcher a[data-lang]');
if (link) {
document.cookie = 'cdna_locale=' + link.dataset.lang + '; path=/; max-age=31536000; SameSite=Lax';
}
});
</script>
<script nonce="{{ _inlineScriptNonce }}">
document.addEventListener('click', function (event) {
var link = event.target.closest('.language-switcher a[data-lang]');
if (link) {
document.cookie = 'cdna_locale=' + link.dataset.lang + '; path=/; max-age=31536000; SameSite=Lax';
}
});
</script>
Use SameSite=Lax, not Strict. A visitor returning from an external page, such as a payment provider, would otherwise arrive without the cookie and see the default language.
Expect fewer cached pages. A page rendered for a cdna_locale that differs from the default is not stored in the shared page cache, so visitors on another language are served uncached pages. Keep those templates light.
The cookie only changes interface strings. It never redirects anyone to another record; the switcher's links do that.
URLs carry no language of their own
There is no language routing: a URL does not select a language, and the platform adds no language prefix. /es/organizaciones is Spanish because the Spanish page is called that and sits under a page called es that editors created. Translated pages usually have translated slugs too, so building a URL by putting /es in front of the English path finds nothing. Always link to a version through getOtherLanguageVersions() or the version's own getFullPath(), and keep the tree for each language in a parent page of its own if you want the language visible in the path.