Translate Your React app with react-i18next

Localisation is very important for every web application to bring a better user experience to all the users from around the globe and not lose your potential customers.

Today, we will look into how to integrate localisation into your React web app including how to handle language changes, formatting, as well as how to load your localisation file from CDN and etc.

First, let’s start with a simple React app with language selections and a simple text display. You can clone or download the starter project to start with, or if you are impatient, you can get the completed code here.

Setting up react-i18next

Once you have the starter project, install the react-i18next dependency:

yarn add i18next react-i18next

Next, we need to setup react-i18next in our app:

1. Create an i18n.config.js inside the src folder. (Actually, you can name anything you want).

2. Add the following codes into the file.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import en from './resources/en.json';
import zh from './resources/zh.json';

i18n
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
    resources: {
      en: {
        translation: en, // Add translation for English
      },
      zh: {
        translation: zh, // Add translation for Chinese
      },
    },
    fallbackLng: "en",
    interpolation: {
      escapeValue: false // No need to escape for react
    }
  });

3. Import the config in App.js to initialize

// Other import
import i18next from 'i18next';
import './i18n.config';

// Rest of the code

4. Create 2 new files in src/resources/en.json and src/resources/zh.json and insert the content as below.

// src/resources/en.json
{
  "welcome": "Welcome to the world of wonder"
}

// src/resources/zh.json
{
  "welcome": "欢迎来到奇幻的世界"
}

Translate using useTranslation hook

To start translating, you can use useTranslation hook provided by react-i18next. Now let’s add our first translated text inside section tag in App.js:

const App = () => {
++const { t, i18n } = useTranslation();
  // Other code
  const handleLanguageSelect = (event) => {
    setSelectedLanguage(event.target.value);
++  i18n.changeLanguage(event.target.value);
  };
  // Other code

  return (
    <div className="h-screen flex justify-center items-center">
      <div className="mx-auto bg-white p-4 rounded space-y-2">
        // Other code
        <section>
++        <p>{t('welcome')}</p>
        </section>
      </div>
    </div>
  );
};

To change the language, you can always use the changeLanguage method from i18n .

Now, you should be able to see the welcome text is translated based on the language you have selected.

react-i18next translation

Interpolation

Interpolation is a very useful and common feature that we will be used in translation, it allows you to add dynamic values to your translated text.

Let’s say we want to display what language has been selected to the users.

const App = () => {
  // Other code

  return (
    <div className="h-screen flex justify-center items-center">
      <div className="mx-auto bg-white p-4 rounded space-y-2">
        // Other code
        <section>
          <p>{t('welcome')}</p>
++        <p>{t('selectedLanguage', { language: selectedLanguage })}</p>
        </section>
      </div>
    </div>
  );
};

Then add the new translation text in src/resources/en.json and src/resources/zh.json .

// src/resources/en.json
{
  "selectedLanguage": "Your selected language is: {{language}}"
}

// src/resources/zh.json
{
  "selectedLanguage": "您选择的语言是: {{language}}"
}

The differences between the welcome text and the selectedLanguage text is that we pass the language as the second parameter for t method to replace the placeholder in the translation text.

You should see something like this now.

For more details about interpolation, please check the official i18next interpolation documentation.

Formatting

Another powerful feature that i18n provides is you can format the interpolation value. You can either use built-in formatting functions based on Intl API or build your own format function.

To have a better understanding of formatting, we will build a simple uppercase format function. Now, add a simple format method in src/i18n.config.js :

// Other codes
i18n
  .use(initReactI18next)
  .init({
    // Other codes

    interpolation: {
      escapeValue: false,

++    format: (value, format, lng) => {
++      if (format === 'uppercase') {
++        return value.toUpperCase();
++      }
++
++      return value;
      }
    }
  });

The function is really self-explanatory: if the format is uppercase, it will convert the value to uppercase, otherwise, it just returns the value.

Next, we can apply the uppercase format in our selectedLanguage text, let’s update the text in src/resources/en.json and src/resources/zh.json :

// src/resources/en.json
{
--"selectedLanguage": "Your selected language is: {{language}}"
++"selectedLanguage": "Your selected language is: {{language, uppercase}}"
}

// src/resources/zh.json
{
--"selectedLanguage": "您选择的语言是: {{language, uppercase}}",
++"selectedLanguage": "Your selected language is: {{language, uppercase}}"
}
react-i18next translation

If you refresh the page now, you could see the language text has been converted to uppercase.

For more information about formatting, you can refer back to i18next formatting document.

Plural

Another common case that we definitely need when doing the translation is pluralisation, and of course, i18next got it for you too. We will simply count how many times have we changed the language to demonstrate the pluralisation in i18next.

First, let’s add the necessary text in src/resources/en.json and src/resources/zh.json.

// src/resources/en.json 
{
++"numOfTimesSwitchingLanguage": "Your have switch language for {{count}} time",
++"numOfTimesSwitchingLanguage_zero": "Your have switch language for {{count}} time",
++"numOfTimesSwitchingLanguage_other": "Your have switch language for {{count}} times"
}

// src/resources/zh.json 
{
++"numOfTimesSwitchingLanguage": "您已更换了语言{{count}}次",
++"numOfTimesSwitchingLanguage_zero": "您已更换了语言{{count}}次",
++"numOfTimesSwitchingLanguage_plural": "您已更换了语言{{count}}次"
}

