The Ultimate Guide: Node.js Internationalization (I18n)

If you are a serious Node.js Software Engineer creating web applications with Express, Koa, or any similar framework, you will need to be able to internationalize your app to let it support different locales. In this tutorial, we will see how to set up i18n in Node.js, and how to organize your translations so that your app can reach as many international users as possible.

Node.js is an asynchronous event-driven JavaScript runtime designed to help build scalable network applications. In essence, it allows JavaScript to run in the backend as a server-side code.

The Node ecosystem is vast and it relies on community projects. Although there are numerous tutorials online exploring Node.js and its libraries, the topic of Node internationalization is almost left behind.

So, when you have the need for scalable I18n solutions that are easy to use and implement, it pays to make some sensible software architecture demissions upfront.

This tutorial will try to fill that gap by showing ways of integrating i18n and adapting to different cultural rules and habits in your Node.js applications in a sensible manner.

For the purposes of this tutorial, I will be using the latest Node.js LTS runtime v8.94 and the code for this tutorial is hosted on GitHub. For convenience, we are going to use the –experimental-modules flag in order to use es6 imports in code. You can achieve the same result using babel with preset-es2015.

Let’s get started!

Setting up the Project

The Node.js runtime library is providing only the basic low-level primitives in order to write server applications. So in order to start with i18n, we have to start from scratch. That’s partly good because it allows us to have total control over the design decisions that may affect our project scope.

Initiate a new Node.js app

For the purposes of this tutorial, I’ve taken a lot of inspiration from this node API boilerplate where you start with a good yet opinionated base project for your Node.js applications.

Create the initial folder structure.

Create a Locale Service

We need to have a service that will handle any i18n calls and will work as a mediator for any underlying i18n library we may use or may not use.

1. Create the service folder:

2. Add the file contents with your favorite editor:

File: app/services/localeService.mjs

The LocaleService  is a simple class that accepts an i18nProvider  object that will help make working with locales a little bit easier. We might have a little bit more flexibility that way as we may decide to switch our provider without breaking much or our API calls.

Now what’s missing is the actual i18n provider. Let’s add one.

Adding an I18n provider

While there are a few reasonable choices when it comes to I18n, the most popular library at the moment is i18n-node.

The installation is pretty forward:

1. Install the package

2. Create a i18n config object because before we use the library we need to configure it:

File: app/i18n.config.mjs

Here we added support for 2 languages with the default one being en. We also defined a locale directory that will be used by the library for autogenerating the initial translation files and strings. The API property is just a mapping from the __ call to translate call in our localeService . If you don’t want that arrangement you can change the call from this.i18nProvider.translate(string, args)  to this.i18nProvider.__(string, args) .

Tip: Look at the whole list of config options for that library here.

3. Test the Locale Service by instantiating an i18n object and a localeService:

File: index.mjs

Then in the command line run the following:

This will automatically generate a locales directory on the root folder containing the relevant translation strings for the current language:

File: locales/en.json

Add the following line in index.mjs to test the generation of translatable strings for the other language:

File: locales/el.json

Run again the app and to verify that the translations are happening:

Wire everything up with a DI container

Currently, in order to use our localeService  class, we have to manually instantiate it and pass on the i18n config object. What’s even worse is that we need to keep only one reference to this object as we need to keep the current locale state in one place.

Would it be nice if we had a way to configure and retrieve on demand our localeService instance whenever in our application requests it? Would it be even nicer than any parameters also that needed to be provided at creation time be resolved also?

It turns out that there is a way to do that with the help of a Dependency Inversion Container. This is just an object that exposes a top-level API that allows us to register our valuable objects or services and request them in another time and place. This Dependency Inversion Container is one form of inversion of control (IoC) and helps with reusability, testability and better control.

For Node.js there is a nice library called awilix that offers those features. Let’s see how can we integrate this library for the sake of a better application structure.

1. Install awilix

2. Create a file named container.mjs that will keep track of all the service registrations that we will need.

File: app/container.mjs

As you can see we have a greater flexibility on how we want our objects to be instantiated. For this example we want the LocaleService class to be a singleton object and the i18n config to be just a value because we have just configure it. There are more options for lifetime management in the awilix documentation.

Let’s hook our LocaleService and our i18n config together at the constructor so every time we resolve the everything is set up for us:

1. Modify the constructor of the LocaleService class to accept an i18nProvider  object:

File: app/services/localeService.mjs

2. Test the resolution of our service by replacing the calls in the index.mjs file with the ones using the container.

File: index.mjs

