Skip to main content

A Complete Guide to i18n in React.js

· 9 min read
Grégoire Mielle

When you want to support a large variety of users, internationalization (i18n) is an essential step to make your app more accessible. Like many concerns in React.js, there's no built-in solution for i18n. Let's explore which package you should use, what's an ideal setup for i18n & how you can collaborate with designers on localizing your app.

Choosing an i18n package for React.js

Internationalization is the first step of adapting a product to multiple locales & regions.

There are many other localization aspects to consider (dates, numbers, content direction, adaptive UI), but here are the questions you should ask yourself when choosing an i18n package:

  • Do I need a library supported in other languages/frameworks used in my organization?
  • Do I need server-side rendering (SSR)?
  • Do I need high type safety with Typescript?

In the React ecosystem, there are two libraries to highlight:

  • react-i18next: Based on the i18next framework available for other UI libraries & languages
  • react-intl: Part of a set of libraries called Format.js, based on Javascript Intl built-ins & industry standards

In this guide, we’ll focus on react-i18next for the following reasons:

  • Bigger community, meaning more material is available online in case of issues
  • Based on a framework you can reuse on the backend, in a mobile app, etc.
  • A simpler setup for typesafe translations (breaks your build when using unknown keys)

Setting up react-i18next

tip

The code used for demonstration in this post is available in this GitHub repository.

Installation

In your React.js project, run the following command to install i18next & react-i18next:

# Using Yarn
yarn add i18next react-i18next

# Using NPM
npm install i18next react-i18next

Configuration

Let's say you want to translate your app in English & French, we're gonna use the following file structure:

└── src
   ├── App.tsx
   ├── i18n
   │   ├── config.ts
   │   └── locales
   │      ├── en-GB.json
   │      └── fr-FR.json
   └── index.tsx

The locales folder contains a JSON file for each language you support. It can be easily synced with a translation management tool like Recontent.app.

The src/i18n/config.ts file contains the setup for react-i18next:

src/i18n/config.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'

import enGB from './locales/en-GB.json'
import frFR from './locales/fr-FR.json'

const defaultLanguage = 'en-GB'

export const defaultNamespace = 'default'

export const resources = {
'en-GB': {
[defaultNamespace]: enGB,
},
'fr-FR': {
[defaultNamespace]: frFR,
},
}

i18n.use(initReactI18next).init({
defaultNS: defaultNamespace,
ns: [defaultNamespace],
resources,
lng: defaultLanguage,
fallbackLng: defaultLanguage,
interpolation: {
escapeValue: false,
},
})

To effectively initialize react-i18next, import that config file in your app entry point:

src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

import './i18n/config'

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)

root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

To make translations available to use in your code, populate JSON files for each language with keys. Keys can be nested in objects or have a flat structure. We'll see in the following section how to reference them in your code.

src/i18n/locales/en-GB.json
{
"dashboard": {
"title": "Welcome to your dashboard!"
}
}

Using react-i18next

Now that your app is setup to use i18n, let's see how to actually reference translations in your code, change the language or have proper autocompletion & typesafety with Typescript.

How to reference translations in your code

react-i18next exposes hooks & components in order to use your translations.

The useTranslation hook is the main one you can use to replace hardcoded text with dynamic references to your translations based on current language.

src/pages/Dashboard.tsx
import { useTranslation } from 'react-i18next'

export const Dashboard = () => {
const { t } = useTranslation()

/**
* Dot notation indicates `react-i18next` to search
* for nested translations in JSON objects
*/
return (
<div>
<h1>{t('dashboard.title')}</h1>
</div>
)
}

Plurals are also supported by using a suffix in your key:

src/pages/Dashboard.tsx
import { useTranslation } from 'react-i18next'

export const Dashboard = () => {
const { t } = useTranslation()
const project_count = 2

/**
* {
* "success_notification": "Project created, you can access it from your dashboard."
* "success_notification_plural": "Projects created, find all of them on your dashboard."
* }
*/
return (
<div>
<h1>{t('success_notification', { count: project_count })}</h1>
</div>
)
}

You can even interpolate your own components within a translation for complex use cases (eg. Status badge, icon):

import { FC } from 'react'
import { Trans, useTranslation } from 'react-i18next'

const StatusBadge = () => {
const { t } = useTranslation()

return (
<div style={{ backgroundColor: 'purple' }}>
{t('utils.status_accepted')}
</div>
)
}

export const Dashboard = () => {
const { t } = useTranslation()

/**
* {
* "utils": {
* "status_badge": "Your invoice has been <StatusBadge />"
* "status_accepted": "Accepted"
* }
* }
*/
return (
<div>
<Header />

<Trans
i18nKey="utils.status_badge"
t={t}
components={{ StatusBadge: <StatusBadge /> }}
/>
</div>
)
}

How to change & detect languages

The i18next ecosystem comes with many handy plugins for concerns like detecting browser language or loading translations from backends. If you wish to detect user languages, you can install i18next-browser-languagedetector & use it in your config.ts file:

src/i18n/config.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'

i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
defaultNS: defaultNamespace,
ns: [defaultNamespace],
resources,
lng: defaultLanguage,
fallbackLng: defaultLanguage,
interpolation: {
escapeValue: false,
},
})

To change the current language, the i18n object has a changeLanguage method that you can call with any registered locale:

import i18next from 'i18next'

export const changeLanguage = async language => {
await i18next.changeLanguage(language)
}

How to test components with i18n

As any component in your React codebase, components using i18n keys can be tested. You can either choose to initialize react-i18next in your tests & make sure translated texts are rendered correctly or rely on keys.

If you choose to rely on keys and not initialize react-i18next in your tests, make sure to mock it:

src/setupTests.ts
import '@testing-library/jest-dom'

jest.mock('react-i18next', () => ({
useTranslation: () => {
return {
t: (key: string) => key,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
}
},
}))

A test for your component using react-testing-library can look like this:

src/components/HelpButton.spec.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HelpButton } from './HelpButton'

describe('HelpButton', () => {
it('displays description after clicking', async () => {
render(<HelpButton />)

expect(screen.getByText('help_button.cta')).toBeInTheDocument()
expect(
screen.queryByText('help_button.description'),
).not.toBeInTheDocument()

await userEvent.click(screen.getByText('help_button.cta'))

expect(screen.getByText('help_button.description')).toBeInTheDocument()
})
})

If you choose to test rendered translations, make sure to use i18n.t() in your tests instead of directly referencing translations. This way, if a key used in your tests is removed, it will fail & prevent your app from being deployed with a missing key. Moreover, your tests will not fail every time your translations are updated.

Tips & Tricks

Here are some tips & tricks to improve your translation & development workflow when working on internationalization.

Split your translation files by scope

As you add features & screens to your app, your translations tend to grow exponentially. This leads to unintended behaviour like:

  • Complex or (over)nested keys
  • Sharing keys for unrelated parts of your app (like your utils/helpers folder)
  • Dead keys not used anymore increasing your bundle size
  • A centralized dependency among developers, source of git conflicts

Thanksfully, with react-i18next, you can split your translations in multiple namespaces. Namespaces & associated translations can be loaded at app intialization or later when navigating in parts of your app.

src/scopes/admin/i18n/config.ts
import i18next from 'i18next'

import enGB from './locales/en-GB.json'
import frFR from './locales/fr-FR.json'

i18next.addResourceBundle('fr-FR', 'admin', frFR)
i18next.addResourceBundle('en-GB', 'admin', enGB)

Use Typescript for autocomplete & typesafety

If you already use Typescript in your React.js codebase, you might want to extend the typesafety you have in your code to i18n keys. Indeed, translations are referenced using strings in your code. It's easy to make a typo & release a broken UI to your users.

Moreover, with proper typing, your IDE will be able to autocomplete your keys for better developer experience.

Create a i18next.d.ts in the root folder and add the following content:

i18next.d.ts
import { resources, defaultNamespace } from './src/i18n/config'

declare module 'i18next' {
interface CustomTypeOptions {
returnNull: false
defaultNS: typeof defaultNamespace
resources: typeof resources['en-GB']
}
}

The CustomTypeOptions is augmented to add types to i18next default resources. Typescript should now only allow existing keys within the src/i18n/locales/en-GB.json file.

Using Recontent to manage the content & translation process

With this setup, your codebase is more maintainable: translations are stored is identified places and updates to them can be made without touching the source code. If you want to deploy your app to new regions, supporting a new language is possible by adding a new locale.json file.

However, within the product cycle, there are still many issues:

  • Only developers can access & edit translations
  • Content-wise, it's hard to review whole parts of the app without digging in GitHub
  • Everything is done manually, from adding a new language, translating keys, etc.

With Recontent.app, you get a friendly interface for designers, developers & product managers to collaborate.

  • Only take care of the technical setup & stop merging PRs to fix typos
  • Let designers import content from Figma, the team translate it & product managers review it
  • Use revisions like GitHub branches & only deploy when ready
  • Use our CDN or AWS S3/GCP Cloud Storage integrations to store your translations