Supporting multiple languages in a PWA — Part One — Clientside routing with React & TypeScript

Leejjon
5 min readDec 26, 2019

Building the next big thing

So you finally have the idea for that web app that you want to make. It’s going to be the next Skype, Facebook, Tinder or Instagram. You could create it in your native language first and focus on obtaining market share in your own country. Testing if it’s a success before attempting to aim at the rest of the world. But then you take the risk that somebody copies your concept and goes international before you do.

Options for supporting multiple languages

If you look at other sites, there are different approaches for supporting multiple languages.

1 — Use one international brand name (“Google” or “Dell”) and domain name (google.com or dell.com)

To serve the desired language from one domain, you can either:

  • Load the language based on the browser settings. This has the disadvantage that only the English page appears in the Google Search results as the Google crawler bot is apparently English.
  • Load the language based on a part of the url, for example https://dell.com/nl-nl or a subdomain like wikipedia does: https://nl.wikipedia.org/wiki/Hoofdpagina This might be some configuration effort on the server side.

2 — Use multiple domains, like ebay.nl points to the Dutch version of Ebay and ebay.com points to the English version.

My approach

I decided to go for multiple domains as it is very easy for me to configure multiple domains in Google App Engine. This blog post will be a tutorial to build a multilingual React app using create-react-app with TypeScript. The app will use client side routing to render the title, meta tags and text in the correct language based on the domain. My example will only support English and Dutch, but it’s easy to add more languages.

If you don’t want to go through the entire tutorial, you can find the result on Github.

Generate a project with create-react-app and TypeScript

For a full guide on how to generate a project with create-react-app, check the github page. You’ll need Node.js and the “npx” command. If you don’t have npx installed, run: npm i -g npx Then run npx create-react-app pwa-seo --template typescript to generate a project.

Run the following commands to start the server:

cd pwa-seonpm start

Adding the routing

Let’s create three pages: “Home”, “News” and “About”. I would use a structure like this:

Fill the About.tsx, Home.tsx and News.tsx with content such as:

import React from 'react';

const About: React.FC = () => {
return (
<div>
<h1 id="pageHeader">About page</h1>
<p>This is the about page</p>
</div>
);
};

export default About;

Install the react-router-dom packages:

npm install react-router-dom @types/react-router-dom

Change the App.tsx to:

import React from 'react';
import {BrowserRouter, Link, Route} from 'react-router-dom';
import Home from './views/home/Home';
import News from './views/news/News';
import About from './views/about/About';

const App: React.FC = () => {
return (
<div className="App">
<BrowserRouter>
<Link id="linkToHome" to="/">Home</Link><br/>
<Link id="linkToNews" to="/news">News</Link><br/>
<Link id="linkToAbout" to="/about">About</Link>

<Route exact path="/" component={Home}/>
<Route exact path="/news" component={News}/>
<Route exact path="/about" component={About}/>
</BrowserRouter>
</div>
);
};

export default App;

Run npm start and verify if you can browse through the three pages by clicking the links.

Testing the routing

First I want a simple test that verifies the page header. It took me a while to get going with the react testing library. I’m used to simply get elements by their id’s and then verify the element in my unit tests.

The author of the React Testing framework wants users to search elements by their label, placeholder, text contents, alt text, title, display value, role, test ID. See his full explanation in his blog post about it. Other people have tried to get a findById method in the React Testing library, but it seems like it won’t happen. Maybe I will consider using that approach in the future, but for now I’ll stick to id’s.

My first attempt looked like this:

test('Verify page content', () => {
const {container} = render(<App/>);
const pageHeaderElement = container.querySelector('#pageHeader');
if (pageHeaderElement) {
const pageHeaderElementContent = pageHeaderElement.firstChild;
if (pageHeaderElementContent) {
// You can either do:
expect(pageHeaderElementContent.textContent)
.toMatch('Home page');
} else {
fail('Should have existed.');
}
} else {
fail('Should have existed.');
}
});

TypeScript has optional chaining since 3.7.0 (and it’s also supported in react-scripts 3.3.0 and later) and when I apply that it makes my test look like:

test('Verify home page content', () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector("#pageHeader")
?.firstChild
?.textContent;
expect(pageHeaderContent)
.toMatch(’Home page’);
});

Let’s make a test that navigates to the news and about pages and verify that the page header changes to the correct header. For this we need to add the fireEvent and waitForElement functions to our existing import in the App.test.tsx file.

import { fireEvent, render, waitForElement } from '@testing-library/react';

I have grouped all tests in a describe block. Here is the block with my two new tests that navigate to the news and about pages.

describe('Test english pages', () => {
test('Verify home page content', async () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector('#pageHeader')?.firstChild?.textContent;
expect(pageHeaderContent).toMatch('Home page');
});

test('Navigate to news', async () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector('#pageHeader')?.firstChild?.textContent;
expect(pageHeaderContent).toMatch('Home page');
const linkToNewsElement: Element = (container.querySelector('#linkToNews') as Element);
fireEvent.click(linkToNewsElement);
const changedPageHeaderContent = await waitForElement(() => container.querySelector('#pageHeader')?.firstChild?.textContent);
expect(changedPageHeaderContent).toMatch('News page');
});

test('Navigate to about', async () => {
const {container} = render(<App/>);
const pageHeaderContent = container.querySelector('#pageHeader')?.firstChild?.textContent;
expect(pageHeaderContent).toMatch('Home page');

const linkToAboutElement: Element = (container.querySelector('#linkToAbout') as Element);
fireEvent.click(linkToAboutElement);
const changedPageHeaderContent = await waitForElement(() => container.querySelector('#pageHeader')?.firstChild?.textContent);
expect(changedPageHeaderContent).toMatch('About page');
});
});

Again, run npm test to verify. You’ll notice that oddly enough the third test fails.

What? All these tests run fine individually. At first I thought that this must have been a bug in React Testing Library. But it actually makes sense that our tests actually navigate to another page and thus actually modify the window.location object.

It took me a long time to figure out a way to reset this properly before each test. The best way I’ve came up with for now is:

beforeEach(() => {
delete window.location;
//@ts-ignore
window.location = new URL('https://localhost');
});

This way we reset the window.location object before every test runs.

Continue to part two

In the next part I’ll show how to add different translations with the i18n-next library.

--

--

Leejjon

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