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 Babel) for installation and configuration instructions.
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.
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.
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's JS engine may not support all Intl
features out of the box. As of 08/2024, 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 require
d by your app. At the same time, modern i18n libraries rely on its presence.
Follow the polyfill installation instructions before proceeding further. Import polyfills from /polyfill-force
to avoid slow initialization time on low-end devices.
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)
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>
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 in mobile apps, 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.
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
- Common i18n patterns in React
@lingui/react
reference documentation@lingui/cli
reference documentation- Localizing React Native apps talk from React Native EU 2022
This guide originally authored and contributed in full by Vojtech Novak.