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