xBlog

BLOG

Making react-native apps RTL ready

Making react-native apps RTL ready
Ayoub Hadar

Ayoub Hadar

01 November, 2017 · 6min 📖

react-native-logo.png

One of the major issues i had to deal with while developing mobile apps with react-native was implementing i18n for both LTR and RTL languages, with the possibility of switching between them on the app settings screen.

the solution provided last year by react-native community on the react-native blog was to use I18nManager component method forceRTL. Here is a code snippets :

import {I18nManager} from 'react-native';
I18nManager.forceRTL(true);

However, while testing the previous approche, the end users are invited to reload the app to apply the changes to Right-To-Left languages. Many developers overcome this issue by using react-native-restart library, after persisting the chosen language locally, then restart the app automatically when Mobile users changes script direction from RTL to LTR and vice-versa as shown in this code snippets :

import {I18nManager, AsyncStorage} from 'react-native';
import RNRestart from 'react-native-restart';
AsyncStorage.setItem('lang','ar',() => {
    AsyncStorage.getItem('lang', (value) => {
      if(value != null){
        I18nManager.forceRTL(true);
        RNRestart.Restart();
      }
    });
})

This approche is considered for many testers as an unhandled bug because during the restart process a blank screen shows up a few seconds before rendering the home screen.

Moreover, developing RTL support app with react-native requires hundling text alignment, animations, images and icons orientation that have a directional meaning because react-native library doesn't handle it out of the box yet.

Making mobile apps RTL ready using react-native and Redux.

To overcome these issues, i figured out an other way of rendering app components with dynamic styles.

1. defining Strings object for different languages:

To define strings object for app languages, i use react-native-localization that uses a native library (ReactLocalization) to get the current interface language, then it loads and displays the strings matching the current interface locale or the default language, and it also inherit from react-localization bunch of useful apis such as setLanguage(languageCode), getLanguage() and getInterfaceLanguage().

First let's add react-native-localization dependency to the node_modules and link it with android and ios projects.

npm install react-native-localization --save
react-native link

then, i picked up arabic and english as the main languages for the demo as shown in the code snippets bellow :

import LocalizedStrings from 'react-native-localization';
export default new LocalizedStrings({
    en:{
        WELCOME:"Welcome to React Native!",
        GET_STARTED: "This  playground is made By Ayoub Hadar",
        EN: "English",
        AR:"Arabic",
        SELECT_LANG:"Select your language",
        POWERED:"Powered By #ReactNative"
    },
    ar: {
        WELCOME:"مرحبا بكم",
        GET_STARTED: " هذا التطبيق مبرمج من طرف أيوب حاضر",
        EN: "الإنكليزية",
        AR:"العربية",
        SELECT_LANG:"اختر لغتك",
        POWERED:"#ReactNative مشغل بواسطة"
    },
});
2. Create dynamic styles

the RTL playground screen contains a react-native dumb component that we would style dynamically depending on RTL state.

const template = (context,styles) => {
    return (
        <View style={{flex:1}}>
            {content(styles)}
            {changeLAng(context,styles)}
        </View>
    );
};


const  content = (styles) => (
    <View style={styles.container}>
        <Text style={styles.welcome}>
            {strings.WELCOME}
        </Text>
        <Text style={styles.instructions}>
            {strings.GET_STARTED}
        </Text>
        <Text style={styles.instructions}>
            {strings.POWERED}
        </Text>
    </View>
);

const changeLAng = (context,styles) => (
    <View style={styles.langContainer}>
        <Text style={{alignSelf:'center'}}>{strings.SELECT_LANG}</Text>
        <View style={styles.select}>
            <TouchableOpacity style={{flex: 1, backgroundColor:"#0F05"}} >
                <Text style={styles.text}>{strings.AR}</Text>
            </TouchableOpacity>
            <TouchableOpacity style={{flex: 1,backgroundColor:"#F005"}} >
                <Text style={styles.text}>{strings.EN}</Text>
            </TouchableOpacity>
        </View>
    </View>
);

First, lets create a class to style the lambda expression above :

export default class StyleSheetFactory{
    static getSheet(isRTL){
        isRTL ? i18n.setLanguage('ar'):i18n.setLanguage('en');
        return StyleSheet.create({
            container: {
                flex: 1,
                justifyContent: 'center',
                alignItems: isRTL ? 'flex-start': 'flex-end',
                backgroundColor: '#F5FCFF',
                paddingRight:isRTL ? 10:5,
                paddingLeft:isRTL ? 5:10,
            },
            welcome: {
                fontSize: 20,
                textAlign: isRTL ? 'right' : 'left',
                margin: 10,
            },
            instructions: {
                textAlign: isRTL ? 'right' : 'left',
                alignSelf: "center",
                marginBottom: 5,
            },
            langContainer:{
                alignItems:'flex-end'
            },
            select:{
                flexDirection: isRTL ? 'row-reverse':'row',
            }
        });
    }
}

Now, StyleSheetFactory class Contains a getSheet function which takes isRTL value as a prop and create our template styleSheet after determining witch language to set for this component depending on isRTL.

then , Let's create a single react-native component which render the template depending on iRTL state.

import React, {Component} from 'react-native';
export default class Module extends Component {
    constructor(){
        this.state{
          isRTL:false
        }
    }
    render() {
        // create and setLanguage for the dumb component
        const styles = StyleSheetFactory.getSheet(this.state.isRTL);
        return template(this,styles);
    }
}

