React Native i18n with Expo and i18next – Part 2

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 2 of this two-part series we'll use the i18n library we built in part 1, and build out our app's i18n-ized screens and functionality.

In part 1 of this series, we built an i18n library for React Native on top of i18next, Moment.js, and Expo. We also built the scaffolding for our app’s navigation. You may want to check out part 1 before proceeding, as this article builds on the library we established in part 1.

As a quick recap, let’s go over our external dependencies and what our app’s functionality is about.

Here are all the NPM libraries we’re using, with versions at time of writing:

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.

Building Our App’s Screens

Our first view will be the ListScreen, which displays a single list’s to-do items.

We’ll be making use of a ListRepo repository that offers a to-do CRUD wrapper around React Native’s AsyncStorage abstraction. We use the repository to persist to-dos locally on the user’s device. For brevity, we won’t cover the repo code here. The repo isn’t crazy complex at all, and you can view the repository’s code on Github if you’re coding along.

The main models we use with our ListRepo is our preloaded lists, which are just a map of to-dos. Each to-do is a map whose keys are the id of the to-do, and whose values have the following shape:

/src/screens/ListScreen.js

Now let’s take a look at how we can build out our ListScreen.

/src/screens/ListScreen.js

A bunch of our ListScreen‘s methods focus on CRUD. We’re also using React Navigation to display a navigation bar. We can customize this bar using the static navigationOptions property, which our StackNavigator will use when displaying our screen.

/src/screens/ListScreen.js (excerpt)

Earlier, we designated each of our list routes to be the name of that list in lowercase English. So our Groceries list will have the routeName of "groceries". This routeName can then be used to  key into our current locale’s translation file via t("lists:groceries") to display our screen’s title in the navbar.

/src/lang/en.json (excerpt)

In the case of the "groceries" ListScreen in English, our navbar title will be "Groceries".

Left-to-Right / Right-to-Left Navigation Headers with React Navigation

We often want to customize the right and left items of our navigation headers. In the case of ListScreen, we want to place a button that opens up our DrawerNavigator to allow our users to navigate between different lists. We can do this via the headerLeft prop on navigationOptions. The cool thing is React Navigation will automatically switch layout direction for the current locale. So, without adding any extra code, a headerLeft component will be displayed to the left of the header in English, and to the right of the header in Arabic.

 

Let’s take a look at our add button’s positioning and then come back to our custom headerLeft component.

Left-to-Right / Right-to-Left Absolute Positioning

You will have noticed our “Add to-do” FAB (floating action button) on our ListScreen. This kind of button comes from Google’s Material Design, and is a common UI pattern in apps these days. Users will expect that our FAB is floating above everything else in the screen, and an easy way to achieve this layout is through absolute positioning.

 

/src/screens/ListScreen.js (excerpt)

This styling will float our FAB at the bottom-right corner of the screen in English, and above all other screen UI. Notice our use of the direction-agnostic end style prop instead of right. We could have used right, and React Native would have mapped that to end for us. In both cases, our FAB will be displayed near the right edge of the screen in English, and near the left edge of the screen in Arabic.

Note » There are, of course, start / left counterpart props to right / end for absolute positioning.

Direction-Specific Styling

Let’s dive into our FAB component’s code.

/src/components/AddButton.js

This builds out a circular button with a “+” in the middle of it. Notice this bit of styling:

/src/components/AddButton.js (excerpt)

Our button’s “+” sign doesn’t center perfectly on the horizontal axis within the button, so we add a slight amount of padding to correct it. We always want to add left padding to the button, regardless of layout direction. However, if we simply use paddingLeft, React Native will map that to paddingStart and we’ll get padding on the left in English, and on the right in Arabic.

Confusing, no? Well, we can use our i18n.select() method to target RTL and LTR separately. Explicit use of paddingStart and paddingEnd can make our intention clear: We want left (end) padding for RTL layouts, and the same left (start) padding for LTR layouts. That’s exactly what the i18n.select() expression above is giving us.

Note » Our i18n.select() wouldn’t work reliably if our i18n library used i18next for locale direction resolution. This is because i18n.select() is being called within StyleSheet.create(), which exists outside of our component’s class or function. This means that StylesSheet.create() will get evaluated when its file is imported, which will happen earlier than component instantiation. i18next may not have fully initialized then. This is exactly why we rely on React Native’s I18nManager for locale direction resolution. I18nManager will be ready when our file is imported, so it’s more reliable than i18next for our purposes here.

Left-to-Right / Right-to-Left Icon Flipping

Ok, let’s revisit our ListScreen‘s headerLeft component.

/src/screens/ListScreen.js (excerpt)

We have a ListHeaderStart that we pass to our StackNavigator‘s headerLeft option.

/src/screens/ListHeaderStart.js

This component corresponds to the button that opens our DrawerNavigator.

