As a business grows, there is a high chance that it will expand to multiple different countries. The developers must ensure the application is available in different languages to accommodate that. Fortunately, we don’t have to create a separate version of our application in each language. Instead, we can use tools that let us dynamically load and display content that aligns with the user’s language.
In this article, we learn how to support multiple languages in a React application and write End-to-End tests using the Playwright library to ensure it works as expected.
Introducing the react-i18next library
Let’s start by creating a very simple component that we will be able to translate later.
Greeting.tsx
1 2 3 |
export const Greeting = () => { return <div>Hello</div>; }; |
We can use the i18next library to display our component in multiple languages.
18 stands for the number of letters between the first i and the last n in the word internationalization and it is a common abbervation.
Configuring the i18next library
Let’s start by installing the necessary dependencies.
1 |
npm install react-i18next i18next |
Now, we need to create a file that configures the i18next library.
i18next.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; const resources = { en: { translation: { greeting: 'Hello', }, }, pl: { translation: { greeting: 'Cześć', }, }, }; i18n .use(initReactI18next) .init({ resources, lng: 'en', interpolation: { escapeValue: false, }, }); export { i18n }; |
Above, we provide translations for every language we want to support. We also set up the i18next library to use react-i18next so that we can use the translations with React Hooks.
It makes sense to move the translations into a separate file, or even fetch them from a server.
We also set escapeValue to false to indicate that we don’t want the i18next library to encode the translation values to prevent the XSS attacks. The React library already handles that.
The last step is to import our configuration file.
main.tsx
1 2 3 4 5 6 7 8 9 10 |
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './i18next'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, ); |
Translating the component
The most straightforward way of translating our component is to use React Hooks.
Greeting.tsx
1 2 3 4 5 6 7 |
import { useTranslation } from 'react-i18next'; export const Greeting = () => { const { t } = useTranslation(); return <div>{t('greeting')}</div>; }; |
When we call the t function, the i18next library chooses the appropriate translation we’ve set up in the configuration.
Switching the language manually
In our configuration, we set the default language to English through lng: 'en'. We can let the users switch the language by using the changeLanguage function.
LanguageSwitch.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { useTranslation } from 'react-i18next'; export const LanguageSwitch = () => { const { i18n } = useTranslation(); const switchToEnglish = () => { i18n.changeLanguage('en'); }; const switchToPolish = () => { i18n.changeLanguage('pl'); }; return ( <div> <button onClick={switchToEnglish}>EN</button> <button onClick={switchToPolish}>PL</button> </div> ); }; |
As soon as the user clicks on one of the buttons, the language of the whole application changes.
Detecting the language automatically
Instead of using English by default, we can use various language detection strategies. To do that, we can use the i18next-browser-languagedetector library.
1 |
npm install i18next-browser-languagedetector |
To set it up, we need to adjust our i18next configuration slightly.
i18next.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; const resources = { en: { translation: { greeting: 'Hello', }, }, pl: { translation: { greeting: 'Cześć', }, }, }; i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, supportedLngs: ['en', 'pl'], interpolation: { escapeValue: false, }, }); export { i18n }; |
Above, we import and use the LanguageDetector. We also provide an array of supported languages instead of setting a single language.
By default, the i18next-browser-languagedetector detects the language from multiple places in the following order:
- the query string
– for example, by opening http://localhost:5173/?lng=pl, we switch the language to Polish - the cookie
– if there is a cookie called i18next that holds the language, the detector will use it - the localStorage
– if there is a value with the key i18nextLng in the local storage, the detector will use it - the sessionStorage
– if there is a value with the key i18nextLng in the session storage, the detector will set it as the current language - the navigator
– the browser sends various data to the server when requesting the website. One of them is the Accept-Language request header, which contains the language set in the browser’s configuration. - the HTML tag
– we can set the language through the lang attribute of the <html> tag
If you want to change the suggested language for the websites you’re visiting using Chrome, go to chrome://settings/languages
Thanks to this order, we can override any language setting if we send the lng query parameter. What’s important is that if the user sets the language, the library stores their choice in the local storage. Thanks to that, we can override the language configuration set in the browser.
You can pass additional options to configure how the language is detected.
Using interpolation
Sometimes, we need to concatenate multiple strings, and some of them can only be known at runtime. For example, let’s display the current day of the week.
CurrentDayOfTheWeek.tsx
1 2 3 4 5 |
export const CurrentDayOfTheWeek = () => { const dayOfTheWeek = new Date().toLocaleString('en', { weekday: 'long' }); return <div>Today is {dayOfTheWeek}.</div>; }; |
Fortunately, the i18next library supports interpolation, allowing us to integrate dynamic values into our translations.
i18next.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; const resources = { en: { translation: { greeting: 'Hello', today_is: 'Today is {{dayOfTheWeek}}.', }, }, pl: { translation: { greeting: 'Cześć', today_is: 'Dziś jest {{dayOfTheWeek}}.', }, }, }; i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, supportedLngs: ['en', 'pl'], interpolation: { escapeValue: false, }, }); export { i18n }; |
Now, our today_is translation accepts a variable called dayOfTheWeek. We can provide it when calling the t function.
Similarly, the i18next library can handle singular and plural versions of translations and the context.
CurrentDayOfTheWeek.tsx
1 2 3 4 5 6 7 8 9 10 11 |
import { useTranslation } from 'react-i18next'; export const CurrentDayOfTheWeek = () => { const { t, i18n } = useTranslation(); const dayOfTheWeek = new Date().toLocaleString(i18n.language, { weekday: 'long', }); return <div>{t('today_is', { dayOfTheWeek })}</div>; }; |
Above, we used the i18n.language property to get the day of the week in the correct language from the toLocaleString function.
Writing End-to-End tests with Playwright
To test whether we display the day of the week correctly using Playwright, we must mock the current date. The most straightforward way is to inject a script into the tested website that mocks the Date constructor.
CurrentDayOfTheWeek.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { test } from '@playwright/test'; test.describe('The current day of the week', () => { test.beforeEach(async ({ page }) => { await page.addInitScript(() => { const mockedDate = new Date('2024-05-14'); window.Date = function () { return mockedDate; }; }); await page.goto('/'); }); // ... }); |
Above, we use page.goto('/') to navigate to our website. It works as expected because we configured the baseURL property using environment variables. If you want to know more, check out JavaScript testing #17. Introduction to End-to-End testing with Playwright
Now, let’s create two different test scenarios, one for each language. Fortunately, Playwright provides an easy way to set the locale per test block with the test.use function. Since this affects the navigator, this approach ensures that our website correctly detects the language set in the browser.
CurrentDayOfTheWeek.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { expect, test } from '@playwright/test'; test.describe('The current day of the week', () => { test.beforeEach(async ({ page }) => { await page.addInitScript(() => { const mockedDate = new Date('2024-05-14'); window.Date = function () { return mockedDate; }; }); await page.goto('/'); }); test.describe('when the locale is set to English', () => { test.use({ locale: 'en', }); test('should be displayed in English', async ({ page }) => { const text = page.getByText('Today is Tuesday.'); await expect(text).toBeVisible(); }); }); test.describe('when the locale is set to Polish', () => { test.use({ locale: 'pl', }); test('should be displayed in Polish', async ({ page }) => { const text = page.getByText('Dziś jest wtorek.'); await expect(text).toBeVisible(); }); }); }); |
Summary
In this article, we’ve learned how to internationalize and translate our React application into different languages. We also implemented automatic language detection based on various factors. Besides that, we ensured that our application worked as expected by writing End-to-End tests using Playwright. To do that, we had to learn how to mock the current date and change the language settings in the browser where the End-to-End tests run.
Thanks to this approach, our application is now prepared for international users while ensuring it is reliable through testing.
Do you have a repo with the source code and the tests by any chance?