React Multi Language App - I18next Tutorial
React Multi Language App - I18next Tutorial

In this post you will learn how to configure React multi language I18n website.

At some point of developing your website you might want to make it multi language. Which actually means that you have not only a single language like English but maybe you need several translations of your website for users from other countries. For example let's say that you want to translate to German. How can you do that?

Typically we use for this a package which is called I18n.

i18n website

This is the official website and I18n is the most popular framework which is used in different languages and frameworks in order to bring multi language support to you really easy. In this post we are mostly focused on the frontend with React framework as an example.

For React we don't use I18n directly but through react-18next.

It contains bindings of I18n to React so we can use it in a React way.

Configuring I18n

Let's try to use it now.

npm i react-i18next i18next

Here we installed a core package i18next and react-i18next which contains all bindings to React framework.

Now we must create a configuration file for I18n.

// src/i18n.js
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

i18n
  .use(initReactI18next)
  .init({
    debug: true,
    fallbackLng: "en",
    interpolation: {
      escapeValue: false,
    }
    resources: {
      en: {
        translation: {
          welcome: "Welcome",
        },
      },
      de: {
        translation: {
          welcome: "Willkommen",
        },
      },
    }
  });

In this file we call init where we provide some initial configuration and resources that we want to have. As you can see we defined English and German. They must have the same key inside like welcome but different value depending on the language. Now we can write any amount of keys inside in order to translate our website.

The next step is to import this file to our project.

// src/main.jsx
import "./i18n";

We just need to import it inside our main.jsx and we are good to go.

Now we can use our translation in any React components.

import { useTranslation } from "react-i18next";

const App = () => {
  const { t } = useTranslation();
  return (
    <div>
      <h1>
        {t("welcome")} Monsterlessons Academy
      </h1>
    </div>
  );
};

We imported useTranslation hook in order to access all our translations. With t('welcome') construction we can access specific translation from our file.

welcome

As you can see in browser our welcome message is visible and we get it because our default language is English.

Now it is time to build a possibility to switch language without page reload.

import { useTranslation } from "react-i18next";

const App = () => {
  const { t, i18n } = useTranslation();
  const languages = [
    { code: "en", name: "English" },
    { code: "de", name: "German" },
  ];
  return (
    <div>
      <h1>
        {t("welcome")} Monsterlessons Academy
      </h1>
      {languages.map((language) => (
        <button
          onClick={() => i18n.changeLanguage(language.code)}
          key={language.code}
        >
          {language.name}
        </button>
      ))}
    </div>
  );
};

Here we read i18n from useTranslation hook. We also rendered 2 buttons on the screen to change a language. In order to make a change we call i18n.changeLanguage and provide a language code inside.

german translation

Now we can click on other language and it changes our translations without page reload.

Scanning translations

We wrote the easiest variant how we can add I18next inside React. The next step that we want to do is to move our translations to external files. Why do we need that? Just imagine that we have lots of translations in multiple languages and we are writing them in a single file.

Even with 100 translations we could have 300+ lines just of our translations. It is much easier to support it if we split it to several files and load additionally. Which actually means it is not optimal to bundle everything in a single bundle.

Additionally to this problem we have another problem. It is really tedious to add every single translation that we want by hands in each language. Typically what a lot of developers would prefer (especially in big teams) to call a command that will parse all our code, find new translations and create these keys inside translation files. Then we don't need to add them by hands.

In order to achieve that we can use an additional package which is called i18next-scanner.

The goal of this library is to scan your code and extract keys from the code.

In order to do that we must create a configuration file for this library.

/i18next-scanner.config.cjs
module.exports = {
  input: [
    "src/**/*.{js,jsx}",
    // Use ! to filter out files or directories
    "!src/**/*.spec.{js,jsx}",
    "!i18n/**",
    "!**/node_modules/**",
  ],
  output: "./",
  options: {
    compatibilityJSON: "v3",
    debug: true,
    func: {
      list: ["i18next.t", "i18n.t", "t"],
      extensions: [".js", ".jsx"],
    },
    trans: {
      extensions: [".js", ".jsx"],
    },
    lngs: ["en", "de"],
    ns: ["translation"],
    defaultLng: "en",
    defaultNs: "translation",
    resource: {
      loadPath: "i18n/{{lng}}/{{ns}}.json",
      savePath: "i18n/{{lng}}/{{ns}}.json",
    },
  },
};

The config here is the default for this library but we updated several lines. First of all we look for js, jsx files in the input. In options.func.list we defined how functions which call translation can look like. It is either i18next.t, i18n.t or t. As we used t in our component it will work just fine. Also our lngs is 2 languages of our app en and de.

Now let's add some translation to our component so it can be parsed.

<h1>
  {t("welcome")} Monsterlessons Academy {t("news")}
</h1>

Here I added a key news to my h1 tag.

npx i18next-scanner --config ./i18next-scanner.config.cjs

Inside console we can call this package and provide a config inside. It will parse all files and extract translation keys.

scanner

As you can see it found a new key and created it in both our language files. Let's look on these files now.

// /i18n/en/translation.json
{
  "welcome": "",
  "news": ""
}

Scanner found 2 translations and put them inside an object. Now we just need to use these files inside of inline translations.

import en from "../i18n/en/translation.json";
import de from "../i18n/de/translation.json";

i18n
  .use(initReactI18next)
  .init({
    ..
    resources: {
      en: {
        translation: en,
      },
      de: {
        translation: de,
      },
    },
  })

Here we imported both json files and put them inside instead of inline object. So essentially we just need to write values in our json files after we are done with implementing a feature. Even not developers can do that without digging in the code.

news

As you can see our translations are still working but now they are coming from internal files.

Translations from API

Now we store our translations in external files, we can even load them dynamically but it is still not flexible enough. Typically, in a huge project you want to store your translations on the backend to serve via API. Then you don't need to load any additional chunks and you can load your language on demand. How can we do that?

// backend/index.js
const express = require("express");
const cors = require("cors");
const path = require("path");
const app = express();
app.use(cors());
app.use("/locales", express.static(path.join(__dirname, "locales")));
app.listen(3005);

Here I created a minimal Express server which will serve our translation files as static. This is exactly this like app.use('/locales'). Now we just need to copy our translation jsons to the backend folder.

// backend/locales/de/translation.json
{
  "welcome": "Willkommen",
  "news": "Nachrichten"
}

When we start our backend we can access translations by localhost:3005/locales/de/translation.json.

The last step of configuration is to configure this url for I18n translations.

npm i i18next-http-backend

We must install an additional dependency for this.

import HttpBackend from "i18next-http-backend";
i18n
  .use(initReactI18next)
  .use(HttpBackend)
  .init({
    ...
    backend: {
      loadPath: "http://localhost:3005/locales/{{lng}}/{{ns}}.json",
    },
  })

Here we added HttpBackend in our configuration and provided a loadPath property instead of our resources. Now they will be loaded from API.

result

As you can see our translations are loaded from the API.

And actually if you want to improve your React knowledge and prepare for the interview I highly recommend you to check my course React Interview Questions.

📚 Source code of what we've done