DEV Community

Cover image for How to internationalize a react-native app
Merlier
Merlier

Posted on

How to internationalize a react-native app

Create a react-native app translated in multi-languages.
In this article, we'll use react-native-localize and i18n-js npm package to manage locales and translations.

The code of the whole app build here is available at https://github.com/Merlier/rn-example-translation.git

Get started

Requirements:

  • react-native >= 0.60

First just init a new react-native project:

$ npx react-native init rn_example_translation
Enter fullscreen mode Exit fullscreen mode

I prefer to create a src folder to put all my JS code so I modify the index.js at the project root dir like this:

import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);
Enter fullscreen mode Exit fullscreen mode

and then pass my app.js in the src folder.

Manage translations

We'll translate the app using 'i18n-js' module so we install it with:

$ npm install --save i18n-js
Enter fullscreen mode Exit fullscreen mode

Then create a file 'i18n.js' with:

import {I18nManager} from 'react-native';
import i18n from 'i18n-js';
import memoize from 'lodash.memoize';

export const DEFAULT_LANGUAGE = 'en';

export const translationGetters = {
  // lazy requires (metro bundler does not support symlinks)
  en: () => require('./assets/locales/en/translations.json'),
  fr: () => require('./assets/locales/fr/translations.json'),
};

export const translate = memoize(
  (key, config) => i18n.t(key, config),
  (key, config) => (config ? key + JSON.stringify(config) : key),
);

export const t = translate;

export const setI18nConfig = (codeLang = null) => {
  // fallback if no available language fits
  const fallback = {languageTag: DEFAULT_LANGUAGE, isRTL: false};
  const lang = codeLang ? {languageTag: codeLang, isRTL: false} : null;

  const {languageTag, isRTL} = lang ? lang : fallback;

  // clear translation cache
  translate.cache.clear();
  // update layout direction
  I18nManager.forceRTL(isRTL);
  // set i18n-js config
  i18n.translations = {[languageTag]: translationGetters[languageTag]()};
  i18n.locale = languageTag;

  return languageTag;
};

Enter fullscreen mode Exit fullscreen mode

And create the empty translation files './src/assets/locales/en/translations.json' and './src/assets/locales/fr/translations.json'

So now we can translate app JS string in english and french just like this:

i18n.t('Hello world!')
Enter fullscreen mode Exit fullscreen mode

Switch locale in app

It's cool to be able to translate string but the translated strings have to match the user language. So first, we'll setup a react context to keep the current user language and a switch to give the opportunity to the user to change the language.

To keep the current user language along the app with a react context, create a file 'LocalisationContext.js' in context folder with:

import React from 'react';

const LocalizationContext = React.createContext();

export default LocalizationContext;

Enter fullscreen mode Exit fullscreen mode

More info about react context

in your app.js:

import React, {useEffect, useCallback} from 'react';
import {StyleSheet} from 'react-native';

import * as i18n from './i18n';
import LocalizationContext from './context/LocalizationContext';
import HomeScreen from './HomeScreen';

const App: () => React$Node = () => {
  const [locale, setLocale] = React.useState(i18n.DEFAULT_LANGUAGE);
  const localizationContext = React.useMemo(
    () => ({
      t: (scope, options) => i18n.t(scope, {locale, ...options}),
      locale,
      setLocale,
    }),
    [locale],
  );

  return (
    <>
      <LocalizationContext.Provider value={localizationContext}>
        <HomeScreen localizationChange={handleLocalizationChange} />
      </LocalizationContext.Provider>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

and create the 'HomeScreen.js' file:

import React, {useContext} from 'react';
import {StyleSheet, SafeAreaView, Text, Button} from 'react-native';

import LocalizationContext from './context/LocalizationContext';

function HomeScreen(props) {
  const {localizationChange} = props;
  const {t, locale, setLocale} = useContext(LocalizationContext);

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.title}>React-Native example translation</Text>
      <Text style={styles.subtitle}>{t('Home screen')}</Text>
      <Text style={styles.paragraph}>Locale: {locale}</Text>

      {locale === 'en' ? (
        <Button title="FR" onPress={() => localizationChange('fr')} />
      ) : (
        <Button title="EN" onPress={() => localizationChange('en')} />
      )}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  title: {
    textAlign: 'center',
    fontSize: 22,
    marginBottom: 40,
  },
  subtitle: {
    textAlign: 'center',
    fontSize: 18,
    marginBottom: 10,
  },
  paragraph: {
    fontSize: 14,
    marginBottom: 10,
  },
  langButton: {
    flex: 1,
  },
});

