Sending multilingual emails with Postmark and Gitloc
Tags: mail, postmark, gitloc, node
Published: 2023-12-08 15:29:35

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](https://spotsmap.com/map) 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](https://spotsmap.com/map), 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:

```html
...
<h2>Arrival date:<h2>
<p>{{ArrivalDate}}</p>
...
```

And we used it like this:

```jsx
// 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}`)
    })
}
```

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:

```html
...
<h2>
{{#en}}Arrival date:{{/en}}
{{#de}}Ankunftsdatum:{{/de}}
{{#es}}Fecha de llegada:{{/es}}
</h2>
<p>{{ArrivalDate}}</p>
...
```

And we made a few changes in our code:

```jsx
// 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}`)
    })
}
```

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](https://spotsmap.com/map) 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:

```html
...
<h2>{{ArrivalDateTitle}}</h2>
<p>{{ArrivalDate}}</p>
...
```

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

```json
{
  "ArrivalDateTitle": "Arrival date",
  ...
}
```

We modified the `sendMail` function accordingly:

```jsx
// 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}`)
    })
}
```

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

```yaml
config:
  defaultLocale: en
  locales:
   - en
   - de
   - es
  directories:
   - templates
```

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](https://gitloc.org/) 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:

- Postmark documentation: https://postmarkapp.com/developer
- Gitloc documentation: https://docs.gitloc.org/

I hope you find these insights valuable!