Skip to main content

Internationalization of React Native apps

In this tutorial, we'll learn how to add internationalization to an existing application in React Native. Before going further, please follow the setup guide for installation and setup instructions.

Warning

With the dependencies installed and set up, before running your app, please clear your Metro bundler cache with npx react-native start --reset-cache or npx expo start -c (if you use Expo).

The React Native tutorial is similar to the one for React and we highly recommend you read that one first because it goes into greater detail on many topics. Here, we will only cover parts that are relevant for React Native.

Hint

If you're looking for a working solution, check out the sources available here and the demo app on Expo. It showcases more functionality than this guide.

Note

This tutorial assumes you use Lingui >= 4.2 and React Native >=0.71 or Expo >=48, with the Hermes JavaScript Engine.

@lingui/core depends on several APIs exposed by the Intl object. Support of the Intl object can vary across React Native and OS versions. If some Intl feature is not supported by your runtime, you can polyfill it.

See here for details about Intl support in the Hermes engine.

Polyfilling Intl APIs

React Native does not support all Intl features out of the box, and we need to polyfill Intl.Locale using @formatjs/intl-locale and Intl.PluralRules using @formatjs/intl-pluralrules. Please note that importing the Intl polyfills can significantly increase the amount of JS that needs to be required by your app. At the same time, modern i18n libraries rely on its presence.

Follow the polyfill installation instructions before proceeding further.

Example component

We're going to translate the following contrived example:

import React from "react";
import { StyleSheet, Text, View, Alert, SafeAreaView, Button } from "react-native";

export const AppRoot = () => {
const [messages, setMessages] = useState<string[]>([]);

const markAllAsRead = () => {
Alert.alert("", "Do you want to set all your messages as read?", [
{
text: "OK",
onPress: () => {
setMessages([]);
},
},
]);
};

return (
<Inbox
markAsRead={markAllAsRead}
messages={messages}
addMessage={() => {
setMessages((msgs) => msgs.concat([`message # ${msgs.length + 1}`]));
}}
/>
);
};

const Inbox = ({ messages, markAsRead }) => {
const messagesCount = messages.length;

return (
<SafeAreaView style={styles.container}>
<View style={styles.container2}>
<Text style={styles.heading}>Message Inbox</Text>

<Button onPress={markAsRead} title="Mark all messages as read" />

<Text>
{messagesCount === 1
? `There's {messagesCount} message in your inbox.`
: `There are ${messagesCount} messages in your inbox.`}
</Text>
{/* additional code for adding messages, etc.*/}
</View>
</SafeAreaView>
);
};

As you can see, it's a simple mailbox application with only one screen.

Internationalization in React (Native)

TL;DR

There are several ways to render translations: You may use the Trans component or the useLingui hook together with the t or msg macros. When you change the active locale or load new messages, all components that consume the Lingui context provided by I18nProvider will re-render, making sure the UI shows the correct translations.

Not surprisingly, this part isn't too different from the React tutorial.

First, we need to wrap our app with I18nProvider and then we can use the Trans macro to translate the screen heading:

import { I18nProvider } from "@lingui/react";
import { Trans } from "@lingui/macro";
import { i18n } from "@lingui/core";
import { Text } from "react-native";

i18n.loadAndActivate({ locale: "en", messages });

<I18nProvider i18n={i18n} defaultComponent={Text}>
<AppRoot />
</I18nProvider>

// later in the React element tree:
<Text style={styles.heading}><Trans>Message Inbox</Trans></Text>
Hint

We're importing the default i18n object from @lingui/core. Read more about the i18n object in the reference.

Translating the heading is done. Now, let's translate the title prop in the <Button title="mark messages as read" /> element. In this case, Button expects to receive a string, so we cannot use the Trans macro here!

The solution is to use the t macro together with the i18n object which we can obtain from the useLingui hook. We use the two like this to get a translated string:

const { i18n } = useLingui()
...
<Button title={t(i18n)`this will be translated and rerendered with locale changes`}/>

Under the hood, I18nProvider takes the instance of the i18n object and passes it to Trans components through React context. I18nProvider will update the context value (which then rerenders components that consume the provided context value) when locale or message catalogs are updated.

The Trans component uses the i18n instance to get the translations from it. If we cannot use Trans, we can use the useLingui hook to get hold of the i18n instance ourselves and get the translations from there.

The interplay of I18nProvider and useLingui is shown in the following simplified example:

import { I18nProvider } from "@lingui/react";
import { t, Trans } from "@lingui/macro";
import { i18n } from "@lingui/core";

<I18nProvider i18n={i18n}>
<AppRoot />
</I18nProvider>;
//...
const Inbox = ({ markAsRead }) => {
const { i18n } = useLingui();
return (
<View>
<Text style={styles.heading}>
<Trans>Message Inbox</Trans>
</Text>
<Button onPress={markAsRead} title={t(i18n)`Mark messages as read`} />
</View>
);
};

Internationalization outside of React

Until now, we have covered the Trans macro and the useLingui hook. Using them will make sure our components are always in sync with the currently active locale and message catalog.

However, you may want to show localized strings outside of React, for example when you want to show an Alert from some business logic code.

In that case you'll also need access to the i18n object, but you don't need to pass it around from some React component. By default, Lingui uses an i18n object instance that you can import as follows:

import { i18n } from "@lingui/core";

This instance is the source of truth for the active locale. For string constants that will be translated at runtime, use the msg macro as follows:

const deleteTitle = msg`Are you sure to delete this?`
...
const showDeleteConfirmation = () => {
Alert.alert(i18n._(deleteTitle))
}

Changing the active locale

The last remaining piece of the puzzle is changing the active locale. The i18n object exposes i18n.loadAndActivate() for that. Call the method and the I18nProvider will re-render the translations. It all becomes clear when you take a look at the final code.

However, we don't recommend that you change the locale like this, as it can cause conflicts in how your app ui is localized. This is further explained here.

Choosing the default locale

Lingui does not ship with functionality that would allow you to determine the best locale you should activate by default.

Instead, please refer to Expo localization or react-native-localize. Both packages will provide you with information about the locales that the user prefers. Combining that information with the locales that your app supports will give you the locale you should use by default.

Rendering and styling of translations

As described in the reference, by default, translation components render translation as text without a wrapping tag. In React Native though, all text must be wrapped in the Text component. This means we would need to use the Trans component like this:

<Text>
<Trans>Message Inbox</Trans>
</Text>

You'll surely agree the Text component looks a little redundant. That's why the I18nProvider component accepts a defaultComponent prop. Just supply the Text component as the defaultComponent prop and the previous example can be simplified to:

<Trans>Message Inbox</Trans>

Alternatively, you may override the default locally on the i18n components, using the render or component props, as documented in the reference. Use them to apply styling to the rendered string.

Nesting components

The Trans macro and Text component may be nested, for example to achieve the effect shown in the picture. This is thanks to how React Native handles nested text.

image

This can be achieved by the following code:

<Trans>
<Text style={{ fontSize: 20 }}>
<Text>Concert of </Text>
<Text style={{ color: "green" }}>Green Day</Text>
<Text style={{ fontWeight: "bold" }}> tonight!</Text>
</Text>
</Trans>

The extracted string for translation will look like this:

"<0><1>Concert of </1><2>Green Day</2><3> tonight!</3></0>"

The important point here is that the sentence isn't broken into pieces but remains together - that will allow the translator to deliver a quality result.

Further reading

This guide originally authored and contributed in full by Vojtech Novak.