Software localization

React Native I18n with Expo and i18next (2)

Learn How to Localize Your React Native application! In Part 2 of This Two-Part Series We'll Use The i18n Library We Built in Part 1!
Software localization blog category featured image | Phrase

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.

🔗 Resource » Check out our Ultimate Guide to JavaScript Localization if you haven't already. We'll cover everything you need to make your JS apps accessible to international users, with lots of insights on different frameworks and more.

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'll use, with versions at the 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 a due date, to one of our lists
  • The ability to mark a to-do item as 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.

🗒 Another note » You can also get all the app's code on its GitHub repo.

This is what our app will look like:

Demo app | Phrase

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.

ListScreen | Phrase

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

{

    id: string,

    due: Date?,

    text: string?,

    isComplete: boolean,

}

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

/src/screens/ListScreen.js

import React, { Component } from 'react';

import {

    Text,

    View,

    StyleSheet,

    ActivityIndicator,

} from 'react-native';

import { t } from '../services/i18n';

import ListRepo from '../repos/ListRepo';

import ListHeaderStart from './ListHeaderStart';

import AddButton from '../components/AddButton';

import ListOfTodos from '../components/ListOfTodos';

class ListScreen extends Component {

    static navigationOptions = ({ navigation }) => {

        return {

            title: t(`lists:${navigation.state.routeName}`),

            headerLeft: <ListHeaderStart navigation={navigation} />,

        };

    };

    constructor(props) {

        super(props);

        this.state = {

            todos: [],

            isLoading: true,

        };

        const listName = props.navigation.state.routeName;

        this.repo = new ListRepo(listName);

        this.willFocusSubscription = props.navigation.addListener(

            'willFocus',

            () => this.loadTodosWithIndicator(),

        );

    }

    componentWillUnmount() {

        this.willFocusSubscription.remove();

    }

    async loadTodosWithIndicator() {

        this.setState({ isLoading: true });

        const todos = await this.getKeyedTodosArray();

        this.setState({ todos, isLoading: false });

    }

    async getKeyedTodosArray() {

        return this.transformToKeyedArray(

            await this.repo.getTodos()

        );

    }

    refreshTodos = async () => {

        const todos = await this.getKeyedTodosArray();

        this.setState({ todos });

    }

    /**

     * @param {Object<string, { id: string }} todos

     * @return {Array<Object>}

     */

    transformToKeyedArray(todos) {

        return Object.keys(todos)

            .reverse().map(id => ({ ...todos[id], key: id }));

    }

    updateItem = async (item) => {

        await this.repo.updateTodo(item);

        this.refreshTodos();

    }

    deleteItem = async (item) => {

        await this.repo.deleteTodo(item);

        this.refreshTodos();

    }

    goToAddTodoScreen = () => {

        this.props.navigation.navigate('AddTodoScreen', {

            listName: this.props.navigation.state.routeName,

        });

    }

    renderContent() {

        if (this.state.todos.length === 0) {

            return (

                <Text style={styles.emptyListText}>

                    {t('ListScreen:empty')}

                </Text>

            );

        }

        return (

            <ListOfTodos

                todos={this.state.todos}

                onItemUpdate={this.updateItem}

                onItemDelete={this.deleteItem}

            />

        );

    }

    render() {

        return (

            <View style={styles.container}>

                {this.state.isLoading

                    ?

                    <ActivityIndicator />

                    :

                    this.renderContent()

                }

                <AddButton

                    style={styles.addButton}

                    onPress={this.goToAddTodoScreen}

                />

            </View>

        );

    }

}

const styles = StyleSheet.create({

    container: {

        flex: 1,

        alignItems: 'center',

        justifyContent: 'center',

    },

    emptyListText: {

        fontSize: 16,

        marginHorizontal: 30,

    },

    addButton: {

        position: 'absolute',

        bottom: 30,

        end: 30,

    },

});

export default ListScreen;

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)

class ListScreen extends Component {

    static navigationOptions = ({ navigation }) => {

        return {

            title: t(`lists:${navigation.state.routeName}`),

            headerLeft: <ListHeaderStart navigation={navigation} />,

        };

    };

// ...

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)

    "lists": {

        "to-do": "To-do",

        "groceries": "Groceries",

        "learning": "Learning",

        "reading": "Reading"

    }

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.

Left-to-Right / Right-to-Left Navigation Headers | Phrase

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.

Absolutely-positioned floating action button | Phrase

/src/screens/ListScreen.js (excerpt)

    addButton: {

        position: 'absolute',

        bottom: 30,

        end: 30,

    },

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.

