React Native i18n with Expo and i18next – Part 1

React's native mobile app cousin, React Native, is changing the way we approach native mobile app development. We can now build native mobile apps with React in JavaScript, achieving performance that's much higher than hybrid apps. We can also cover both Android and iOS with a single code base, often needing very little device-specific forking in our apps. One problem that isn't widely covered yet is i18n-izing and localizing our React Native applications. In part 1 of this two-part series, we'll build our scaffolding and framework for our i18n work in React Native, laying down a solid foundation for our app.

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
  • i18n-izing 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 tutorial that covers just that.

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 time of writing:

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 exp init from the command line. Once Expo spins up our project, we can create this directory structure to keep ourselves organized:

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

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 imports and requires. 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

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

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 a 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.getCurrentLocaleAsync() will resolve with "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.

Note » Expo’s localization library is under its DangerZone namespace. That means that it’s experimental and that its API can change quickly. Make sure that as you upgrade Expo you test any code that relies on DangerZone features, to ensure that your app continues to run smoothly.

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)

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

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’s I18nManager.

/src/services/i18n/index.js

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 and TextInput. 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

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 Navigator 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)

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 Expo.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.

Note » In my experimentation the above layout direction issue only affects iOS. Android seems to behave a bit better here. 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/Navigator.js

Our component takes an array of strings that represent our lists as a prop. 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 RootNavigator is a DrawerNavigator, commonly found in Android apps and modern websites. It allows us to slide open a drawer of navigable items. Each of these items 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. We nest all of these StackNavigators under our root DrawerNavigator. This allows us to achieve the following navigation structure in our app.

In practice this looks a bit like this:

React Navigation’s DrawerNavigator and StackNavigator are largely bi-directional out of the box and there’s very little we need to do to make them right-to-left outside of the configuration we already set in App.js. However, to get a header in our DrawerNavigator we need to provide a custom content component for the navigator.

/src/navigation/DrawerContent.js

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, PhraseApp can make your life as a developer easier! Feel free to learn more about PhraseApp, referring to the Getting Started guide.

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 is 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, the final part of this series.

React Native i18n with Expo and i18next – Part 1
Rate this post
Related Posts
Comments