React.js and Node.js Localization: Step-by-Step Best Practices

Benjamin Iduwe
9 min readDec 1, 2024

--

Localization of Node.js and React.js

Localization refers to adapting a software application to meet users’ cultural, linguistic, and regional preferences in different geographical locations. This goes beyond mere text translation and involves customizing various software elements to suit local norms and expectations. Localization (l10n) typically works alongside internationalization (i18n), which is the process of designing the application to make it easier to localize. Internationalization focuses on creating a flexible foundation, while localization tailors the application to specific locales.

Key Aspects of Localization:

  • Translating the user interface (UI) text, help documentation, and other content into the target language(s).
  • Adjusting the formatting of dates, times, numbers, and currencies to match local conventions (e.g., DD/MM/YYYY vs. MM/DD/YYYY, commas vs. periods in numbers).
  • Adapting features or workflows to fit local requirements or restrictions (e.g., supporting specific payment methods in certain regions).
  • Supporting bidirectional text for languages like Arabic and Hebrew.
  • Ensuring UI accommodates text expansion or contraction due to language differences.

This article will focus on the first two key aspects of localization: language translation, date/time, and number formats. To help with these tasks, we’ll explore i18next, one of the most popular JavaScript libraries for internationalization.

i18next is a robust internationalization framework designed to meet all your internationalization needs. It provides tools for managing translation files, detecting languages, handling interpolations, pluralizations, and formatting, as well as offering caching mechanisms to optimize performance. This library is highly extensible, with a wide range of plugins enhancing its functionality.

For this article, we’ll focus on two languages: Spanish (es) and English (en). We’ll cover how i18next can be used to implement language translation, and how it handles formatting of dates, times, and numbers across these two languages.

Localization in Node.js

Localizing a backend is generally more straightforward compared to front-end applications, as the main focus is on API response messages and emails. One common practice is to return a code in the API responses, which can be handled by the front-end or mobile application. For example, when a user successfully registers, the conventional message might be User created successfully. However, it’s more efficient to return a code like USER_CREATED in the API response. The front end can then map this code to the appropriate message and display it to the user in the correct language.

Let’s set up i18next on a Node.js backend application. We can categorize the Node.js application into two different categories.

  1. Server-based Node.js application: These are applications deployed on a virtual machine either through Docker, Kubernetes, or other deployment services.
  2. Serverless Node.js application: These are applications deployed to a Serverless service like AWS Lambda, Azure functions, Google Cloud Functions, Vercel, etc. While serverless architectures offer scalability and ease of deployment, they may require additional considerations for managing translation files, especially when dealing with resource constraints like Lambda layer size limits.

Getting started

In this guide, we’ll walk you through setting up i18next on a Node.js backend. This tutorial is framework-agnostic, meaning it can be applied to any Node.js framework of your choice. While the setup process may vary slightly depending on the framework, the principles remain the same across the board.

When it comes to managing and structuring your translation files, there are four common approaches. The best option depends on the volume of translations you handle and the architecture of your backend. Let’s dive in and explore the setup and considerations for your project.

Installation

# npm
$ npm install i18next --save
# yarn
$ yarn add i18next

The first approach (The import method): This is recommended for small projects or projects that choose to translate their toast (success and error message) on the front end. You can store the languages in the same folder.

The translation files.

locales/en.json

{
"translation": {
"invalid_credentials": "Credentials are not valid",
"verify_your_account": "Please verify your account before logging in",
"password_reset": "An email has been sent to your inbox",
"invalid_password_reset_code": "Invalid password reset code",
"password_reset_code_expired": "Password reset link has expired",
"login_success": "Login successful"
}
}

locales/es.json

{
"translation": {
"invalid_credentials": "Las credenciales no son válidas",
"verify_your_account": "Por favor verifique su cuenta antes de iniciar sesión",
"password_reset": "Se ha enviado un correo electrónico a su bandeja de entrada.",
"invalid_password_reset_code": "Código de restablecimiento de contraseña no válido",
"password_reset_code_expired": "El enlace para restablecer la contraseña ha caducado",
"login_success": "Iniciar sesión exitosamente"
}
}

