Internationalization, or i18n, is supporting different languages in your website or app. It allows you to gain users from different parts of the world, which leads to growing your website's traffic.

In this tutorial, we'll learn how to internationalize a React website including translating content and changing the layout's direction based on the language chosen.

You can find the full code for this tutorial in this GitHub repository.

Setup Website

First, we'll set up the React website with Create React App (CRA).

Run the following command:

npx create-react-app react-i18n-tutorial

Once that is done, change the directory to the project:

cd react-i18n-tutorial

You can then start the server:

npm start

Install Dependencies

The easiest way to internationalize a React app is to use the library i18next. i18next is an internationalization framework written in Javascript that can be used with many languages and frameworks, but most importantly with React.

Run the following command to install i18next:

npm install react-i18next i18next --save

In addition, we need to install i18next-http-backend which allows us to fetch translations from a directory, and i18next-browser-languagedetector which allows us to detect the user's language:

npm i i18next-http-backend i18next-browser-languagedetector

Last, we'll install React Bootstrap for simple styling:

npm install react-bootstrap@next bootstrap@5.1.0

Create the Main Page

We'll create the main page of the website before working on the internationalization.

We first need the Navigation component. Create src/components/Navigation.js with the following content:

import { Container, Nav, Navbar, NavDropdown } from "react-bootstrap";

function Navigation () {
    
  return (
    <Navbar bg="light" expand="lg">
      <Container>
        <Navbar.Brand href="#">React i18n</Navbar.Brand>
        <Navbar.Toggle aria-controls="basic-navbar-nav" />
        <Navbar.Collapse id="basic-navbar-nav">
          <Nav className="me-auto">
            <NavDropdown title="Language" id="basic-nav-dropdown">
              <NavDropdown.Item href="#">English</NavDropdown.Item>
              <NavDropdown.Item href="#">العربية</NavDropdown.Item>
            </NavDropdown>
          </Nav>
        </Navbar.Collapse>
      </Container>
    </Navbar>
  );
}

export default Navigation;

Heading

Then, we'll create src/components/Greeting.js with the following content:

function Greeting () {

  return (
    <h1>Hello</h1>
  );
}

export default Greeting;

Text

Next, we'll create src/components/Text.js with the following content:

function Text () {

  return (
    <p>Thank you for visiting our website.</p>
  )
}

export default Text;

Finally, we need to show these components on the website. Change the content of src/App.js:

import React from 'react';
import { Container } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import Greeting from './components/Greeting';
import Loading from './components/Loading';
import Navigation from './components/Navigation';
import Text from './components/Text';

function App() {

  return (
    <>
      <Navigation />
      <Container>
      	<Greeting />
      	<Text />
      </Container>
    </>
  );
}

export default App;

Run the server now, if it isn't running already. You'll see a simple website with a navigation bar and some text.

Configuring i18next

The first step of internationalizing React with i18next is to configure and initialize it.

Create src/i18n.js with the following content:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from 'i18next-http-backend';
import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(Backend)
  .use(I18nextBrowserLanguageDetector)
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
    fallbackLng: 'en',
    debug: true,

    interpolation: {
      escapeValue: false // react already safes from xss
    }
  });

  export default i18n;

We're first importing i18n from i18next. Then, we're adding i18next-http-backend and i18next-browser-languagedetector as plugins to i18n. We're also adding initReactI18next as a plugin to ensure that i18next works with React.

Next, we're initializing i18n by passing it an object of options. There are many options you can pass to the initializer, but we're passing 3 only.

fallbackLng acts as the default language in i18n if no language is detected. Language is detected either from the user's preferred language, or a language they previously chose when using the website.

debug enables debug messages in the console. This should not be used in production.

As for escapeValue in interpolation, we're setting it to false since React already escapes all strings and is safe from Cross-Site Scripting (XSS).

Adding the Translation Files

By default, i18next-http-backend looks for translation files in public/locales/{language}/translation.json, where {language} would be the code of the language chosen. For example, en for English.

In this tutorial, we'll have 2 languages on our website, English and Arabic. So, we'll create the directory locales and inside we'll create 2 directories en and ar.

Then, create the file translation.json inside en:

{
  "greeting": "Hello",
  "text": "Thank you for visiting our website.",
  "language": "Language"
}

This will create 3 translation keys. When these keys are used, the string value that the key corresponds to will be outputted based on the chosen language. So, each language file should have the same keys but with the values translated to that language.

Next, we'll create the file translation.json inside ar:

{
  "greeting": "مرحبا",
  "text": "شكرا لزيارة موقعنا",
  "language": " اللغة"
}

Using the i18n Instance

The next step is importing the file with the settings we just created in App.js:

import i18n from './i18n';

Next, to make sure that the components are rendered once i18next and the translation files have been loaded, we need to surround our components with Suspense from React:

<Suspense fallback={<Loading />}>
	<Navigation />
	<Container>
		<Greeting />
        <Text />
    </Container>
</Suspense>

As you can see, we're passing a new component Loading as a fallback while i18next loads with the translation files. So, we need to create src/components/Loading.js with the following content:

import { Spinner } from "react-bootstrap";

function Loading () {
  return (
    <Spinner animation="border" role="status">
      <span className="visually-hidden">Loading...</span>
    </Spinner>
  )
}

export default Loading;

Now, we're able to translate strings in the App components and its sub-components.

Translating Strings with useTranslation

There are different ways you can translate strings in i18next, and one of them is using useTranslation hook. With this hook, you'll get the translation function which you can use to translate strings.

We'll start by translating the Greeting component. Add the following at the beginning of the component:

