DEV Community

Cover image for Sending multilingual emails with Postmark and Gitloc
Arsenii Kozlov
Arsenii Kozlov

Posted on • Edited on

Sending multilingual emails with Postmark and Gitloc

In today's globalized world, effective communication with customers in their preferred language is key for businesses. Whether it's via emails, messengers, or websites, using a client's native language fosters a stronger connection and increases the likelihood of successful transactions. This holds true even for transactional or notification emails, as information can be easily overlooked when presented in a non-native language.

In this post, I'll share our experience in implementing multilingual transactional emails for spotsmap.com and the evolution of our approach.

Note: While I'll use Node.js for illustration, the concepts discussed are applicable to various development languages.


Working on spotsmap.com, we faced the challenge of creating a localized booking confirmation email. Packed with details such as sport types, dates, costs, and course information, the HTML template alone exceeded 500 lines of code.

First Solution: Language-Specific Templates

Initially, we opted for different templates for each supported language.

Our source code included a configuration object allowing us to select a template based on the user's language. The template for English looked like this, and almost identical templates existed for every language:

...
<h2>Arrival date:<h2>
<p>{{ArrivalDate}}</p>
...
Enter fullscreen mode Exit fullscreen mode

And we used it like this:

// config.js
export default {
  services: {
    email: {
      postmark: {
        apiKey: process.env.POSTMARK_API_KEY,
        templates: {
          bookingConfirmation: {
            en: 123456,
            de: 123457,
            es: 123458
          }
        }
      }
    }
  }
}

// services/postmark.js
export const sendMail = ({ from, to, template, locale, params }) => {
  const request = client.sendEmailWithTemplate({
    From: from,
    To: to,
    MessageStream: 'outbound',
    TemplateId: postmarkConfig.templates[template][locale],
    TemplateModel: params || {},
  })
  request
    .then(result => {
      console.log(`Email sent to ${to}`)
    })
    .catch(err => {
      console.log(`Email was not sent, with error code ${err.statusCode}, ${err.message}`)
    })
}
Enter fullscreen mode Exit fullscreen mode

It looks easy; however, we had to change the approach.

Why? We initially had three large and very similar templates. Adding a new language was relatively simple – just copy the template and adjust it for the new language. However, over time, we found the need to restructure the template. We introduced the capability to book multiple courses in a single order, and replicating this change across all templates became laborious, even with just three languages.

So, we decided to move to another solution.

Second Solution: Conditional Templates

Our second attempt involved consolidating templates with conditionals.

So we rearranged the template and made only one template for all supported languages. It looked like this:

...
<h2>
{{#en}}Arrival date:{{/en}}
{{#de}}Ankunftsdatum:{{/de}}
{{#es}}Fecha de llegada:{{/es}}
</h2>
<p>{{ArrivalDate}}</p>
...
Enter fullscreen mode Exit fullscreen mode

And we made a few changes in our code:

// config.js
export default {
  services: {
    email: {
      postmark: {
        apiKey: process.env.POSTMARK_API_KEY,
        templates: {
          bookingConfirmation: 123456
        }
      }
    }
  }
}

// services/postmark.js
export const sendMail = ({ from, to, template, locale, params }) => {
  const request = client.sendEmailWithTemplate({
    From: from,
    To: to,
    MessageStream: 'outbound',
    TemplateId: postmarkConfig.templates[template],
    TemplateModel: { ...params, [locale]: true },
  })
  request
    .then(result => {
      console.log(`Email sent to ${to}`)
    })
    .catch(err => {
      console.log(`Email was not sent, with error code ${err.statusCode}, ${err.message}`)
    })
}
Enter fullscreen mode Exit fullscreen mode

Okay, it looks even better.

However, we had over 40 localized strings in the template. Even with just three supported languages, this translates to writing 120 lines of code solely for template localization. Now, consider the scenario with 10 or 40 languages.

For 40 supported languages, it would entail a staggering 1600 lines of code in the email template. This is hardly maintainable. However, our objective is to support 40 or more languages, aiming to make spotsmap.com accessible to people worldwide.

As a result, we decided to explore another solution.

Third (and Current) Solution: Code-Based Localization

To address these challenges, we transitioned to a more efficient approach. We moved localization strings from the template to our codebase and adopted a standardized translation process used for website UI localization.

The revised template structure:

...
<h2>{{ArrivalDateTitle}}</h2>
<p>{{ArrivalDate}}</p>
...
Enter fullscreen mode Exit fullscreen mode

We created a JSON file, e.g., templates/en/bookingConfirmation.json, housing localization strings for English:

{
  "ArrivalDateTitle": "Arrival date",
  ...
}
Enter fullscreen mode Exit fullscreen mode

We modified the sendMail function accordingly:

// services/postmark.js
export const sendMail = ({ from, to, template, locale, params }) => {
  const strings = require(`../templates/${locale}/${template}.json`
  const request = client.sendEmailWithTemplate({
    From: from,
    To: to,
    MessageStream: 'outbound',
    TemplateId: postmarkConfig.templates[template],
    TemplateModel: { ...params, ...strings },
  })
  request
    .then(result => {
      console.log(`Email sent to ${to}`)
    })
    .catch(err => {
      console.log(`Email was not sent, with error code ${err.statusCode}, ${err.message}`)
    })
}
Enter fullscreen mode Exit fullscreen mode

Additionally, we created a gitloc.yaml file to automate the translation process:

config:
  defaultLocale: en
  locales:
   - en
   - de
   - es
  directories:
   - templates
Enter fullscreen mode Exit fullscreen mode

This approach allowed us to easily generate translations for all supported languages by pushing changes to the remote repository and pulling translations locally. Notably, Gitloc facilitated seamless support for new languages by updating the gitloc.yaml file.


Now, we can effortlessly support numerous languages while maintaining a consistent and manageable codebase.

Explore further:

I hope you find these insights valuable!

Top comments (0)