Left-to-Right / Right-to-Left button | Phrase

🗒 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

import {

    Text,

    Platform,

    StyleSheet,

    TouchableOpacity,

} from 'react-native';

import React, { Component } from 'react';

import i18n from '../services/i18n';

class AddButton extends Component {

    static defaultProps = {

        style: {},

    }

    render() {

        return (

            <TouchableOpacity

                onPress={this.props.onPress}

                style={[styles.addButton, this.props.style]}

            >

                <Text style={styles.addButtonText}>+</Text>

            </TouchableOpacity>

        );

    }

}

const styles = StyleSheet.create({

    addButton: {

        width: 50,

        height: 50,

        ...i18n.select({

            rtl: { paddingEnd: 1 },

            ltr: { paddingStart: 1 },

        }),

        paddingBottom: 3,

        alignItems: 'center',

        justifyContent: 'center',

        borderRadius: 25,

        backgroundColor: Platform.OS === 'ios' ? '#0076FF' : '#2962FF',

        ...Platform.select({

            ios: {

                shadowOpacity: 0.3,

                shadowRadius: 1,

                shadowColor: 'black',

                shadowOffset: { width: 0, height: 1 },

            },

            android: {

                elevation: 2,

            }

        })

    },

    addButtonText: {

        fontSize: 30,

        color: 'white',

        textAlign: 'center',

    },

});

export default AddButton;

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

/src/components/AddButton.js (excerpt)

        ...i18n.select({

            rtl: { paddingEnd: 1 },

            ltr: { paddingStart: 1 },

        }),

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.

Morpheus meme | Phrase

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)

    static navigationOptions = ({ navigation }) => {

        return {

            title: t(`lists:${navigation.state.routeName}`),

            headerLeft: <ListHeaderStart navigation={navigation} />,

        };

    };

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

Left-to-Right / Right-to-Left Icon Flipping | Phrase

/src/screens/ListHeaderStart.js

import React, { Component } from 'react';

import { StyleSheet } from 'react-native';

import IconButton from '../components/IconButton';

class ListHeaderStart extends Component {

    openListsDrawer = () => {

        this.props.navigation.openDrawer();

    }

    render() {

        return (

            <IconButton

                style={styles.button}

                icon="format-list-bulleted"

                onPress={this.openListsDrawer}

            />

        );

    }

}

const styles = StyleSheet.create({

    button: {

        marginStart: 20,

    },

});

export default ListHeaderStart;

This component corresponds to the button that opens our DrawerNavigator.

IconButton | Phrase

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

import React, { Component } from 'react';

import { TouchableOpacity, StyleSheet } from 'react-native';

import { MaterialCommunityIcons } from '@expo/vector-icons';

import i18n from '../services/i18n';

class IconButton extends Component {

    static defaultProps = {

        style: {},

        color: '#333',

        flipForRTL: true,

        iconComponent: MaterialCommunityIcons,

    }

    getFlipForRTLStyle() {

        if (!this.props.flipForRTL) { return {}; };

        return {

            transform: [{

                scaleX: i18n.isRTL ? -1 : 1,

            }],

        };

    }

    render() {

        return (

            <TouchableOpacity

                onPress={this.props.onPress}

                style={[styles.button, this.getFlipForRTLStyle(),

                    this.props.style]}

            >

                <this.props.iconComponent

                    size={24}

                    name={this.props.icon}

                    color={this.props.color}

                />

            </TouchableOpacity>

        );

    }

}

const styles = StyleSheet.create({

    button: {

        width: 40,

        height: 40,

        paddingTop: 2,

        paddingStart: 1,

        alignItems: 'center',

        justifyContent: 'center',

    },

});

export default IconButton;

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

/src/components/IconButton.js (excerpt)

    getFlipForRTLStyle() {

        if (!this.props.flipForRTL) { return {}; };

        return {

            transform: [{

                scaleX: i18n.isRTL ? -1 : 1,

            }],

        };

    }

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.

List | Phrase

/src/components/ListOfTodos.js

import React, { Component } from 'react';

import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';

import { Text, View, StyleSheet, FlatList, Platform } from 'react-native';

import Checkbox from './Checkbox';

import IconButton from './IconButton';

import i18n, { t } from '../services/i18n';

class ListOfTodos extends Component {

    toggleComplete(item) {

        const newItem = {...item, isComplete: !item.isComplete };

        this.props.onItemUpdate(newItem);

    }

