React native (+expo): Automatic machine translation
Tags: reactnative
Published: 2023-02-07 15:48:51

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.

```bash
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.

```bash
npx expo install expo-localization
```

```js
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.

```js
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
```json
{
  "Hello!": "Hello!"
}
```

locales/es/translation.json
```json
{
  "Hello!": "Hola!"
}
```

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

## Automate it! 🚀🚀

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d4xwoz7jwy9nlz84wwpf.jpg)
 

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.

```bash
yarn add -D babel-plugin-i18next-extract
```

and in `babel.config.js` we need to configure the i18next-extract plugin.

```js
    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.

```bash
yarn add -D json-autotranslate
```
and in package.json in the script section add add:
```json
"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:
```bash
npx expo install @react-native-picker/picker
```

If you are using react native
```bash
yarn add @react-native-picker/picker
npx pod-install
```

```js
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>
  );
}
```