As your application grows and attracts more users, one of the most effective ways to expand your user base is through localization. By supporting multiple languages, you can make your app accessible to a wider audience. That’s why it’s smart to consider localization in Node.js early in your development process, even if you’re not planning to launch with multi-language support right away.
Node.js is a popular cross-platform, open-source JavaScript runtime environment that allows you to build server-side applications. While JavaScript was traditionally used as an in-browser language, Node.js has significantly broadened its capabilities, making it a strong choice for backend development.
As a result, many services now use JavaScript or languages that compile down to JavaScript, like TypeScript and CoffeeScript. With modern versions of JavaScript introducing new features and specifications, it’s becoming more powerful and flexible.
If you’re unfamiliar with the term i18n, it’s a common abbreviation for “internationalization”. The number 18 refers to the number of letters between the first letter (“i”) and the last letter (“n”) in “internationalization.” Developers use this abbreviation to keep things short.
So, when we talk about “Node.js i18n,” we’re really talking about making Node.js apps multilingual. In this article, we’ll focus on implementing i18n in a Node.js app using the i18next library, which is one of the most popular solutions for handling internationalization.
The source code for this article can be found on GitHub.
Project setup for localization in Node.js
Prerequisites
Before diving into the setup, make sure you have the following:
- Node.js (version 14 or higher) and npm installed on your machine.
- Basic knowledge of JavaScript and Express.js.
- Familiarity with Pug templating (optional, for rendering views).
If you don’t have Node.js installed, grab it from Node.js official website, and you’re ready to roll.
Step 1: Initialize a new Node.js project
First, create a new directory for your project and initialize it with a package.json
file:
mkdir book_shop cd book_shop npm init -y
This will generate a basic package.json
file in your project directory. It’s the default config file that Node.js uses to track the dependencies and metadata of your app.
To enable ES modules (so we can use import
/export
instead of require
), open up the package.json
file and add the following line:
{ "type": "module", // ... other attributes ... }
Step 2: Install required dependencies
Next, install the necessary dependencies for building a TypeScript-based Node.js application using Express and Pug:
npm install express pug
- Express: A minimal and flexible Node.js framework for building server-side applications.
- Pug: A template engine for rendering HTML on the server side. It lets you write templates in a cleaner, less verbose syntax.
Step 3: Create the project structure
A well-organized folder structure is crucial for maintaining larger apps. Here’s what your project should look like:
book_shop/ ├── src/ | ├── controllers/ | └── homeController.js │ ├── routes/ │ │ └── index.js │ ├── views/ | └── layout.pug │ │ └── index.pug │ └── app.js ├── package.json
- src/controllers/: Contains the logic for handling requests and returning responses.
- src/routes/: Holds route definitions and mappings to specific controller functions.
- src/views/: Houses Pug templates for rendering HTML.
- app.js: Sets up the Express app and connects all parts together.
Step 4: Create a controller
Controllers handle the core logic of your application. Create src/controllers/homeController.js
and add:
export const getHomePage = (req, res) => { res.render('index', { title: "Homepage", message: "Welcome to the Book shop!", }); };
Here, getHomePage
is a simple function that renders the index.pug
template and passes two values: title
and message
. This data will be displayed dynamically when the view is rendered.
Step 5: Create a simple route
Now, let’s map a route to the controller function. Create src/routes/index.js
and paste:
import { Router } from 'express'; import { getHomePage } from '../controllers/homeController.js'; const router = Router(); router.get('/', getHomePage); export default router;
This snippet sets up a new router for the homepage (/
) and binds it to getHomePage
. The Router
is like a mini-express app that only handles specific routes, keeping your main app file clean and organized.
Step 6: Create a basic Express app
Inside src
, create a file named app.js
:
import express from 'express'; import path from 'path'; import indexRouter from './routes/index.js'; const app = express(); const PORT = 3000; // Set up views and view engine app.set('views', path.join(process.cwd(), 'src/views')); app.set('view engine', 'pug'); // Define routes app.use('/', indexRouter); // Start the server app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });
Here’s what’s happening:
- Setting up the view engine:
app.set('view engine', 'pug')
tells Express to use Pug for rendering views. - Connecting routes:
app.use('/', indexRouter)
wires up the routes defined insrc/routes/index.js
. - Starting the server:
app.listen()
starts the server and logs the URL where it’s accessible.
Step 7: Create a basic Pug template
Let’s build the UI. Create src/views/layout.pug
and add:
doctype html html head meta(charset='UTF-8') meta(name='viewport', content='width=device-width, initial-scale=1.0') title Book Shop body header h1 Book Shop block content footer p © 2024 Book Shop
Then, inside src/views/index.pug
, add:
extends layout block content h2= title p= message
The extends layout
tells Pug to use the layout.pug
template as a base. This helps avoid repetitive code in every page template, making the views DRY (Don’t Repeat Yourself).
Step 8: Adding start command in package.json
Open package.json
and add this script:
{ "scripts": { "start": "node app.js" } // ... other config ... }
Now, run the app with:
npm start
Your server should be up and running at http://localhost:3000
!
Installing i18next for localization in Node.js
To implement localization in Node.js app, you need a solid internationalization library.
These libraries handle a range of essential tasks, including:
- Managing language switching to adapt the app based on user preferences.
- Loading and organizing translation resources from files, turning static text into dynamic, multilingual content.
- Providing translation helpers, allowing you to efficiently implement translation logic in your app.
- Handling variable interpolation and pluralization, so your translations make sense grammatically.
- Supporting complex translations, like date formats, nested keys, and conditionals.
For this tutorial, we’ll use i18next, a powerful internationalization framework that supports both frontend and backend JavaScript applications, including Node.js. Its flexibility and ease of use make it a great choice for managing translations in a variety of projects.
Installing i18next
and middleware for express
Let’s start by installing the required packages:
npm install i18next i18next-http-middleware i18next-fs-backend
Here’s a quick breakdown of each package:
i18next
: The core internationalization library.i18next-express-middleware
: Middleware that connects i18next with Express, making it easy to localize responses based on user language preferences.i18next-fs-backend
: A file-system backend fori18next
that loads translations from JSON files stored in your project directory.
Translation management system: Lokalise
While i18next
is great for managing translations inside your codebase, it doesn’t handle the workflow challenges that come with maintaining translations for multiple languages. For a professional and scalable solution, you’ll want to use an efficient, advanced, and reliable translation management system (TMS).
Why Use a TMS?
A TMS, like Lokalise, will help you streamline and manage translations for complex apps by providing:
- Collaboration tools for translators, ensuring no one edits the same files at the same time.
- Version control to track changes and revert if needed.
- Consistency checks across all languages to ensure complete coverage and proper formatting.
- Automatic synchronization with your i18n files so your app stays up-to-date effortlessly.
Setting up i18next
To enable localization in Node.js app, we need to configure i18next
along with the necessary middleware and backend. We’ll keep things straightforward by setting everything up in a single file to avoid unnecessary complexity.
Step 1: Import the required libraries
Open src/app.js
and add the following imports at the top:
import i18next from 'i18next'; import Backend from 'i18next-fs-backend'; import middleware from 'i18next-http-middleware';
Here’s a quick overview of what these libraries do:
- i18next: The core library that provides internationalization functionalities.
- Backend: A file-system backend for i18next, used to load translation files stored in your project.
- middleware: Integrates i18next into Express, allowing you to handle language detection and routing easily.
Step 2: Configure i18next
Now, configure i18next right below the import statements by adding the following code:
i18next .use(Backend) // Connects the file system backend .use(middleware.LanguageDetector) // Enables automatic language detection .init({ backend: { loadPath: path.join(process.cwd(), 'src/locales', '{{lng}}', '{{ns}}.json'), // Path to translation files }, detection: { order: ['querystring', 'cookie'], // Priority: URL query string first, then cookies caches: ['cookie'], // Cache detected language in cookies }, fallbackLng: 'en', // Default language when no language is detected preload: ['en', 'ru'], // Preload these languages at startup });
Step 3: Understanding the configuration options
backend
configuration
The backend
configuration controls how i18next
loads translation files. Here’s a breakdown:
loadPath
: This option specifies the path where i18next will look for translation files. The syntax uses placeholders:{{lng}}
: Represents the language code (e.g.,en
for English,ru
for Russian).{{ns}}
: Stands for namespace, which is typicallytranslation
unless specified otherwise.
In our case, path.join(process.cwd(), 'src/locales', '{{lng}}', '{{ns}}.json')
constructs a path like this: /your-project-directory/src/locales/en/translation.json
.
detection
configuration
The detection
block determines how i18next identifies the active language for each request. Key options:
order
: Specifies the order in which to look for language information. Our setup checks:- Query string: A URL parameter like
?lng=ru
(useful for testing and sharing localized URLs). - Cookies: To persist language preferences across sessions.
- Query string: A URL parameter like
caches
: Defines where to store the detected language. Here, we usecookie
to remember user preferences.
fallbackLng
When no language is detected or the requested language is unavailable, the app will fall back to en
(English). This prevents issues if a user requests a language that isn’t supported yet.
preload
This option ensures that specified languages are loaded into memory during the app startup, so they’re immediately available. In our case, English (en
) and Russian (ru
) will be preloaded.
Step 3: Integrating i18next
middleware into the Express app
To connect i18next with Express, we need to use its middleware. Below the i18next configuration block, add:
app.use( middleware.handle(i18next) );
Creating and using translation files
To manage your translations, we’ll use separate JSON files for each language. These files will contain key-value pairs that map each text label to its respective translation. This approach to localization in Node.js helps keep code clean and makes it easier to add more languages without touching your core logic.
For example, instead of hardcoding text like "Welcome to the Book Shop!"
in your view templates, we’ll store it in a separate JSON file and reference it using a key like "message"
.
Step 1: Setting up the directory structure
Create a locales
folder inside your src
directory to store language-specific JSON files. Your folder structure should look like this:
src/ └── locales/ ├── en/ │ └── translation.json └── ru/ └── translation.json
- Each sub-folder (
en
,ru
) corresponds to a different language. - Inside each language folder, create a
translation.json
file. This file will store the key-value pairs for text labels in that language.
Step 2: Add English translations
Let’s start by adding the English translations. Open up the src/locales/en/translation.json
file and add the following content:
{ "title": "Homepage", "message": "Welcome to the Book Shop!" }
In this file:
"title"
: Represents the title text."message"
: Represents the welcome message."about"
: Represents an example message for a potential “About” page.
Step 3: Add translation for other languages
Now, let’s add a Russian version of these translations. Open up src/locales/ru/translation.json
and add:
{ "title": "Главная страница", "message": "Добро пожаловать в книжный магазин!" }
You can expand this file with more keys and nested structures as needed.
Step 4: Using translations in the controller
With the translation files in place, let’s update the controller to use these translations instead of hardcoded text.
Open src/controllers/homeController.js
and update the getHomePage
function like so:
export const getHomePage = (req, res) => { res.render('index', { title: req.t('title'), message: req.t('message'), }); };
req.t
is a translation function provided byi18next
. It allows us to retrieve the translated text for a given key (e.g.,'title'
and'message'
).- This approach decouples the displayed text from your logic, making it easy to switch languages dynamically without modifying the controller code.
Adding language switcher
To add a language switcher to your web page, you’ll want to make it easy for users to select their preferred language and then update the page content accordingly. We’ll implement this by creating a simple dropdown menu that lets users switch between available languages, and then we’ll adjust our Express app to handle the language change.
Step 1: Create a language switcher in your template
Let’s add a basic language switcher to your layout.pug
template. Update the src/views/layout.pug
file with the following:
doctype html html head meta(charset='UTF-8') meta(name='viewport', content='width=device-width, initial-scale=1.0') title Book Shop body header h1 Book Shop // Add language switcher dropdown menu form#lang-switcher(action='/change-language', method='get') select(name='lng', onchange='this.form.submit()') option(value='en', selected=lng === 'en') English option(value='ru', selected=lng === 'ru') Русский block content footer p © 2024 Book Shop
- Dropdown Form: We’ve added a form with a dropdown menu containing language options (
English
andРусский
). - Auto-Submit: When the user selects a language, the form automatically submits to the
/change-language
route. - Language Context (
lng
): Theselected
attribute checks the currently active language (lng
) to mark it as selected.
Step 2: Create a middleware to set the current locale
Middleware is a key part of Express, letting us add custom logic between receiving an HTTP request and sending the response. We’ll use middleware to pass the current language (lng
) to the templates.
Create a new file named src/middleware/setLanguageMiddleware.js
:
export const setLanguage = (req, res, next) => { res.locals.lng = req.language; next(); };
res.locals.lng = req.language;
makes the detected language (req.language
) available in your views as lng
. This allows the dropdown to show the active language without additional logic.
Now, integrate this middleware in your main app file. Open src/app.js
and add:
// other imports import { setLanguage } from './middleware/setLanguageMiddleware.js'; // config and create app ... app.use(setLanguage); // other code ...
We imported setLanguage
and used app.use(setLanguage);
to pass the lng
variable to all templates. This way, we can dynamically update the dropdown based on the active language.
Step 3: Create a route to handle language switching
Now, let’s create a controller to handle the language change and enhance localization in Node.js. Create a new file named src/controllers/languageController.js
:
export const changeLanguage = (req, res) => { const { lng } = req.query; // Get the new language from query parameters res.cookie('i18next', lng); // Set the new language in a cookie // Use `Referrer` header or fallback to `/` if it's not set const redirectPath = req.get('Referrer') || '/'; res.redirect(redirectPath); // Safely redirect back };
const { lng } = req.query;
: Extracts thelng
parameter from the query string (?lng=en
or?lng=ru
).res.cookie('i18next', lng);
: Sets the new language in a cookie namedi18next
.const redirectPath = req.get('Referrer') || '/';
: Determines the path to redirect the user after the language is switched. IfReferrer
is not available (e.g., user accesses/change-language
directly), it defaults to/
.
Now update your src/routes/index.js
file to include this new route:
import { Router } from 'express'; import { getHomePage } from '../controllers/homeController.js'; import { changeLanguage } from '../controllers/languageController.js'; // Import the new controller const router = Router(); router.get('/', getHomePage); router.get('/change-language', changeLanguage); // Use the extracted controller function export default router;
Your app should now support dynamic language switching!
Handling interpolation and pluralization with i18next
To create more dynamic and user-friendly content, you often need to inject variables into your translations or handle text that changes depending on quantity (e.g., "1 item"
vs. "3 items"
). i18next makes these tasks straightforward with its built-in support for interpolation and pluralization.
Let’s see how to implement these concepts using a simple example: displaying a welcome message with the user’s name and showing the number of items they have in their cart.
Step 1: Updating the translation files
First, we’ll add some new keys to our existing translation files to include placeholders and plural rules.
English (src/locales/en/translation.json
):
{ "title": "Homepage", "message": "Welcome to the Book Shop!", "welcome_user": "Hello, {{name}}!", "cart_items_zero": "You have {{count}} items in your cart.", "cart_items_one": "You have {{count}} item in your cart.", "cart_items_other": "You have {{count}} items in your cart." }
Russian (src/locales/ru/translation.json
):
{ "title": "Главная страница", "message": "Добро пожаловать в книжный магазин!", "welcome_user": "Привет, {{name}}!", "cart_items_zero": "У вас {{count}} товаров в корзине.", "cart_items_one": "У вас {{count}} товар в корзине.", "cart_items_few": "У вас {{count}} товара в корзине.", "cart_items_many": "У вас {{count}} товаров в корзине.", "cart_items_other": "У вас {{count}} товаров в корзине." }
Explanation:
"welcome_user"
: This key includes{{name}}
as a placeholder for the user’s name. Note that placeholders have to be provided in curly braces."cart_items_one"
/"cart_items_many"
: These keys handle plural forms for the item count.
Step 2: Using interpolation and pluralization in the controller
Now, let’s modify the homeController.js
to use these new keys. Open src/controllers/homeController.js
and update it like this:
export const getHomePage = (req, res) => { const userName = req.query.name || 'Guest'; // Get the user's name from query parameters, default to 'Guest' const cartItemCount = parseInt(req.query.items, 10) || 0; // Get item count from query parameters, default to 0 res.render('index', { title: req.t('title'), welcomeMessage: req.t('welcome_user', { name: userName }), // Interpolate user's name cartMessage: req.t('cart_items', { count: cartItemCount }), // Display singular/plural based on count }); };
- Username interpolation:
req.t('welcome_user', { name: userName })
: The second argument is an object with the variable to interpolate.{{name}}
in the translation file will be replaced by the value ofuserName
.
- Pluralization handling:
req.t('cart_items', { count: cartItemCount })
: i18next automatically chooses betweencart_items
andcart_items_plural
based on thecount
value. Ifcount
is 1, it usescart_items
; otherwise, it defaults tocart_items_plural
.
Step 3: Updating the view template
Next, update your src/views/index.pug
file to display these messages:
extends layout block content h2= title p= welcomeMessage p= cartMessage
This template will now display the dynamically generated welcomeMessage
and cartMessage
based on the data passed from the controller.
Step 4: Trying out localization in Node.js
You can now proceed to the home page adjusting the username and count via the GET params, for example:
http://localhost:3000/?name=John&items=5
Great job!
Conclusion
Node.js is a powerful solution for building modern web applications. Its high performance, extensive ecosystem, and vibrant community make it a solid choice for both small projects and large-scale applications. With JavaScript’s growing popularity and versatility, developers can leverage cutting-edge libraries and frameworks to build sophisticated applications using a single language across the entire stack.
When it comes to localization, Node.js shines with its ability to implement complex internationalization solutions seamlessly. The i18next library, in particular, stands out as an excellent tool for managing translations. Its rich feature set, cross-platform support, and flexibility make it a reliable choice for localizing Node.js apps.
With i18next, you can easily manage dynamic content, handle pluralization, and inject variables into your translations—all while keeping your code clean and maintainable. The library also offers powerful middleware support for Express.js, making it easy to integrate localization into your routes and controllers. Its debug output is detailed, providing insight into each action, which is a major help when troubleshooting issues during development.
Thank you for staying with me, and until next time!