Next, let’s add the logic for counting the number of times we change the language in App.js .

const App = () => {
  const { t, i18n } = useTranslation();
  const [selectedLanguage, setSelectedLanguage] = useState('en');
++const [count, setCount] = useState(0);

  const handleLanguageSelect = (event) => {
    setSelectedLanguage(event.target.value);
    i18n.changeLanguage(event.target.value);
++  setCount(count => count + 1);
  };

  return (
    <div className="h-screen flex justify-center items-center">
      <div className="mx-auto bg-white p-4 rounded space-y-2">
        // Other codes
        <section>
          <p>{t('welcome')}</p>
          <p>{t('selectedLanguage', { language: selectedLanguage })}</p>
++        <p>{t('numOfTimesSwitchingLanguage', { count })}</p>
        </section>
      </div>
    </div>
  );
};

Let’s refresh the page and see the outcome.

Great! we can see it changes to plurals once the count is more than 1. One important note here is that we must use count in order for pluralisation to work.

You can do more complex pluralisation with i18next. Please feel free to check out the i18next plural document if you like to.

Persisting and Detecting Language

We have explored a couple of functionality of i18next, but there is one imperfection of our language changes app because it does not persist in the language selection after you refresh the page. Luckily, there is a i18next-browser-languagedetector plugin that going to help us achieve this. This plugin also helps us to detect languages from cookies, URL, browser settings.

Now, let’s install the plugin.

yarn install i18next-browser-languagedetector

Then, add the plugin in src/i18n.config.js:

// Other import
++import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(initReactI18next)
++.use(LanguageDetector)
  .init({
    // Other configs
  });

Previously, we have set en as the default language, but now we want to use i18next.language as the default language. Thus, let’s make some changes in App.js.

const App = () => {
  const { t, i18n } = useTranslation();
--const [selectedLanguage, setSelectedLanguage] = useState('en');
++const [selectedLanguage, setSelectedLanguage] = useState(i18n.language);
  const [count, setCount] = useState(0);
  // Other codes
};

Alright, we can try to switch language to zh and refresh the page now. We should be able to see the default language stays at zh.

Loading Translation Texts From Server

If you’re working with translators or to get better performance with CDN, you may want to load the translation texts from CDN instead of embedding everything in your code.

To do this, i18next-http-backend plugin is here to help us load the translation texts from the server easily.

First of all, let’s install the plugin.

yarn install i18next-http-backend

Then, we need to change some of the configurations in i18n.config.js.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
--import en from './resources/en.json';
--import zh from './resources/zh.json';
import LanguageDetector from 'i18next-browser-languagedetector';
++import HttpApi from 'i18next-http-backend';

i18n
  .use(initReactI18next)
  .use(LanguageDetector)
++.use(HttpApi)
  .init({
--  resources: {
--    en: {
--     translation: en,
--    },
--    zh: {
--     translation: zh
--    },
--  },
    fallbackLng: 'en',

    interpolation: {
      escapeValue: false,
      /**
       * Add interpolation format method to customize the formatting
       */
      format: (value, format, lng) => {
        if (format === 'uppercase') {
          return value.toUpperCase();
        }

        return value;
      },
    },

++  backend: {
++    loadPath: '/resources/{{lng}}.json',
++  },
  });

Basically, we have made 2 changes: first, we tell the i18n to use the plugin and set the loadPath to /resources/{{lng}}.json where lng will be replaced by the currently selected language code. Secondly, we also remove resources key because it is no longer being used as we are loading the translation texts remotely.

Next, we need to move the src/resources folder to public/resources folder so that the files can be loaded publicly.

Lastly, we need to wrap our application with Suspense in index.js.

import React, { Suspense } from 'react';
// Other codes

ReactDOM.render(
  <React.StrictMode>
++  <Suspense fallback="Loading...">
      <App />
++  </Suspense>
  </React.StrictMode>,
  document.getElementById('root')
);

In practice, we may want to add Suspense around a specific component instead of at the root level. However, for demo purposes, we can just wrap it in the root level.

If you refresh the app, it should work as normal.

Bonus – Namespaces

Speaking of performance, there is another thing that you can do to speed up the loading time of your long translation texts if your application grows large, and that is namespacing. Namespacing allows you to load the small chunk/group of translation texts at a time when you need without preloading everything upfront which is a waste as well as slow down the first load time.

You just need to change your i18next-http-backend loaded path to use the namespaces and add the namespace as the argument to useTranslation .

// i18n.config.js
{
  backend: {
--  loadPath: '/resources/{{lng}}.json',
++  loadPath: '/resources/{{lng}}/{{ns}}.json',
  }
}

// App.js
-- const { t, i18n } = useTranslation();
++ const { t, i18n } = useTranslation('YOUR_NAMESPACE');

To understand more about namespacing, please check out i18next namespaces documentation.

Conclusion

i18next is a robust and sustainable solution to localise your web application. It has many features and plugins to achieve what you need for localisation.

In this post, we are just exploring the tip of the iceberg, there is still much more to learn. I will suggest having a read on react-i18next and i18next documentation to learn how to use them in depth.

I hope you learned something today and please feel free to give any feedback/comment below!

Leave a Reply

%d bloggers like this: