What You Need to Know About RTL Language Support in Mobile Apps
Imagine: you enter the UAE market, launch an Arabic version of your app — and get a flood of negative reviews. The menu is misaligned, buttons are unclickable, text gets cut off. Launching the app in Arabic, you may find that the "Back" button points right, and slides come from the left. Users are accustomed to the opposite order — and this causes cognitive dissonance. Without proper RTL support, even a functionally identical app feels broken, leading to a 30% drop in retention (data from our projects). We've adapted over 20 projects for Arabic and Hebrew and know all the pitfalls. Our team is ready to take on the full audit and turnkey RTL implementation.
Simply changing the locale and setting layoutDirection = rtl is only 20% of the work. The rest is manual adaptation of every component: icons, animations, navigation, text, and even gestures (e.g., swipes). Here's what actually breaks:
| Component |
iOS |
Android |
Flutter |
React Native |
| Icons |
SF Symbols with RTL variant work, but custom PNGs don't |
autoMirrored="true" for VectorDrawable, BitmapDrawable doesn't mirror |
Directionality doesn't affect CustomPainter |
I18nManager.isRTL for conditional rendering |
| Animations |
UIView.animate with absolute coordinates |
Animator with translationX requires checking layoutDirection |
SlideTransition with initialOffsetX — manual logic |
Animated.timing with useNativeDriver doesn't mirror |
| Text |
NSTextAlignment.natural mirrors, .left does not |
textAlignment="viewStart" for TextView |
textDirection: TextDirection.rtl for Text |
textAlign: 'auto' instead of 'left' |
| Navigation |
UINavigationController mirrors back button automatically |
ActionBar requires android:supportsRtl="true" |
Navigator automatically, but custom transitions don't |
ReactNavigation requires manual setup |
Our approach with custom flipping transforms is 2x faster than traditional rewriting of all screens for RTL from scratch and reduces rework time by 40%. This saves 30–50% of the budget compared to a full overhaul — typically $1,000–$3,000 per platform. We use automated tests to verify mirroring, catching errors early and lowering QA costs.
Why Standard RTL Solutions Don't Cover All Cases?
The detailed table above shows typical issues. Additionally, on iOS custom draw() and Core Graphics require conditional flipping to mirror content. On Android autoMirrored works only for VectorDrawable, not for BitmapDrawable. In Flutter Directionality doesn't affect CustomPainter, so we manually apply canvas.scale(-1, 1). In React Native, check the version: on Android I18nManager.isRTL works only in RN >=0.63; otherwise, forceRTL with a reload is required. We also handle advanced Unicode bidirectional algorithm features like explicit override characters and BidiReordering for mixed scripts.
How We Implement RTL Support
-
Audit. Run the app on Arabic locale via adb shell setprop persist.sys.locale ar-AE (Android) and Settings → General → Language & Region (iOS Simulator). This reveals 80% of issues. Capture artifacts on each screen — typically 10–15 per app with 50 screens.
-
Classification. Divide problems into 4 types: layout, icons, text, animations. For each type, a specific fix recipe. For example, for icons we apply conditional flipping; for animations, replace absolute coordinates with relative ones.
-
Fixes. On iOS, rewrite custom draw() for conditional flipping, use UIView.appearance(whenContainedInInstancesOf:) for system inheritance of semanticContentAttribute. On Android, replace left/right with start/end in XML and programmatically via MarginLayoutParamsCompat. For Flutter, mirror CustomPainter via canvas.scale(-1, 1). For React Native, check I18nManager.isRTL in each component.
-
Testing. Always on real devices: Samsung with One UI changes VectorDrawable behavior, and on older iOS versions UIView.appearance may not work. Add screenshot tests with RTL locales in CI. Run on 3–4 physical devices to cover 95% of cases.
More about testing
Use XCTest with XCUIRemote to simulate gestures and check mirroring. On Android, Espresso with DeviceLocale to switch locale. Integrate Firebase Test Lab for testing on real devices in the cloud.
Что входит в работу
- Полный аудит кода и выявление артефактов на всех экранах
- Исправление верстки, иконок, текста, анимаций
- Адаптация кастомных компонентов (Canvas, Core Graphics, CustomPainter)
- Настройка интеграции с бэкендом для передачи контекста RTL
- Ручное тестирование на 4+ реальных устройствах
- Интеграция автоматических скриншотных тестов RTL в CI/CD
- Документация по поддержке RTL для команды
- Доступ к репозиторию с примерами реализаций
- Обучение команды работе с RTL (2 часа онлайн)
- Месяц технической поддержки после сдачи
How Long Does RTL Rework Take Per Platform?
| Platform |
Audit (days) |
Fixes (days) |
Testing (days) |
Total |
| iOS |
1–2 |
2–3 |
1 |
4–6 |
| Android |
1–2 |
2–4 |
1 |
4–7 |
| Flutter |
1 |
1–2 |
0.5 |
2–3.5 |
| React Native |
1 |
2–3 |
0.5 |
3–4.5 |
Common Mistakes in RTL Development
- Hardcoded
textAlign: 'left' in custom components instead of 'auto' or 'start'
-
transform: [{scaleX: -1}] applied to icon without checking I18nManager.isRTL — icon always mirrors
- Number localization:
١٢٣ (eastern arabic numerals) vs 123 — depends on display context, need to explicitly set NSLocale / Locale
- Bidirectional text (Arabic + English name):
NSAttributedString with explicit NSWritingDirectionAttributeName, otherwise word order breaks
How to Incorporate RTL in the Design Phase
The best approach is to use start/end instead of left/right from the beginning, avoid absolute coordinates in animations, and check custom Drawable for autoMirrored. If the project is already built, we conduct an audit and fix all inconsistencies. Our engineers ensure correct operation in Arabic and Hebrew on all supported devices with clean, maintainable code. Мы гарантируем совместимость с последними версиями iOS и Android, а наши сертифицированные специалисты имеют 5+ лет опыта в локализации.
Don't let your users get lost in a mirrored maze — contact us for an RTL audit consultation. Order a consultation — we'll calculate the timeline and cost. The investment pays off within 2-3 months after entering the market.
Apple recommends using UIView.appearance() for inheriting semantic attributes; for more details, see RTL (Right-to-Left) on Wikipedia.
Mobile App Localization: i18n, RTL, Dynamic Language Switching
When localizing apps, we often encounter non-obvious errors: date 04/05 for an American is April 5, for a European it is May 4. Amount "1,000.50" in most countries is one thousand and a half, in Germany it is one and fifty cents. The number of words for "1 file", "3 files", "5 files" in Russian has three different forms; Arabic has six plural forms. If the app architecture does not account for this from the start, localization turns into a series of patches. Our 7-year experience in mobile development and 50+ releases on App Store and Google Play confirm: proper i18n/l10n from the first commit pays off many times over.
How to Set Up Basic Localization Infrastructure?
iOS: From Localizable.strings to String Catalogs
Historically, strings in iOS were stored in Localizable.strings — a simple key=value format. With Xcode 15, String Catalogs (.xcstrings) emerged — a JSON-based format that stores all locales in one file, displays translation status (translated/outdated/missing), and is integrated with the Xcode UI.
String(localized: "welcome_title") in Swift 5.7+ replaces NSLocalizedString(...). Shorter, type-safe. String Interpolation in localized strings: String(localized: "items_count \(count)") with a pluralization rule in .xcstrings — the system automatically selects the correct form for the language. Pluralization via .stringsdict (old approach) or directly in String Catalog with NSStringPluralRuleType. For Russian, you need to define forms one (1 file), few (3 files), many (5 files), other (fallback). Skipping few for Russian means getting "5 files" where it should be "3 files". String Catalogs cut setup time by half compared to .stringsdict.
Android: XML Resources and String Formats
res/values/strings.xml for base locale (en), res/values-ru/strings.xml for Russian. Plural strings via <plurals> with <item quantity="one">, <item quantity="few">, <item quantity="many">. resources.getQuantityString(R.plurals.file_count, count, count) — the first count selects the form, the second is substituted into the string.
In Compose: stringResource(R.string.key) and pluralStringResource(R.plurals.file_count, count, count). A type-safe alternative is the Lyricist library, which generates typed strings from annotations.
Android App Bundle with android:splitByLocale="true" in bundle.gradle — resources are delivered only for device languages. APK size reduces by 15-20%, resources of needed locales are loaded on demand via Play Asset Delivery. Important: on Android 8+ Configuration.locales is a list, not a single language.
Flutter: intl and Abstraction Layers
Flutter intl package is the standard. AppLocalizations.of(context).welcomeTitle is generated from ARB files (app_en.arb, app_ru.arb). flutter gen-l10n generates typed code. Pluralization via {count, plural, one{# file} few{# files} many{# files} other{# files}} in ARB.
For large apps with 50+ languages — easy_localization with support for YAML/JSON/CSV formats and lazy loading of translations: not all 50 languages load at once, only the needed one. This reduces initial load size by 30%.
Comparison Table of Approaches
| Parameter |
iOS (String Catalogs) |
Android (XML) |
Flutter (ARB) |
| Storage format |
JSON (.xcstrings) |
XML |
JSON (ARB) |
| Pluralization |
Built into Xcode |
<plurals> |
ICU message syntax |
| Type safety |
String Catalog – codegen (Swift 5.9) |
R.java / ViewBinding |
gen-l10n |
| Translation status |
Visual in Xcode |
Third-party tools only |
Third-party tools only |
| RTL out of the box |
Auto Layout (leading/trailing) |
supportsRtl + start/end |
Directionality Widget |
How to Implement RTL Support Without Rewriting UI?
Arabic, Hebrew, Persian, Urdu are RTL (Right-to-Left) languages. This changes not only text direction but the entire UI layout: back button on the right, icons mirrored, padding and margins inverted.
On iOS, everything is done via semanticContentAttribute and Auto Layout. Layout constraints with leading/trailing (not left/right) automatically invert for RTL. UIView.semanticContentAttribute = .forceRightToLeft for a specific component. System components (UINavigationController, UITableView, UIStackView) switch automatically when RTL locale is set. Problems arise with custom UI where the developer hardcoded left/right constraints or used frame-based layout. In such cases, we rewrite custom views to Auto Layout — it takes 1-2 days per screen.
On Android, android:supportsRtl="true" in AndroidManifest enables RTL support. Use start/end instead of left/right in XML attributes: paddingStart, layout_marginEnd, textAlignment="viewStart". Use LayoutInflater with android:layoutDirection="rtl" for preview. Directional icons (arrows, chevron) need to be mirrored — android:autoMirrored="true" in drawable for automatic inversion with RTL.
On Flutter, Directionality widget with TextDirection.rtl controls direction for the subtree. Use Padding(EdgeInsetsDirectional.fromSTEB(...)) instead of EdgeInsets.only(left:...). Row automatically respects TextDirection from Directionality. Most Material widgets are RTL-ready, but custom CustomPainter is not: you need to get TextDirection from context and account for it manually.
Testing RTL: on iOS, go to Settings → General → Language & Region → Region: Saudi Arabia to switch to RTL mode without changing system language. On Android, adb shell setprop debug.force.rtl 1 forces RTL for debugging. This catches up to 80% of RTL issues before release.
Why Is Dynamic Language Switching a Bottleneck?
Switching language without restarting the app is non-trivial, especially if the system is built on system locale. We identified three main approaches.
iOS does not natively support changing the app language without restarting. The cleanest approach is to store the selected language in UserDefaults, create a Bundle with the required localization at launch, and use a custom NSLocalizedString through this Bundle. Bundle.setLanguage("ru") via swizzling Bundle.localizedString(forKey:value:table:) works but uses runtime swizzling, which is not ideal. Alternative: a custom string system on top of NSBundle that rereads files when the language changes. On switch, recreate the root ViewController.
Android with API 33: LocaleManager.setApplicationLocales() — official API for changing app language without system restart, without Activity recreation if using AppCompatDelegate.setApplicationLocales(). Below API 33 — Configuration.setLocale() + recreate() for Activity. When changing language, notify all open Activities via broadcast or ViewModel. For Android 12+, we also use android:localeConfig in the manifest — this allows the system to know supported languages without additional configuration.
Flutter — the simplest of the three. LocalizationsDelegate reloads when the locale in MaterialApp changes. Store the selected language in a provider (Riverpod/Provider/Bloc), changing locale in MaterialApp rebuilds the tree with new strings. Virtually no boilerplate when using easy_localization. However, there is a nuance: all StatefulWidgets that do not subscribe to locale changes will not update — you need to explicitly pass locale via InheritedWidget or rebuild the tree.
How to Format Dates, Numbers, and Currencies?
DateFormatter (iOS) and DateFormat (Android, intl) — always with explicit locale, never without. DateFormatter().dateStyle = .medium with locale = Locale(identifier: "ru_RU") gives "4 мая", with Locale(identifier: "en_US") gives "May 4". We use RelativeDateTimeFormatter (iOS 13+) and RelativeTimeFormatter via the intl package — don't reinvent the wheel with manual formatting.
NumberFormatter / NumberFormat.currency() for currencies. Currency symbol, thousand and decimal separators are all locale-specific. Hardcoding "₽" or "." as separator is an error. Locale(identifier: "ru_RU") + NumberFormatter.numberStyle = .currency with currencyCode = "RUB" gives correct formatting automatically.
What Are Typical Localization Mistakes?
String concatenation instead of formatting: "Hello, " + name + "!" works for SVO languages, but in Japanese the name comes before the greeting. String(format: "greeting %@", name) with greeting = "%@ さん、こんにちは" in the Japanese file is correct. Fixed UI size for text: German is on average 30% longer than English. Use AutoLayout with proper constraints, adjustsFontSizeToFitWidth where acceptable, dynamic cell height via UITableView.automaticDimension. Images with embedded text require localized versions or replacement with text overlay.
Work Process: How We Localize an App Turnkey
-
Analysis and audit (2-5 days) — code review for i18n readiness, identify hardcoded strings, assess RTL complexity, prepare screen map.
-
Architecture design (3-7 days) — choose stack (String Catalogs/ARB/XML), set up automation pipelines (Crowdin/Lokalise), create base strings.
-
Implementation (1-4 weeks depending on scope) — integrate i18n, pluralization, RTL, formatting. In parallel, translate content.
-
Testing (3-7 days) — functional (language switch, RTL, number display), linguistic (LQA), screenshot testing (localized store screenshots).
-
Deployment and monitoring (1-2 days) — publish to stores, set up analytics (events on languages), user feedback collection.
Deliverables
- Source code with full i18n infrastructure
- Documentation on adding a new language (playbook)
- Translation files (ARB/XML/xcstrings) + glossary
- Automation: CI/CD integration with translation services
- Access to repository with full revision history
- Training for the client's team (1-2 sessions)
- Code warranty — 6 months, free localization bug fixes
Timeline and Cost
| Stage |
Timeline (approx.) |
What's included |
| Adding one new language (no RTL) |
2-3 days technical work + translation time |
String setup, testing, store screenshots |
| First-time localization from scratch (10-15 screens, no RTL) |
2-3 weeks |
Architecture, translation, testing |
| Project with RTL support and dynamic switching |
4-6 weeks |
Everything included + UI adaptation, custom component rewrite |
Pricing is customized based on project scope. Contact us for a detailed estimate. On average, automation achieves 40% budget savings. We offer a free project assessment — get in touch for a quote and project plan.
Why Choose Us
We are certified developers (Apple WWDC Scholarship, Google Associate Android Developer). Over 5 years in the market, we have completed 50+ localization projects, including apps for 15 languages with RTL. We guarantee compliance with App Store Review Guidelines (Section 4.2/5.1) and Google Play Policies (User Data). Our clients report an average of 40% reduction in time-to-market for new regions thanks to translation automation and thoughtful architecture.
Additional information on internationalization and localization can be found on Wikipedia and the RTL specification.
Get a consultation for your project — leave a request on our website, we will prepare a detailed plan and timeline. Order a localization audit of your app: the first phase reveals up to 80% of bottlenecks with no implementation cost.