    renderRow = ({ item }) => {

        return (

            <View style={styles.row}>

                <Checkbox

                    style={styles.checkbox}

                    checked={item.isComplete}

                    onToggle={() => this.toggleComplete(item)}

                />

                <Text

                    numberOfLines={1}

                    style={styles.text}

                >

                    {item.text}

                </Text>

                <View style={styles.dueDateContainer}>

                    {!!item.due &&

                        <View style={styles.dueDateInner}>

                            <MaterialCommunityIcons

                                size={18}

                                color="#555"

                                name="calendar-clock"

                            />

                            <Text style={styles.dueDateText}>

                                {formatDate(item.due)}

                            </Text>

                        </View>

                    }

                </View>

                <IconButton

                    icon="trash-2"

                    flipForRTL={false}

                    iconComponent={Feather}

                    style={styles.deleteButton}

                    onPress={() => this.props.onItemDelete(item)}

                    color={Platform.OS === 'ios' ? '#FF3824' : '#F44336'}

                />

            </View>

        );

    }

    render() {

        return (

            <FlatList

                data={this.props.todos}

                style={styles.flatList}

                renderItem={this.renderRow}

            />

        );

    }

}

/**

 * @param {Date} date

 * @returns {String}

 */

function formatDate(date) {

    if (date.getYear() === new Date().getYear()) {

        return t("ListOfTodos:dueDateShort", { date });

    }

    return t("ListOfTodos:dueDateFull", { date });

}

const styles = StyleSheet.create({

    flatList: {

        width: '100%',

    },

    row: {

        flexDirection: 'row',

        justifyContent: 'space-between',

        alignItems: 'center',

        borderBottomWidth: 1,

        borderBottomColor: '#ddd',

        paddingVertical: 10,

        paddingStart: 20,

        paddingEnd: 10,

    },

    checkbox: {

        width: 40,

        ...i18n.select({

            rtl: { marginBottom: 4 },

        }),

    },

    deleteButton: {

        ...i18n.select({

            ltr: { marginBottom: 4 },

            rtl: { marginBottom: 6 },

        }),

    },

    text: {

        flex: 1,

        fontSize: 18,

        color: '#333',

        textAlign: 'left',

    },

    dueDateContainer: {

        width: 80,

        height: 30,

    },

    dueDateInner: {

        height: '100%',

        flexDirection: 'row',

        alignItems: 'center',

    },

    dueDateText: {

        marginStart: 4,

        fontSize: 12,

        fontWeight: '100',

        color: '#555',

    },

});

export default ListOfTodos;

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)

/**

 * @param {Date} date

 * @returns {String}

 */

function formatDate(date) {

    if (date.getYear() === new Date().getYear()) {

        return t("ListOfTodos:dueDateShort", { date });

    }

    return t("ListOfTodos:dueDateFull", { date });

}

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)

    "ListOfTodos": {

        "dueDateShort": "{{date, DD MMM}}",

        "dueDateFull": "{{date, DD/MM/YYYY}}"

    },

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)

    <IconButton

        icon="trash-2"

        flipForRTL={false}

        iconComponent={Feather}

        style={styles.deleteButton}

        onPress={() => this.props.onItemDelete(item)}

        color={Platform.OS === 'ios' ? '#FF3824' : '#F44336'}

    />

// ...

    deleteButton: {

        ...i18n.select({

            ltr: { marginBottom: 4 },

            rtl: { marginBottom: 6 },

        }),

    },

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

    deleteButton: {

        ...i18n.select({

            ltr: { paddingStart: 2 },

            rtl: { paddingEnd: 1 },

        }),

    },

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

Left-to-Right / Right-to-Left Text Inputs | Phrase

/src/screens/AddTodoScreen.js

import React, { Component } from 'react';

import {

    Text,

    View,

    Button,

    Platform,

    TextInput,

    StyleSheet,

} from 'react-native';

import ListRepo from '../repos/ListRepo';

import i18n, { t } from '../services/i18n';

import DatePicker from '../components/DatePicker';

class AddTodoScreen extends Component {

    static navigationOptions = () => ({

        title: t("AddTodoScreen:title"),

    })

    state = {

        text: '',

        due: new Date(),

        hasChosenDueDate: false,

        isDatePickerOpen: false,

    }

    onChangeText = text => this.setState({ text })

    onAddDatePressed = () => {

        this.setState({

            isDatePickerOpen: true,

            hasChosenDueDate: true,

        });

    }

    onDateChange = due => {

        if (due === null) {

            this.setState({ isDatePickerOpen: false });

        } else {

            this.setState({ due });

        }

    }

