Switching to Arabic: Layout, Arrows, and the Restart That Lost the Language
You open your mobile app settings, toggle the interface language to Arabic, and wait for the screen to reboot. The application restarts, but to your surprise, the labels appear right back in English. Even stranger, the layout flipped horizontally, the product card margins are lopsided, and the top back arrow points directly into the screen border.
Persist the language before restarting, and restart only when the direction flips. Lay out with start and end, and flip only the icons that point somewhere.
This is Day 15 of Ship Native, the first of two lessons on Arabic and RTL architecture. In our previous module, we finished Day 14: Offline, API Down, or Slow: Show Saved Data and the Right Message. Today, we tackle layout direction in React Native: why naive restarts drop your chosen locale, how to lay out screens using logical properties, and how to mirror directional navigation icons while keeping media controls upright.
The companion video features interactive state diagrams, synthetic narration, and timed bilingual subtitles. The code patterns below come from production implementations tested in active consumer applications.
The symptom: a restart that forgot the language
In our consumer application Liana, QA engineers reported a baffling bug. A user would choose Arabic in settings, observe the splash screen appear during restart, and land back on a home screen rendered completely in English.
The bug occurred intermittently. On fast physical devices or during cold debug runs, Arabic would persist. On busy simulators or older hardware, the app rebooted in English almost half the time.
At the same time, screens that did switch had visual defects. A product card had extra margin on the wrong side, badges collided with headers, and navigation chevrons pointed backwards.
Translation runtime updates versus native direction flags
Understanding this bug requires separating text translation from layout direction:
- Text translation: Strings update at runtime in JavaScript using libraries like
i18next. Callingi18n.changeLanguage('ar')updates React context immediately without requiring an application restart. - Layout direction: The reading direction (Left-to-Right versus Right-to-Left) is controlled by the native iOS and Android bridge through
I18nManager.forceRTL. Changing this native flag takes effect only upon the next application launch.
Because the native direction flag takes effect on next launch, developers frequently call RNRestart.restart() right after changing the setting.
Here is the flaw in that approach:
// Anti-pattern: bare timer before restart
const switchLanguage = (newLang: 'en' | 'ar') => {
I18nManager.forceRTL(newLang === 'ar');
store.dispatch(setAppLanguage(newLang));
i18n.changeLanguage(newLang);
// BUG: RNRestart terminates the JavaScript thread immediately.
// The 300ms timer does not guarantee that asynchronous disk writes finished!
setTimeout(() => {
RNRestart.restart();
}, 300);
};
When RNRestart.restart() executes, it immediately terminates the JavaScript thread and reboots the native host activity. If Redux Persist or MMKV had pending asynchronous writes in queue, the process dies before the new language string reaches the disk. Upon reboot, the application reads the previous stored state and boots back in English.
The fix: await persistor.flush before rebooting
In our enterprise application Jeyad, we resolved this race condition by forcing all asynchronous write buffers to commit to disk before triggering the native restart:
// Jeyad AppHeader/index.tsx:59-63
export const handleLanguageSwitch = async (newLang: 'en' | 'ar') => {
const isArabic = newLang === 'ar';
I18nManager.forceRTL(isArabic);
I18nManager.allowRTL(isArabic);
await i18n.changeLanguage(newLang);
store.dispatch(setAppLanguage(newLang));
// Explicitly flush pending storage writes to physical disk:
await persistor.flush();
// Allow the native queue to stabilize, then restart safely:
setTimeout(() => {
RNRestart.restart();
}, 300);
};
By awaiting persistor.flush(), you guarantee that the updated language state is written to storage before the native process is killed.
Restart only when the layout direction flips
Rebooting an entire mobile application is jarring for users. If a customer switches from English to French, or from Arabic to Persian, the reading direction remains unchanged.
In Liana, we gate the restart check by comparing the current direction with the new direction:
// Liana languageConfig.ts:74-95
export const switchLanguage = async (newLanguage: AppLanguage): Promise<void> => {
const currentLanguage = store.getState().general.appLanguage;
const currentIsRTL = RTL_LANGUAGES.includes(currentLanguage);
const newIsRTL = RTL_LANGUAGES.includes(newLanguage);
// Update store and runtime strings:
store.dispatch(setAppLanguage(newLanguage));
await i18n.changeLanguage(newLanguage);
// Restart ONLY if the RTL direction changed:
if (currentIsRTL !== newIsRTL) {
I18nManager.forceRTL(newIsRTL);
I18nManager.allowRTL(newIsRTL);
await persistor.flush();
setTimeout(() => {
RNRestart.restart();
}, 300);
}
};
When switching between languages that share the same direction, the interface updates its strings instantaneously in place with zero downtime.
First-launch direction configuration
On initial install, configure the direction before React mounts. If a device locale is Arabic but the app defaults to English, setting the flag after mount would trigger a jarring visual flash or an immediate reboot loop.
Resolve the persisted language in your native root entry (index.js) or during your native splash screen sequence:
// Annia index.js:11
export const configureInitialDirection = (persistedLang?: string): void => {
const targetLanguage = persistedLang ?? 'en';
const shouldBeRTL = targetLanguage === 'ar';
if (I18nManager.isRTL !== shouldBeRTL) {
I18nManager.forceRTL(shouldBeRTL);
I18nManager.allowRTL(shouldBeRTL);
RNRestart.restart();
}
};
Physical versus logical styling properties
In React Native styling, physical properties bind permanently to the physical edges of the device glass, while logical properties adapt to the reading flow:
| Physical property (Avoid) | Logical property (Recommended) | Behavior in RTL (Arabic) |
|---|---|---|
marginLeft: 16 |
marginStart: 16 |
Flips to the right margin |
marginRight: 16 |
marginEnd: 16 |
Flips to the left margin |
paddingLeft: 12 |
paddingStart: 12 |
Flips to the right padding |
paddingRight: 12 |
paddingEnd: 12 |
Flips to the left padding |
left: 0 |
start: 0 |
Aligns to the right edge |
right: 0 |
end: 0 |
Aligns to the left edge |
borderTopLeftRadius: 8 |
borderTopStartRadius: 8 |
Rounds the top-right corner |
borderBottomLeftRadius: 8 |
borderBottomStartRadius: 8 |
Rounds the bottom-right corner |
Notice that marginStart and marginEnd mirror your intent automatically when the layout flips to Arabic.
Grep sweep and triage buckets
A regex sweep across your codebase finds candidate styles that might break in RTL:
grep -rnE '(padding|margin|border[A-Za-z]*)(Left|Right)[A-Za-z]*\s*:|[^A-Za-z](left|right)\s*:\s*[-0-9]' src/
Never assume every match is an error. When we audited a production codebase with 71 raw hits, triaging a sample of ten sites revealed three distinct categories:
- Real layout bugs (5 sites): Spacing on cards and lists written with
marginLeftthat crowded text against borders in Arabic. - Intentional physical anchors (3 sites): Close buttons and notification dots that deliberately stay pinned to the physical device edge regardless of reading direction.
- Noise (2 hits): Comments and non-layout identifier strings matching the search pattern.
Always review candidate lines in context before applying mechanical bulk replacements.
The textAlign verification
Many codebases contain conditional ternaries like textAlign: isRTL ? 'right' : 'left'.
In React Native, setting textAlign: 'left' already aligns text to the natural start of the reading line. In LTR English, it aligns to the left. In RTL Arabic, it aligns to the right:
// Liana SearchScreen/styles.ts:33
const styles = StyleSheet.create({
searchInput: {
flex: 1,
color: '#ffffff',
// React Native has no logical value for textAlign.
// 'left' is the default and flips with the RTL layout above it:
textAlign: 'left',
},
});
We verified this behavior directly on screen: setting textAlign: 'left' flips naturally with the container in Arabic, removing the need for redundant ternary logic.
Directional versus symbolic icons
A frequent pitfall in RTL migration is flipping every graphic asset unconditionally.
In an earlier release of Rakd, styles applied an unconditional rotation:
// Rakd Home/styles.ts:74
// Anti-pattern: rotating all graphics unconditionally
imgRTL: {
transform: [{ rotate: '180deg' }],
}
This caused product illustrations and badges to render upside down.
Divide your icons into two explicit categories:
- Directional icons: Icons that represent navigation sequence, progress, or back steps (back arrows, next chevrons). These must mirror in RTL.
- Symbolic icons: Icons that represent physical objects or concepts without direction (search magnifying glass, checkmark, clock, camera, audio volume). These must never mirror.
Crucially, media playback controls (PlayIcon, fast-forward) represent time moving forward along a timeline. In both English and Arabic, media timelines advance from left to right. Never mirror a play button.
Bake the direction guard into the icon component itself:
// Dynamo ArrowIcon.tsx:15
export const ArrowIcon: React.FC<IconProps> = ({
size = 24,
color = '#ffffff',
isRTL = I18nManager.isRTL,
}) => {
// Navigation chevrons mirror in RTL:
const rotation = isRTL ? '180deg' : '0deg';
return (
<Svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
style={{ transform: [{ rotate: rotation }] }}
>
<Path
d="M15 19l-7-7 7-7"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
};
In Arabic-first applications like Dynamo, the default SVG asset was designed pointing left. In that architecture, Arabic is 0deg and the English build is rotated 180deg.
Architectural limitations
Keep these technical constraints in mind:
- Static grep limitations: A script flags candidates, not confirmed bugs. Overlays, custom drawing canvases, and camera previews often require physical pins.
- Text inside SVGs: Applying
transform: [{ scaleX: -1 }]to an entire SVG container will mirror any text elements or numbers embedded inside the graphic. - Platform restart timing: iOS and Android handle native activity restarts with slight timing differences. Always verify the restart sequence on both platforms before shipping to production.
Verification steps
To verify language switching and RTL layout in your application:
- Switch language from English to French: confirm that labels update instantly in place with zero application restart.
- Switch language from English to Arabic: verify that
persistor.flush()finishes, the app restarts cleanly, and the interface boots in Arabic. - Verify navigation arrows: ensure that header back buttons point to the right in Arabic.
- Verify media controls: ensure that play buttons and search icons remain unmirrored.
- Inspect product cards: check that card margins and rounded corners mirror correctly using
marginStartandborderTopStartRadius. - Cold boot test: kill the app process completely and relaunch to confirm that the chosen language and RTL direction persist reliably.
Series roadmap
- Previous: Day 14: Offline, API Down, or Slow: Show Saved Data and the Right Message
- Next: Day 16: Arabic Text That Renders Wrong: Mixed Direction, Digits, and Dates
Comments