DEV Community

Cover image for Python i18n - Localizing Transactional Email Templates
Camillo Visini
Camillo Visini

Posted on • Originally published at camillovisini.com

Python i18n - Localizing Transactional Email Templates

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

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:

https://github.com/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.

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)
Enter fullscreen mode Exit fullscreen mode

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

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)
Enter fullscreen mode Exit fullscreen mode

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).

{
  "company_name": "Company Name",
  "product_name": "Product Name",
  "product_website": "https://www.example.com/"
}
Enter fullscreen mode Exit fullscreen mode

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.

{
  "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!"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

{% extends "layouts/call_to_action.html" %}
{% block body_top %}
{{localized.thank_you}}
{% endblock %}
{% block body_bottom %}
{{localized.contact_us_for_help}}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

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.

{% extends "layouts/call_to_action.txt" %}
{% block body_top %}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
{{localized.thank_you}}
{% endblock %}
{% block body_bottom %}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
{{localized.contact_us_for_help}}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

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}}
{
  "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."
  }
}
Enter fullscreen mode Exit fullscreen mode

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

{
  "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!'}}"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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 <sender@example.com>"
RECIPIENT = "recipient@example.com"
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"])
Enter fullscreen mode Exit fullscreen mode

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.

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

Top comments (2)

Collapse
 
clementjanssens profile image
Clément Janssens

Wow, thanks Camillo for your article
I created a tool based on Tailwind to send i18n emails easily.
It's called mailhub.sh
You can create a template, add i18n and send emails with a simple API.
Feel free to try it 😉
See you soon

Collapse
 
visini profile image
Camillo Visini

Let me know what you think – I'm looking forward to your comments!