Node.js localization and internationalization

Localization in Node.js and Express.js with i18n examples

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 in src/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 for i18next 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 typically translation 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.
    • caches: Defines where to store the detected language. Here, we use cookie 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 by i18next. 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
    
    1. Dropdown Form: We’ve added a form with a dropdown menu containing language options (English and Русский).
    2. Auto-Submit: When the user selects a language, the form automatically submits to the /change-language route.
    3. Language Context (lng): The selected 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 the lng parameter from the query string (?lng=en or ?lng=ru).
    • res.cookie('i18next', lng);: Sets the new language in a cookie named i18next.
    • const redirectPath = req.get('Referrer') || '/';: Determines the path to redirect the user after the language is switched. If Referrer 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
      });
    };
    1. 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 of userName.
    2. Pluralization handling:
      • req.t('cart_items', { count: cartItemCount }): i18next automatically chooses between cart_items and cart_items_plural based on the count value. If count is 1, it uses cart_items; otherwise, it defaults to cart_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!

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.