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
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
- Home.tsx
export const Home = () => {
return (
<main>
<h1>Multi-language app</h1>
<span>Select another language!</span>
</main>
);
};
- 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.
- 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;
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.
- 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);
};
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).
- 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}}"
}
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.
- 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;
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. π
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/
Top comments (9)
this post is just what I need! thanks!
Exactly what i need right now, thanks alot!!
Great article, thanks for sharing!
I have a question about this function:
getCurrentLang()
. You put it intosrc/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?Amazing way to descripe the steps, really helps a lot π
I just have a small enhancement to make the host dynamically fetched.
It makes the loadPath more flexible. Thanks
Thank you for the comprehensive tutorial!!!
Great, thanks. Just change 18n to i18n.use(initReactI18next)...
This is fantastic. Would be even better if you includes adding the language path to the URL
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.