Supporting Right-to-Left (RTL) languages like Arabic and Hebrew is an important part of building globally accessible mobile apps. React Native already includes RTL support through I18nManager, but getting everything to work smoothly in an Expo-managed app usually takes a bit more setup.
In this guide, we’ll walk through a practical approach to implementing RTL support using i18next for localization, AsyncStorage for saving the user’s selected language, and I18nManager for controlling layout direction. We’ll also cover platform-specific behavior, including the iOS restart issue and a patch-based workaround for older React Native versions. :contentReference[oaicite:1]{index=1}
Step 1: Setting Up Translations
Start by organizing translations using JSON files. Two sample files might look like this:
translations/en.json
{
"welcome": "Welcome",
"change_language": "Change Language",
"hello": "Hello"
}
translations/ar.json
{
"welcome": "مرحبا",
"change_language": "تغيير اللغة",
"hello": "مرحبا"
}
Step 2: Initializing i18next with React Native
To handle localization, i18next and react-i18next are configured alongside AsyncStorage to persist the selected language. Here's the implementation, broken into logical chunks:
Import necessary modules
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { I18nManager } from 'react-native';
import en from './translations/en.json';
import ar from './translations/ar.json';
Define translation resources
const resources = {
en: { translation: en },
ar: { translation: ar },
};
Retrieve stored language preference (or default based on layout direction)
const getStoredLanguage = async () => {
const stored = await AsyncStorage.getItem('appLanguage');
if (stored) return stored;
return I18nManager.isRTL ? 'ar' : 'en';
};
Initialize i18n after fetching the preferred language
const initI18n = async () => {
const language = await getStoredLanguage();
await i18n.use(initReactI18next).init({
resources,
lng: language,
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});
const isRTL = language === 'ar';
I18nManager.allowRTL(isRTL);
I18nManager.forceRTL(isRTL);
};
initI18n();
export default i18n;
Important: Import this i18n setup file in your app's root layout before rendering the rest of your application. This guarantees that translations and layout direction are initialized before any UI is displayed.
Step 3: Switching Languages Dynamically
To allow users to change language at runtime and reflect RTL changes in the layout, the following function handles the switch:
import { I18nManager } from 'react-native';
import RNRestart from 'react-native-restart';
import AsyncStorage from '@react-native-async-storage/async-storage';
import i18n from './i18n';
const switchLanguage = async (selectedLanguage) => {
const isRTL = selectedLanguage === 'ar';
await AsyncStorage.setItem('appLanguage', selectedLanguage);
await i18n.changeLanguage(selectedLanguage);
I18nManager.forceRTL(isRTL);
RNRestart.Restart();
};
-
selectedLanguageis a regularuseStatevariable used to track the language the user wants to switch to. -
I18nManager.forceRTL()is used to change the layout direction. - After making this change,
RNRestart.Restart()is called to restart the app, ensuring that the layout updates are applied immediately.
Step 4: Handling Platform-Specific RTL Behavior
React Native applies RTL layout changes differently across platforms:
-
Android: Works correctly after a single reload using
RNRestart. -
iOS: Requires a full app restart and does not reflect changes even after reloads in versions prior to
0.79.0.
This behavior was addressed in React Native 0.79.0, where layout context updates dynamically. For projects using earlier versions, manual patching is necessary.
GitHub issue reference: https://github.com/facebook/react-native/pull/49455
Step 5: Supporting RTL in iOS for Versions Below 0.79.0
To enable RTL on iOS without upgrading React Native, a patch can be applied:
Install patch-package
npm install patch-package
# or
yarn add patch-package
Modify internal RN layout handling
Navigate to:
node_modules/react-native/Libraries/Utilities/I18nManager.js
Locate this line:
forceRTL: (shouldBeRTL: boolean): void => {
Add the following line immediately after the opening brace:
NativeI18nManager?.doLeftAndRightSwapInRTL(shouldBeRTL);
Generate the patch
npx patch-package react-native
Ensure patch is applied on every install
Update package.json:
{
"scripts": {
"postinstall": "patch-package"
}
}
This ensures the patch persists across installs and CI/CD pipelines.
Step 6: RTL-Aware Styling Guidelines
To build components that adapt seamlessly between LTR and RTL:
Use logical padding/margin properties
const styles = StyleSheet.create({
container: {
paddingStart: 16, // instead of paddingLeft
paddingEnd: 16, // instead of paddingRight
marginStart: 8,
marginEnd: 8,
},
});
Flip layout direction conditionally
import { I18nManager } from 'react-native';
const isRTL = I18nManager.isRTL;
const rowStyle = {
flexDirection: isRTL ? 'row-reverse' : 'row',
};
Align text appropriately
const textStyle = {
textAlign: isRTL ? 'right' : 'left',
writingDirection: isRTL ? 'rtl' : 'ltr',
};
These styling practices make the UI adaptive and prevent hardcoded visual inconsistencies.
Final Notes
Right-to-left support in React Native Expo can be achieved smoothly using a combination of i18next, persistent storage, I18nManager, and platform-specific fixes. By structuring the localization setup clearly and applying conditional logic for layout direction, applications can offer a rich, multilingual experience without disrupting the user journey, even in legacy environments.
Originally published on GeekyAnts Blog
Top comments (0)