Tip: You can choose to name your resolved service as you like. For this example, we maintained the same name for readability.

Example with Express.js and Mustache.js

Let’s see how can we utilize what we have in an example application using Express.js. Express.js is a small but powerful Web Framework for Node.js that allows you to create a robust set of features for web and mobile applications.

Before we install Express.js though we need to add a few more abstractions on our app in order to accommodate this change.

We need an App class that will accept a Server class object.  The App class will know only how to use the server object to start the Application Server and the Server object will know how to start our Express.js application.

Adding Express.js

1. Create an application.mjs file and add a constructor accepting a server object:

File: app/application.mjs

We want the server to start async mode as we need to perform additional tasks when this is resolved

2. Add our Application class to the resolution container:

File: app/container.mjs

3. Install Express.js

4 Create a file named server.mjs that contain our initialization logic:

File: app/server.mjs

Our Server currently does nothing but opening a port on 8000 and logging the info.

Let’s hook it up to our Application now:

5. Add the Server class to the resolution container:

File: app/container.mjs

6.  Add this code to resolve the app and start the server:

File: index.mjs

7. Start the server to test that it runs:

Adding Mustache.js

Express.js does not have a template rendering engine by default. It is however very customizable and open to extensions. For the purposes of this example, we are going to use a very popular template engine for rendering our translations called Mustache.js

1. Install Mustache and its helper.

2. Configure and add the rendering engine to our Server.js

Now we are ready to use our engine to render HTML pages with translatable strings. For that, we have the flexibility to use a middleware function that is supplied from the i18n library. This will inject its API into the req object as provided by the framework so we can use it without importing anything else.

1. Inject the i18nProvider in our server and add the middleware to the Express.js flow.

2. Create our index.html view that will render when we visit the initial path:

File: views/index.html

3. Run the server and navigate to localhost:8000/  or  localhost:8000/:param  to see the following result:

Switching Locale

On the practical side of things, the user of the applications ideally wants to change the locale from the UI. So the application needs to determine which is the current locale based on some client parameters. There are several ways to detect that preference. We can use a cookie that stores the current locale, a query parameter that requests a specific locale or an accept-language header that specifies which languages are which languages the client is able to understand, and which locale variant is preferred.

For the purposes of this tutorial, we are going to use a query parameter as it’s relatively easy to understand and implement.

We need to define a parameter that we will be using in order to determine the current locale setting. Let’s call it lang. Adding it to our app is relevantly simple.

1. Add the lang parameter to our i18nProvider object:

File: app/i18n.config.mjs

2. Add the relevant translations for the target language:

File: locales/el.json

3. Start the Server and navigate to  localhost:8000/?lang=el

Adding Template helpers

If you don’t like adding the translation keys and values inside the application code you can include template helpers that parse the keys and automatically apply the translation value directly from the template files. Let’s see how can we do that using Mustache.

1. Add the following middleware function on our Server.mjs file:

File: app/Server.mjs

Here res.locals refers to all the functions available from our mustache.js engine. What we actually do is adding one more template helper for the i18n tag that will just call the req.__ method that is attached from the i18nProvider and by supplying the required parameters.

2. Add extra tags in our index.html file and test that the translations are happening:

File: views/index.html

File: locales/el.json

Now in order to support plural translations using template helpers, we need to provide a different function that will accept 2 tags, one for the message key and one for the count:

File: views/index.html

File: app/Server.mjs

In that case, we use the render method supplied from mustache to find the replace the correct count from our data store. Just don’t forget to add this key to our response variables:

File: app/server.mjs

Run again the app to see the result:



Using our own locale middleware to change language

If you are interested in using our localeService object to detect and set the current locale based on the query parameter then you only have to add the following middleware method to our LocaleService class:

That way we can re-use our localeService object without cross-referencing other libraries.

I leave as an exercise for the reader the replacement of the calls to translate the strings using our localeService  object instead of the req object in our Server.mjs file.


Phrase supports many different languages and frameworks, including Node.js and Javascript. It allows to easily import and export translations data and search for any missing translations, which is really convenient. On top of that, you can collaborate with translators as it is much better to have professionally done localization for your website. If you’d like to learn more about Phrase, refer to the Getting Started guide. You can also get a 14-day trial. So what are you waiting for?


This article made a valiant attempt to describe in detail the steps required in order to add i18n to your Node.js application. I hope you enjoyed the article and that it helped you understand what is required to localize Node.js apps.

This is by no means an exhaustive guide as every application has different needs and scope requirements. Please stay put for more detailed articles regarding this subject.

4.9 (98.33%) 12 votes