We are going to setup a simple machine translation pipeline for an expo-based React Native app. I am currently playing around with expo-router, so this will be based on using expo, but the steps are exactly the same if you were using a regular, react-native init
based project.
First, we are going to add some dependencies. The actual internationalization package we are going to use is i18next
and it's react counterpart, react-i18next
. Although the new(ish) Hermes JS engine now supports Intl on both Android and iOS, unfortunately the support isn't complete. To make matters worse, the old JS engine's (JSC) Intl support differs between iOS and Android. That being said, all we need to do is load a few Intl polyfills that satisfy i18next.
yarn add i18next react-i18next @formatjs/intl-locale @formatjs/intl-pluralrules @formatjs/intl-displaynames
Now that we have those installed, we need to create a directory and a json file for each of our target languages. In the root of your project, create a locales
folder, and inside that a folder for each language you are going to translate your app into. These need to be named using standard language codes. In this tutorial we'll use English (en) and Spanish (es), so we need to make locales/en
and locales/es
.
As for the json files, they'll be named locales/en/translation.json
and locales/es/translation.json
.
Next, in index.js
we need to initialize i18next and load the polyfills. Note that I am using getLocales
from expo-localization, but this could really be anything that returns the language code for the users default language preference.
npx expo install expo-localization
import { Platform } from "react-native";
const isAndroid = Platform.OS === "android";
const isHermes = !!global.HermesInternal;
if (isAndroid || isHermes) {
require("@formatjs/intl-locale/polyfill");
require("@formatjs/intl-pluralrules/polyfill");
require("@formatjs/intl-pluralrules/locale-data/en");
require("@formatjs/intl-pluralrules/locale-data/es");
require("@formatjs/intl-displaynames/polyfill");
require("@formatjs/intl-displaynames/locale-data/en");
require("@formatjs/intl-displaynames/locale-data/es");
}
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// Could be anything that returns default preferred language
import { getLocales } from "expo-localization";
// Import all the languages you want here
import en from "./locales/en/translation.json";
import es from "./locales/es/translation.json";
i18n.use(initReactI18next).init({
// Add any imported languages here
resources: {
en: {
translation: en,
},
es: {
translation: es,
}
},
lng: getLocales()[0].languageCode,
fallbackLng: "en", // This is the default language if none of the users preffered languages are available
interpolation: {
escapeValue: false, // https://www.i18next.com/translation-function/interpolation#unescape
},
returnNull: false,
});
Now we have that setup, let's actually use them. Any time you have a string you want translated, you just wrap it in the t
function that comes from the useTranslation hook. Here's an example component that just has the text "Hello!" (or "Hola!") in it. We might as well add some buttons to switch languages, but obviously this is just for the demo and you'll need to add something similar into your app's preferences somewhere.
import {View, Text, Button} from 'react-native'
import { useTranslation } from "react-i18next";
const Hello = () => {
const { t, i18n } = useTranslation();
return (
<View>
<Text>{t("Hello!")}</Text>
<Button title="Yo hablo Español" onPress={() => i18n.changeLanguage('es') />
<Button title="I speak English" onPress={() => i18n.changeLanguage('en') />
</View>
)
}
export default Hello
Then, back in our translation.json files, we need to add an entry for "Hello!". The key for each translation is going to be the English string, so all key/value pairs in locales/en/translation.json
are just going to be the same, where as in locales/es/translation.json
the key is the English string and the value is the translation.
locales/en/translation.json
{
"Hello!": "Hello!"
}
locales/es/translation.json
{
"Hello!": "Hola!"
}
And now back in our app if you hit the buttons the language should change.
Automate it! 🚀🚀
That was nice and easy, but obviously keeping the translation files up to date by hand will be a pain, so let's automate that! We are going to use a babel plugin that scans all our code for those t("Im a string")
functions and extracts them into our english translation.json file for us.
Step one is to remove what we already added to both our translation.json files and make them empty objects ({}
). Then, add the development dependency.
yarn add -D babel-plugin-i18next-extract
and in babel.config.js
we need to configure the i18next-extract plugin.
plugins: [
[
"i18next-extract",
{
locales: ["en"],
outputPath: "locales/{{locale}}/{{ns}}.json",
keyAsDefaultValue: ["en"],
keySeparator: null,
nsSeparator: null,
},
],
// ...other plugins here
],
Now, restart the dev server and magically the english translation file should be populated. Notice we are only doing the english locale, this is on purpose, the next step will populate and translate the other languages.
Next (and final!) step is to automatically translate our translation json files. We are going to use a package named json-autotranslate
to do that. json-autotranslate
supports a number of translation providers but we are going to be using the DeepL-free provider. Head on over to their web site and get an api key to use.
yarn add -D json-autotranslate
and in package.json in the script section add add:
"translate": "json-autotranslate -i locales -m i18next -s deepl-free -c paste-api-key-here
Now, every time we add a new string to our app, the babel plugin will automatically populate the english translation file with it. Then, all we have to do is run yarn translate
and json-autotranslate will take care of populating the spanish file and translating it using one of the providers.
Some pointers
Want to add another language? All you have to do is add the folder and translation.json file and import it in index.js
. You will have to update the polyfill requires in index.js
as well.
Lets say I wanted to add German, I would mkdir -p locales/de
and create the file locales/de/translation.json
with the content of {}
. I'd import that file into index.js
and make sure to remember to add it to the resources: { .. }
object and I'd add the required polyfill requires in index.js
. Then all I'd have to do is run yarn translate
and that would be it!
Not using English as the default language?
in babel.config.js
change the locales
and keyAsDefaultValue
values to which ever language code you want as the default language. Then in index.js
change fallbackLng
to that language code as well.
Bonus
Here's a component that gets a list of all the languages you've translated your app into and allows the user to switch between them. It retrieves the proper name of the language using the Intl api.
If you are using expo:
npx expo install @react-native-picker/picker
If you are using react native
yarn add @react-native-picker/picker
npx pod-install
import { useState } from "react";
import { View, Text } from "react-native";
import { useTranslation } from "react-i18next";
import { Picker } from "@react-native-picker/picker";
export default function LanguagePicker() {
const [selectedLanguage, setSelectedLanguage] = useState("en");
const { t, i18n } = useTranslation();
return (
<View>
<Text>{t("Choose a language")}</Text>
<Picker
style={{ flex: 1 }}
selectedValue={selectedLanguage}
onValueChange={(itemValue) => {
setSelectedLanguage(itemValue);
i18n.changeLanguage(itemValue);
}}
>
{Object.keys(i18n.options.resources || [])
.sort()
.map((lang) => {
const nameGenerator = new Intl.DisplayNames(lang, {
type: "language",
});
const displayName = nameGenerator.of(lang);
return <Picker.Item key={lang} label={displayName} value={lang} />;
})}
</Picker>
</View>
);
}
Top comments (0)