How it works
There are two things to manage:- Languages — the list of locales your brand supports (e.g.
en,pt-br,de). One language is your default. - Entries — for each language, a list of
key → translationrows. The same key (e.g.hero.title) lives in every language.
Translation keys
Keys use dot notation so you can group related copy together —hero.title, checkout.cta, faq.refund_policy.
Use letters, numbers, underscores, and dots in keys (e.g. hero.title,
form.email_placeholder). The admin may allow a wider pattern for stored
keys; sticking to this subset keeps lookups predictable.
Each key is unique within a language but is meant to repeat across
languages — that’s how the system knows the English hero.title and
the German hero.title are the same slot.
Managing languages and translations
Open Pages → Advanced → Translations to manage everything in one place.Add a language
- Click Add language.
- Enter a locale code — short BCP-47 tag like
en,de,pt-br. - Optionally add a display label (shown only in the admin).
- Toggle Default language for the language that should be served when no other language is selected.
- Use Sort order to control the order in language pickers.
Add translation entries
- In the Entries section, pick a language from the dropdown.
- Click Add entry.
- Enter a Key (e.g.
hero.title) and the translated Value. - Save.
Bulk edit
Click Bulk edit above the entries table to switch to a spreadsheet-style editor. Add or change many keys at once, then click Save all changes. The whole batch saves together — if any row fails validation, nothing is saved.Permissions
Access to translations is controlled by the Translations module permissions in your role:view, create, update, delete.
Choosing the active language
ElasticFunnels derives a locale hint from the request, then snaps it to a language row configured for the brand (when any exist). Locale hint priority (first match wins):- Wildcard / route params —
hlorlocalefrom the matched slug (e.g.{hl}in wildcard routes), also merged Expressparamswhen present. - Query string —
hl, thenlocale, thenlang. - Session —
session.locale. - Domain —
locale/languagefrom domain config. - Brand —
locale/languagefrom brand variables. Accept-Language— primary two-letter subtag of the first range (e.g.en-US→en).en— final fallback for the hint.
- If the hint matches a configured code exactly (after normalization), that language is used.
- Otherwise the brand default language is used, or the first language by sort order if no default is set.
pt-br → pt alias today: add both codes in
the admin if you need regional variants.
If the brand has no languages configured, the hint is still set on
the request for built-in helpers, but getTranslations() and
dictionary-backed t() / | t paths will not load entries from
the database.
Clean URLs per language
If you want URLs like/en/about and /pt-br/about to switch the
active language, capture the locale segment in your page slug:
- Page slug:
{hl}/about - URLs that match:
/en/about,/pt-br/about
{hl} placeholder feeds into the route params used in step (1)
above.
Using translations in templates
There are two sources of copy:- Brand dictionary (Advanced → Translations) —
t(),| t, andgetTranslations()use the resolved language for the request. - Built-in product strings (
src/localeson the server) — used whent()or| tdoes not find a brand entry for the key; today the shipped catalog is English; extra locale files extend this over time.
getTranslation(key) is still exposed for brand-only lookups (no built-in fallback, returns '' if missing). For strings in pages and scripts, prefer t() or | t so product copy can fall through to the shipped catalog when useful.
Use {{ locale }} for the active resolved code on the page context, or getResolvedLanguage() inside expressions when you need the snapped code explicitly.
Using translations in backend scripts
Backend scripts (<script scope="backend">) expose the same brand
helpers as templates (all async in the sandbox):
t(key, params?) tries the brand dictionary first, then built-in
locale JSON (same as templates). Use await. getLocale() returns
the same resolved code as {{ locale }} (async in the sandbox).
There is no pipe-filter syntax in backend scripts — use await t('key').
For the full host surface, see
Backend Scripts → Data Functions.
Caching
Translations are served from a fast in-memory cache (per app process). Saving languages or entries from the admin should bust that cache via the worker (brand_translation or full brand clear) so
the next request reloads from the database.
Patterns and pitfalls
- Always set a default language in Advanced → Translations so snapping has a predictable target when the URL hint does not match any row.
- Use stable keys. Renaming a key in the admin does not update your
templates. Once
hero.titleexists, keep using it; rename only when the meaning of the slot truly changes. - Missing entries. When the brand has no value for a key in the active
language,
t()/| tfall back to built-in strings when a catalog entry exists; otherwise prefer a sensible default in markup or adefault:filter.getTranslation(brand-only) returns''when missing. - Exact language codes. Add
ptandpt-brseparately if you need both; the runtime does not auto-collapse regions.
Related
- Wildcard routes — capture an
hlpath segment for clean URLs like/en/about. - Page variables — non-translation per-locale switches (currency formatting, date masks, etc.).
- Backend scripts → data functions — backend reference for translation helpers.