DEV Community

Mike Hamilton
Mike Hamilton

Posted on

React native (+expo): Automatic machine translation

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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,
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!"
}
Enter fullscreen mode Exit fullscreen mode

locales/es/translation.json

{
  "Hello!": "Hola!"
}
Enter fullscreen mode Exit fullscreen mode

And now back in our app if you hit the buttons the language should change.

Automate it! 🚀🚀

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
    ],
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you are using react native

yarn add @react-native-picker/picker
npx pod-install
Enter fullscreen mode Exit fullscreen mode
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)