Lokalise graphic for React-intl guide

Go Global with React and i18next: A Comprehensive Tutorial for Internationalizing Your React App

Internationalization is an important step in overcoming the language barrier among people who use a particular software application. For example, the application’s target users may speak different languages and have varying conventions for numbers, dates, or strings.

In JavaScript, there are a handful of libraries for internationalization, such as i18next, Globalize, node-polyglot, and FBT. In this article, we will be using the i18next library for internationalization with React 18+, but most of the provided instructions should be valid for earlier versions as well.

Check out how our translation management system can help you translate your React apps faster.

You can find a working demo in the CodeSandbox. The source code can also be found on GitHub.

    Get a demo of Lokalise

    Get a demo

    Why is i18next better than other libraries?

    i18next is an internationalization framework that has been written for JavaScript. It provides a complete method for product localization as well as the other standard i18n features. Furthermore, i18next has integrations for many frontend libraries, including React.js and Vue.js.

    i18next is flexible enough to adapt to developer needs, and another important feature is scalability. This allows us to separate translations into multiple files and to load them on demand. It also contains a rich ecosystem that consists of a large number of modules.

    Prerequisites

    To get started, you should install Node.js (I used Node 18 for this tutorial) and npm on your computer. Alternatively, you can use an online IDE like CodeSandbox to follow along: in this case, no additional setup is required. Moreover, you need to have some experience with simple HTML, JavaScript, and basic npm commands, before jumping to React i18n.

    Getting started with React i18next

    You can create a new React project with the following command:

    npx create-react-app lokalise-react-i18next

    The wizard might suggest installing create-react-app — you can simply press Enter. It will probably take quite a bit of time to create a new project, so grab some tea or coffee in the meantime. Next, let’s navigate to the project folder using the cd lokalise-react-i18next command. We’ll need to install the following dependencies for our application:

    1. i18next — our main star today.
    2. react-i18next — the library that enables i18next goodies for React.
    3. i18next-browser-languagedetector — this is an i18next plugin used to detect user language in the browser. It also supports features such as session storage, local storage, paths, and HTML tags across multiple locales. You can learn more about it in the docs.
    4. i18next-http-backend — we’ll use this solution 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

    Before moving on, let’s also create translation files for our project. Please make sure that you use the correct paths, otherwise the translations won’t be loaded properly.

    1. First of all, create a locales folder inside the public directory of your app.
    2. For each language that your app should support, create a new folder in the locales directory. These new folders should be named after the locale codes of the corresponding languages. As I’m going to translate my app into English, Spanish, and Latvian, I’ll name my folders en, es, and lv.
    3. 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, so 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 then setting up i18n. Note that we’re attaching all the additional libraries with the help of .use(). Then, we enable the 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 instead. Of course, you can provide other options as explained in the docs. For example, you can adjust the default app language with the help of the lng option, but please be aware that in this case you’re effectively overriding automatic language detection. Next, be sure to 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 the currently loaded languages, switch the language, and so on.
    • 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 i18next and React

    Of course, you’re eager to see how to actually translate something, so let’s discuss this topic now.

    First of all, we have to provide translations in our JSON files, so 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 then 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 locale:

    // ... 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!

    i18next features

    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!

    Get started instantly with a free trial of Lokalise


    Start now

    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 it 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 will make the process of applying internationalization to your project more interesting. You will no longer have to waste your time and energy translating each text and element. And while we’re on the subject of the right tool, Lokalise is the perfect tool for effortless app internationalization.

    The process of integrating Lokalise with your application is quite easy. First of all, 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, so navigate to Lokalise, log in to the system, click on the avatar in the bottom left corner, and proceed 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 as necessary as well as the list of all the supported languages. Be sure to set the base-lang-iso to the “main” language of your app (this is the language you’ll be translating from). The above command is going to 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
    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

    To experience even more interesting features, why not join Lokalise and integrate it into your applications?

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

    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
    [write] e:\js\lokalise-react\public\locales\es\translation.json
    [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.jsfile:

    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.

    Conclusion

    So, 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 as well. Finally, we discussed how we can integrate the Lokalise translation management system into our application in order 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

    Talk to one of our localization specialists

    Book a call with one of our localization specialists and get a tailored consultation that can guide you on your localization path.

    Get a demo

    Related posts

    Learn something new every two weeks

    Get the latest in localization delivered straight to your inbox.

    Related articles
    Localization made easy. Why wait?