Skip to main content
A template holds your content once, in your default language, and carries a translation of each piece of it for every language you support. Courier sends each recipient the version in their language.

How it works

  1. You write the template in your default language. Each element (the subject line, a paragraph, a button) gets an id.
  2. You add translations, one locale at a time. Each element stores its translations in a locales map, keyed by a locale code such as es or fr. Get them from your translators or a translation tool such as Lokalise, Crowdin, or Phrase and write them back through the API, or generate them with AI Translation in Design Studio.
  3. Courier picks the translation at send time. It reads the recipient’s locale and swaps in the matching text, element by element. An element with no translation for that locale sends in the default language.
Here’s one element with Spanish and French translations:
Each locale stores only the fields it translates, usually the text, so a layout change carries into every language. The same locales map also works on content you pass inline to a send in message.content. Short strings you reuse across many templates, like a common button label, can instead live in your workspace translation strings, one .po file per locale, rendered with {{t}}.

How Courier picks a locale

Courier takes the recipient’s locale from message.to.locale on the send, or from the locale stored on their when the send doesn’t set one. With neither, the default content sends. Locale codes match exactly, including case. A template translated as es serves recipients whose locale is es, and a recipient set to es-MX or ES gets the default content. Design Studio names locales in lowercase, such as es or es-mx, so store the same form on your profiles. Sends use the published template, so translations reach recipients once you publish.

Translate a template with the API

Translating a template through the API takes six steps. The translating itself happens outside Courier, in whatever tool or process you use, and every other step is one call: For a template that already exists, start at step 2. When the default text changes later, export again and translate only the strings whose checksum changed.

1. Create the template

takes your default content and returns the template ID as id (nt_01kx4h2jdafq8bk9aftxak4b40 in the later steps). Courier also gives every element an id, which is how translations find their element. You can set your own instead, such as "id": "subject", as long as each one is unique in the template. Elements also accept a locales map here if you already have translations. The brand, subscription, and routing IDs below are placeholders. Set brand and subscription to null if you don’t use them, and set routing.strategy_id to one of your . Content created without a scope field, like this example, reads the send’s data at the root, so the body uses {{order_number}} rather than {{data.order_number}}.

2. Export the strings

returns every element with its id, its default text, a checksum, and any translations already stored. Pass version=draft to read unpublished work. Without it you get the published version, and a template that has never been published returns 404. Templates from the Classic Designer (Legacy) return blocks and channels instead of elements. This workflow is for Elemental templates.
The response is the template’s element tree, with an id and a checksum on every element:
Checksums tell you what needs re-translating. An element’s checksum changes when its own content changes, and a container’s changes when anything inside it does. Adding or editing translations leaves it alone, and each translation carries its own checksum inside locales. Store each string’s element id and checksum when you export. On the next export, a changed checksum means new source text to translate, a new id is a new string, and a missing id needs nothing. Export with version=draft each time.

3. Translate the strings

The export is a tree, so walk into every elements array and skip the locales maps. From each element, take its text: title on meta, and content on text, action, quote, and html. Each channel holds its own copy of the content, so a template that sends email and inbox has two of each string. A text element built from inline nodes (bold, links) needs its translation written back as nodes, covered in formatted text. Hand the strings, keyed by element id, to whoever translates them: your own translators, or a translation tool such as Lokalise, Crowdin, or Phrase. Add a note on what each string is so translators have context, and ask them to keep {{variables}} exactly as written. What you send out is a list of strings:
What comes back is the same list with the translation for each locale:
The file format is up to you and your tool. Courier only needs each translation matched to its element id and field in the next step.

4. Write a locale back

sets one locale’s translations. Turn what came back into one request per locale: each entry names an element by id and carries its translated fields. Add any localized links yourself, like the Spanish href for the button below. The fields an element accepts depend on its type (see which fields a locale can override).
The write merges into what’s there. It touches only the locale in the path and only the elements you list, so your default content and every other language stay as they are. Within an element, each field you send replaces that field’s translation and the fields you leave out keep theirs. That means you can send a translation tool’s changes for a handful of strings without re-sending the rest. Leave untranslated strings out, and they send in the default language. Every id must exist in the template. An unknown id returns a 400 that lists each one, and nothing is written.

5. Publish

Writes land on the draft by default. Publish with once the translations are in, or pass "state": "PUBLISHED" on the locale write to publish in the same call. Publishing releases everything on the draft, including edits other than yours.

6. Send it

Send the template as usual. Set locale on the recipient, or store it on their profile so every send picks it up.
user_123 gets “Pedido confirmado” with the Spanish body and button. Check the rendered message in or with .

Translate in Design Studio

translates a template for you. Open the template, select the globe icon, and add a language. With Translate with AI checked, which is the default, Courier translates every string, including subject lines, headings, body copy, and button text. Review the result in a two-column view, with the default on the left and the translation on the right, and edit any string. When you change the default content, Courier marks translations that may be out of date, and you can re-translate them one at a time or all together. Publish the template to send the translations. AI Translation runs on AI credits, which you can buy on the Business or Enterprise plan. Each translation request costs 2.5 credits plus token overages, from the same balance as . A long template can take more than one request per locale. Translations made in Design Studio and through the API are the same locales data, so you can use both on one template.

Other ways to write translations

  • Replace all content. writes the whole element tree, translations included. Read the draft with version=draft, remove every checksum (on elements and inside locales), and keep each element’s id and locales. Templates translated in Design Studio also carry keys starting with an underscore, such as _sourceHash, inside locales. Remove those too, because the write rejects them. An element sent without an id gets a new one, and content sent without locales has no translations.
  • Replace one element. replaces a single element, so include its locales in the body to keep them. For translation work, the locale endpoint above is the better fit.
  • Remove a locale. The locale endpoint adds and updates. To remove a language, read the draft, delete that code from each element’s locales, and write the content back with as above.
  • Journey templates. A template that belongs to a journey has the same calls under the journey, with the same bodies: to export, to write a locale, and to publish.

Which fields a locale can override

The fields a locale entry accepts depend on the element type: divider, jsonnet, partial, and comment elements take no locale entry. A field the element type doesn’t accept returns a 400 naming it. Sending elements or raw in a locale replaces that field’s whole value for the element, so include everything each time.

Formatted text

A text element can hold its copy as a content string or as an elements array of inline nodes (string, link, img). Give a formatted element its translations as elements too, so bold, italic, and links carry into every language:

Text node resolution

A locale can use either format. Courier uses the locale field that matches the element’s own format, and falls back to the other:

Workspace translation strings

Workspace translations hold short strings reused across templates, like button labels and common phrases. They’re keyed by locale and rendered with the {{t}} Handlebars helper. Manage them with and reference a string with {{t "welcome_headline"}}. Use this for a shared glossary, and per-locale template content for the body of a notification.
  • The payload is a .po file, the gettext format most translation tools already export. Send the file as the raw request body with Content-Type: text/plain.
  • domain is always default. Any other value returns a 400.
  • Read them back with , which returns the stored .po content, to compare what’s live with what your translation system holds.
Upload a locale’s file like this:
{{t}} renders from the recipient’s locale. A send renders a {{t}} string only when the recipient has a locale with an uploaded .po file. With no locale, or a locale that has no file, the message fails with translate helper: Could not find translations. A key missing from an uploaded file renders as the key itself. Translations are per workspace. A customer who needs different wording from a needs a different template.

Manage templates with the API

Create, read, update, and publish templates.

Elemental

The content format every element and locale lives in.