export default HomeScreen;

Enter fullscreen mode Exit fullscreen mode

So here we can translate strings in JS and change the user language.

Handle localization system change

A nice thing would be to detect user language automatically from the user system. To do this, we have to install the react-native-localize module:

$ npm install --save react-native-localize
Enter fullscreen mode Exit fullscreen mode

and modify the app.js to this:

import React, {useEffect, useCallback} from 'react';
import {StyleSheet} from 'react-native';
import * as RNLocalize from 'react-native-localize';

import * as i18n from './i18n';
import LocalizationContext from './context/LocalizationContext';
import HomeScreen from './HomeScreen';

const App: () => React$Node = () => {
  const [locale, setLocale] = React.useState(i18n.DEFAULT_LANGUAGE);
  const localizationContext = React.useMemo(
    () => ({
      t: (scope, options) => i18n.t(scope, {locale, ...options}),
      locale,
      setLocale,
    }),
    [locale],
  );

  const handleLocalizationChange = useCallback(
    (newLocale) => {
      const newSetLocale = i18n.setI18nConfig(newLocale);
      setLocale(newSetLocale);
    },
    [locale],
  );

  useEffect(() => {
    handleLocalizationChange();

    RNLocalize.addEventListener('change', handleLocalizationChange);
    return () => {
      RNLocalize.removeEventListener('change', handleLocalizationChange);
    };
  }, []);

  return (
    <>
      <LocalizationContext.Provider value={localizationContext}>
        <HomeScreen localizationChange={handleLocalizationChange} />
      </LocalizationContext.Provider>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

and the 'i18n.js' file to this:

import {I18nManager} from 'react-native';
import * as RNLocalize from 'react-native-localize';
import i18n from 'i18n-js';
import memoize from 'lodash.memoize';

export const DEFAULT_LANGUAGE = 'en';

export const translationGetters = {
  // lazy requires (metro bundler does not support symlinks)
  en: () => require('./assets/locales/en/translations.json'),
  fr: () => require('./assets/locales/fr/translations.json'),
};

export const translate = memoize(
  (key, config) => i18n.t(key, config),
  (key, config) => (config ? key + JSON.stringify(config) : key),
);

export const t = translate;

export const setI18nConfig = (codeLang = null) => {
  // fallback if no available language fits
  const fallback = {languageTag: DEFAULT_LANGUAGE, isRTL: false};
  const lang = codeLang ? {languageTag: codeLang, isRTL: false} : null;

# Use RNLocalize to detect the user system language
  const {languageTag, isRTL} = lang
    ? lang
    : RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
      fallback;

  // clear translation cache
  translate.cache.clear();
  // update layout direction
  I18nManager.forceRTL(isRTL);
  // set i18n-js config
  i18n.translations = {[languageTag]: translationGetters[languageTag]()};
  i18n.locale = languageTag;

  return languageTag;
};

Enter fullscreen mode Exit fullscreen mode

Generate translations

Last useful thing, to generate the language files you can use i18next-scanner.

Juste install it globally:

npm install -g i18next-scanner
Enter fullscreen mode Exit fullscreen mode

create a 'i18next-scanner.config.js' file at your project dir root with:

const fs = require('fs');
const chalk = require('chalk');

module.exports = {
  input: [
    'src/**/*.{js,jsx}',
    // Use ! to filter out files or directories
    '!app/**/*.spec.{js,jsx}',
    '!app/i18n/**',
    '!**/node_modules/**',
  ],
  output: './',
  options: {
    debug: false,
    removeUnusedKeys: true,
    func: {
      list: ['i18next.t', 'i18n.t', 't'],
      extensions: ['.js', '.jsx'],
    },
    trans: {
      component: 'Trans',
      i18nKey: 'i18nKey',
      defaultsKey: 'defaults',
      extensions: [],
      fallbackKey: function (ns, value) {
        return value;
      },
      acorn: {
        ecmaVersion: 10, // defaults to 10
        sourceType: 'module', // defaults to 'module'
        // Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
      },
    },
    lngs: ['en', 'fr'],
    ns: ['translations'],
    defaultLng: 'en',
    defaultNs: 'translations',
    defaultValue: '__STRING_NOT_TRANSLATED__',
    resource: {
      loadPath: 'src/assets/locales/{{lng}}/{{ns}}.json',
      savePath: 'src/assets/locales/{{lng}}/{{ns}}.json',
      jsonIndent: 2,
      lineEnding: '\n',
    },
    nsSeparator: false, // namespace separator
    keySeparator: false, // key separator
    interpolation: {
      prefix: '{{',
      suffix: '}}',
    },
  },
  transform: function customTransform(file, enc, done) {
    'use strict';
    const parser = this.parser;

    const options = {
      presets: ['@babel/preset-flow'],
      plugins: [
        '@babel/plugin-syntax-jsx',
        '@babel/plugin-proposal-class-properties',
      ],
      configFile: false,
    };

    const content = fs.readFileSync(file.path, enc);
    let count = 0;

    const code = require('@babel/core').transform(content, options);
    parser.parseFuncFromString(
      code.code,
      {list: ['i18next._', 'i18next.__']},
      (key, options) => {
        parser.set(
          key,
          Object.assign({}, options, {
            nsSeparator: false,
            keySeparator: false,
          }),
        );
        ++count;
      },
    );

    if (count > 0) {
      console.log(
        `i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(
          JSON.stringify(file.relative),
        )}`,
      );
    }

    done();
  },
};
Enter fullscreen mode Exit fullscreen mode

Here you can use the command :

$ i18next-scanner
Enter fullscreen mode Exit fullscreen mode

It will generate and pre-fill the translation files './src/assets/locales/en/translations.json' and './src/assets/locales/fr/translations.json'. You will just have to change in these files by the right translations.

Running

$ npx react-native run-android
Enter fullscreen mode Exit fullscreen mode
$ npx react-native run-ios
Enter fullscreen mode Exit fullscreen mode

The code of the whole app build here is available at https://github.com/Merlier/rn-example-translation.git

rn-example-translation home screen

Top comments (7)

Collapse
 
guiw5 profile image
Guiw5

Thanks you! @merlier from what I've seen over there all these dependencies seems to be linked dependencies.. what about this case? are you assuming we are using autolinking for it or was not needed at all?

Thanks in advance

Collapse
 
merlier profile image
Merlier

I suggested react-native >= 0.60 as requirement so It assumes using autolinking. To link manually the dependencies, you can check the installation process on each dependency page.
Hope It will help you!

Collapse
 
guiw5 profile image
Guiw5

yes, thanks. but how is the way to link is not the point, I was looking for no native libraries. and this works good until the part for autodetection locale from the system.. hope this i18n features would be there in RN API someday.. expo already has one. 😊 thanks again!

Collapse
 
formifan2002 profile image
formifan2002

Hi @merlier ,
when I start the app I get the message "missing en.Home screen translation". Only after changing the language via the button, the text is correctly displayed (either in French or in English). Do you have a hint why this happens?
Thanks in advance.

Collapse
 
ulisesescamilla profile image
Ulises Escamilla

Hi @merlier , thanks for sharing, I'm interested in how we can implement a context like this section on i18njs.com/

Do you know if it is possible and how we can implement it? What if I want to use Plurals or Genders with this approach?

Thanks for your help

Collapse
 
merlier profile image
Merlier

It should be possible as roddeh-i18n seems close to i18n-js module. You could just add the roddeh-i18n config (en = i18n.create({values:{"Hello":"Hello"}})) in the setI18nConfig function from the i18n.js file. About plurals and genders, it's pretty cool and manage just by the roddeh-i18n so it should works without much more work.

Collapse
 
dozgrou profile image
Dorian Lecoeur

Hey, great article @merlier .
I would recommend to set the locale state in the useState by calling i18n.setI18nConfig() to avoid an unnecessary render 😉.