    onSavePressed = async () => {

        const { text, hasChosenDueDate } = this.state;

        const due = hasChosenDueDate ? this.state.due : null;

        await ListRepo

            .with(this.props.navigation.state.params.listName)

            .addTodo({ text, due });

        this.props.navigation.goBack();

    }

    render() {

        return (

            <View style={styles.container}>

                <View style={styles.form}>

                    <Text style={styles.label}>

                        {t("AddTodoScreen:todoLabel")}

                    </Text>

                    <View style={styles.textInputContainer}>

                        <TextInput

                            numberOfLines={1}

                            returnKeyType="done"

                            style={styles.textInput}

                            value={this.state.text}

                            onChangeText={this.onChangeText}

                            underlineColorAndroid="transparent"

                            placeholder=

                                {t("AddTodoScreen:todoPlaceholder")}

                        />

                    </View>

                    <Text style={styles.label}>

                        {t("AddTodoScreen:dueDateLabel")}

                    </Text>

                    {this.state.isDatePickerOpen

                        ?

                        <DatePicker

                            date={this.state.due}

                            minDate={new Date()}

                            onDateChange={this.onDateChange}

                        />

                        :

                        <Button

                            title={t("AddTodoScreen:addDueDateButton")}

                            onPress={this.onAddDatePressed}

                        />

                    }

                </View>

                <Button

                    title={t("AddTodoScreen:saveButton")}

                    onPress={this.onSavePressed}

                />

            </View>

        );

    }

}

const styles = StyleSheet.create({

    container: {

        flex: 1,

        paddingTop: 20,

        paddingBottom: 35,

        paddingHorizontal: 16,

    },

    form: {

        flex: 1,

    },

    label: {

        fontSize: 18,

        color: '#333',

        marginBottom: 8,

        textAlign: 'left',

    },

    textInputContainer: {

        borderWidth: 1,

        borderColor: '#ddd',

        marginBottom: 40,

        ...Platform.select({

            ios: {

                shadowOpacity: 0.1,

                shadowRadius: 1,

                shadowColor: 'black',

                shadowOffset: { width: 0, height: 1 },

            },

            android: {

                elevation: 2,

            }

        })

    },

    textInput: {

        height: 44,

        width: '100%',

        fontSize: 18,

        paddingHorizontal: 8,

        color: '#333',

        backgroundColor: 'white',

        textAlign: i18n.isRTL ? 'right' : 'left',

    },

});

export default AddTodoScreen;

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

    <TextInput

        numberOfLines={1}

        returnKeyType="done"

        style={styles.textInput}

        value={this.state.text}

        onChangeText={this.onChangeText}

        underlineColorAndroid="transparent"

        placeholder=

            {t("AddTodoScreen:todoPlaceholder")}

    />

// ...

    textInput: {

        height: 44,

        width: '100%',

        fontSize: 18,

        color: '#333',

        paddingHorizontal: 8,

        backgroundColor: 'white',

        textAlign: i18n.isRTL ? 'right' : 'left',

    },

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.

Android Date Picker | Phrase

/src/components/DatePicker.android.js

import React, { Component } from 'react';

import { DatePickerAndroid, Text } from 'react-native';

import { t } from '../services/i18n';

class DatePicker extends Component {

    state = {

        hasSelectedDate: false,

    }

    componentDidMount() {

        const { date, minDate } = this.props;

        DatePickerAndroid

            .open({ date, minDate })

            .then(({ action, year, month, day }) => {

                if (action !== DatePickerAndroid.dismissedAction) {

                    this.setState({ hasSelectedDate: true });

                    this.props.onDateChange(new Date(year, month, day));

                } else {

                    this.props.onDateChange(null);

                }

            })

            .catch(({ message }) =>

                console.warn(`Could not open DatepickerAndroid: ${message}`));

    }

    render() {

        if (this.state.hasSelectedDate) {

            return (

                <Text>

                    {t("DatePickerAndroid:dueDate", { date: this.props.date })}

                </Text>

            );

        }

        return null;

    }

}

export default DatePicker;

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.

Rick and Morty | Phrase

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.

iOS Date Picker

/src/components/DatePicker.ios.js

import React from 'react';

import { DatePickerIOS } from 'react-native';

import i18n from '../services/i18n';

const DatePicker = (props) => (

    <DatePickerIOS

        mode="date"

        date={props.date}

        locale={i18n.locale}

        minimumDate={props.minDate}

        onDateChange={props.onDateChange}

    />

);

export default DatePicker;

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.

🗒 A final 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, 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 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.