Email API

Localising Transactional Email: Language, RTL, Dates, Currency

Send transactional email in each user's language: storing a locale, template fallbacks, right-to-left layout, date and currency formatting, and the API.…

A receipt in the wrong language is not just unfriendly, it gets reported as spam by people who do not recognise it. Localising transactional email is mostly plumbing: know each user's locale, pick the right template, format dates, numbers and money for that locale, and handle right-to-left scripts in the HTML. This guide covers each piece with the details that break.

Quick answer

Store a locale per user (language plus region, like pt-BR or fr-CA), choose it from their explicit setting first, then the language they signed up in, then a default. Keep one template per message per language with a fallback chain, format every date, number and amount with locale-aware libraries at render time, set the html lang and dir attributes, and pass a localised subject and text part to the send API alongside the html.

Decide the locale

  1. Explicit setting in the user's profile wins.
  2. Otherwise the locale captured at signup from the Accept-Language header or the UI language they used. Store it then; browsers change.
  3. For emails to a customer of your customer (an invoice from your user's business), use the recipient's locale if known, else the sender's.
  4. Fall back through the chain pt-BR to pt to your default. Never send an empty template.
  5. Store the locale used on each sent email so support can see why someone got German.

Templates and strings

Keep a template per message type and per language, in files named like order-confirmed.fr.html, and a strings file for subjects and short phrases. Use a real i18n library (i18next, Fluent, Rails I18n, Laravel lang files, gettext) so plurals and gender work: "1 item" versus "2 items" is different in every language, and Arabic and Polish have more than two plural forms. Do not translate by concatenating sentence fragments; give translators whole sentences with named placeholders like {orderNumber}. Translate the subject line and the plain text part too; a French email with an English subject is the most common half-done localisation.

Dates, numbers and money

  • Format at render time with the user's locale and time zone: Intl.DateTimeFormat and Intl.NumberFormat in JavaScript, Babel in Python, NumberFormatter in PHP, I18n.l in Rails.
  • Dates: "21 août 2026" in fr, "Aug 21, 2026" in en-US, "21.08.2026" in de. Include the weekday for appointments.
  • Numbers: 1,234.56 in en, 1.234,56 in de, 1 234,56 in fr. Never format money with string concatenation.
  • Currency: format the amount with the currency code, for example Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }). Show the code as well as the symbol when the symbol is ambiguous ($ for USD, CAD, AUD).
  • Time zones are separate from locale; a fr-CA user in Vancouver is on Pacific time.

Right-to-left languages

For Arabic, Hebrew, Persian and Urdu set dir="rtl" on the html element and lang to the language code. Table-based layouts flip correctly when you use align="right" on cells and avoid hard-coded left padding; use padding on both sides symmetrically. Numbers and Latin product codes inside RTL text remain left-to-right automatically, but mixed strings like an order number next to Arabic text can display in a surprising order; wrap such fragments in a span with dir="ltr". Test in Gmail and Outlook with an RTL sample; both support dir but some clients ignore CSS direction, so use the attribute, not the style.

The send request

const t = translations[user.locale] || translations.en;
const fmtMoney = new Intl.NumberFormat(user.locale, { style: "currency", currency: order.currency });
const fmtDate = new Intl.DateTimeFormat(user.locale, { dateStyle: "long", timeZone: user.timeZone });

const res = await fetch("https://api.oqumail.com/api/v1/emails", {
  method: "POST",
  headers: { "Authorization": "Bearer " + process.env.OQUMAIL_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    from: "orders@yourdomain.com",
    to: user.email,
    subject: t.orderConfirmedSubject.replace("{orderNumber}", order.number),
    html: renderTemplate("order-confirmed." + t.code + ".html", {
      dir: t.rtl ? "rtl" : "ltr", lang: t.code,
      total: fmtMoney.format(order.total), date: fmtDate.format(order.createdAt), order
    }),
    text: t.orderConfirmedText.replace("{orderNumber}", order.number).replace("{total}", fmtMoney.format(order.total))
  })
});

Details that break

  • Encoding: send UTF-8 everywhere and declare it in the HTML meta tag. The API takes UTF-8 JSON, so accented and non-Latin characters in subject and body arrive intact.
  • Subject length: translations are often longer; keep subjects under about 60 characters in every language or they truncate on phones.
  • Legal footers differ by country. Keep the footer in the strings file, not the layout.
  • Do not machine-translate security emails without review; a slightly wrong phrase in a password email reads like phishing.
  • From names: keep the brand the same in every language, but the from name can be "Acme Commandes" in French if the mailbox is used by a French-speaking team.

Where it sends from

Localisation changes the content, not the sending identity. Send every language from the same verified domain so the reputation is shared, from role addresses that make sense in every language (orders@, billing@, support@). OquMail sends UTF-8 subjects and bodies without any extra configuration, keeps the mailboxes for replies on the same domain, and records each message in the delivery log, which is useful when a receiver in one country starts deferring a particular language variant.

Common questions

Should I send bilingual emails?

For markets with two official languages (Canada, Belgium, Switzerland) a bilingual email is common and acceptable. Put the user's preferred language first. Everywhere else, pick one language per user.

How do I handle a user with an unsupported language?

Fall back to your default language and make sure the email still contains the numbers and links that matter, which are universal. Offer a language setting in the app so they can pick from what you do support.

Do I need separate templates for en-US and en-GB?

Usually just one template with locale-formatted dates and currency, and a strings file for the few words that differ. Separate templates are only worth it when the legal footer or content differs.

Free business email on your own domain

OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Most teams are live in under fifteen minutes. Start at oqumail.com.

Get started free

Ready for business email on your domain?

Up to 15 free mailboxes, guided DNS, webmail, and a transactional API — start in minutes.

Create your free workspace