The Ultimate Guide: Node.js Internationalization (I18n)

If you are a serious Node.js Software Engineer that creates web applications with Express, Koa or any similar framework, you will need to be able to Internationalise your app in order to support different locales. In this tutorial we will see you how to setup I18n in Node.js and how to organise your translations, so to make your application reachable to a much wider audience.

Node.js is an asynchronous event-driven JavaScript runtime and is designed to help build scalable network applications. In essence, it allows Javascript to run in the backend as 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 I18n 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 article is about trying to fill that big gap; by showing ways you can integrate 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

Create the initial folder structure.

1

2

$mkdir node-i18n-example&&cdnode-i18n-example

$npm init--yes

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:

1

$mkdir-papp/services&&cd app/services

2. Add the file contents with your favorite editor:

1

$touch localeService.mjs

File: app/services/localeService.mjs

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

/**

* LocaleService

*/

exportclassLocaleService{

/**

*

* @param i18nProvider The i18n provider

*/

constructor(i18nProvider){

this.i18nProvider=i18nProvider;

}

/**

*

* @returns {string} The current locale code

*/

getCurrentLocale(){

returnthis.i18nProvider.getLocale();

}

/**

*

* @returns string[] The list of available locale codes

*/

getLocales(){

returnthis.i18nProvider.getLocales();

}

/**

*

* @param locale The locale to set. Must be from the list of available locales.

*/

setLocale(locale){

if(this.getLocales().indexOf(locale)!==-1){

this.i18nProvider.setLocale(locale)

}

}

/**

*

* @param string String to translate

* @param args Extra parameters

* @returns {string} Translated string

*/

translate(string,args=undefined){

returnthis.i18nProvider.translate(string,args)

}

/**

*

* @param phrase Object to translate

* @param count The plural number

* @returns {string} Translated string

*/

translatePlurals(phrase,count){

returnthis.i18nProvider.translateN(phrase,count)

}

}

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

1

$npm install i18n--save

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

1

$touch app/i18n.config.mjs

File: app/i18n.config.mjs

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import i18n from'i18n';

import path from'path';

i18n.configure({

locales:['en','el'],

defaultLocale:'en',

queryParameter:'lang',

directory:path.join('./','locales'),

api:{

'__':'translate',

'__n':'translateN'

},

});

export defaulti18n;

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

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

1

2

3

4

$tree locales

locales

├──el.json

└──en.json

File: locales/en.json

1

2

3

4

5

6

7

{

"Hello":"Hello",

"You have %s message":{

"one":"You have %s message",

"other":"You have %s messages"

}

}

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

1

localeService.setLocale('el');

File: locales/el.json

1

2

3

4

5

6

7

{

"Hello":"Για Σας",

"You have %s message":{

"one":"Έχεις %s μύνημα",

"other":"Έχεις %s μύνηματα"

}

}

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

1

2

3

$node--experimental-modules index.mjs

ΓιαΣας

Έχεις3μύνηματα

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

1

$npm install awilix--save

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

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

1

2

3

4

5

6

7

8

9

10

11

12

/**

* LocaleService

*/

