Add i18n translation to emails in nestjs app with nestjs-i18n

Reading Time: 6 minutes

Loading

Introduction

This project is a proof of concept (POC) to apply i18n translation to emails. Our vendor has a different solution to translate emails at work; therefore, this solution is not picked up. Nonetheless, I want to show my work in this post to demonstrate how to use nestjs-i18n library to translate the content of Mjml template and render the html email. In development mode, we preview emails of different languages on browser. In production mode, we send test emails to Mailtrap to verify the set up of our email service.

let's go

Create a new nest application

nest new nesti18n-mjml-poc

Add i18n support in nest

npm i --save nestjs-i18n

Create an i18n directory under src/ and create en, es and hk folders under src/i18n/. The folder names, en, es and hk, are locales of English, Spanish and Chinese respectively.

 i18n
├── en
│   └── email.json
├── es
│   └── email.json
└── hk
    └── email.json

Create JSON translation files

English language, src/i18n/en/email.json

{
  "welcomeUser": {
    "THANKS_AND_BEST_REGARDS": "Thanks and best regards,",
    "WELCOME": "Welcome to {0.organization}.",
    "MEMBERSHIP_FEE": "Membership fee is {0.total} dollars and starts at {0.startDate}.",
    "TITLE": "Dear {0.name},",
    "ADMIN": "Administrator",
    "SUBJECT": "Registration completed"
  }
}

Spanish language, src/i18n/es/email.json

{
  "welcomeUser": {
    "THANKS_AND_BEST_REGARDS": "Gracias y un saludo,",
    "WELCOME": "Bienvenido a {0.organization}.",
    "MEMBERSHIP_FEE": "La cuota de membresía es {0.total} dolares y comienzas en {0.startDate}.",
    "TITLE": "Quierdo {0.name},",
    "ADMIN": "Administrador",
    "SUBJECT": "Registro completado"
  }
}

Chinese language, src/i18n/hk/email.json

{
  "welcomeUser": {
    "THANKS_AND_BEST_REGARDS": "謝謝,",
    "WELCOME": "歡迎來到{0.organization}。",
    "MEMBERSHIP_FEE": "會員費{0.total}港幣和開始於{0.startDate}。",
    "TITLE": "親愛的{0.name},",
    "ADMIN": "管理員",
    "SUBJECT": "註冊完成"
  }
}

Copy i18n folder and mjml files to dist/ in watch mode

Edit nest-cli.json to copy i18n folder and any mjml file to dist/ in watch mode

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": [
      {
        "include": "**/*.mjml",
        "watchAssets": true
      },
      {
        "include": "i18n/**/*",
        "watchAssets": true
      }
    ]
  }
}

Inject I18nModule to enable i18n support

imports: [
    I18nModule.forRoot({
      fallbackLanguage: 'en',
      parser: I18nJsonParser,
      parserOptions: {
        path: path.join(__dirname, '/i18n/'),
        watch: true,
      },
      resolvers: [new HeaderResolver(['language'])],
    }),
],

fallbackLanguage property indicates that the fallback language is English.

parserOptions: {
    path: path.join(__dirname, '/i18n/'),
    watch: true,
}

watch: true flag enables live translation and monitors i18n folder for changes in translations.

resolvers: [new HeaderResolver(['language'])],

looks up language in HTTP request header. If request header specifies language, the language value is used to translate message and return it back to the client. If request header does not contain language, we use the fallback language for translation.

Install dependencies for apply i18n translation to emails

npm i cross-env lodash mjml nodemailer preview-email
npm i --save-dev @types/lodash @types/mjml @types/nodemailer @types/preview-email
npm i class-validator class-transformer

Use case of apply i18n translation to emails

The use case is to send welcome user email to users according to their language. When language is en, we send English email. Similarly, user receives Spanish email when language is es and Chinese email when language is hk.

English email

Spanish Email

Chinese mail

Architecture of the application

The architecture of the application has two modules: core module that is consisted of core services and user module that sends user emails

Responsibility of the components

  • MjmlService – A service that renders Mjml email template in html format
  • MailserService – A service that previews email on browser in development mode or send email to Mailtrap in production mode
  • AppConfigService – A service that return configuration values of .env file
  • UserController – A controller that routes http request to send welcome user email
  • UserService – A service that calls MjmlService and MailerService to send welcome user email

