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
Translating in Twig
Translation and internationalisationPage 24 of 25Updated
The platform registers no translation filter or tag for Twig, but every Twig template receives the platform's translator as __, reading the same files as a Smarty site. One complete approach, from the files to a macro library and its use in templates, attributes and scripts, including what prints nothing when the import is missing.
Who this is for. The developer making a Twig site multi-language. It assumes the files and the lookup order on Interface strings. Afterwards you can ship translated templates on a Twig site with one macro and no extension.
What is there, and what is not
Not there: a translation extension. There is no |trans filter and no {% trans %} tag, and a theme cannot register one, because a site has no PHP and the platform's Twig environment takes no extensions from a site. Using |trans fails the template with Unknown "trans" filter.
There: the translator itself. Every template receives it as the variable __, the same object Smarty's tags call, with the same files, the same domains and the same lookup order. In a page render it is a Twig global, so macros and only includes see it. The platform's own Twig templates translate this way.
So a Twig site supplies one thing: a short way to call the translator. Everything else, the files, the language a visitor gets and translated content, is the same as on a Smarty site.
Calling the translator
__.translate(key, parameters, domain) returns the translation as a string. Parameters are a map whose keys have no percent signs; pass {} when there are none and you need the third argument.
That works anywhere, and it is long. Written out on every label, the domain is repeated hundreds of times and a mistyped one prints a raw key.
The approach: one macro, imported where needed
1. The translation files
Create translations/site-shop.en.xlf beside templates/, and one file per further language, as on Interface strings. Nothing about the files is specific to Twig.
2. A macro library
Template codetemplates/includes/i18n.twigTwig only
{#
Interface strings for the site. t() looks a key up in the site's domain,
with optional parameters; pass a domain to borrow from a shared one.
#}
{% macro t(key, params = {}, domain = 'site-shop') -%}
{{- __.translate(key, params, domain) -}}
{%- endmacro %}
The whitespace modifiers keep the macro's own line breaks out of its output, so a label inside an attribute or a button carries no stray spaces. The domain is written once, in the macro's default.
3. Import it, and use it
Template codetemplates/pages/pages/shop.htmlTwig only
{% extends 'includes/base.html' %}
{% import 'includes/i18n.twig' as i18n %}
{% block content %}
<h1>{{ i18n.t('shop.heading') }}</h1>
<button type="submit" aria-label="{{ i18n.t('cart.button.add')|e('html_attr') }}">+</button>
<p>{{ i18n.t('cart.items', {'count': itemCount}) }}</p>
{# a parameter from the visitor: escape it before it goes in #}
<p>{{ i18n.t('search.results_for', {'query': _request.getParameter('q')|default('')|e}) }}</p>
{# a generic label borrowed from a shared platform domain #}
<label>{{ i18n.t('address.city', {}, 'common') }}</label>
{% endblock %}
Import in every template that translates. A Twig import belongs to the template that declares it: the base template's import does not reach a child's blocks, and in a child that forgot its own import, i18n.t() prints nothing at all, with no error, because strict variables are off. Put the import at the top of each template, after extends, or inside the block that uses it.
Call the translator directly here rather than the macro: it returns plain strings, which |json_encode quotes and escapes for the script.
Setting a default domain instead
The platform's own Twig templates set the domain once and then call the translator without it:
Template codeTwig only
{% do __.setDefaultDomain('site-shop') %}
<h2>{{ __.translate('cart.title') }}</h2>
The default lasts for the rest of the request, including components rendered later and the platform's partials, so prefer the macro, which names the domain on every call and leaves the default alone.
What to expect from it
A missing key prints the key, as in Smarty, and is logged as a warning outside development environments.
Nothing is escaped: the text and its parameters are printed as they are, which is why a visitor's value is escaped before it goes in. Autoescaping is off on Core dna, so Twig adds nothing either.
The interface language is chosen exactly as for Smarty: the cdna_locale cookie, then the DXP language setting, then the browser (Which interface language a visitor gets).
Template code typed into the admin receives no __ and no template context: translate there only through the variables that feature passes, or not at all.