const template = (context,styles) => {
    return (
        <View style={styles.container}>
            <Text style={styles.welcome}>
                {i18n.WELCOME}
            </Text>
            <Text style={styles.instructions}>
                {strings.GET_STARTED}
            </Text>
            <View style={styles.langContainer}>
                <Text>{strings.SELECT_LANG}</Text>
                <View style={styles.select}>
                    <TouchableOpacity style={{flex:1}} onPress={() => context.seState({isRTL:true})}>
                        <Text style={styles.text}>{strings.AR}</Text>
                    </TouchableOpacity>
                    <TouchableOpacity style={{flex:1}}  onPress={() => context.seState({isRTL:false})>
                        <Text style={styles.text}>{strings.EN}</Text>
                    </TouchableOpacity>
                </View>
            </View>
        </View>
    );
};

That's it , your component is now RTL Ready ! the styles will be re-rendered every time the state of isRTL changes.

the rendered screen looks like this :

Simulator-Screen-Shot---iPhone-6---2017-10-23-at-00.33.55.png
Simulator-Screen-Shot---iPhone-6---2017-10-23-at-00.33.51.png

the main purpose of the next step is to make sure that the other components switch to the desired script direction and language while the state is updated.

2. Implementing Redux.

Let's create a single module that handle his own state and dispatches it to the other modules.

here is a react-native module used for this demonstration :

Mwymodule.png

first, let's install the dependencies needed for this part :

npm install redux --save
npm install react-redux --save

then, let's create a reducer to handles state changes for our component and create a redux store that wraps the app through it's Provider component.

const defaultState = {
    isRTL:false
};

export default function settingsReducer(state = defaultState, action) {
    switch (action.type) {
        case "CHANGE_TO_AR":
            return {
                ...state,             // keep the existing state,
                isRTL:true
            };
        case "CHANGE_TO_EN":
            return {
                ...state,             // keep the existing state,
                isRTL:false
            };
        default:
            return state;
    }
};
import React, {Component} from 'react';
import {Provider} from 'react-redux';
import {combineReducers, createStore} from 'redux';
import Module from './module'
import settings from './module/reducer'

const store = createStore(combineReducers({settings}), {});

export default class App extends Component {
    constructor() {
        super();
    }

    render() {
        return (
            <Provider store={store}>
                <Module/>
            </Provider>
        );
    }
}

Now, to change isRTL state, let's connect our module with react-redux component as following :

import {connect} from 'react-redux';
const mapStateToProps = (state) => ({
    isRTL: state.settings.isRTL
});

const mapDispatchToProps = (dispatch) => ({
    changeAR: () => {dispatch({type: "CHANGE_TO_AR"})},
    changeEN: () => {dispatch({type: "CHANGE_TO_EN"})}
});

export default connect(mapStateToProps, mapDispatchToProps)(Module);

Each function inside the second argument is assumed to be a Redux action creator.

Moreover, the first argument inside connect function will be called any time the store is updated witch means that the other components wrapped inside the store provider will be re-rendered. We need to connect our components to the store and merge isRTL state with props by adding this code snippet :

const mapStateToProps = (state) => ({
    isRTL: state.settings.isRTL
});

export default connect(mapStateToProps)(OtherModules);

our Final demo component looks like this :

import React, {Component} from 'react';
import {TouchableOpacity, View, Text} from 'react-native'
import {connect} from 'react-redux'
import StyleSheetFactory from './style'
import strings from './strings'


class Module extends Component {
    render() {
        const styles = StyleSheetFactory.getSheet(this.props.isRTL);
        return template(this, styles);
    }
}
const template = (context,styles) => {
    return (
        <View style={{flex:1}}>
            {content(styles)}
            {changeLAng(context,styles)}
        </View>
    );
};


const  content = (styles) => (
    <View style={styles.container}>
        <Text style={styles.welcome}>
            {strings.WELCOME}
        </Text>
        <Text style={styles.instructions}>
            {strings.GET_STARTED}
        </Text>
        <Text style={styles.instructions}>
            {strings.POWERED}
        </Text>
    </View>
);

const changeLAng = (context,styles) => (
    <View style={styles.langContainer}>
        <Text style={{alignSelf:'center'}}>{strings.SELECT_LANG}</Text>
        <View style={styles.select}>
            <TouchableOpacity style={{flex: 1, backgroundColor:"#0F05"}} onPress={context.props.changeAR}>
                <Text style={styles.text}>{strings.AR}</Text>
            </TouchableOpacity>
            <TouchableOpacity style={{flex: 1,backgroundColor:"#F005"}} onPress={context.props.changeEN}>
                <Text style={styles.text}>{strings.EN}</Text>
            </TouchableOpacity>
        </View>
    </View>
);




const mapStateToProps = (state) => ({
    isRTL: state.settings.isRTL
});

const mapDispatchToProps = (dispatch) => ({
    changeAR: () => {
        dispatch({type: "CHANGE_TO_AR"})
    },
    changeEN: () => {
        dispatch({type: "CHANGE_TO_EN"})
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(Module);

You'll find related code for this demo on react-native-rtl-playground repository in github:

Ayoub Hadar

Ayoub Hadar

Ayoub est un codeur, passionné de nouvelles technologies, curieux et pragmatique, cherchant constamment à améliorer ses compétences sur les technologies Mobile et Iot

Tags:

signature

Ayoub Hadar has no other posts

Aloha from xHub team 🤙

We are glad you are here. Sharing is at the heart of our core beliefs as we like to spread the coding culture into the developer community, our blog will be your next IT info best friend, you will be finding logs about the latest IT trends tips and tricks, and more

Never miss a thing Subscribe for more content!

💼 Offices

We’re remote friendly, with office locations around the world:
🌍 Casablanca, Agadir, Valencia, Quebec

📞 Contact Us:

🤳🏻 Follow us:

© XHUB. All rights reserved.

Made with 💜 by xHub

Terms of Service