Building a multi-language app with React JS 🌐
Tags: javascript, react, tutorial, webdev
Published: 2023-02-17 14:02:08

Nowadays, creating an app that supports multiple languages is becoming more and more indispensable to reach a wide range of users. So this time, with the help of React we are going to build it.

&nbsp;

## Table of contents.

> 📌 [Technologies to be used.](#1)  
> 📌 [Creating the project.](#2)  
> 📌 [First steps.](#3)  
> 📌 [Configuring i18n.](#4)  
> 📌 [Using useTranslation.](#5)  
> 📌 [Moving translations to separate files.](#6)  
> 📌 [Conclusion.](#7)
>
> > 📌 [Demo.](#8)  
> > 📌 [Source Code.](#9)

&nbsp;

## 🈵 Technologies to be used. <a id="1"></a>

- ▶️ React JS **18.2.0**
- ▶️ i18next **22.4.9**
- ▶️ Vite JS **4.0.0**
- ▶️ TypeScript **4.9.3**
- ▶️ CSS vanilla (You can find the styles in the repository at the end of this post)

## 🈵 Creating the project. <a id="2"></a>

We will name the project: **`multi-lang-app`** (optional, you can name it whatever you like).

```bash
npm init vite@latest
```

We create the project with Vite JS and select React with TypeScript.  
Then we run the following command to navigate to the directory just created.

```bash
cd multi-lang-app
```

Then we install the dependencies.

```bash
npm install
```

Then we open the project in a code editor (in my case VS code).

```bash
code .
```

## 🈵 First steps. <a id="3"></a>

First we are going to install a library to be able to create routes in our app. In this case we will use **react-router-dom**.

```bash
npm install react-router-dom
```

Create a folder **src/pages** and inside create 2 files that will be our pages and will be very simple

1. **Home.tsx**

```tsx
export const Home = () => {
  return (
    <main>
      <h1>Multi-language app</h1>
      <span>Select another language!</span>
    </main>
  );
};
```

2. **About.tsx**

```tsx
export const About = () => {
  return (
    <main>
      <h1>About</h1>
    </main>
  );
};
```

We will also create a simple Menu component so that you can move between paths and change the language from any path.

But first, let's define the languages to use, in a separate file. In my case I will create them in a folder **src/constants** we create a file **index.ts** and add:

```tsx
export const LANGUAGES = [
  { label: "Spanish", code: "es" },
  { label: "English", code: "en" },
  { label: "Italian", code: "it" },
];
```

Now we create a folder **src/components** and inside the file **Menu.tsx** and add the following:

```tsx
import { NavLink } from "react-router-dom";
import { LANGUAGES } from "../constants";

const isActive = ({ isActive }: any) => `link ${isActive ? "active" : ""}`;

export const Menu = () => {
  return (
    <nav>
      <div>
        <NavLink className={isActive} to="/">
          Home
        </NavLink>
        <NavLink className={isActive} to="/about">
          About
        </NavLink>
      </div>

      <select defaultValue={"es"}>
        {LANGUAGES.map(({ code, label }) => (
          <option key={code} value={code}>
            {label}
          </option>
        ))}
      </select>
    </nav>
  );
};
```

Finally we will create our router in the **src/App.tsx** file, adding the pages and the Menu component.

```tsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Menu } from "./components/Menu";
import { About } from "./pages/About";
import { Home } from "./pages/Home";

const App = () => {
  return (
    <BrowserRouter>
      <Menu />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
};
export default App;
```

And that's it, we have a simple two-route application.

## 🈵 Configuring i18n. <a id="4"></a>

First we are going to install these dependencies.

```bash
npm install i18next react-i18next
```

**react-i18next** is the package that will help us to translate our pages in a React project in an easier way, but for that you need another package which is **i18next** to make the internationalization configuration.

So basically, i18next is the ecosystem itself, and react-i18next is the plugin to complement it.

Now let's create a new file named **i18n.ts** we will create it inside the **src** folder (src/i18n.ts).
Inside we are going to import the i18next package and we are going to access the **use** method because we are going to load the initReactI18next plugin to use the internationalization with React easier.

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

i18n.use(initReactI18next);

export default i18n;
```

Now we will access its init method to add a configuration object.

- **lng**: Default language.
- **fallbackLng**: Language that will be loaded in case the translations the user is looking for are not available.
- **resources**: an object with the translations to be used in the application.
- **interpolation.escapeValue**: used to escape the values and avoid XSS attacks, we will set it to false, because React already does it by default.

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

i18.use(initReactI18next).init({
  lng: "en",
  fallbackLng: "en",
  interpolation: {
    escapeValue: false,
  },
  resources: {},
});

export default i18n;
```

In the resources part, it has to be created as follows:

The key of the object must be the language code, in this case **"en "** of **"English "** and then inside an object **translation** that inside will come all the translations, identified by key-value.

And it is important, keep the same name of the key of the objects, the only thing that changes is its value. **Note how in both translation objects, inside they have the same title key**.

```tsx
resources:{
    en: {
        translation: {
            title: 'Multi-language app',
        }
    },
    es: {
        translation: {
            title: 'Aplicación en varios idiomas',
        }
    },
}
```

This is what our file will look like once the translations have been added.

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

i18n
  .use(i18nBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    lng: getCurrentLang(),
    interpolation: {
      escapeValue: false,
    },
    resources: {
      en: {
        translation: {
          title: "Multi-language app",
          label: "Select another language!",
          about: "About",
          home: "Home",
        },
      },
      es: {
        translation: {
          title: "Aplicación en varios idiomas",
          label: "Selecciona otro lenguaje!",
          about: "Sobre mí",
          home: "Inicio",
        },
      },
      it: {
        translation: {
          title: "Applicazione multilingue",
          label: "Selezionare un'altra lingua ",
          about: "Su di me",
          home: "Casa",
        },
      },
    },
  });

export default i18n;
```

Finally this file will only be imported in the **src/main.tsx** file.

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

import "./i18n";

import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

## 🈵 Using useTranslation. <a id="5"></a>

Well now that we finished the i18n configuration, let's use the translations we created. So, in the **src/components/Menu.tsx** file, we are going to use the **useTranslation** hook that react-i18next gives us.

We are going to use the hook that react-i18next gives us which is the **useTranslation**.

From this hook, we retrieve the object **i18nm** and the function **t**.

```tsx
const { i18n, t } = useTranslation();
```

To use the translations is as follows:

By means of brackets we execute the function **t** that receives as parameter a string that makes reference to the key of some value that is inside the translation object that we configured previously. (Verify in your configuration of the file i18n.ts exists an object with the key home and that contains a value).

> Depending on the default language you set, it will be displayed.

```tsx
<NavLink className={isActive} to="/">
  {t("home")}
</NavLink>
```

Well, now let's switch between languages.

- First a function that executes every time the select changes.
- We access the value of the event.
- Through the object i18n we access the method changeLanguage and we pass the value by parameter.

```tsx
const onChangeLang = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const lang_code = e.target.value;
  i18n.changeLanguage(lang_code);
};
```

Now if you switch between languages you will see how the texts of your app change.

The Menu.tsx file would look like this.

```tsx
import { useTranslation } from "react-i18next";
import { NavLink } from "react-router-dom";
import { LANGUAGES } from "../constants/index";

const isActive = ({ isActive }: any) => `link ${isActive ? "active" : ""}`;

export const Menu = () => {
  const { i18n, t } = useTranslation();

  const onChangeLang = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const lang_code = e.target.value;
    i18n.changeLanguage(lang_code);
  };

  return (
    <nav>
      <div>
        <NavLink className={isActive} to="/">
          {t("home")}
        </NavLink>
        <NavLink className={isActive} to="/about">
          {t("about")}
        </NavLink>
      </div>

      <select defaultValue={i18n.language} onChange={onChangeLang}>
        {LANGUAGES.map(({ code, label }) => (
          <option key={code} value={code}>
            {label}
          </option>
        ))}
      </select>
    </nav>
  );
};
```

Now let's go to the other pages to add the translation to the texts.

**Home.tsx**

```tsx
import { useTranslation } from "react-i18next";

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

  return (
    <main>
      <h1>{t("title")}</h1>
      <span>{t("label")} </span>
    </main>
  );
};
```

**About.tsx**

```tsx
import { useTranslation } from "react-i18next";

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

  return (
    <main>
      <h1>{t("about")}</h1>
    </main>
  );
};
```

Well, now let's quickly show you how to interpolate variables.

Inside the t function, the second parameter is an object, which you can specify the variable to interpolate.

Note that I add the property **name**. Well then this property name, I have to take it very much in account

```tsx
import { useTranslation } from "react-i18next";

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

  return (
    <main>
      <h1>{t("about")}</h1>
      <span>{t("user", { name: "Bruce Wayne 🦇" })}</span>
    </main>
  );
};
```

Now let's go to a json file (but whatever I do in one, it has to be replicated in all the translations json files).

- First I add the new property **user**, since I didn't have it before.
- Then using double brackets I add the name of the property I assigned before, which was **name**.

```tsx
{
    "title": "Multi-language app",
    "label": "Select another language!",
    "about": "About me",
    "home": "Home",
    "user": "My name is: {{name}}"
}
```

And in this way we interpolate values.

## 🈵 Moving translations to separate files. <a id="6"></a>

But what happens when the translations are too many, then your i18n.ts file will get out of control. The best thing to do is to move them to separate files.

For this we will need to install another plugin.

```tsx
npm install i18next-http-backend
```

This plugin will load the resources from a server, so it will be on demand.

Now we are going to create **inside the public folder** a **i18n** folder (public/i18n).
And inside we are going to create .json files that will be named according to their translation, for example.
The file es.json will be for the Spanish translations, the file it.json will be only for the Italian translations, etc.
At the end we will have 3 files because in this app we only handle 3 languages.

Then, we move each translation object content from the i18n.ts file to its corresponding JSON file.
For example the en.json file.

```tsx
{
    "title": "Multi-language app",
    "label": "Select another language!",
    "about": "About",
    "home": "Home"
}
```

Once we have done that with the 3 files, we go to the i18n.ts and we are going to modify some things.

- First we are going to remove the resources property.
- We are going to import the i18next-http-backend package and by means of the use method, we pass it as parameter so that it executes that plugin.

```tsx
import i18n from "i18next";
import i18nBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";

i18n
  .use(i18nBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    lng: "en",
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;
```

Finally, we need to add a new property, which is **backend** that receives an object, which we will access to the _loadPath_ property.

The **loadPath** property, receives a function that contains the language and must return a string.
But a simpler way is to interpolate the **lng** variable.

This way we will have our path where the translations will be obtained, note that I am pointing to the public folder.

Now when you want to add a new language, just add the json file in the i18n folder inside public.

```tsx
import i18n from "i18next";
import i18nBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";

i18n
  .use(i18nBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    lng: "en",
    interpolation: {
      escapeValue: false,
    },
    backend: {
      loadPath: "http://localhost:5173/i18n/{{lng}}.json",
    },
  });

export default i18n;
```

But there is one more step to do, if you notice in the loadedPath property, the host is **http://localhost:5173** and when you upload it to production, the translations will not work, so we must validate if we are in development mode or not, in order to add the correct host.

```tsx
import i18n from "i18next";
import i18nBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";

const getCurrentHost =
  import.meta.env.MODE === "development"
    ? "http://localhost:5173"
    : "LINK TO PROD";

i18n
  .use(i18nBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    lng: "en",
    interpolation: {
      escapeValue: false,
    },
    backend: {
      loadPath: `${getCurrentHost}/i18n/{{lng}}.json`,
    },
  });

export default i18n;
```

One more tip is that the translations as they are in the backend could still be loaded while the page is ready, so it is advisable to manage a Suspense in the app.

```tsx
import { Suspense } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Menu } from "./components/Menu";
import { About } from "./pages/About";
import { Home } from "./pages/Home";

const App = () => {
  return (
    <Suspense fallback="loading">
      <BrowserRouter>
        <Menu />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </BrowserRouter>
    </Suspense>
  );
};
export default App;
```

The **Suspense** component pauses the app until it is ready, and in the fallback property is what is shown to the user while waiting for the application to be ready, here is a perfect place to put a loading or spinner.

> You probably won't notice a considerable improvement, since ours has very few translations. **But it is a good practice**.

## 🈵 Conclusion. <a id="7"></a>

Creating a multi-language app is now easier thanks to i18n and its plugins.

I hope you liked this post and I also hope I helped you to understand how to make this kind of applications in an easier way. 🙌

If you know any other different or better way to make this application you can comment all your observations and suggestions, I would appreciate it a lot.

> **I invite you to check my portfolio in case you are interested in contacting me for a project!. [Franklin Martinez Lucas](https://franklin-dev.netlify.app/)**

> 🔵 Don't forget to follow me also on twitter: [**@Frankomtz361**](https://twitter.com/Frankomtz361)

### 🈵 Demo. <a id="8"></a>

[https://multi-lang-app-react.netlify.app/](https://multi-lang-app-react.netlify.app/)

### 🈵 Source code. <a id="9"></a>

[https://github.com/Franklin361/multi-lang-app](https://github.com/Franklin361/multi-lang-app)