Building a multi-language app with React JS 🌐

Technologies to be used.

Creating the project.

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

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.

cd multi-lang-app

Then we install the dependencies.

npm install

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

code .

First steps.

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 .

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
export const Home = () => {
  return (
    <main>
      <h1>Multi-language app</h1>
      <span>Select another language!</span>
    </main>
  );
};
  1. About.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:

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:

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.

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.

First we are going to install these dependencies.

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.

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.

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 .

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.

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.

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.

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 .

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.

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

Well, now let’s switch between languages.

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.

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

import { useTranslation } from "react-i18next";
 
export const Home = () => {
  const { t } = useTranslation();
 
  return (
    <main>
      <h1>{t("title")}</h1>
      <span>{t("label")} </span>
    </main>
  );
};

About.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

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).

{
    "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.

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.

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.

{
    "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.

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.

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.

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.

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.

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. 🙌

Demo.

https://multi-lang-app-react.netlify.app/

Source code.

https://github.com/Franklin361/multi-lang-app


🔵 Don't forget to follow me also on X (Twitter): @Frankomtz030601

⚫ Don't forget to follow me also on GitHub: Franklin361