Building a global user base means making your app accessible in multiple languages, which is essential for software internationalization.
In this tutorial, we’ll guide you through internationalizing your React application using react-i18next
. You’ll learn how to set up and manage translation files, ensuring effective translation management system throughout the process. You’ll learn how to initialize i18next
, translate text, handle advanced features like pluralization and date formatting, and add a language switcher. We’ll also show you how to simplify the translation process using Lokalise.
Incorporating a structured localization process will not only streamline translation management but also ensure that your application meets the specific needs of global users.
By the end, your React app will be ready to serve users around the world seamlessly. Shall we start?
You can find a working demo in the CodeSandbox. The source code can also be found on GitHub.
Why react-i18next?
i18next is an internationalization framework for JavaScript. It offers a complete method for product localization along with other standard i18n features. Additionally, i18next integrates with many frontend libraries, including React.js and Vue.js.
React-i18next is flexible enough to meet developer needs and scalable enough to handle multiple translations by separating them into different files and loading them on demand. It also boasts a rich ecosystem with numerous modules.
Prerequisites
To get started, you need to install Node.js (Node 18 is used for this tutorial) and npm on your computer. Alternatively, you can use an online IDE like CodeSandbox, which requires no additional setup. Also, make sure you have some experience with basic HTML, JavaScript, and npm commands before jumping into React i18n.
Getting started with React i18next
Create a new React native localization project with the following command:
npx create-react-app lokalise-react-i18next
The wizard might suggest installing create-react-app
— simply press Enter. It might take some time to create the new project, so grab a tea or coffee in the meantime. Next, navigate to the project folder using the command:
cd lokalise-react-i18next
We’ll need to install the following dependencies for our application:
- i18next — our main star today.
- react-i18next — the library that brings i18next goodies for React.
- i18next-browser-languagedetector — a plugin used to detect the user’s language in the browser, supporting session storage, local storage, paths, and HTML tags across multiple locales.
- i18next-http-backend — used to load our translation files.
Use this command to install all dependencies:
npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend
Great!
Translation files structure for React i18next
Before moving on, let’s create translation files for our project. Ensure you use the correct paths, or the translations won’t load properly.
First, create a locales
folder inside the public
directory of your app.
For each language your app should support, create a new folder in the locales
directory. These folders should be named after the locale codes of the corresponding languages. Since I’ll translate my app into English, Spanish, and Latvian, I’ll name my folders en
, es
, and lv
.
Finally, inside the en
, es
, and lv
folders, create translation.json
files.
Your folder structure should look like this:
public └── locales ├── en │ └── translation.json ├── es │ └── translation.json └── lv └── translation.json
This is it! Now we can proceed to the next part of the article.
Initializing i18next
Now we’ll load all the necessary modules. Create a new src/i18n.js
file with the following content:
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ debug: true, fallbackLng: 'en', }); export default i18n;
In this file, we’re importing all the necessary dependencies and setting up i18n. Note that we’re attaching all the additional libraries with the help of .use()
. Then, we enable debug mode and set the fallback language to English (en
). This way, if the requested locale cannot be found, we’ll revert to the English version.
You can provide other options as explained in the docs. For example, you can adjust the default app language with the lng
option, but be aware that this overrides automatic language detection.
Next, open the src/index.js
file and add i18n.js
to the list of imports:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import './i18n'; // <--- add this // ... other code ...
No other changes should be made to this file. Finally, open the src/App.js
file and add the following imports:
import { Suspense } from 'react'; import { useTranslation} from 'react-i18next'; // ...
We’re loading the Suspense
component as well as i18next-related modules. The Suspense
component is needed because we’re loading translation files asynchronously and thus have to wait until they are ready. Now, let’s modify the App
function:
function App() { const { t, i18n } = useTranslation(); return ( <div> <h1></h1> </div> ); }
t
is the function that we’ll use to perform translations.i18n
is the object that can be used to get the list of currently loaded languages, switch the language, and more.- The
h1
header is empty for now, but we’ll add a translation in a moment.
Lastly, use the Suspense
component:
import { Suspense } from 'react'; import { useTranslation } from 'react-i18next'; function App() { const { t, i18n } = useTranslation(); return ( <div> <h1></h1> </div> ); } export default function WrappedApp() { return ( <Suspense fallback="...loading"> <App /> </Suspense> ) }
Now we can move on to the next section and see how to perform translations with i18next and React.
Translating plain text with React-i18next
Of course, you’re eager to see how to actually translate something, so let’s discuss this topic now.
First, we have to provide translations in our JSON files. Open the public/locales/en/translation.json
file and add the following:
{ "main": { "header": "Welcome to the app!" } }
header
is the translation key that we’ll use inside the source code, whereas the “Welcome to the app!” string is a value that will be displayed to the user if they have chosen the English locale. Note that it’s possible to nest translation keys for better manageability; in the example above, we use the main
key as the parent. To learn more about organizing your translation keys, please refer to this guide.
So far so good. Next, let’s provide Latvian translations in the public/locales/lv/translation.json
file:
{ "main": { "header": "Laipni lūdzam lietotnē!" } }
And finally, Spanish:
{ "main": { "header": "¡Bienvenido/a a la app!" } }
Awesome! How do we use these translations? It’s simple really: you just have to call the t
function that we created in the previous section and pass the key name to it. Then, when the app is served to the end user, the corresponding translation value will be displayed automatically. However, if you use nesting, be sure to use the dot notation when providing key names, for example: main.header
.
Let’s go to the src/App.js
file and provide a new translation for the h1
tag:
// ... imports ... function App() { const { t, i18n } = useTranslation(); return ( <div> <h1>{t('main.header')}</h1> </div> ); } // ... other code ...
So, the t('main.header')
works its magic here and shows the translated text to the user.
Now simply boot the app by running:
npm start
Proceed to http://localhost:3000
in your browser, and make sure the text is displayed on the main page.
For demonstration purposes, let’s also temporarily switch the app language to Latvian. Add the following line in the i18n.js
file:
// ... i18n config goes here ... i18n.changeLanguage('lv'); // <--- add this export default i18n;
Reload the page and check out the changes! Before moving on to the next section, don’t forget to remove the changeLanguage()
function call from the i18n.js
file.
Adding a language switcher
Currently, we don’t provide a way to choose the desired app language, so let’s fix this issue now.
To get started, open the App.js
file and add an object with all the supported locales and their names:
// ... imports ... const locales = { en: { title: 'English' }, lv: { title: 'Latviski' }, es: { title: 'Español' }, }; // ... other code ...
Next, iterate over the object and display a button to switch locales:
// ... imports ... const locales = { en: { title: 'English' }, lv: { title: 'Latviski' }, es: { title: 'Español' }, }; function App() { const { t, i18n } = useTranslation(); const [messages, setMessages] = useState(0); return ( <div> <ul> {Object.keys(locales).map((locale) => ( <li key={locale}><button style={{ fontWeight: i18n.resolvedLanguage === locale ? 'bold' : 'normal' }} type="submit" onClick={() => i18n.changeLanguage(locale)}> {locales[locale].title} </button></li> ))} </ul> <h1>{t('main.header')}</h1> </div> ); } // ... other code ...
Reload the app and check that the buttons work. Great job!
React-i18next features
Check out how our translation management system can help you translate your React apps faster.
Pluralization and interpolation in i18next
The next important topics to discuss are pluralization in i18next and the ability to interpolate values into your translations. Suppose we want to display how many messages the user has and also present a button to increase the count.
Open the App.js
file and import the useState
component:
import { Suspense, useState } from 'react';
Next, prepare the variable and function inside the App()
:
const [messages, setMessages] = useState(0);
Finally, display the number of messages and the button to increase the count:
function App() { const { t, i18n } = useTranslation(); const [messages, setMessages] = useState(0); return ( <div> <!-- ... header and switcher ... --> <button onClick={() => setMessages(messages + 1)}>+1 message</button> <p> {t('main.new_messages', { count: messages })} </p> </div> ); }
Here we use a new translation key called new_messages
. Note that we also have to pass the actual number of messages to the t
function in order to perform pluralization (in other words, we want to say “You have 1 new message“, but “You have 5 new messages“).
The { count: messages }
part is the actual interpolation that passes our number to the translation so that we can insert it into the text. However, it’s very important to remember that in the case of pluralization, the interpolated variable must be called count
as stated in the docs.
Pluralization rules in English are quite simple because we have only two plural forms: one and other. Therefore, open the JSON file with English translations and add this new content:
{ "main": { "header": "Welcome to the app!", "new_messages_one": "You have one new message", "new_messages_other": "You have {{count}} new messages" } }
See that we’re using _one
and _other
postfixes for the new_messages
base key. Take note of the {{count}}
part: this is how you interpolate a value in your translation.
Spanish translations:
{ "main": { "header": "¡Bienvenido/a a la app!", "new_messages_one": "Tienes un mensaje nuevo", "new_messages_other": "Tienes {{count}} mensajes nuevos" } }
In Latvian, there are three plural forms — zero, one, and other — therefore, let’s reflect that fact:
{ "main": { "header": "Laipni lūdzam lietotnē!", "new_messages_zero": "Jums nav nevienas jaunas ziņas", "new_messages_one": "Jums ir viena jauna ziņa", "new_messages_other": "Jums ir {{count}} jaunas ziņas" } }
And this is it. Now your text is pluralized!
Date and time formatting in i18next
Now, let’s see how to work with date and time in i18next. For this, we’ll need to install an additional library, called Luxon, which makes working with datetime in JS a breeze:
npm i luxon
Add a new paragraph to the App.js
file:
function App() { const { t, i18n } = useTranslation(); const [messages, setMessages] = useState(0); return ( <div> <!-- ... --> <p> {t('main.current_date', { date: new Date() })} </p> </div> ); }
We’re going to display the current date.
Add a new English translation:
{ "main": { "header": "Welcome to the app!", "new_messages_one": "You have one new message", "new_messages_other": "You have {{count}} new messages", "current_date": "Today is {{date, DATE_LONG}}" } }
Spanish translations:
{ "main": { "header": "¡Bienvenido/a a la app!", "new_messages_one": "Tienes un mensaje nuevo", "new_messages_other": "Tienes {{count}} mensajes nuevos", "current_date": "Hoy es {{date, DATE_LONG}}" } }
And new Latvian translations:
{ "main": { "header": "Laipni lūdzam lietotnē!", "new_messages_zero": "Jums nav nevienas jaunas ziņas", "new_messages_one": "Jums ir viena jauna ziņa", "new_messages_other": "Jums ir {{count}} jaunas ziņas", "current_date": "Šodien ir {{date, DATE_LONG}}" } }
Note the use of the DATE_LONG
identifier: that’s the name of the format to use. However, this format is not available out of the box, therefore we have to define it manually. To achieve this, open the i18n.js
file and import the DateTime
from Luxon:
import { DateTime } from 'luxon';
Next, add a new formatter in the following way:
// ... other code ... i18n.services.formatter.add('DATE_LONG', (value, lng, _options) => { return DateTime.fromJSDate(value).setLocale(lng).toLocaleString(DateTime.DATE_HUGE) }); export default i18n;
We use the fromJSDate
Luxon method here and pass the initial value. Next, convert this value to the locale string while taking the currently set locale into consideration. You can also use other Luxon formatters, for example, DATE_SHORT
.
Adding context and working with gender information
Now let’s see how to work with gender information by providing context. For example, suppose we’d like to display the following text: “You have a new message from Ann. She says: “How are you?”.
However, we have a problem because messages might arrive from different people with different names and genders. Of course, we can use interpolation to provide the name and the message contents, but what about the “she says” part? That’s where the context steps in.
Let’s add a new paragraph to the App.js
file:
function App() { const { t, i18n } = useTranslation(); const [messages, setMessages] = useState(0); return ( <div> <!-- ... --> <p> {t('main.incoming_message', { from: 'Ann' })}<br/> {t('main.message_contents', { body: 'How are you doing?', context: 'female' })} </p> </div> ); }
Note the use of the context
parameter here. It allows us to provide different translations based on the passed value.
Now provide English translations:
{ "main": { "header": "Welcome to the app!", "new_messages_one": "You have one new message", "new_messages_other": "You have {{count}} new messages", "current_date": "Today is {{date, DATE_LONG}}", "incoming_message": "You have a new message from {{from}}", "message_contents": "They say: {{body}}", "message_contents_male": "He says: {{body}}", "message_contents_female": "She says: {{body}}" } }
Here we have three translations for the message_contents
key: a gender-neutral variant, as well as variants for when the sender is male or a female. Nice!
Let’s do the same for Latvian:
{ "main": { "header": "Laipni lūdzam lietotnē!", "new_messages_zero": "Jums ir {{count}} jaunas ziņas", "new_messages_one": "Jums ir viena jauna ziņa", "new_messages_other": "Jums ir {{count}} jaunas ziņas", "current_date": "Šodien ir {{date, DATE_LONG}}", "incoming_message": "Jums ir jauna ziņa no {{from}}", "message_contents": "Viņi saka: {{body}}", "message_contents_male": "Viņš saka: {{body}}", "message_contents_female": "Viņa saka: {{body}}" } }
Finally, Spanish translations:
{ "main": { "header": "¡Bienvenido/a a la app!", "new_messages_one": "Tienes un mensaje nuevo", "new_messages_other": "Tienes {{count}} mensajes nuevos", "current_date": "Hoy es {{date, DATE_LONG}}", "incoming_message": "Tienes un nuevo mensaje de {{from}}", "message_contents": "Dice: {{body}}", "message_contents_male": "Dice: {{body}}", "message_contents_female": "Dice: {{body}}" } }
As you can see, working with context is not that complex and still allows us to achieve good results.
Simplifying the translation process with Lokalise
Developing an application with internationalization is not as difficult as it may seem. Using the right tools and libraries makes the process of applying internationalization to your project more interesting and efficient. You no longer have to waste time and energy translating each text and element manually. And while we’re on the subject of the right tool, Lokalise is perfect for effortless app internationalization.
The process of integrating Lokalise with your application is quite easy. First, get a free trial to enjoy all Lokalise features for 2 weeks.
Then, install the Lokalise CLI on your PC. You’ll need to generate an API token to work with the CLI. Navigate to Lokalise, log in, click on the avatar in the bottom left corner, and go to Profile settings. Next, click API tokens and generate a new read/write token.
Create a new translation project by running:
lokalise2 project create --name ReactI18next --token YOUR_TOKEN_HERE --languages "[{\"lang_iso\":\"en\"},{\"lang_iso\":\"lv\"},{\"lang_iso\":\"es\"}]" --base-lang-iso "en"
Provide your API token, and adjust the project name and the list of all supported languages. Be sure to set the base-lang-iso
to the “main” language of your app (the language you’ll be translating from). The above command will return an object with the project details. Copy the project_id
as we will need it in a moment.
Now upload translation files to the newly created project:
lokalise2 file upload --lang-iso en --file "PATH_TO_PROJECT\public\locales\en\translation.json" --project-id PROJECT_ID --token YOUR_TOKEN lokalise2 file upload --lang-iso lv --file "PATH_TO_PROJECT\public\locales\lv\translation.json" --project-id PROJECT_ID --token YOUR_TOKEN<br>lokalise2 file upload --lang-iso es --file "PATH_TO_PROJECT\public\locales\es\translation.json" --project-id PROJECT_ID --token YOUR_TOKEN
Next, you can head on to Lokalise and work with the uploaded translations. You can also invite more contributors, hire professional translators, and perform other actions.
Once you are ready, download the edited translations to your React project by running:
lokalise2 file download --unzip-to PROJECT_PATH\public\locales --format json --token YOUR_TOKEN --project-id PROJECT_ID
Extracting translations
This part was initially written by Alex Terehov.
There are multiple ways to extract translation strings. We would not recommend using runtime extraction as it’s prone to errors and cannot be run from CI.
So, let’s choose an extractor from the following options (all under MIT license):
- https://github.com/gilbsgilbs/babel-plugin-i18next-extract — too young, coupled with Babel, which may make it harder to manage dependencies.
- https://github.com/i18next/i18next-scanner — maintained by i18next organization and started in 2015, but has had low activity since 2019.
- https://github.com/i18next/i18next-parser — started in 2014 and has active contributions in recent years. This is going to be our choice.
While the i18next parser supports multiple modes, we would use it as a CLI tool to have the least possible dependencies in tooling.
Install it by running:
npm i --save-dev i18next-parser
Create i18next-parser.config.js
inside the root repository with the following content:
module.exports = { defaultNamespace: 'translation', lexers: { js: ['JsxLexer'], // we're writing jsx inside .js files default: ['JavascriptLexer'], }, locales: ['en', 'lv'], output: 'public/locales/$LOCALE/$NAMESPACE.json', input: [ 'src/*.js', ], }
Add the extract-translations
command to the scripts
section in package.json
:
"scripts": { ... "extract-translations": "i18next -c i18next-parser.config.js --fail-on-warnings" },
Now, you can run the extractor:
npm run extract-translations
The extractor should scan your JS files, detect translation keys, and create the corresponding entries in your translation files:
> lokalise-react@0.1.0 extract-translations > i18next -c i18n-parser.config.js --fail-on-warnings i18next Parser -------------- Input: Output: public/locales/$LOCALE/$NAMESPACE.json [read] e:\js\lokalise-react\src\App.js [read] e:\js\lokalise-react\src\App.test.js [read] e:\js\lokalise-react\src\i18n.js [read] e:\js\lokalise-react\src\index.js [read] e:\js\lokalise-react\src\reportWebVitals.js [read] e:\js\lokalise-react\src\setupTests.js [write] e:\js\lokalise-react\public\locales\en\translation.json [write] e:\js\lokalise-react\public\locales\en\translation_old.json [write] e:\js\lokalise-react\public\locales\lv\translation.json [write] e:\js\lokalise-react\public\locales\lv\translation_old.json<br> [write] e:\js\lokalise-react\public\locales\es\translation.json<br> [write] e:\js\lokalise-react\public\locales\es\translation_old.json Stats: 6 files were parsed
However, there’s one thing I have to mention. When the extractor creates new entries in your translation files, the values are set to empty strings. The problem is, by default i18next will also display these translations as empty strings and, as a result, it might be hard for you to detect missing translations when browsing the app. To fix this problem, you can set the returnEmptyString
to false
in the i18n.js
file:
i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ returnEmptyString: false, // <--- add this debug: true, fallbackLng: 'en', });
Now, if a key does not have a translation value, the key’s name will be displayed instead. This way it is clear when a translation is missing.
You can check the final version of the app in our sandbox.
Conclusion
In this tutorial, we discussed internationalization of React applications. We used the react-i18next
library, which is the React.js i18next adapter for internationalization. We learned how to translate plain texts, use different date formats using the Luxon library, apply pluralization techniques, and much more related to the world of localization.
Additionally, we created a simple React application and used internationalization techniques in it. Finally, we discussed how we can integrate the Lokalise translation management system into our application to automate the translation process.
Learn more about how Lokalise can help with React app internationalization with a product demo or a free trial today!
Further reading
- React i18n: A step-by-step guide to react-intl
- React Native localization and internationalization
- I18n and l10n: List of developer tutorials
- The ultimate introductory guide to software localization
- Going global: How to successfully translate your website
- Vue 3 i18n: Building a multi-language app with locale switcher
- Localization in Node.js and Express.js with i18n examples