Developers

A Complete Guide to i18n in React.js

Grégoire Mielle
#react#i18n#frontend
Feature image

When you want to support a large variety of users, internationalization (i18n) is an essential step to make your app more accessible. Like many concerns in React.js, there’s no built-in solution for i18n. Let’s explore which package you should use, what’s an ideal setup for i18n & how you can collaborate with designers on localizing your app.

Choosing an i18n package for React.js

Internationalization is the first step of adapting a product to multiple locales & regions.

There are many other localization aspects to consider (dates, numbers, content direction, adaptive UI), but here are the questions you should ask yourself when choosing an i18n package:

In the React ecosystem, there are two libraries to highlight:

In this guide, we’ll focus on react-i18next for the following reasons:

Setting up react-i18next

The code used for demonstration in this post is available in this GitHub repository.

Installation

In your React.js project, run the following command to install i18next & react-i18next:

# Using Yarn
yarn add i18next react-i18next

# Using NPM
npm install i18next react-i18next

Configuration

Let’s say you want to translate your app in English & French, we’re gonna use the following file structure:

└── src
    ├── App.tsx
    ├── i18n
    │   ├── config.ts
    │   └── locales
    │       ├── en-GB.json
    │       └── fr-FR.json
    └── index.tsx

The locales folder contains a JSON file for each language you support. It can be easily synced with a translation management tool like Recontent.app.

The src/i18n/config.ts file contains the setup for react-i18next:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

import enGB from "./locales/en-GB.json";
import frFR from "./locales/fr-FR.json";

const defaultLanguage = "en-GB";

export const defaultNamespace = "default";

export const resources = {
  "en-GB": {
    [defaultNamespace]: enGB,
  },
  "fr-FR": {
    [defaultNamespace]: frFR,
  },
};

i18n.use(initReactI18next).init({
  defaultNS: defaultNamespace,
  ns: [defaultNamespace],
  resources,
  lng: defaultLanguage,
  fallbackLng: defaultLanguage,
  interpolation: {
    escapeValue: false,
  },
});

To effectively initialize react-i18next, import that config file in your app entry point:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

import "./i18n/config";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

To make translations available to use in your code, populate JSON files for each language with keys. Keys can be nested in objects or have a flat structure. We’ll see in the following section how to reference them in your code.

{
  "dashboard": {
    "title": "Welcome to your dashboard!"
  }
}

Using react-i18next

Now that your app is setup to use i18n, let’s see how to actually reference translations in your code, change the language or have proper autocompletion & typesafety with Typescript.

How to reference translations in your code

react-i18next exposes hooks & components in order to use your translations.

The useTranslation hook is the main one you can use to replace hardcoded text with dynamic references to your translations based on current language.

import { useTranslation } from "react-i18next";

export const Dashboard = () => {
  const { t } = useTranslation();

  /**
   * Dot notation indicates `react-i18next` to search
   * for nested translations in JSON objects
   */
  return (
    <div>
      <h1>{t("dashboard.title")}</h1>
    </div>
  );
};

Plurals are also supported by using a suffix in your key:

import { useTranslation } from "react-i18next";

export const Dashboard = () => {
  const { t } = useTranslation();
  const project_count = 2;

  /**
   * {
   *   "success_notification": "Project created, you can access it from your dashboard."
   *   "success_notification_plural": "Projects created, find all of them on your dashboard."
   * }
   */
  return (
    <div>
      <h1>{t("success_notification", { count: project_count })}</h1>
    </div>
  );
};

You can even interpolate your own components within a translation for complex use cases (eg. Status badge, icon):

import { FC } from "react";
import { Trans, useTranslation } from "react-i18next";

const StatusBadge = () => {
  const { t } = useTranslation();

  return (
    <div style={{ backgroundColor: "purple" }}>
      {t("utils.status_accepted")}
    </div>
  );
};

export const Dashboard = () => {
  const { t } = useTranslation();

  /**
   * {
   *   "utils": {
   *      "status_badge": "Your invoice has been <StatusBadge />"
   *      "status_accepted": "Accepted"
   *   }
   * }
   */
  return (
    <div>
      <Header />

      <Trans
        i18nKey="utils.status_badge"
        t={t}
        components={{ StatusBadge: <StatusBadge /> }}
      />
    </div>
  );
};

