This is part 1 of a two-part series. Check out part 2 here.
When considering our i18n and l10n laundry list, we'll want to handle the following right off the bat:
- Locale determination from the user's device
- Loading a language file for the current locale
- Internationalizing our UI so that strings are loaded from the current language file
- Handling locale direction, left-to-right or right-to-left
- Displaying dates
🗒Note » If you're interested in web/browser i18n with React, we have an in-depth React i18n tutorial that covers just that.
🔗 Resource »Learn all the steps you need to make your JS apps ready for users around the globe inour Ultimate Guide to JavaScript Localization.
We'll use the Expo framework to get up and running quickly with our React Native app. i18next and Moment.js will help us build our i18n library for React Native.
Here are all the NPM libraries we'll use, with versions at the time of writing:
- Expo SDK 35.0 (which comes with React Native 0.59)
- We'll also need Expo's localization library, which we can install with the expo CLI
- i18next 17.3
- Moment.js 2.24
- React Navigation 4.0 — After we initialize our demo app, we can install react-navigtation and its dependencies with expo.
- We'll also need these additional React Navigation add-ons:
- react-navigation-drawer — installed via NPM
- react-navigation-stack — installed via NPM
React Navigation will help us build the glue between the screens of our app. Speaking of which...
The App
Our app will be a simple to-do list demo with:
- Pre-loaded lists that we can switch between
- The ability to add a to-do item, with due date, to one of our lists
- The ability to mark a to-do item complete or incomplete
- The ability to delete a to-do item
- And, of course, the ability to use the app in multiple languages, including left-to-right and right-to-left languages: we'll cover English and Arabic localization here but we'll build the i18n out so you can add additional languages
🗒Note » You can load the app on your Expo client by visiting https://expo.io/@mashour/react-native-i18n-demo and using the QR code for Android or "Request a Link" for iOS.
🗒Note » You can also get all the app's code on its GitHub repo.
This is what our app will look like:
Alright, let's get started.
We can use the Expo CLI to initialize our app with expo init
from the command line. Once Expo spins up our project, we can create this directory structure to keep ourselves organized:
/├── src/│ ├── components/│ ├── config/│ ├── lang/│ ├── navigation/│ ├── repos/│ ├── screens/│ ├── services/│ │ └── i18n/│ └── util/└── App.js
Using i18next and Moment.js for our Core i18n Library
i18next is an awesome JavaScript i18n library that's robust and extensible enough for us to use as the foundation of i18n in our React Native app. To cover our native mobile needs, we can build our own locale-detection plugin for i18next. The library will also allow us to plug-in custom translation loaders and date formatters. Let's get to all that.
🗒Note » We have a dedicated article on i18next and Moment.js that is focused on web development.
First, let's get some configuration in place.
/src/config/i18n.js
export const fallback = "en";export const supportedLocales = { en: { name: "English", translationFileLoader: () => require('../lang/en.json'), // en is default locale in Moment momentLocaleLoader: () => Promise.resolve(), }, ar: { name: "عربي", translationFileLoader: () => require('../lang/ar.json'), momentLocaleLoader: () => import('moment/locale/ar'), },};export const defaultNamespace = "common";export const namespaces = [ "common", "lists", "ListScreen", "ListOfTodos", "AddTodoScreen", "DatePickerAndroid",];
We setup our fallback locale that i18next will use if it doesn't find a translation for a given string in our current locale's translation file. The supportedLocales
map lists the locales our app covers, providing their translation files and the locale files Moment.js provides for date formatting.
React Native and Dynamic Imports
We don't want to statically load all our translation files and Moment.js locale files, since that wouldn't scale well as we add more and more locales to our app. Instead, we want to dynamically load only the files relevant to our user's current locale. To do this, we can use the dynamic import()
construct for modules and require()
for static files.
However, the React Native JavaScript runtime doesn't allow for dynamic strings in its import
s and require
s. For example, import('../foo/' + bar)
would throw an error in React Native. So we wrap import expressions, with static paths to our files, in functions. This way we can invoke our functions to lazy-load our locale files once we've determined the user's current locale.
🗒Note » React Native 0.56 removed dynamic import support from the framework. If you want to use dynamic imports with React Native 0.56+, check out the Babel Dynamic Import plugin.
i18next Namespaces
You may have noticed our defaultNamespace
and namespaces
exports above. Namespaces are simply a way for us to logically group translations. For example, we could call i18next.t("HomeScreen:greeting")
to access the namespaced string at HomeScreen.greeting.
🗒Note » You have to register every namespace with i18next before you use it. Otherwise, the library won't load your namespaces' translation strings. We've configured all the namespaces we'll use in our demo app above, and we'll wire them up with i18next shortly.
Moment.js Setup for Localized Date Formatting
With our configuration in place, we can now wrap Moment.js in a module that will load localized date strings and providing a date formatting function.
/src/services/i18n/date.js
import moment from 'moment';import * as config from '../../config/i18n';const date = { /** * Load library, setting its initial locale * * @param {string} locale * @return Promise */ init(locale) { return new Promise((resolve, reject) => { config .supportedLocales[locale] .momentLocaleLoader() .then(() => { moment.locale(locale); return resolve(); }) .catch(err => reject(err)); }); }, /** * @param {Date} date * @param {string} format * @return {string} */ format(date, format) { return moment(date).format(format); }}export default date;
The date.init()
function takes an ISO 639-1 locale code, e.g. "en", and loads the locale's appropriate Moment.js locale module as per our configuration.
format()
is just a wrapper around Moment's formatting API, and will return a formatted date string corresponding to the currently loaded Moment.js locale.
Our Custom Locale Detector
i18next is nicely extensible, and allows us to plug in core parts of the library to suit our needs. We'll want to do this for language / locale detection, since in a pure Expo app we need to use Expo's localization library to dive into the native mobile environment and get the user's current locale.
/src/services/i18n/language-detector.js
import * as Localization from 'expo-localization';const languageDetector = { type: 'languageDetector', async: true, detect: (callback) => { // We will get back a string like "en-US". We // return a string like "en" to match our language // files. callback(Localization.locale.split('-')[0]); }, init: () => { }, cacheUserLanguage: () => { },};export default languageDetector;
The two most important keys in our language detector are async
and detect
. The first designates our detector as asynchronous, so i18next will wait for us to invoke the given callback
in detect()
once we've figured out the user's current locale.
We find this locale by using Expo, which provides an add-on Localization
library that gets the user's locale as per her device settings. So if the user has set her mobile device's language to English (Canada), Localization.locale
will be "en-CA"
. We yank the "en"
part of the string out of the locale to match our language files, and let i18next know that we've detected the current locale by invoking callback("en")
.
🗒Note » We're following i18next's plugin boilerplate here, and the library requires all of the languageDetector
's values, even ones we may not use. To get around this we just provide void-returning functions for the fields that don't interest us.
Our Custom Translation Loader
To keep our code nice and modular, let's make one more use of i18next's plugin system to quickly build out a translation loader.
Our translation files will look something like this.
/src/lang/en.json (excerpt)
{ "common": { "lists": "Lists" }, "lists": { "to-do": "To-do", "groceries": "Groceries", "learning": "Learning", "reading": "Reading" }, "ListScreen": { "empty": "No to-dos in this list! Use the + button to add a to-do." }, // ...}
We have namespaced keys that we have to grab to resolve our translation strings. With that in mind, we can write our loader.
/src/services/i18n/translation-loader.js
import * as config from '../../config/i18n';const translationLoader = { type: 'backend', init: () => {}, read: function(language, namespace, callback) { let resource, error = null; try { resource = config .supportedLocales[language] .translationFileLoader()[namespace]; } catch (_error) { error = _error; } callback(error, resource); },};export default translationLoader;
Our loader's job is to make locale namespaces available to i18next. To resolve a namespace in our loader, we call our loader function, and given the locale's configuration, resolve the namespace in the loaded file. i18next will then do the work of refining further into the namespace and resolve a given key. So for "lists:groceries"
, we just have to provide the "lists"
bit when using i18next's translation function, t()
, in our UIs.
Let's use our loader and language detector plugins along with our date wrapper to build the core of our i18n service around i18next. We'll get locale direction , LTR or RTL, from the native environment through React Native'sI18nManager
.
/src/services/i18n/index.js
import i18next from 'i18next';import { I18nManager as RNI18nManager } from 'react-native';import * as config from '../../config/i18n';import date from './date';import languageDetector from './language-detector';import translationLoader from './translation-loader';const i18n = { /** * @returns {Promise} */ init: () => { return new Promise((resolve, reject) => { i18next .use(languageDetector) .use(translationLoader) .init({ fallbackLng: config.fallback, ns: config.namespaces, defaultNS: config.defaultNamespace, interpolation: { escapeValue: false, format(value, format) { if (value instanceof Date) { return date.format(value, format); } } }, }, (error) => { if (error) { return reject(error); } date.init(i18next.language) .then(resolve) .catch(error => reject(error)); }); }); }, /** * @param {string} key * @param {Object} options * @returns {string} */ t: (key, options) => i18next.t(key, options), /** * @returns {string} */ get locale() { return i18next.language; }, /** * @returns {'LTR' | 'RTL'} */ get dir() { return i18next.dir().toUpperCase(); }, /** * @returns {boolean} */ get isRTL() { return RNI18nManager.isRTL; }, /** * Similar to React Native's Platform.select(), * i18n.select() takes a map with two keys, 'rtl' * and 'ltr'. It then returns the value referenced * by either of the keys, given the current * locale's direction. * * @param {Object<string,mixed>} map * @returns {mixed} */ select(map) { const key = this.isRTL ? 'rtl' : 'ltr'; return map[key]; }};export const t = i18n.t;export default i18n;
Our i18n service is just an adapter around i18next with some added niceties. The i18n.init()
method gets our library booted up, initializing i18next with our plugins and namespaces, and using our custom date formatter in i18next's interpolation.format()
. i18next will have determined the current locale through Expo once it's initialized, and we can use this locale to initialize our date
wrapper via date.init()
.
🗒Note » Since we generally output our strings to native mobile views and not a browser, HTML escaping will show unparsed HTML entities in React Native Text
andTextInput
. So we turn off i18next's HTML escaping by passing false
to interpolation.escapeValue
when we initialize the library. However, you may want to be careful if you're outputting text to a WebView, which displays a browser, or anywhere else web code can be harmful.
We wrap i18next's t
, language
, and dir
members with our own t,
locale
, and dir
, respectively, to provide a single API for our app's i18n. We will use the as we build our little to-do app.
Our isRTL
property relies on the React Native I18nManager
to determine layout and text direction from the native mobile environment. We use the native environment as the single source of truth for direction because we will sometimes need isRTL
before our i18n library has fully initialized (we'll see why a bit later). So we dig into the native environment for a more consistent source of locale direction.
select()
uses isRTL
and is a simple convenience method. We'll see how it works when we get to our views.
Loading our i18n Library and Forcing Direction
Let's use our i18n library in our main App
component.
/App.js
import { Updates } from 'expo';import React, { Component } from 'react';import { View, StyleSheet, ActivityIndicator, I18nManager as RNI18nManager,} from 'react-native';import { createAppContainer } from 'react-navigation';import i18n from './src/services/i18n';import AppNavigator from './src/navigation/AppNavigator';const AppNavigatorContainer = createAppContainer(AppNavigator);export default class App extends Component { state = { isI18nInitialized: false } componentDidMount() { i18n.init() .then(() => { const RNDir = RNI18nManager.isRTL ? 'RTL' : 'LTR'; // RN doesn't always correctly identify native // locale direction, so we force it here. if (i18n.dir !== RNDir) { const isLocaleRTL = i18n.dir === 'RTL'; RNI18nManager.forceRTL(isLocaleRTL); // RN won't set the layout direction if we // don't restart the app's JavaScript. Updates.reloadFromCache(); } this.setState({ isI18nInitialized: true }); }) .catch((error) => console.warn(error)); } render() { if (this.state.isI18nInitialized) { return <AppNavigatorContainer />; } return ( <View style={styles.loadingScreen}> <ActivityIndicator /> </View> ); }}const styles = StyleSheet.create({ loadingScreen: { flex: 1, alignItems: 'center', justifyContent: 'center', }});
We don't want to show any app content before our i18n library is initialized, because our screens will have localized content that won't be ready until our i18n library is. Our state.isI18nInitialized
flag helps us with this, and allows us to conditionally load our root AppNavigatorContainer
only when our i18n is ready. We'll get to navigation in a bit. But first,you may have noticed this odd bit of code above:
/App.js (excerpt)
const RNDir = RNI18nManager.isRTL ? 'RTL' : 'LTR'; // RN doesn't always correctly identify native // locale direction, so we force it here. if (i18n.dir !== RNDir) { const isLocaleRTL = i18n.dir === 'RTL'; RNI18nManager.forceRTL(isLocaleRTL); // RN won't set the layout direction if we // don't restart the app's JavaScript. Updates.reloadFromCache(); }
Well, this has to do with how React Native with Expo handles layout direction. In the native environment, switching your device's language from, say, English to Arabic will automatically switch the OS's text and layout direction. Similarly, all native apps that support languages in two directions will automatically switch as well. However, React Native with Expo doesn't seem to currently do this out-of-the-box for right-to-left languages, and we have to force the RTL switching ourselves.
🗒Note » If you're not using Expo or create-react-native-app, or otherwise have access to native code, there does seem to be a way to configure your native environment to enable React Native's own RTL switching. Check out Facebook's official blog post on RTL support for React Native apps for more information.
On app load we check if the i18next locale direction matches what React Native thinks the direction is. If these two values don't match we need to force React Native's direction. This won't take effect immediately, however, and our JavaScript app needs to be restarted to complete the process. The Updates.reloadFromCache()
is meant for reloading our app's JavaScript bundle, and we use it to do just that to finalize the direction switch.
🗒Note » We only perform our direction-switching logic when our i18next locale direction is different than React Native's. This is important because restarting the JavaScript bundle can take a noticeable amount of time, so we don't want to do it on every app load. In production the switch should realistically only happen on the first load of our app, since the majority of users don't change their system language after initially setting up their device.
🗒 Another note » In my experimentation the above layout direction issue only affects iOS. Android seems to behave a bit better here. Also if you noticed that Expo's Localization
has its own isRTL
property, and that we're not using it here, then know we have good reason to do so: we're preferring I18nManager.isRTL
here because on iOS Expo's Localization.isRTL
can get it wrong sometimes. React Native's I18nManager
seems to be reliable, however. Make sure to test your app on the OS configurations you support to make sure that you're getting expected behavior from them.
That's about it for our i18n scaffolding. Let's get to our app's Navigator
component.
Navigation
/src/navigation/AppNavigator.js
import React, { Component } from 'react';import { createStackNavigator } from 'react-navigation-stack';import { createDrawerNavigator } from 'react-navigation-drawer';import lists from '../config/lists';import i18n, { t } from '../services/i18n';import DrawerContent from './DrawerContent';import ListScreen from '../screens/ListScreen';import AddTodoScreen from '../screens/AddTodoScreen';function getListNavItems() { return lists.reduce((items, name) => { const stackNavigator = createStackNavigator({ [name]: ListScreen, AddTodoScreen, }); stackNavigator.navigationOptions = ({ navigation }) => ({ title: t(`lists:${navigation.state.routeName}`), }); return { ...items, [name]: stackNavigator }; }, {});};const AppNavigator = createDrawerNavigator( getListNavItems(), { contentComponent: DrawerContent, drawerPosition: i18n.isRTL ? 'right' : 'left', },);export default AppNavigator;
We're using React Navigation to build our screen navigation. Covering React Navigation in detail is a bit outside the scope of this article, but we'll go over what we're basically doing here. Our AppNavigator
is a DrawerNavigator
, commonly found in Android apps and modern websites. It allows us to slide open a drawer of navigable items.
Each of the items inside our DrawerNavigator
is a StackNavigator
which allows its internal screens to open up on top of one another, and provides the ability to back out of a screen to see the screen before it. We build a StackNavigator
for each one of our pre-loaded lists, making sure to use our t()
function to show localized names for our list titles. Our lists
are stored in a configuration file, and are just a list of string keys that we can use to retrieve translated string names. We nest all of these StackNavigators
under our root DrawerNavigator
. This allows us to achieve the following navigation structure in our app.
DrawerNavigator│├── To-do StackNavigator│ ├── ListScreen│ └── AddTodoScreen│├── Groceries StackNavigator│ ├── ListScreen│ └── AddTodoScreen|├── Reading StackNavigator│ ├── ListScreen│ └── AddTodoScreen...
In practice this looks a bit like this:
[youtube https://www.youtube.com/watch?v=ypaXgBE1YDQ?rel=0&controls=0&showinfo=0&w=560&h=315]
React Navigation's StackNavigator
is largely bi-directional out of the box and there's very little we need to do to make it right-to-left outside of the configuration we already set in App.js
. However, our DrawerNavigator
needs a bit more work. You may have noticed that in the code above we had to set its layout direction explicitly depending on our locale's direction.
To get a header in our DrawerNavigator
we need to provide a custom content component for the navigator.
/src/navigation/DrawerContent.js
import React, { Component } from 'react';import { ScrollView, Text, StyleSheet } from 'react-native';import { SafeAreaView } from 'react-navigation';import { DrawerItems } from 'react-navigation-drawer';import { t } from '../services/i18n';const DrawerContent = (props) => ( <ScrollView> <SafeAreaView style={styles.container} forceInset={{ top: 'always', horizontal: 'never' }} > <Text style={styles.header}>{t('lists')}</Text> <DrawerItems {...props} /> </SafeAreaView> </ScrollView>);const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 40, }, header: { fontSize: 18, fontWeight: '100', textAlign: 'left', marginStart: 16, marginBottom: 8, }});export default DrawerContent;
This is based on the official React Navigation documentation for custom drawer content. We can pass the props that React Navigation gives our component to its own DrawerItems
, since we're not customizing those. The main bit of customization we're doing here is that we're adding a header on top of our drawer items.
🗒Note » You may have noticed that there is no namespace in our t('lists')
call. That's because the lists
key belongs to the common
namespace, which we registered as the default namespace with i18next.
Left-to-Right / Right-to-Left Style Props
Of special importance to us are the textAlign
and marginStart
style props. Many React Native left / right layout props like margin and padding have direction-agnostic equivalents. These will adapt to the current locale's direction. So by using marginStart
we get our margin on the left in LTR locales, and on the right in RTL locales.
However, we don't need to use these direction-agnostic props in React Native if we don't want to. React Native will map marginLeft
to marginStart
, and marginRight
to marginEnd
, behind the scenes. So if we had set marginLeft: 16
above, our header would have 16 points of margin to its left in English, and 16 points of margin to its right in Arabic.
Text alignment also gets this default mapping. So our textAlign: 'left'
above will make sure that our header's text is aligned to the left in English and aligned to the right in Arabic.
🗒Note » For directional text alignment to work, we must explicitly specify the direction. So if we omitted the textAlign
prop above or set it to 'auto'
, our header's text wouldn't always respect our locale's direction. When we explicitly set it to 'left'
, however, we get the desired behavior.
Also,according to Facebook, Android and iOS handle default text alignment a bit differently. "In iOS, the default text alignment depends on the active language bundle, they are consistently on one side. In Android, the default text alignment depends on the language of the text content, i.e. English will be left-aligned and Arabic will be right-aligned." This is yet another reason to explicitly set our textAlign
.
Ok that's it for scaffolding. In the next part of this series, we'll go over building our app's screens.
🗒Note » You can load the app on your Expo client by visiting https://expo.io/@mashour/react-native-i18n-demo and using the QR code for Android or "Request a Link" for iOS.
🗒Note » You can peruse all of the app's code on its GitHub repo.
Closing Out Part 1
Writing code to localize your app is one task, but working with translations is a completely different story. Many translations for multiple languages may quickly overwhelm you which will lead to the user’s confusion. Fortunately, Phrase can make your life as a developer easier! Feel free to learn more about Phrase, referring to the Phrase Localization Suite.
I think React Native is one of a few libraries that are paving the way for a new generation of cross-platform native mobile development frameworks. The coolest thing about React Native is that it uses React and JavaScript to allow for a lean, declarative, component-based approach to mobile development.
React Native brings React's easy-to-debug, uni-directional data flow to mobile, and opens up a ton of JavaScript NPM packages for use in mobile development. The framework is still maturing, and one of the areas that are still not under lock-and-key is i18n and l10n with RN. I hope I shed some light on that topic here, and I hope you'll join me as we round out our app in part 2 of this series.
Authored by Mohammad Ashour.
Last updated on January 15, 2023.
FAQs
What is the difference between i18n and i18next? ›
i18n is an abbreviation that is used across all the programming languages and software development world. i18next is a JS framework/set of libraries.
How do I use i18next in react-native? ›Basic usage
import React from 'react' import { Text } from 'react-native' import { useTranslation } from 'react-i18next' const Component: React. FC = () => { const { t } = useTranslation() return <Text>{t('demoScope. title')}</Text> // -> "i18next is Great!" }
- Creating a fresh React Native Project and installing i18next and react-i18next dependencies.
- Creating a JSON file for each language.
- Configuring i18next in i18n. js .
- Initializing i18next by importing it in App. js.
- Importing language functions in App. js and changing the language.
formatjs comes with built in formatting using the browsers intl API (depending on browser you will have to polyfill that). i18next comes with a custom formatter function developers can define to handle formats (using the intl API or moment. js depending on the need of developers).
What is the difference between internationalization i18n and localization l11n )? ›Localization (l10N) involves adapting your product or content to particular locales. Internationalization (i18n) is the process of preparing your software-based product for localization.
What is the difference between next translate and i18next? ›next-translate vs next-i18next
next-i18next is an extension of base i18next plugin which allows you to localize many different frameworks and languages. next-translate has been built by Aral Roca Gomez especially for NextJS framework and it integrates very well with NextJS i18n API.
Running your React Native application
Install the Expo Go app on your iOS or Android phone and connect to the same wireless network as your computer. On Android, use the Expo Go app to scan the QR code from your terminal to open your project. On iOS, use the built-in QR code scanner of the default iOS Camera app.
Install the expo package and Expo module infrastructure to your native project: npx install-expo-modules . Install the Expo modules you want to use: e.g., to install expo-av, run npx expo-cli install expo-av ; or, if you have expo-cli globally installed (recommended), you can run: expo install expo-av .
How do I deploy react-native app to Expo? ›- sudo npm install -g expo-cli. Installing Expo CLI.
- expo login. Logiging in to Expo.
- expo publish. Publish the application on Expo.
- Use Appropriate Navigation Strategies. ...
- Avoid Using Scroll View to Render Huge Lists. ...
- Avoid Passing Functions Inline as Props. ...
- Go for Resizing & Scaling Images. ...
- Cache Images. ...
- Avoid Unnecessary Renders. ...
- Use Hermes.
Is React Native better than flutter? ›
Industry Trends. React Native - According to the recent StackOverflow survey of 2019, 62.5% of developers loved React Native. Since it has been around for a while now and uses React and JavaScript, it leads to better job opportunities. Flutter - Flutter ranked higher with 65.4%.
Is React Native enough to make an app? ›If you need to develop an app for both iOS and Android, React Native is the best tool out there. It can reduce the codebase by about 95%, saving you time and money. On top of that, React Native has a number of open-source libraries of pre-built components which can help you further speed up the development process.
What is the use of i18next? ›i18next is an internationalization-framework written in and for JavaScript. But it's much more than that! i18next goes beyond just providing the standard i18n features such as (plurals, context, interpolation, format). It provides you with a complete solution to localize your product from web to mobile and desktop.
What is the difference between localization and Internationalisation? ›Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting internationalized software for a specific region or language by translating text and adding locale-specific components.
What are the different types of localization? ›There are three main types of language localization services: translation, transcription, and interpreting. Translation services render the original text into another language, while transcribing services produce a copy of the source text with correct grammar and punctuation.
Why is it called i18n? ›Internationalization is also called i18n (because of the number of letters, 18, between “i” and “n”). Internationalization ensures your software is localizable and is typically done by software developers and engineers.