Here’s how to set up your i18n file

import i18n from 'i18next'
import en from '../locales/en.json'
import es from '../locales/es.json'

const resources = {
en,
es
}

i18n.init({
resources,
lng: 'en',
fallbackLng: 'en',
supportedLngs: Object.keys(resources)
})

export default i18n

The second approach (the file system approach): This is recommended for large projects with a lot of translations. For instance, we have two namespaces on our backend, which are the common and auth namespaces. The translation files are stored as part of your source code. Here’s how they would be stored below:

en: locales/en/common.json, locales/en/auth.json

es: locales/es/common.json, locales/es/auth.json

Here’s how to set up your i18n file

First, install the i18next-fs-backend package on your Node.js application, then set it up using the code below.

i18n.js

import i18n from "i18next"
import path from "path"
import Backend from 'i18next-fs-backend'

i18n.use(Backend).init({
debug: true,
lng: "en",
fallbackLng: "en",
ns: ["common", "auth"],
defaultNS: "common",
backend: {
loadPath: path.join(__dirname, "../locales/{{lng}}/{{ns}}.json")
}
})

export default i18n

For serverless applications, you’ll need to include the locales directory in your Lambda layer to ensure the translation files are accessible during execution. However, this approach is not recommended for applications with a large volume of translation files due to the size limitations of serverless platforms.

For example, AWS imposes a 50 MB limit per Lambda layer. While this can be extended to 250 MB by combining multiple layers, managing large translation files in a serverless environment can still be challenging. In such cases, consider alternative solutions, such as externalizing translations to a database or a dedicated translation service, to maintain scalability and efficiency.

The third approach (Cloud storage): This approach is ideal for serverless applications that manage a large volume of translation files. You can store the translation files in cloud storage, such as Amazon S3 or any other provider of your choice, and access them directly via file URLs.

To optimize costs and performance, consider using a Content Delivery Network (CDN) to serve the translation files. A CDN provides caching, reduces latency, and minimizes costs by delivering files from edge locations, rather than directly from the S3 bucket. This not only improves scalability but also enhances the user experience with faster access times.

Using S3 bucket: my-i18n-bucket

s3://my-i18n-bucket/translations/
├── en/
│ ├── common.json
│ ├── auth.json
├── es/
│ ├── common.json
│ ├── auth.json

Setup i18n.js file

import i18n from 'i18next';
import HttpApi from 'i18next-http-backend';

i18n
.use(HttpApi)
.init({
backend: {
// Load path includes namespace and language
loadPath: '<https://my-i18n-bucket.s3.amazonaws.com/translations/{{lng}>}/{{ns}}.json',
},
lng: 'en',
fallbackLng: 'en',
ns: ['common', 'auth'],
defaultNS: 'common'
});


export default i18n

You can update the file path on the load-path field above using your S3 bucket name or your CDN file URL. The loadPath field takes a string or a function, you can use the function value if you want to access files using a signed URL.

The fourth approach (TMS): A Translation Management System (TMS) is a specialized platform designed to simplify and enhance the translation and localization process. It enables organizations to efficiently manage content across multiple languages, ensuring consistency, high quality, and faster turnaround times.

One of the recommended TMS platforms for i18n is Locize. Locize seamlessly integrates with i18next and offers advanced features such as real-time translation updates. You can watch a detailed video tutorial on setting up i18n with Locize here. There are other options like Phrase, Crowdin, and Transifex.

For backends with large localization files, you can combine the cloud storage (third approach) or the TMS approaches. While the setup might be slightly more complex, it provides the significant advantage of updating translations dynamically without requiring code redeployment.

Accessing Translation

import i18n from '../i18n.js'

const run = async () => {
// Change language
await i18n.changeLanguage('es')

// Access currently set language
console.log('language', i18n.language)

// Direct access.
console.log(i18n.t('invalid_credentials'))

// Using a namespace
console.log(i18n.t('auth:invalid_credentials'))
}

run()

Localizing a React.js application

