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
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);
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
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;
};
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!')
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;
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>
</>
);
};
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;
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
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>
</>
);
};
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;
};
Generate translations
Last useful thing, to generate the language files you can use i18next-scanner.
Juste install it globally:
npm install -g i18next-scanner
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();
},
};
Here you can use the command :
$ i18next-scanner
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
$ npx react-native run-ios
The code of the whole app build here is available at https://github.com/Merlier/rn-example-translation.git
Top comments (7)
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
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!
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!
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.
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
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.
Hey, great article @merlier .
I would recommend to set the locale state in the useState by calling i18n.setI18nConfig() to avoid an unnecessary render 😉.