Testing i18n in React Applications

2 mins |

May 12, 2021

Usually when we are testing applications with React Testing, we write queries like screen.getByRole("list", { name: /fruits/i }). But when our applications have to support Internationalization(i18n) and/or localization(l10n), we won’t be able to use same queries as above. Because our labels would most likely be something like t('list.heading'). Of course, in this case, our identifier for the label can be t('Fruits') but this doesn’t scale well for longer and namespaced texts.

I would be using the i18next library for managing languages and jest as a test runner. However, the pattern followed here can be applied to other libraries too.

What do we want

We want to support two kinds of queries.

1screen.getByRole("list", { name: /fruits/i })
2screen.getByRole("list", { name: "list.heading" })

screen.getByRole(“list”, { name: /fruits/i })

Supporting these kinds of queries would make our writing and reading tests easier. As it would be much closer to what the user would read. But this comes with the cost of loading translations in every test, as this could be painful in Unit/Integrations tests.

screen.getByRole(“list”, { name: “list.heading” })

These kinds of queries are easy to write, as we don’t have to load the translations in every test. But it gets difficult to read later. Then why do we want to support such queries if they can become hard to read? These kinds of queries help write tests where the text is not relevant.

Since we want actual translations to work, we wouldn’t be mocking the i18next.

Let’s write some code

Let’s start with creating utils

1import i18n from "i18next"
2
3const DEFAULT_LANGUAGE = "en"
4const DEFAULT_NAMESPACE = "translations"
5
6export function initI18n(translations = {}) {
7 i18n.use(initReactI18next).init({
8 lng: DEFAULT_LANGUAGE,
9 fallbackLng: DEFAULT_LANGUAGE,
10 ns: [DEFAULT_NAMESPACE],
11 defaultNS: DEFAULT_NAMESPACE,
12 debug: false,
13 interpolation: {
14 escapeValue: false,
15 },
16 resources: { [DEFAULT_LANGUAGE]: { [DEFAULT_NAMESPACE]: translations } },
17 })
18}

So we have created a utility that would initialize i18next with a default language and translation.

Let’s initiate this before we run any test. We can add this in setupFilesAfterEnv

1import { initI18n } from "./test-utils"
2
3beforeAll(() => {
4 initI18n()
5})

I prefer initialising with no translations. This would ensure where ever translations are required we can load them on demand.

Anyways, we should be able to add some test now.

1it("should render list of fruits", () => {
2 render(<FruitList />)
3 const list = screen.getByRole("list", {
4 name: /list.heading/i,
5 })
6})

This looks okay, but we should be able to add actual text in the tests too. So let’s create a utility to do that too.

1export function addI18nResources(
2 resource = {},
3 { ns = DEFAULT_NAMESPACE, lang = DEFAULT_LANGUAGE } = {}
4) {
5 i18n.addResourceBundle(lang, ns, resource, true, true)
6}

Now we should be able to load translations.

1import { addI18nResources } from "test-utils"
2
3it("should render list of fruits", () => {
4 addI18nResources({ landing: { heading: "Fruits" } })
5 render(<FruitList />)
6 const list = screen.getByRole("list", {
7 name: /fruits/i,
8 })
9})

This one looks great, but there is a minor bug. Are you able to spot it?

Let me help you.

Snippet 1

1import { addI18nResources } from "test-utils"
2
3it("should render list of fruits without translation", () => {
4 render(<FruitList />)
5 const list = screen.getByRole("list", {
6 name: /list.heading/i,
7 })
8})
9
10it("should render list of fruits with translation", () => {
11 addI18nResources({ landing: { heading: "Fruits" } })
12 render(<FruitList />)
13 const list = screen.getByRole("list", {
14 name: /fruits/i,
15 })
16})

Snippet 2

1import { addI18nResources } from "test-utils"
2
3it("should render list of fruits with translation", () => {
4 addI18nResources({ landing: { heading: "Fruits" } })
5 render(<FruitList />)
6 const list = screen.getByRole("list", {
7 name: /fruits/i,
8 })
9})
10
11it("should render list of fruits without translation", () => {
12 render(<FruitList />)
13 const list = screen.getByRole("list", {
14 name: /list.heading/i,
15 })
16})

Our Snippet 1 would pass but Snippet 2 would fail.

Stop here if you want to think about why Snippet 1 and Snippet 2 would act differently.

.

.

.

.

.

.

.

.

.

.

Okay, so the problem is when we add mock using addI18nResources the resources(mocks) are loaded, but they are never cleaned. We can call addI18nResources in one of our setupFilesAfterEnv.

Wrapping up

Now it should something like

1import { initI18n } from "./test-utils"
2
3const INITIAL_TRANSLATION = {}
4
5beforeAll(() => {
6 initI18n(INITIAL_TRANSLATION)
7})
8
9afterEach(() => {
10 // this would remove all existing translation and load initial one.
11 addI18nResources(INITIAL_TRANSLATION)
12})

And our test-utils.js whould look like

1import i18n from "i18next"
2
3const DEFAULT_LANGUAGE = "en"
4const DEFAULT_NAMESPACE = "translations"
5
6export function initI18n(translations = {}) {
7 i18n.use(initReactI18next).init({
8 lng: DEFAULT_LANGUAGE,
9 fallbackLng: DEFAULT_LANGUAGE,
10 ns: [DEFAULT_NAMESPACE],
11 defaultNS: DEFAULT_NAMESPACE,
12 debug: false,
13 interpolation: {
14 escapeValue: false,
15 },
16 resources: { [DEFAULT_LANGUAGE]: { [DEFAULT_NAMESPACE]: translations } },
17 })
18}
19
20export function addI18nResources(
21 resource = {},
22 { ns = DEFAULT_NAMESPACE, lang = DEFAULT_LANGUAGE } = {}
23) {
24 i18n.addResourceBundle(lang, ns, resource, true, true)
25}

Note: We are mocking the translation resources without actually calling the network request. I felt this was a simple approach without setting up the actual network mocks in every test. Reach out to me if you want me to cover that in this blog too.

Now we should be able to support both kinds of queries in our tests. I hope this was helpful.


Got a Question? Bala might have the answer. Get them answered on #AskBala

Edit on Github

Subscribe Now! Letters from Bala

Subscribe to my newsletter to receive letters about some interesting patterns and views in programming, frontend, Javascript, React, testing and many more. Be the first one to know when I publish a blog.

No spam, just some good stuff! Unsubscribe at any time

Written by Balavishnu V J. Follow him on Twitter to know what he is working on. Also, his opinions, thoughts and solutions in Web Dev.