Localization is an essential step in making your React application accessible to a global audience. By adapting your app’s content to different languages and cultural preferences, you can enhance user experience and expand your reach. In this guide, we’ll explore how to localize a React application effectively, covering tools like i18next, and react-i18next, and best practices for managing translations. Whether you’re building a single-page app or a complex web platform, localization ensures your app resonates with users across diverse regions. For React, you can also serve your translation files using a CDN or a TMS. Let’s get started, first install i18n and react-i18next

npm install react-i18next i18next --save

# if you'd like to detect user language and load translation
npm install i18next-http-backend i18next-browser-languagedetector --save

The translation files are stored in public/locales/{{lng}}/{{ns}}.json

public/locales/en/home.json

{
"home": "Home",
"welcome_user": "Welcome {{user}}",
"logout": "logout"
}

public/locales/es/home.json

{
"home": "Hogar",
"welcome_user": "Bienvenido {{user}}",
"logout": "cerrar sesión"
}

i18n.js

import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import HttpBackend from 'i18next-http-backend'

i18n
.use(HttpBackend)
.use(initReactI18next)
.init({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'en',
supportedLngs: [
'en',
'es'
],
ns: ['home'],
react: {
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],
useSuspense: true
}
})

export default i18n

import i18n.js in your index.js or the equivalent on your React.js

import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import { App } from "./App"
import reportWebVitals from "./reportWebVitals"
import './i18n'

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)

reportWebVitals()

To access your translation use the useTranslation hook.

import React, { Suspense } from 'react';
import { useTranslation } from 'react-i18next';

function MyComponent() {
const { t, i18n } = useTranslation();

return <h1>{t('home')}</h1>
}

Access a specific namespace with the useTranslation hook.

const { t, i18n } = useTranslation('home');
const { t, i18n } = useTranslation(['home', 'common']);

To change the language, you can also use this hook

import React from "react"
import { useTranslation } from "react-i18next"

function MyComponent() {
const { i18n } = useTranslation()

const changeLanguage = async (lang) => {
await i18n.changeLanguage(lang)
}

return (
<div>
<p>Change language</p>
<button onClick={() => changeLanguage("en")}>EN</button>
<button onClick={() => changeLanguage("es")}>ES</button>
</div>
)
}

Localization of Numbers

To localize numbers in your React application, you can use JavaScript’s built-in Internationalization API (Intl). This powerful library allows you to format numbers based on specific language and country codes, ensuring that your application meets the cultural conventions of your users.

For instance, you can use the Intl.NumberFormat object to format numbers with ease. All you need to do is specify the language code (e.g., en for English) and the country code (e.g., US for the United States). This combination ensures that numbers are displayed correctly, including currency, percentages, and decimal separators, for the target locale.

const number = 123456.789;

console.log(
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(
number,
),
);
// Expected output: "123.456,79 €"
// The Japanese yen doesn't use a minor unit
console.log(
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(
number,
),
);
// Expected output: "¥123,457"
// Limit to three significant digits
console.log(
new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(
number,
),
);
// Expected output: "1,23,000"

Localization of Date

Localizing dates is an important part of making your application user-friendly for a global audience. Users in different regions have unique preferences for how dates are displayed, including formats, calendars, and time zones. JavaScript’s Intl.DateTimeFormat object, part of the built-in Internationalization API, provides an efficient way to handle date localization.

To format dates for a specific locale, you can use Intl.DateTimeFormat. Simply provide the language code (e.g., en for English) and optionally the country code (e.g., GB for Great Britain) to display the date in the desired format.

const date = new Date();

// Example: Format date for US English
const usDate = new Intl.DateTimeFormat('en-US').format(date);
console.log(usDate); // Output: "MM/DD/YYYY"
// Example: Format date for British English
const ukDate = new Intl.DateTimeFormat('en-GB').format(date);
console.log(ukDate); // Output: "DD/MM/YYYY"
// Example: Format date for Japanese
const jpDate = new Intl.DateTimeFormat('ja-JP').format(date);
console.log(jpDate); // Output: "YYYY/MM/DD"

Translating JSON files

For translating your translation JSON files, you can build a simple Node.js script using Google Translate or Amazon Translate SDK or use i18next translate.

--

--

No responses yet