Notice our IconButton in the above code. This is a reusable UI component that has a bit of direction logic that comes in handy for LTR / RTL layouts.

/src/components/IconButton.js

Of special interest to us is the flipForRTL prop and its companion method, getFlipForRTLStyle().

/src/components/IconButton.js (excerpt)

We use React Native’s transform style prop to flip the icon on the horizontal axis for RTL layouts. We also make this behaviour opt-out through the flipForRTL boolean prop, so that we can disable directional icon flipping when we don’t want it.

List Rows

You may have noticed that our ListScreen makes use of a ListOfTodos component. This component wraps a React Native FlatList to display a list’s to-dos.

/src/components/ListOfTodos.js

Our ListofTodos wraps a FlatList that takes an array of to-do items takes a Notice, however, that basic row LTR / RTL is handled for us by React Native because we already set up layout direction in our App.js. We didn’t need to add any code to manage our layout direction at the list or row levels.

Formatting Dates

Our to-dos’ due dates are being conditionally formatted to provide a somewhat better user experience.

/src/components/ListOfTodos.js (excerpt)

When a to-do’s due date is within the current year, we only want to show the day and the month of the due date. When the due date is past this year, we want to show the full date. Date formatting is handled by our custom date formatter, which uses Moment.js. We hook into it by providing some special syntax in our translation strings.

/src/lang/ar.json (excerpt)

We’re using i18next’s interpolation by passing in the date as a param when we call t(). And by providing the , DD MMM syntax, we let i18next know that we want our date to be formatted as per our given format string. Moment.js understands many format strings, and handles the date formatting for us because we wired it up to do so in our i18n library.

Watch Out for Double-Flipping

Notice our delete button and its styles.

/src/components/ListOfTodos.js (excerpt)

We turn off RTL-flipping for the delete button’s icon via flipForRTL={false}. This doesn’t really make a difference here since the button is symmetrical on the horizontal axis. However, if we had added direction-specific horizontal styling to the button, like for example…

…we would get unexpected results if we hadn’t set flipForRTL to false. This is because the prop will reverse our directional styling. So it’s important to be aware of the interplay of flipping through transformation and direction-specific styling.

Left-to-Right / Right-to-Left Text Inputs

We can now build out our AddTodoScreen.

/src/screens/AddTodoScreen.js

To-dos have text and a due date when we add them. We’ll get to due dates and date pickers in a moment. First, let’s take a closer look at our text input and its styles.

/src/screens/AddTodoScreen.js

Notice that we’re setting our TextInput‘s alignment to match the current locale’s direction: textAlign: i18n.isRTL ? 'right' : 'left'. That’s because, weirdly enough, React Native does not seem to map textAlign: 'left' to textAlign: 'start' when it comes to TextInputs. So unlike the Text component, we actually just straight-up tell TextInputs the exact text alignment they should conform to. This alignment will affect both inputted text and placeholders.

Platform-Specific Date Pickers

To add due dates, we use a DatePicker component in our AddTodoScreen. We actually fork here and provide one date picker for Android and another for iOS, since the two platforms can handle date input somewhat differently. We can just expose a unified API to our platform-specific pickers, and let React Native auto-load the correct component by including platform-specific extensions in our filenames.

The Android Date Picker

Let’s start with Android.

/src/components/DatePicker.android.js

The React Native DatePickerAndroid API is more imperative than most of the framework’s other components and doesn’t really provide any JSX. We simply .open() the picker and wait for a promise to resolve. open()‘s promise resolution indicates that the user has either canceled out of the picker or chosen a date. We make sure that the allowable date range for selection starts at today’s date and extends into the future since it doesn’t make sense for a to-do to have a due date in the past. Unless you have a time machine. No, you can’t borrow mine. To be honest I’m not sure you want to. Time-travel isn’t always a picnic.

If the user has indeed selected a date we render it out as formatted text. That’s really about it for our Android picker.

The iOS Date Picker

Ok, time for iOS.

/src/components/DatePicker.ios.js

React Native’s DatePickerIOS is more React-y than its Android counterpart, and provides a spinner date selector rather than a modal. In the case of iOS, we have to explicitly supply the locale so that the picker renders the correct numbers and names.

Note » I couldn’t really find a way to get DatePickerIOS to display an RTL spinner set that is ordered day, month, year from right to left. This, to my mind, would be the natural ordering of a date picker in Arabic. If you have information to the contrary or have found way to achieve the desired ordering, please leave a comment below. However, providing the locale prop will at least get numbers and month titles to appear in the current language.

Well, that’s about it. At this point, we have a small working to-do list app that demonstrates several of the problems we may encounter when i18n-izing a React Native app, and some solutions to those problems.

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.

And That’s a Wrap, Folks

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 area 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 certainly hope you enjoyed reading this two-part series (check out part 1 if you haven’t already). It’s an exciting time to be a developer. Happy coding, amigos.

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