Localization
One template, many languages. Static copy lives in a translations table instead of being hard-coded into each template, so adding a new locale doesn't require new templates.
The model
- A translation key identifies a string (e.g.
order_scheduled.greeting). - A locale identifies a language (
en,el,it, …). - A value is the translated text for that (key, locale) pair.
Each row in the translations table is a single tuple of those three fields.
How templates use it
Inside the MJML, every piece of static copy references a translation key
rather than literal text, through the t filter
({{ 'order_scheduled.greeting' | t }}). At send time, the system resolves each
key against the active locale and substitutes the localized value.
This means:
- The same template renders in any supported language.
- A new locale is a data change, not a template change.
- Translators work in a single dataset, not across many template bodies.
Locale resolution
The locale used to render a template comes from the caller of the render or preview operation. Template authors can also override the locale per usage when a specific block must be rendered in a fixed language (for example, a legal disclaimer that must stay English).
Composing dynamic strings
Translations can be combined with data substitution to assemble dynamic sentences. A line like "Your order 0080245144 has been processed" is typically composed of:
- a translated prefix (
We would like to inform you that your order) - a value from the model (
{{ Order.OrderNumber }}) - a translated suffix (
has been processed and is scheduled for delivery.)
Splitting strings this way keeps both languages grammatical without forcing positional placeholders that translators tend to misuse.
Missing-translation behavior
If a key isn't found for the active locale, the system returns the key itself unchanged. Partially translated content stays readable and the missing entry is obvious in QA — preferable to throwing an exception that takes the whole email down.