Python i18n - Localizing Transactional Email Templates

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

Python i18n - Localizing Transactional Email Templates - Post illustration

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:

Project source code available at...
visini/email-templates-i18n

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/messages.py
Python
class EmailVerification(EmailMessage):
    def __init__(self, config, locale, variables):
        email_type = "email_verification"
        required = ["cta_url"]
        super().__init__(config, email_type, required, locale, variables)

class PasswordReset(EmailMessage):
    def __init__(self, config, locale, variables):
        email_type = "password_reset"
        required = ["cta_url", "operating_system", "browser_name"]
        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/messages.py
Python
message = EmailVerification(config, "en-US", {"foo": "bar"})
# print(message.subject)  # localized subject
# print(message.html)     # localized HTML with inlined CSS
# print(message.txt)      # localized plaintext email
# ...
send_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
JSON
{
  "company_name": "Company Name",
  "product_name": "Product Name",
  "product_website": "https://www.example.com/"
}

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
JSON
{
  "global": {
    "greetings": "Viele Grüsse,",
    "all_rights_reserved": "Alle Rechte vorbehalten."
  },
  "email_verification": {
    "subject": "E-Mail-Adresse bestätigen",
    "thank_you": "Vielen Dank für deine Registrierung!"
  }
}

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
HTML
{% extends "layouts/call_to_action.html" %} {% block body_top %}
{{localized.thank_you}} {% endblock %} {% block body_bottom %}
{{localized.contact_us_for_help}} {% 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.html
HTML
{% extends "layouts/call_to_action.txt" %} {% block body_top %}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
{{localized.thank_you}} {% endblock %} {% block body_bottom %}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
{{localized.contact_us_for_help}} {% 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
JSON
{
  "global": {
    "greetings": "Regards,",
    "team_signature": "Your {{product_name}} Team",
    "support_phone_us": "1-800-000-0000"
  },
  "password_reset": {
    "subject": "Password Reset Request",
    "support_message": "Need help? Call {{localized.support_phone_us}}.",
    "security_information": "This request was received from a {{variables.operating_system}} device."
  }
}

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

data/lang/en-US.json
JSON
{
  "discounted_items_cart_abandonment_notification": {
    "subject": "There are {{variables.no_items_in_cart}} items in your shopping cart!",
    "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!'}}"
  }
}

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/ses_example.py
Python
import boto3
from botocore.exceptions import ClientError

from src.messages import EmailVerification

config_path = "./src/data"
lang_path = "./src/data/lang"
templates_path = "./src/templates"

# Message config
locale = "en-US"
variables = {"cta_url": "https://www.example.com/"}
message = EmailVerification(config, locale, variables)

#  Application config
SENDER = "Sender Name <[email protected]>"
RECIPIENT = "[email protected]"
CONFIGURATION_SET = "ConfigSet"
AWS_REGION = "us-west-2"

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

try:
    response = client.send_email(
        Destination={"ToAddresses": [RECIPIENT] },
        Message={
            "Body": {
                "Html": {"Charset": "UTF-8", "Data": message.html},
                "Text": {"Charset": "UTF-8", "Data": message.txt},
            },
            "Subject": {"Charset": "UTF-8", "Data": message.subject},
        },
        Source=SENDER,
        ConfigurationSetName=CONFIGURATION_SET,
    )

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

Conclusion

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.

Project source code available at...
visini/email-templates-i18n

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

© 2023 Camillo Visini
Imprint