DEV Community

Cover image for Building a multi-language app with React JS 🌐
Franklin Martinez
Franklin Martinez

Posted on

Building a multi-language app with React JS 🌐

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.

Β 

Table of contents.

πŸ“Œ Technologies to be used.

πŸ“Œ Creating the project.

πŸ“Œ First steps.

πŸ“Œ Configuring i18n.

πŸ“Œ Using useTranslation.

πŸ“Œ Moving translations to separate files.

πŸ“Œ Conclusion.

πŸ“Œ Demo.

πŸ“Œ Source Code.

Β 

🈡 Technologies to be used.

  • ▢️ 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.

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

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

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

code .
Enter fullscreen mode Exit fullscreen mode

🈡 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
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  1. About.tsx
export const About = () => {
  return (
    <main>
      <h1>About</h1>
    </main>
  );
};
Enter fullscreen mode Exit fullscreen mode

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" },
];
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

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

export default i18n;
Enter fullscreen mode Exit fullscreen mode

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',
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

🈡 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();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
const onChangeLang = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const lang_code = e.target.value;
  i18n.changeLanguage(lang_code);
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

About.tsx

import { useTranslation } from "react-i18next";

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

  return (
    <main>
      <h1>{t("about")}</h1>
    </main>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.
{
    "title": "Multi-language app",
    "label": "Select another language!",
    "about": "About me",
    "home": "Home",
    "user": "My name is: {{name}}"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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.
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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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. πŸ™Œ

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

πŸ”΅ Don't forget to follow me also on twitter: @Frankomtz361

🈡 Demo.

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

🈡 Source code.

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

Top comments (9)

Collapse
 
flash010603 profile image
Usuario163

this post is just what I need! thanks!

Collapse
 
abdulkarimogaji profile image
Abdulkarim

Exactly what i need right now, thanks alot!!

Collapse
 
bogdanfromkyiv profile image
Bogdan Bendziukov

Great article, thanks for sharing!

I have a question about this function: getCurrentLang(). You put it into src/i18n.ts file but never implemented it. So how can I save user's choice of selected language? So user won't need to change language from default each time page reloads/switches to another?

Collapse
 
imoaazahmed profile image
Moaaz Ahmed

Amazing way to descripe the steps, really helps a lot πŸ™

I just have a small enhancement to make the host dynamically fetched.

loadPath: `${window.location.origin}/i18n/{{lng}}.json`,
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mohamedaldik profile image
Mohamed Aldik

It makes the loadPath more flexible. Thanks

Collapse
 
mshariq profile image
Muhammad Shariq

Thank you for the comprehensive tutorial!!!

Collapse
 
awicone profile image
Vadim Ryabov

Great, thanks. Just change 18n to i18n.use(initReactI18next)...

Collapse
 
basehunter profile image
Nomen Ama

This is fantastic. Would be even better if you includes adding the language path to the URL

Collapse
 
parnazpirhadi profile image
parnazphd

hi , thank you a lot for your post , i have a suggestion for changing layout
you can use document.body.dir = i18n.dir() to change the direction of layout.