Python i18n - Localizing Transactional Email Templates

Python i18n - Localizing Transactional Email Templates

Leverage Python, Jinja and i18n Ally to define, localize, and preview transactional emails

Published on February 25, 2020

Applications which support frontend localization usually also require some localization strategies for backend services. A prominent example is the generation of localized transactional emails. This article discusses i18n strategies and workflows for transactional emails processed via Python backends. It relies mainly upon the following resources:

If you are interested in reviewing the complete implementation:

Defining Email Messages

For each "type" or kind of email message, inherit from a generic parent class EmailMessage, which in turn implements the required attributes and methods (e.g., rendering templates). See the companion repository for a possible implementation.

  src ›
1class EmailVerification(EmailMessage):
2    def __init__(self, config, locale, variables):
3        email_type = "email_verification"
4        required = ["cta_url"]
5        super().__init__(config, email_type, required, locale, variables)

6class PasswordReset(EmailMessage):
7    def __init__(self, config, locale, variables):
8        email_type = "password_reset"
9        required = ["cta_url", "operating_system", "browser_name"]
10        super().__init__(config, email_type, required, locale, variables)

Instantiate a new message with locales and variables, and retrieve all required attributes for sending localized emails:

  src ›
1message = EmailVerification(config, "en-US", {"foo": "bar"})
2# print(message.subject)  # localized subject
3# print(message.html)     # localized HTML with inlined CSS
4# print(message.txt)      # localized plaintext email
5# ...
6send_email(recepient, message.subject, message.html, message.txt)

Locales and Templates

All locale strings are stored in JSON format. This ensures flexibility with both localization workflow and should be relatively resilient against changing requirements. Global variables are defined across locales, since they usually do not depend on locale context (e.g., company or product names).

  data ›global.json
2  "company_name": "Company Name",
3  "product_name": "Product Name",
4  "product_website": ""

Locale strings files contain a global key and a key for each email message type, for instance email_verification. The former contain "localized globals", i.e., strings reusable across various email message types. The latter define all strings of a particular email message type.

  data › lang ›de-CH.json
2  "global": {
3    "greetings": "Viele Grüsse,",
4    "all_rights_reserved": "Alle Rechte vorbehalten."
5  },
6  "email_verification": {
7    "subject": "E-Mail-Adresse bestätigen",
8    "thank_you": "Vielen Dank für deine Registrierung!"
9  }

Use blocks to inherit the layout from higher order, more generic templates. In the companion repository, a three-layer hierarchical inheritance is proposed (barebone layout with basic styling → reusable email layout → specific email message template). Template files interpolate both locale strings and variables with double curly brace notation.

  templates ›email_verification.html
1{% extends "layouts/call_to_action.html" %}
2{% block body_top %}
4{% endblock %}
5{% block body_bottom %}
7{% endblock %}

Define a separate template for plain text emails - add dividers to structure the template without any markup. See Postmark's best practices for more details about how to format plain text emails.

  templates ›email_verification.txt
1{% extends "layouts/call_to_action.txt" %}
2{% block body_top %}
5{% endblock %}
6{% block body_bottom %}
9{% endblock %}

Besides in template files, variables can be used for interpolating values in localization files with the same notation. Interpolate...

  • global strings → {{product_name}}
  • locale-specific strings ("localized globals") → {{localized.support_phone_us}}
  • variables → {{variables.operating_system}}
  data › lang ›en-US.json
2  "global": {
3    "greetings": "Regards,",
4    "team_signature": "Your {{product_name}} Team",
5    "support_phone_us": "1-800-000-0000"
6  },
7  "password_reset": {
8    "subject": "Password Reset Request",
9    "support_message": "Need help? Call {{localized.support_phone_us}}.",
10    "security_information": "This request was received from a {{variables.operating_system}} device."
11  }

It's even possible to use more complex Jinja functionality, such as conditional statements, directly within locale strings.

  data › lang ›en-US.json
2  "discounted_items_cart_abandonment_notification": {
3    "subject": "There are {{variables.no_items_in_cart}} items in your shopping cart!",
4    "promo_message": "{{'One item has a discount!' if variables.no_items_in_cart_discounted < 2 else variables.no_items_in_cart_discounted + ' items have a discount!'}}"
5  }

A more maintainable approach however is to offload all logic to the respective templates and only use simple variable interpolation in locale string files for convenience.

Localization Workflow

i18n Ally is a VS Code extension for localization. It integrates with a variety of frameworks (e.g., Vue.js), but can also be used to speed up localization workflows of a static directory of locale JSON files. It even includes features to collaboratively review and discuss localizations.

Localize email message strings in context to achieve consistency across locales

Localize email message strings in context to achieve consistency across locales

Track progress of localization across locales and email messages with i18n Ally

Track progress of localization across locales and email messages with i18n Ally

Previewing Generated Emails

In the companion repository, a sample implementation for a thin utility to generate and serve rendered templates is provided.

  • Auto-reload upon detected file changes (templates and locale strings)
  • Interactively switch between locale, email type, and format (HTML and plain text)
  • Responsive view (e.g., Chrome Devtools) allows viewing rendered templates in various scenarios
  • Based on Vue.js and FastAPI, extensible and with minimal overhead
  • Work in progress
Interactively preview rendered email templates in all implemented formats and locales

Interactively preview rendered email templates in all implemented formats and locales

Sending Email (AWS SES)

Access the class attributes of the instantiated email message for implementing email sending functionality. For illustration purposes, an example for AWS SES is provided below:

  utils ›
1import boto3
2from botocore.exceptions import ClientError

3from src.messages import EmailVerification

4config_path = "./src/data"
5lang_path = "./src/data/lang"
6templates_path = "./src/templates"

7# Message config
8locale = "en-US"
9variables = {"cta_url": ""}
10message = EmailVerification(config, locale, variables)

11#  Application config
12SENDER = "Sender Name <>"
15AWS_REGION = "us-west-2"

16client = boto3.client("ses", region_name=AWS_REGION)

18    response = client.send_email(
19        Destination={"ToAddresses": [RECIPIENT] },
20        Message={
21            "Body": {
22                "Html": {"Charset": "UTF-8", "Data": message.html},
23                "Text": {"Charset": "UTF-8", "Data": message.txt},
24            },
25            "Subject": {"Charset": "UTF-8", "Data": message.subject},
26        },
27        Source=SENDER,
28        ConfigurationSetName=CONFIGURATION_SET,
29    )

30except ClientError as e:
31    print(e.response["Error"]["Message"])
33    print("Email sent! Message ID:"),
34    print(response["MessageId"])


Supporting multiple locales for transactional emails requires some additional considerations for templates and locale strings definition. The proposed approach includes classes and additional tooling to implement i18n transactional emails in Python applications.

I hope you found this article informative for how to approach i18n in Python backends!

Let's stay in touch!

If you'd like to get notified about updates, feel free to

Enter your email address below to receive an email whenever I write a new post.

Note: You can unsubscribe by clicking on the unsubscribe link included at the bottom of every email.