function Greeting () {
  const { t } = useTranslation();
    ...
}

Then, inside the returned JSX, instead of just placing the text "Hello", we'll replace it with the translation function t that we received from useTranslation:

return (
    <h1>{t('greeting')}</h1>
  );

Note how we're passing the translation function a key that we added in the translation.json files for each of the languages. i18next will fetch the value based on the current language.

We'll do the same thing for the Text component:

import { useTranslation } from "react-i18next";

function Text () {
  const { t } = useTranslation();

  return (
    <p>{t('text')}</p>
  )
}

export default Text;

Finally, we'll translate the text "Language" inside the Navigation component:

<NavDropdown title={t('language')} id="basic-nav-dropdown">

If you open the website now, you'll see that nothing has changed. The text is still in English.

Although technically nothing has changed, considering we are using the translation function passing it the keys instead of the actual strings and it's outputting the correct strings, that means that i18next is loading the translations and is displaying the correct language.

If we try to change the language using the dropdown in the navigation bar, nothing will happen. We need to change the language based on the language clicked.

Changing the Language of the Website

The user should be able to change the language of a website. To manage and change the current language of the website, we need to create a context that's accessible by all the parts of the app.

Creating a context eliminates the need to pass a state through different components and levels.

Create the file src/LocaleContext.js with the following content:

import React from "react";

const defaultValue = {
  locale: 'en',
  setLocale: () => {} 
}

export default React.createContext(defaultValue);

Then, create the state locale inside src/App.js:

function App() {
  const [locale, setLocale] = useState(i18n.language);

As you can see, we're passing i18n.language as an initial value. The language property represents the current language chosen.

However, as it takes time for i18n to load with the translations, the initial value will be undefined. So, we need to listen to the languageChanged event that i18n triggers when the language is first loaded and when it changes:

i18n.on('languageChanged', (lng) => setLocale(i18n.language));

Finally, we need to surround the returned JSX with the provider of the context:

<LocaleContext.Provider value={{locale, setLocale}}>
      <Suspense fallback={<Loading />}>
        <Navigation />
          <Container>
            <Greeting />
            <Text />
          </Container>
	</Suspense>
</LocaleContext.Provider>

Now, we can access the locale and its setter from any of the subcomponents.

To change the language, we need to have a listener function for the click events on the dropdown links.

In src/components/Navigation.js get the locale state from the context at the beginning of the function:

const { locale } = useContext(LocaleContext);

Then, add a listener component that will change the language in i18n:

  function changeLocale (l) {
    if (locale !== l) {
      i18n.changeLanguage(l);
    }
  }

Finally, we'll bind the listener to the click event for both of the dropdown links:

<NavDropdown.Item href="#" onClick={() => changeLocale('en')}>English</NavDropdown.Item>
              <NavDropdown.Item href="#" onClick={() => changeLocale('ar')}>العربية</NavDropdown.Item>

If you go on the website and try to change the language, you'll see that the language changes successfully based on what you choose. Also, if you try changing the language then refreshing the page, you'll see that the chosen language will persist.

Changing the Location of the Translation Files

As mentioned earlier, the default location of the translation files is in public/locales/{language}/translation.json. However, this can be changed.

To change the default location, change this line in src/i18n.js:

.use(Backend)

To the following:

.use(new Backend(null, {
    loadPath: '/translations/{{lng}}/{{ns}}.json'
  }))

Where the loadPath is relative to public. So, if you use the above path it means the translation files should be in a directory called translations.

{{lng}} refers to the language, for example, en. {{ns}} refers to the namespace, which by default is translation.

You can also provide a function as a value of loadPath which takes the language as the first parameter and the namespace as the second parameter.

Changing Document Direction

The next essential part of internationalization and localization is supporting different directions based on the languages you support.

If you have Right-to-Left (RTL) languages, you should be able to change the direction of the document when the RTL language is chosen.

If you use our website as an example, you'll see that although the text is translated when the Arabic language is chosen, the direction is still Left-to-Right (LTR).

This is not related to i18next as this is done through CSS. In this tutorial, we'll see how we can use RTL in Bootstrap 5 to support RTL languages.

The first thing we need to do is adding the dir and lang attributes to the <html> tag of the document. To do that, we need to install React Helmet:

npm i react-helmet

Then, inside Suspense in the returned JSX of the App component add the following:

<Helmet htmlAttributes={{
          lang: locale,
          dir: locale === 'en' ? 'ltr' : 'rtl'
        }} />

This will change the lang and dir attributes of <html> based on the value of the locale.

The next thing we need to do is surround the Bootstrap components with ThemeProvider which is a component from react-bootstrap:

<ThemeProvider dir={locale === 'en' ? 'ltr' : 'rtl'}>
	<Navigation />
	<Container>
		<Greeting />
		<Text />
	</Container>
</ThemeProvider>

As you can see we're passing it the dir prop with the direction based on the locale. This is necessary as react-bootstrap will load the necessary stylesheet based on whether the current direction is rtl or ltr.

Finally, we need to change the class name of Nav in the Navigation component:

<Nav className={locale === 'en' ? 'ms-auto' : 'me-auto'}>

This is only necessary since there seems to be a problem in the support for ms-auto when switching to RTL.

If you try opening the website now and changing the language to Arabic, you'll see that the direction of the document is changed as well.

Conclusion

i18next facilitates internationalizing your React app, as well as other frameworks and languages. By internationalizing your app or website, you are inviting more users from around the world to use it.

The main parts of internationalization are translating the content, supporting the direction of the chosen language in your website's stylesheets, and remembering the user's choice. Using i18next you're able to easily translate the content as well as remembering the user's choice.