Supporting multiple languages in a PWA — Part Two — Using the i18next library

You can find the first part of this post here.

Adding internationalization

There are a couple of internationalization aka i18n libraries for React. I picked react-i18next. It is a wrapper around i18next that makes it usable in React. “i18next” has it has good documentation and is the most downloaded internationalization library for React on npm. You can install it with: npm install i18next react-i18next

Create a file called i18n.ts

Mine looks like:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// the translations
// (tip move them in a JSON file and import them)
const resources = {
en: {
translation: {
"ABOUT_CONTENT": "This is the about page",
"ABOUT_HEADER": "About page",
"ABOUT_LINK": "About",
"HOME_CONTENT": "This is the home page",
"HOME_HEADER": "Home page",
"HOME_LINK": "Home",
"NEWS_CONTENT": "This is the news page",
"NEWS_HEADER": "News page",
"NEWS_LINK": "News",
"TITLE": "pwa-seo",
"DESCRIPTION": "Multilingual Progressive Web App created with React and TypeScript"
}
}
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: 'en',
keySeparator: false,
interpolation: {
escapeValue: false // react already safes from xss
}
});

In your index.tsx, add an import to make sure initialization happens before the app starts:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import './locales/i18n';

ReactDOM.render(<App />, document.getElementById('root'));

After initializing the react-i18next library, how do we use it?

In the App.tsx, add the following import:

import { useTranslation } from 'react-i18next';

In the render method, destructure “t” from the useTranslation and use it to replace the static texts in the links. Pass the keys from the i18n.ts file to the t() function to retrieve their respective translation:

const App: React.FC = () => {
const { t } = useTranslation();
return (
<div className="App">
<BrowserRouter>
<Link id="linkToHome" to="/">
{t('HOME_LINK')}
</Link><br/>
<Link id="linkToNews" to="/news">
{t('NEWS_LINK')}
</Link><br/>
<Link id="linkToAbout" to="/about">
{t('ABOUT_LINK')}
</Link>
<Route exact path="/" component={Home}/>
<Route exact path="/news" component={News}/>
<Route exact path="/about" component={About}/>
</BrowserRouter>
</div>
);
};

In the Home.tsx, About.tsx and News.tsx you can do the same.

import React from 'react';
import {useTranslation} from "react-i18next";
const Home: React.FC = () => {
const { t } = useTranslation();
return (
<div>
<h1 id="pageHeader">{t('HOME_HEADER')}</h1>
<p>{t('HOME_CONTENT')}</p>
</div>
);
};
export default Home;

But, this breaks some tests…

If you run npm test you will notice the tests broke:

It cannot resolve the texts from the i18n library because we didn’t import the i18n.ts file in our test. To fix this, simply add the i18n.ts import to the App.test.tsx:

import './locales/i18n';

This should fix the tests again.

Adding another language

In the i18n.ts file add a Dutch translation:

const resources = {
en: {
translation: {
"ABOUT_CONTENT": "This is the about page",
"ABOUT_HEADER": "About page",
"ABOUT_LINK": "About",
"HOME_CONTENT": "This is the home page",
"HOME_HEADER": "Home page",
"HOME_LINK": "Home",
"NEWS_CONTENT": "This is the news page",
"NEWS_HEADER": "News page",
"NEWS_LINK": "News",
"TITLE": "Multilingual Progressive Web App created with React and TypeScript"
}
},
nl: {
translation: {
"ABOUT_CONTENT": "Dit is de over pagina",
"ABOUT_HEADER": "Over pagina",
"ABOUT_LINK": "Over",
"HOME_LINK": "Thuis",
"HOME_HEADER": "Thuis pagina",
"HOME_CONTENT": "Dit is de thuis pagina",
"NEWS_CONTENT": "Dit is de nieuws pagina",
"NEWS_HEADER": "Nieuws pagina",
"NEWS_LINK": "Nieuws",
"TITLE": "pwa-seo",
"DESCRIPTION": "Meertalige Progressieve Web App gemaakt met React en TypeScript"
}
}
};

For this example I didn’t buy a Dutch and English domain. Let’s pretend localhost is an English .com domain and 127.0.0.1 is the .nl (Dutch) domain. If the hostname is 127.0.0.1, load Dutch. For all other hostnames just load English. Here is how I implement this logic in i18n.ts:

const locale = window.location.hostname === '127.0.0.1' ? 'nl' : 'en';i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: locale,
keySeparator: false,
interpolation: {
escapeValue: false // react already safes from xss
}
});

If you run it with npm start and visit the url http://127.0.0.1:3000 to see if you app renders correctly in Dutch. Here you can see both versions:

Testing Dutch

I figure we want to automate testing whether the Dutch labels actually keep working when we change something (for example upgrading libraries). We should create at least one test that initializes the app in Dutch that verifies some text.

So how do we tell our test to load the app in Dutch? My first idea was to use the same hack we did in our English tests, but then point it to our “Dutch domain”, 127.0.0.1:

// This DOES NOT WORK!
beforeEach
(() => {
delete window.location;
//@ts-ignore
window.location = new URL('https://127.0.0.1');
});

The problem here is that the import ‘./locales/i18n’; loads the bundle before this beforeEach happens, and then the i18n is already initialized with English.

Then I found is that the i18next library has a changeLanguage function. So import this “i8next” to the file and call the changeLanguage function in a beforeAll block:

import i18next from "i18next";describe('Test pages in Dutch', () => {
beforeAll(() => {
i18next.changeLanguage('nl');
});
test('Verify home page content', async () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector('#pageHeader')?.firstChild?.textContent;
expect(pageHeaderContent).toMatch('Thuis pagina');
});
});

Now all tests pass again!

Continue to part three

In the next part I’ll show how to add dynamic page titles and meta data. Or you can check out the source code directly on GitHub.

Java/TypeScript Developer. Interested in web/mobile/backend/database/cloud. Freelancing, only interested in job offers from employers directly. No middle men.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store