Skip to content

Feature: Template-level i18n — auto-select or translate email content based on contact language #268

@Swahjak

Description

@Swahjak

Context

With the recent additions of console i18n (#176, v26.12) and automatic contact language sync via the notification center (#232, v27.2), contacts now reliably have a language field. However, as discussed in #153, there is still no built-in way to send the right email content based on that language — the gap between knowing a contact's language and acting on it remains.

Today the workaround is:

  • Transactional: create a separate template per language, select the right template ID in your backend code
  • Broadcasts: create a segment per language, send separate broadcasts with different templates

This works but is manual, error-prone, and doesn't scale well as the number of languages grows.

Prior art

Novu's Translations feature solves this with a translation keys approach: templates reference translatable strings via {{t.key}} variables, and translation files (JSON, one per locale) provide the actual content. The system selects the right translation at render time based on the subscriber's locale. This is worth looking at for inspiration, though the specifics below are adapted to Notifuse's architecture (Liquid, MJML, contact.language).

Proposal

Two possible approaches (not mutually exclusive):

Option A: Translation keys with a string catalog

Templates reference translatable strings via a dedicated syntax (e.g. a Liquid t object or filter), and translations are managed as key-value pairs per locale.

Template authoring — the email builder uses translation keys instead of hardcoded text:

{{ t.welcome.subject }}
{{ t.welcome.greeting }}, {{ contact.first_name }}!
{{ t.welcome.body }}

Translation files — JSON (or similar) per locale, scoped to the template or workspace:

// en.json
{
  "welcome": {
    "subject": "Welcome!",
    "greeting": "Hello",
    "body": "Thanks for signing up."
  }
}
// fr.json
{
  "welcome": {
    "subject": "Bienvenue !",
    "greeting": "Bonjour",
    "body": "Merci de vous être inscrit."
  }
}

Editor integration ideas:

  • A translations panel in the template editor to manage keys and their per-locale values
  • Autocomplete when typing {{ t. in text blocks
  • A locale preview switcher to preview the rendered template in each language
  • Marking translations as "outdated" when the default locale value changes

Import/export — bulk upload/download translation files (JSON) per template or workspace, similar to how template import/export already works.

Option B: Template variants

A template gets linked language variants under a single identifier. When the transactional API (or broadcast engine) renders a template, it resolves the variant matching contact.language.

This is simpler conceptually (each variant is a full template in the visual editor) but means maintaining N copies of the layout/structure, not just the text.

Language resolution & fallback

Whichever approach is chosen, a sensible fallback chain is important:

  1. Exact match on contact.language (e.g. pt-BR)
  2. Base language match (e.g. pt)
  3. Template default language (e.g. en)

If no translation exists at all, render the default language rather than failing or sending blank content.

API surface

The transactional API shouldn't need to change — language selection should be automatic based on contact.language:

{
  "template_id": "welcome_email",
  "contact": { "email": "user@example.com", "language": "fr" }
}

The system looks up the fr translation/variant of welcome_email and falls back to the default if none exists. No routing logic needed on the caller's side.

Use cases

  • Transactional emails (primary): password resets, order confirmations, welcome emails — content should match the user's language without the sender maintaining routing logic
  • Broadcasts: send a single campaign that automatically delivers the right language variant per recipient, instead of creating N broadcasts × N segments

Current state

Piece Status
contact.language field Exists, synced via notification center (v27.2)
Console i18n (admin UI) Done (v26.12)
Template-level i18n Missing
Auto-routing by language Missing

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions