Update: An example project demonstrating how this works can be found here- https://github.com/bmanley91/express-i18n-example
Recently my team was charged with internationalizing our product. We needed to support both English and Spanish in our application with multiple user-facing clients. Since our only common point was our Express backend, we decided that it would serve up message strings to be consumed by our clients. This allowed us to reduce the code impact of the project and assured that the clients would have consistent user messaging going forward.
This article will cover the approach we took to internationalize our Express backend.
Setting the stage
Let's say we already have a very simple express application. It has a /greeting
endpoint which will respond to GET
requests with a message.
const express = require('express');
const app = express();
app.get('/greeting', (req, res) => {
const response = 'hello!';
res.status(200);
res.send(response);
});
app.listen(8080, () => console.log('App listening on port 8080!'));
If you've worked with Express before, this example likely looks pretty familiar. If not, I recommend this tutorial which helped me get up and running when learning Node.
Enter the message file
Typically you'll want to avoid setting strings that go to the end user right in your code. The idea behind a message file or a collection of message files is to centralize where message strings are held so that they can be easily located and updated. As we'll see later in this post, libraries that deal with i18n often require utilization of a message file.
We'll create a message file named message.json
containing our greeting.
{
"greeting": "hello!"
}
To keep things organized, we'll also create a resources
directory to hold our new file. After this, our directory structure looks something like this.
.
├── server.js
├── resources
| └── message.json
├── package.json
├── package-lock.json
├── node_modules
And we'll modify our app to pull from this file like this.
...
const messages = require('./resources/messages.json');
app.get('/greeting', (req, res) => {
const response = messages.greeting;
res.status(200);
res.send(response);
});
...
So far not much has really changed. We've just centralized where our messages are located.
Internationalize!
Next we'll introduce the modules that'll do most of the lifting for us. We'll be using the following:
- i18next - our core i18n framework
- i18next-express-middleware - adds i18n functionality to our express routes
- i18next-node-fs-backend - lets us pull messages from our message file
After introducing these dependencies, there is one organizational modification we'll need to make. i18next can use our directory structure to decide what files to use for what languages. We'll rename our messages.json
file to translation.json
and move it to a new directory resources/locales/en/
.
Our directory structure should now look like this.
.
├── server.js
├── resources
| └── locales
| └── en
| └── translation.json
├── package.json
├── package-lock.json
├── node_modules
Now that we have all we need, let's run through how to get it up and running.
Initialization
We initialize i18next
like this.
const i18next = require('i18next');
const Backend = require('i18next-node-fs-backend');
const i18nextMiddleware = require('i18next-express-middleware');
i18next
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
backend: {
loadPath: __dirname + '/resources/locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'en',
preload: ['en']
});
const app = express();
app.use(i18nextMiddleware.handle(i18next));
There's a decent amount going on here, so let's walk through it.
First, with use(Backend)
we're telling i18next to use i18next-node-fs-backend as its backing resource. This means we'll be getting our strings from the filesystem.
Second, we're setting up language detection with use(i18nextMiddleware.LanguageDetector)
. This lets our application decide what language it will use based on the Accept-Language
header sent from consumers.
Next, we actually init i18next.
The backend
object specifies the path that i18next will load our messages from. The curly bracketed parameters will come into play later. {{lng}}
represents the language in the directory and {{ns}}
represents the "namespace" of the strings in the file. The namespace is useful for larger applications that may have tons of strings they need to serve up. Since we're only going to serve up a few strings, we're just going to use one namespace here.
The preload
parameter takes an array of languages that i18next will load at time of initialization. fallback
defines your default language that will be used if there's no translated string for a certain message.
Lastly, with app.use(i18nextMiddleware.handle(i18next));
we tell Express to use i18next's middleware.
Alright i18n is all set up! Let's actually use it now.
t
One of the things that i18next-express-middleware gets us is the t
function on our Express request. This function will look for a key in the messages that i18next has loaded and return it in the specified language.
Here's how we can use t
in our example project.
const express = require('express');
const i18next = require('i18next');
const Backend = require('i18next-node-fs-backend');
const i18nextMiddleware = require('i18next-express-middleware');
i18next
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
backend: {
loadPath: __dirname + '/resources/locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'en',
preload: ['en']
});
const app = express();
app.use(i18nextMiddleware.handle(i18next));
app.get('/greeting', (req, res) => {
const response = req.t('greeting');
res.status(200);
res.send(response);
});
app.listen(8080, () => console.log('Example app listening on port 8080!'));
Now, our app is sending back a string that it's getting from i18next! This isn't exactly exciting since we only have our en
language file so far. So let's get another language set up.
(I am sadly monolingual. So we're going with "hello" and "hola" here. 😔)
Create a new language file in resources/locales/es/translation.json
like this.
{
"greeting": "¡hola!"
}
Next, modify the i18next init call by adding the es
locale to the preload
array.
...
.init({
backend: {
loadPath: __dirname + '/resources/locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'en',
preload: ['en', 'es']
});
...
Test it out
We've set up our translation files and configured i18next to use them. Next, we need to test it out. Let's start up the express server with node server.js
.
Our app will decide what language to use based on the Accept-Language
header. As we've set it up here, it will return Spanish for es
and English for anything else, including if no language header is sent.
We'll use curl localhost:8080/greeting
to test our base case. With no header we should get this response.
hello!
Now let's actually test that our i18n works with curl localhost:8080/greeting -H "Accept-Language: es"
. We should get this response.
¡hola!
We did it! We can now display strings for our users in multiple languages! Now begins the fun of translating every message in your app.
Happy translating!
If you'd like to see a working example of everything outlined here and more, check out https://github.com/bmanley91/express-i18n-example.
Top comments (0)