Node.js i18n and Express.js Localization

Node.js i18n and Express.js localization

When you have a successful application sooner or later, you need to find new sources of growth. It turns out that one of the most productive sources of new users is localization of your app. Therefore, if you start developing your app with localization in mind, it dramatically reduces potential pain. That’s why even if you not planning to support a multi-language interface right now, it is a good idea to know how you can quickly create a localization solution with Node.js i18n libraries.

Node.js is a popular cross-platform open-source JavaScript runtime environment. Traditionally JavaScript was evolving as an in-browser language and Node.js opened a broad range of other valuable applications. That’s why many services are now using JavaScript or languages that are transcompiled into JavaScript as a backend language. As JavaScript has some shortcomings popularized by many online meme pictures, several such languages appear. Notable examples are TypeScript and CoffeeScript. JavaScript also doesn’t stand still, and new, more modern specifications of the scripting language appear.

If you are wondering what i18n is about it’s just an abbreviation of the word “internationalization”. For this reason, often programming libraries to keep names short use this abbreviation. Therefore, Node.js i18n simply means Node.js internationalization. In this particular article, we are going to cover i18next library which enables Node.js i18n.

Installing and Configuring Express.js

Let’s start with creating a primitive application based on Express.js by using an express-generator to scaffold it. A small disclaimer: in this article I’m going to cover mostly translation staff. So if you are going to create some production application, I encourage you to follow guidelines and practices to use patterns, extract different logic to separate files, use inheritance etc.

Let’s install express-generator globally and create a new app:

npm install -g express-generator

express --view=pug book_shop

The command creates a folder with the following structure:

  • bin folder contains start script.
  • node_modules is a legendary black hole with all installed Node.js libraries for your application.
  • public here lives your static files like robots.txt, CSS, or images.
  • routes here we map paths of URI to the logic inside your application.
  • views the name says for itself.
  • app.js is the root file of your application.
  • package.json contains meta-information about your application.

First, you need to install all packages listed in package.json along with their dependencies. Do it with the following command:

cd book_shop && npm install

To start your application type:

npm start

By default the web server starts on TCP port 3000. Therefore, open your browser and navigate to:

http://localhost:3000

You may change something in the views/index.pug file, for example add:

p It's a translation article application

Installing Node.js I18n Library: I18next

Next, we want to have some sort of a Node.js i18n library. Such libraries are helping us with a few things:

  • Manage language switching.
  • Working with language resource files. Typically we have some labels in our application that define parts where some text should be translated in a specific language. Translation libraries on some stage of application work replace these labels with text from resource files.
  • Providing translation functions or helpers which help us implement translation logic.
  • Understands formats of resource files, allow us to use embedded variables and nesting.
  • Use pluralization formats, where forms of words change depends on object counts.
  • Some of them automatically translate dates, weekdays, months, and other standard things.

In this article we are going to use I18next: a full-fledged internationalization framework supporting both vanilla JavaScript applications, client-side frameworks, and Node.js.

Let’s start by installing the necessary modules:

npm install i18next --save

npm install i18next-express-middleware --save

npm install i18next-node-fs-backend --save

Translation Management System

Using some kind of an Node.js I18n library will greatly help you as a developer. However, doing the actual translations into multiple languages will still be very painful, especially for complex apps. You will need to make sure that your translators do not edit the same file simultaneously, that translation values are present for each language, that interpolation is done properly etc. To further enhance the process of translating your app, a solid translation management system is required. Meet Lokalise: a next generation service which will immensely help you with all the listed problems (and much more!).

To get started, grab your free trial.

You may use either graphical user interface to work with Lokalise, or a command-line interface. If you prefer the latter, download and install Lokalise CLIv2. Next, go to your profile page, open the “API tokens” section, and create a read/write token. This token will be used later to upload and download translation files.

Lastly, create a new project, give it a name, and select English as a base language (the language that you are going to translate from).

Setting Up I18n Library: I18next

To configure i18next and all necessary packages, we need to change the app.js file. Again, I’m a big fan of decomposing each general file into purpose-specific small ones. But to keep it simple and not overwhelm you with unrelated details, I’ll do everything most straightforwardly.

Import the i18next library:

const i18next = require('i18next');

Import i18next-express-middleware to avoid writing a custom middleware component:

const i18nextMiddleware = require('i18next-express-middleware');

Middleware functions allow us to insert some logic somewhere between an HTTP request and an HTTP response. A middleware has access to the request object, response object, and the next function. It can write some data to the response object, let’s say HTTP response code like 500 Server Error and halt further steps. This concept is widespread for web server development. With middleware, you can do such things as authentication, access control, logging, and service parameter processing. In our case i18next-express-middleware allow us to parse parameters to set the current language automatically, wire up i18next data to request object, add localized routes.

We are planning to work with resource files from the filesystem so import the corresponding module:

const Backend = require('i18next-node-fs-backend');

Next, we need to initialize everything and configure it.

Configuring I18next Localization Library

i18next
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
backend: {
loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
},
detection: {
order: ['querystring', 'cookie'],
caches: ['cookie']
},
fallbackLng: 'en',
preload: ['en', 'ru']
});
view raw app.js hosted with ❤ by GitHub

The backend block is in charge of loading language file resources. loadPath tells where files are. Here we see two confounding variables ns and lng. lng is the language code, and by default, it’s undefined. ns is a namespace, and the default value is translation. Namespaces may be useful in some cases, like having separate language files.

detection parameters tell the middleware where to look for a language to set as current language. In our case, middleware checks the query string first, and if there is no language parameter provided, it goes to cookies. As we can see from the caches parameter, the middleware saves an active language in cookies. fallbackLng sets the language, if no active language parameters found anywhere. preload makes listed languages load on the spot. If you have large files, sometimes it’s better to load a specific language later in the controller. You can make it anytime if you use i18next.loadLanguages method.

If something goes wrong and you don’t know why – set debug: true to enable debugging.

app.use(i18nextMiddleware.handle(i18next));

This string integrates our middleware into our app workflow.

Finally, our app.js file looks like this:

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
const i18next = require('i18next');
const i18nextMiddleware = require('i18next-express-middleware');
const Backend = require('i18next-node-fs-backend');
i18next
.use(i18nextMiddleware.LanguageDetector)
.use(Backend)
.init({
backend: {
loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json'
},
debug: true,
detection: {
order: ['querystring', 'cookie'],
caches: ['cookie']
},
preload: ['en', 'ru'],
saveMissing: true,
fallBackLng: ['en']
});
app.use(i18nextMiddleware.handle(i18next));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
view raw app.js hosted with ❤ by GitHub

Creating a Controller

Now we need to use our new localization library in the application code. Let’s do a little grooming to extract logic from routers to controllers. We are creating a controllers folder and a index_controller.js file inside:

var express = require('express');
var router = express.Router();
const i18next = require('i18next');
class IndexController {
static list(req, res, next) {
res.render('index', { title: 'Express'});
}
}
module.exports = IndexController;
view raw index_controller.js hosted with ❤ by GitHub

First, we import express and router libraries to make sure the Express.js logic works. Next, we import i18next to play with it.

We make a class that contains methods for processing requests. In the class, we are creating a list method with parameters we take from the router function. req is the request object and res is a response object. Our middleware extends this object with custom i18next parameters that we can use. We can refer to i18next with req.i18n inside the class. Later we’ll see what can be achived with it.

The next thing we pay attention to is index.pug. PUG is a template language, which is lexically compact and very easy to read. You can use any convenient template language if you want. We use PUG to emphasize essential parts and don’t pay attention to HTML code.

extends layout
block content
h1 Welcome to #{title}
p It's a translation article application
view raw index.pug hosted with ❤ by GitHub

As you can see, PUG use indentation to define code blocks. #{title} is a variable that we pass from the controller’s render function, as you can see from our controller. To glue everything together, we connect our controller to the router:

var express = require('express');
var router = express.Router();
const IndexContoller = require('../controllers/index_controller');
/* GET home page. */
router.get('/', IndexContoller.list);
module.exports = router;
view raw index.js hosted with ❤ by GitHub

Language Resource Files

Let’s also create resource files for each language we are going to work with. According to our configuration, we must create locales folders and two subfolders: ru and en (they are going to contain Russian and English translations respectively). Inside we should place files with the same names: translation.json. If we change our namespace to some other more suitable name, then our translation file should have the same name. The format of the files is standard key-value JSON. i18next also supports nesting for logical structuring:

{ home: { description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur,  adipisci velit" } }

In this case, to access the description nested key, we would use home.description.

locales/en/translation.json

{
"home": {
"title": "Book shop",
"total": "There are {{counter}} books in the list",
"buttonText": "Refresh button",
"buttonHTML": "<button onClick='window.location.reload();'>$t(buttonText)</button>",
}
}
view raw translation.json hosted with ❤ by GitHub

locales/ru/translation.json

{
"home": {
"title": "Книжный магазин",
"total": "Всего в списке {{counter}} книг",
"buttonText": "Обновить страницу",
"buttonHTML": "<button onClick='window.location.reload();'>$t(buttonText)</button>",
},
"Something for Nothing": "Кое что задаром",
"Harry Potter and the Philosopher's Stone": "Гарри Поттер и Философский камень"
}
view raw translation.json hosted with ❤ by GitHub

Uploading to Lokalise

Once your translation files have some initial content, you may upload them to your new Lokalise project:

lokalise2 file upload --token <token> --project-id <project_id> --lang-iso en --file locales/en/translation.json

lokalise2 file upload --token <token> --project-id <project_id> --lang-iso ru --file locales/ru/translation.json

We obtained token in one of the previous sections. As for the project_id, it can be found by clicking “More” button and clicking “Settings” while on the project’s page.

If you prefer to use GUI, then simply navigate to your project and click the “Upload” button as instructed in the documentation. Next, add your translation files and press “Import the files”. After a couple of seconds, you will be good to go!

Now you may translate your strings, add more translation keys, languages, and much more.

Language Switching

Now we have everything in the place but how can a user change languages in our app? Our middleware should change the current language if we provide the link with the proper query string. Let’s make two links for each language and translate our title into Russian language using the t helper:

extends layout
block content
h1 Welcome to #{title}
p Welcome #{t("home.title")}
p It's a translation article application
a(href='?lng=en') English <br>
a(href='?lng=ru') Russian
view raw index.pug hosted with ❤ by GitHub

Now we have a link, and our translation works. What if we don’t want to have a persistent query parameter in the URL field of a browser? Conveniently, there is an option for this. Just change the string of middleware integration:

app.use(
middleware.handle(i18next, {
ignoreRoutes: ["/foo"], // or function(req, res, options, i18next) { /* return true to ignore */ }
removeLngFromUrl: false
})
);
view raw app.js hosted with ❤ by GitHub

Here you also can ignore some specific routes if you don’t want trigger language switching there.

Using the translation helper

Let’s create a little bit more complex page structure and introduce a super simple table. Create an array with books in the index_controller. First, we need to write a table using pug and to make it readable — style it a little bit:

index.pug

extends layout
block content
h1 Welcome to #{title}
p Welcome #{t("home.title")}
p It's a translation article application
a(href='?lng=en') English <br>
a(href='?lng=ru') Russian
<br>
table
th Book
th Author
each book in books
tr
td= t(book.title)
td= t(book.author)
tr
td= t('home.total', { counter: counter })
view raw index.pug hosted with ❤ by GitHub

style.css

body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
table {
width: 100%;
border-collapse: collapse;
}
table th {
background-color: #f2f2f2;
}
th, tr, td, {
border: 1px solid black;
}
th {
height: 50px;
}
view raw styles.css hosted with ❤ by GitHub

What if we use not simple keys, but the whole sentence like book names? As you see from this code, it works without any problem. If we don’t have a translation, you just have your keys displayed.

I18next supports the following formats of resource strings:

  • {'simpleString': 'Simple string'} the most common case.
  • {'simpleNestedString': 'This is simple string that contains $t(simpleString)} we can nest translations with $t.
  • {'simpleStringWithVariable': 'Simple string with {{ variable1 }} and {{variable2}} } to display variable we pass it as a second parameter. For example:
    var a = 'first';
    var b = 'second';
    req.i18n.t('simpleStringWithVariable', { variable1: a, variable2: b } ).
  • {'simpleStringWithNumeral': '{{count}} apple}, { 'simpleStringWithNumber_plural': '{{count}} apples} with postfix _plural we can define a situation when you have several objects and want to set a proper declension.
  • Similarly, we can provide the context parameters { 'key_male': 'He is good', 'key_female': 'She is good' } to utilize this feature just call t with context-param t('key', { context: 'male' }). You may combine context with plurals.
  • You can also use arrays in resource files. { 'key': [ { 'name': 'Roman' }, { 'name': 'Elena' } ]} array elements can refer by using the number of an element as an attribute key.0.name will produce Roman.

Translations in Code

What if we must translate something not in the template, but inside the code? As we’ve previously noted, we have i18next object embedded into request object by our middleware.

So you can perform translations in the controller by using the t method: req.i18n.t("home.title").

t function is a powerful instrument that we can use in several ways:

  • t('key') resolve a key.
  • t('key.subkey') we are searching for a nested subkey.
  • t('common:key') resolve a key in a namespace called “common”.
  • t(['error.404', 'error.unspecified']) we are searching for the first key; if the first key undefined, we are looking for a second and so forth.

One more thing. What if we pass an insecure HTML code to the i18next interpolation? Fortunately, the library escapes all strings. If you want to display insecure content like HTML code you can use following notations inside translation strings:
var HTML = <button onClick=<button onlick="window.location.reload();">Refresh page</button>
{{- html }} produces insecure output HTML.
{{ html }} produces secure string output.

Downloading Translation Files From Lokalise

After you’ve edited your translations on Lokalise, download them back by running:

lokalise2 file download --token <token> --project-id <project_id> --directory-prefix locales/%LANG_ISO% --format json

This is going to download translation files back in JSON format and place them in locales/<LANG_ISO> folder inside the current folder. Take a look at the official documentation for the command-line interface to read about other commands and options.

If you are using GUI, press the “Download” button and adjust the following options:

  • Format – JSON
  • File structure – Use previously assigned filenames with the following directory prefix: locales/%LANG_ISO%

Adjust other additional options as needed. Once you are ready, press the “Build and download” button. You will get an archive containing all your translation keys and values which may be used in the application!

Conclusion

Node.js is a perfect solution for building web applications. It has a high performance, vast ecosystem, great community, a high number of different purpose libraries. As JavaScript as a language now has coverage more than ever, many JavaScript libraries implement state of the art ideas.

You can build fairly complex translation solutions with Node.js libraries. I18next Translation framework is an excellent solution with very few shortcomings. That’s why it’s a good choice for Node.js i18n and localizing Node.js apps. There are tons of different options for each library’s function that you can browse in the documentation. Support for many platforms, resource sources, middlewares are in place. Even debug output is very well developed and contains much useful information about each action the library makes. The translation is available both in code and templates. Moreover, translation functions and helpers have many useful options. Resource files support nesting. The only thing that looks a little bit disappointing is plurals support, but you can use custom context to mitigate this issue.

Related posts

Sign up to our newsletter

Get the latest articles on all things localization and translation management delivered straight to your inbox.

Read also
Localization made easy. Why wait?
The preferred localization tool of 1500+ leading global companies