Add Configurations for the application

First, use nest-cli to generate core module and then AppConfigService service in core/services folder

nest g mo core
nest g s core/services/appConfig --flat

Define configurations of node environment and smtp server in the service

Setup Mjml service to render html email

Next, Use nest cli to generate Mjml Service in the core module

nest g s core/services/mjml --flat

Implement MjmlService

We design the generate template flow as follows:

  1. Load the Mjml template into a string
  2. Use Lodash template to create a compiled template
  3. Compile the template with i18n translations to replace variables in the template
  4. Execute mjml2html to render Mjml template in html format

Create Mailer Service to preview i18n translated emails

Then, use nest cli to generate Mailer Service in the core module

nest g s core/services/mailer --flat

Implement MailerService

In the constructor, we read the from address from the configuration service and create nodemailer smtp transporter.

Define send mail logic

When the application runs in development mode, we preview the email on browser. Otherwise, nodemailer sends the emails with a mail server.

The reason that I choose Mailtrap is the functionality comes out of the box. If I do not use it, I will either create a Docker container with Mailtrap image or configure SMTP server on cloud.

Configure SMTP environment variables

In order to use nodeMailer SMTP transporter to send mail, we have to find a free and safe SMTP server. In this blog post, we choose Mailtrap because it does not require DevOp or networking background to configure a SMTP server.

Finally, we use Mailtrap sandbox to test send email in production mode after development work is done.

First, we navigate to Mailtrap -> Inboxes -> SMTP Settings and select Nodemailer Integrations. Then, we copy SMTP host, SMTP port, SMTP user and SMTP password to .env file

NODE_ENV=development
PORT=3000
MAILER_FROM=no-reply@example.com
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
SMTP_USER=<mailtrap username>
SMTP_PASSWORD=<mailtrap password>

Implement i18n translation to email in development mode

Step 1 is create a DTO to send welcome user email

import { IsEmail, IsNotEmpty, IsString } from 'class-validator'

export class WelcomeUserDto {
  @IsNotEmpty()
  @IsString()
  organization: string

  @IsNotEmpty()
  @IsString()
  name: string

  @IsNotEmpty()
  @IsEmail()
  email: string
}

Step 2 is create endpoint in user controller to send welcome user email

Inject i18n, Mjml and Mail services in the constructor

Create a helper function that accepts i18n language and arguments, and translates the text

Step 3 is add send welcome user email method in the service

Step 4 is test the endpoint with CURL

Open a terminal and start nest server in development mode

npm run start:dev

Make a HTTP request to preview Spanish email

curl --location --request POST 'http://localhost:3000/user/welcome-useurse ' \
--header 'language: es' \
--header 'Content-Type: application/json' \
--data-raw '{
    "organization": "House of Nest",
    "name": "John Doe",
    "email": "john.doe@example.com"
}'

Repeat i18n translation to emails in production mode

npm run start:production

Since we have set up a running Mailtrap SMTP server, we can mimic the scenario that users receive their mail in inbox from mail provider such as AWS or Google.

As a result, we can call the endpoint to send English, Spanish and Chinese emails to the inbox.

The result is the same as development mode except the destination is mailbox and not the browser.

Up to this point, we have a working solution that is capable of rendering i18n emails in three languages. However, the application can make 2 minor improvements in language extraction and docker.

Improvements

  1. Add a middleware to store language in continuous local storage and retrieve the value in user service.
  2. Create docker-compose.yml to install docker image of mailtrap and test send email locally

Final thoughts

Another use case of nestjs-i18n is to translate email according to the language of http request. We translate the contents by message keys and pass the arguments into Mjml template to replace variables by i18n values. Then, we can preview the emails on browser and send to inbox of email client.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, Nest and other technologies.

Resources:

  1. Github Repository: https://github.com/railsstudent/nesti18n-mjml-poc
  2. Mjml: https://documentation.mjml.io/#inside-node-js
  3. nestjs-i18n: https://github.com/ToonvanStrijp/nestjs-i18n
  4. Mailtrap: https://mailtrap.io/