exportclassLocaleService{

/**

*

* @param i18nProvider The i18n provider

*/

constructor(opts){

this.i18nProvider=opts.i18nProvider;

}

...

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

1

2

3

4

5

6

7

8

import container from'./app/container';

constlocaleService=container.resolve('localeService');

localeService.getLocales();// ['en', 'el']

localeService.getCurrentLocale();// 'en'

localeService.setLocale('el');

console.log(localeService.translate('Hello'));

console.log(localeService.translatePlurals('You have %s message',3));

1

2

3

$node--experimental-modules index.mjs

ΓιαΣας

Έχεις3μύνηματα

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:

1

$touchapp/application.mjs

File: app/application.mjs

1

2

3

4

5

6

7

8

9

exportclassApplication{

constructor({server}){

this.server=server;

}

async start(){

await this.server.start();

}

}

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

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

1

2

3

4

5

6

import{Server}from'./server.mjs'

container

.register({

server:awilix.asClass(Server,{lifetime:awilix.Lifetime.SINGLETON}),

})

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

File: index.mjs

1

2

3

4

5

6

7

8

9

import container from'./app/container';

constapp=container.resolve('app');

app

.start()

.catch((error)=>{

console.warn(error);

process.exit();

})

7. Start the server to test that it runs:

1

2

node--experimental-modules index.mjs

[p99922]Listening atport8000

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.

1

$npm install consolidate mustache--save

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

1

2

3

4

5

6

7

8

9

10

11

12

import express from'express';

import consolidate from'consolidate';

exportclassServer{

constructor(){

...

// setup mustache to parse .html files

this.express.set('view engine','html');

this.express.engine('html',consolidate.mustache);

}

...

}

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.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

exportclassServer{

constructor({i18nProvider}){

...

this.express.use(i18nProvider.init);

this.express.get('/:name?',(req,res)=>{

constname=req.params.name;

res.render('index',{

'currentLocale':res.locale,

'name':name||'Theo',

'hello':req.__('Hello'),

'message':req.__('How are you?')

});

});

}

start(){

returnnewPromise((resolve)=>{

consthttp=this.express.listen(8000,()=>{

const{port}=http.address();

console.info(`[p${process.pid}]Listeningatport${port}`);

resolve();

});

});

}

}

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

1

$touch views/index.html

File: views/index.html

1

2

3

4

5

6

7

8

9

<!DOCTYPE html>

<html lang="{{currentLocale}}">

<head>

<title>Nodei18n</title>

<body>

{{hello}}{{name}}<br>

{{message}}

</body>

</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

1

2

3

4

5

6

7

8

9

10

11

import i18n from'i18n';

import path from'path';

i18n.configure({

locales:['en','el'],

defaultLocale:'en',

queryParameter:'lang',

directory:path.join('./','locales')

});

export defaulti18n;

2. Add the relevant translations for the target language:

File: locales/el.json

1

2

3

4

5

6

7

8

{

"Hello":"Για Σας",

"How are you?":"Πώς είστε?",

"You have %s message":{

"one":"'Έχεις %s μύνημα",

"other":"'Έχεις %s μύνηματα"

}

}

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

1

2

3

4

5

6

//https://github.com/janl/mustache.js#functions

this.express.use((req,res,next)=>{

// mustache helper

res.locals.i18n=()=>(text,render)=>req.__(text,render);

next();

});

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 i18nProviderand 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

1

2

3

4

5

6

7

8

9

10

11

12

<!DOCTYPE html>

<html lang="{{currentLocale}}">

<head>

<title>Nodei18n</title>

<body>

{{hello}}{{name}}<br>

{{message}}

<div>

{{#i18n}}This is a translation helper{{/i18n}}

</div>

</body>

</html>

File: locales/el.json

1

2

3

4

5

6

7

8

9

{

"Hello":"Για Σας",

"How are you?":"Πώς είστε?",

"You have %s message":{

"one":"'Έχεις %s μύνημα",

"other":"'Έχεις %s μύνηματα"

},

"This is a translation helper":"Αυτός είναι βοηθός μετάφρασης"

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

<!DOCTYPE html>

<html lang="{{currentLocale}}">

<head>

<title>Nodei18n</title>

<body>

{{hello}}{{name}}<br>

{{message}}

<div>

{{#i18n}}This is a translation helper,{{/i18n}}<br>

{{#i18np}}You have %s message,{{messageCount}}{{/i18np}}

</div>

</body>

</html>

File: app/Server.mjs

1

2

3

4

5

6

7

8

9

10

11

this.express.use((req,res,next)=>{

// mustache helper

res.locals.i18np=()=>(text,render)=>{

constparts=text.split(',');

if(parts.length>1){

constrenderedCount=render(parts[1]);

returnreq.__n(parts[0],renderedCount,render)

}

};

next();

});

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

1

2

3

4

5

6

7

8

9

10

11

this.express.get('/:name?',(req,res)=>{

constname=req.params.name;

res.render('index',{

'currentLocale':res.locale,

'name':name||'Theo',

'hello':req.__('Hello'),

<strong>'</strong>messageCount':5,

'message':req.__('How are you?')

});

});

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

/**

*

* @returns {Function} A middleware function to use with Web Frameworks.

*/

getMiddleWare(){

return(req,res,next)=>{

constqueryParameter='lang';

if(req.url){

consturlObj=url.parse(req.url,true);

if(urlObj.query[queryParameter]){

constlanguage=urlObj.query[queryParameter].toLowerCase();

this.setLocale(language);

}

}

next();

}

}

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.

PhraseApp

PhraseApp 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 PhraseApp, refer to the Getting Started guide. You can also get a 14-days trial. So what are you waiting for?

Conclusion

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.

Published on March 8th, 2018 by Theo Despoudis.
Last updated at March 9th, 2018 .