How it works
- You write the template in your default language. Each element (the subject line, a paragraph, a button) gets an
id. - You add translations, one locale at a time. Each element stores its translations in a
localesmap, keyed by a locale code such asesorfr. 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. - 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.
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 frommessage.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 asid (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 itsid, 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.
id and a checksum on every element:
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 everyelements 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:
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 byid 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).
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. Setlocale 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 samelocales 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 everychecksum(on elements and insidelocales), and keep each element’sidandlocales. Templates translated in Design Studio also carry keys starting with an underscore, such as_sourceHash, insidelocales. Remove those too, because the write rejects them. An element sent without anidgets a new one, and content sent withoutlocaleshas no translations. - Replace one element. replaces a single element, so include its
localesin 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 acontent 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
.pofile, the gettext format most translation tools already export. Send the file as the raw request body withContent-Type: text/plain. domainis alwaysdefault. Any other value returns a400.- Read them back with , which returns the stored
.pocontent, to compare what’s live with what your translation system holds.
{{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.
Related docs
Manage templates with the API
Create, read, update, and publish templates.
Elemental
The content format every element and locale lives in.