How to change & detect languages

The i18next ecosystem comes with many handy plugins for concerns like detecting browser language or loading translations from backends. If you wish to detect user languages, you can install i18next-browser-languagedetector & use it in your config.ts file:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// highlight-start
import LanguageDetector from "i18next-browser-languagedetector";
// highlight-end

i18n
  .use(initReactI18next)
  // highlight-start
  .use(LanguageDetector)
  // highlight-end
  .init({
    defaultNS: defaultNamespace,
    ns: [defaultNamespace],
    resources,
    lng: defaultLanguage,
    fallbackLng: defaultLanguage,
    interpolation: {
      escapeValue: false,
    },
  });

To change the current language, the i18n object has a changeLanguage method that you can call with any registered locale:

import i18next from "i18next";

export const changeLanguage = async (language) => {
  await i18next.changeLanguage(language);
};

How to test components with i18n

As any component in your React codebase, components using i18n keys can be tested. You can either choose to initialize react-i18next in your tests & make sure translated texts are rendered correctly or rely on keys.

If you choose to rely on keys and not initialize react-i18next in your tests, make sure to mock it:

import "@testing-library/jest-dom";

jest.mock("react-i18next", () => ({
  useTranslation: () => {
    return {
      t: (key: string) => key,
      i18n: {
        changeLanguage: () => new Promise(() => {}),
      },
    };
  },
}));

A test for your component using react-testing-library can look like this:

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { HelpButton } from "./HelpButton";

describe("HelpButton", () => {
  it("displays description after clicking", async () => {
    render(<HelpButton />);

    expect(screen.getByText("help_button.cta")).toBeInTheDocument();
    expect(
      screen.queryByText("help_button.description")
    ).not.toBeInTheDocument();

    await userEvent.click(screen.getByText("help_button.cta"));

    expect(screen.getByText("help_button.description")).toBeInTheDocument();
  });
});

If you choose to test rendered translations, make sure to use i18n.t() in your tests instead of directly referencing translations. This way, if a key used in your tests is removed, it will fail & prevent your app from being deployed with a missing key. Moreover, your tests will not fail every time your translations are updated.

Tips & Tricks

Here are some tips & tricks to improve your translation & development workflow when working on internationalization.

Split your translation files by scope

As you add features & screens to your app, your translations tend to grow exponentially. This leads to unintended behaviour like:

Thanksfully, with react-i18next, you can split your translations in multiple namespaces. Namespaces & associated translations can be loaded at app intialization or later when navigating in parts of your app.

import i18next from "i18next";

import enGB from "./locales/en-GB.json";
import frFR from "./locales/fr-FR.json";

i18next.addResourceBundle("fr-FR", "admin", frFR);
i18next.addResourceBundle("en-GB", "admin", enGB);

Use Typescript for autocomplete & typesafety

If you already use Typescript in your React.js codebase, you might want to extend the typesafety you have in your code to i18n keys. Indeed, translations are referenced using strings in your code. It’s easy to make a typo & release a broken UI to your users.

Moreover, with proper typing, your IDE will be able to autocomplete your keys for better developer experience.

Create a i18next.d.ts in the root folder and add the following content:

import { resources, defaultNamespace } from "./src/i18n/config";

declare module "i18next" {
  interface CustomTypeOptions {
    returnNull: false;
    defaultNS: typeof defaultNamespace;
    resources: (typeof resources)["en-GB"];
  }
}

The CustomTypeOptions is augmented to add types to i18next default resources. Typescript should now only allow existing keys within the src/i18n/locales/en-GB.json file.

Using Recontent.app to manage the content & translation process

With this setup, your codebase is more maintainable: translations are stored is identified places and updates to them can be made without touching the source code. If you want to deploy your app to new regions, supporting a new language is possible by adding a new locale.json file.

However, within the product cycle, there are still many issues:

With Recontent.app, you get a friendly interface for designers, developers & product managers to collaborate.

← Back to Blog