Skip to main content
ElasticFunnels gives you a single, brand-wide system for translating page copy without forking pages or maintaining duplicate variants. You write your page once, reference translation keys in templates (or backend scripts), store translations per language in the admin, and the page shows the right copy based on the visitor’s language. The system is intentionally narrow: it only translates copy. Layout, images, links, and behavior stay identical across languages. Use Page Variants when you need a fully different experience per audience.

How it works

There are two things to manage:
  1. Languages — the list of locales your brand supports (e.g. en, pt-br, de). One language is your default.
  2. Entries — for each language, a list of key → translation rows. The same key (e.g. hero.title) lives in every language.
Once a language has entries, any template or script that uses that key under the resolved locale for the request will receive the matching value.

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

  1. Click Add language.
  2. Enter a locale code — short BCP-47 tag like en, de, pt-br.
  3. Optionally add a display label (shown only in the admin).
  4. Toggle Default language for the language that should be served when no other language is selected.
  5. Use Sort order to control the order in language pickers.

Add translation entries

  1. In the Entries section, pick a language from the dropdown.
  2. Click Add entry.
  3. Enter a Key (e.g. hero.title) and the translated Value.
  4. Save.
Use the search box to filter by key or value when your dictionary grows.

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):
  1. Wildcard / route paramshl or locale from the matched slug (e.g. {hl} in wildcard routes), also merged Express params when present.
  2. Query stringhl, then locale, then lang.
  3. Sessionsession.locale.
  4. Domainlocale / language from domain config.
  5. Brandlocale / language from brand variables.
  6. Accept-Language — primary two-letter subtag of the first range (e.g. en-USen).
  7. en — final fallback for the hint.
Snapping to brand languages (when the brand has at least one language in Advanced → Translations):
  • 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.
There is no automatic pt-brpt 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
The {hl} placeholder feeds into the route params used in step (1) above.

Using translations in templates

There are two sources of copy:
  1. Brand dictionary (Advanced → Translations) — t(), | t, and getTranslations() use the resolved language for the request.
  2. Built-in product strings (src/locales on the server) — used when t() or | t does 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.title exists, 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() / | t fall back to built-in strings when a catalog entry exists; otherwise prefer a sensible default in markup or a default: filter. getTranslation (brand-only) returns '' when missing.
  • Exact language codes. Add pt and pt-br separately if you need both; the runtime does not auto-collapse regions.