Реализация поддержки RTL-языков (арабский, иврит) в мобильном приложении
Добавить арабский или иврит к уже работающему приложению — это не «поменять locale и выставить layoutDirection = rtl». Типичная картина после такой попытки: иконки «назад» смотрят вправо, но стрелка прогресс-бара так и идёт слева направо; текст в TextField выровнен правильно, но placeholder по-прежнему слева; анимации slide-in «приезжают» с неправильной стороны, а кастомный Canvas рисует графики зеркально только в режиме отладки на эмуляторе — на реальном устройстве нет.
Где реально ломается RTL
iOS — Auto Layout и SwiftUI
В UIKit поведение определяется semanticContentAttribute у UIView. По умолчанию .unspecified — система сама решает по locale. Проблема начинается с кастомных view: UIView.draw(_:), CALayer, Core Graphics — всё это игнорирует semanticContentAttribute и рисует в абсолютных координатах. Нужно явно применять CGAffineTransform(scaleX: -1, y: 1) или переписывать логику под userInterfaceLayoutDirection.
В SwiftUI HStack и padding(.leading) автоматически зеркалятся при layoutDirection == .rightToLeft. Но alignment: .leading в тексте при RTL — это уже правый край. Если вы явно указываете .trailing, всё переворачивается. Кастомные Shape через Path рисуются в абсолютных координатах — зеркалить придётся вручную через .environment(\.layoutDirection, .rightToLeft) в сочетании с transform.
Иконки из SF Symbols: часть из них имеет RTL-вариант (например, arrow.right → автоматически arrow.left при RTL). Но логотипы и брендовые иконки зеркалить нельзя — для них .semanticContentAttribute = .forceLeftToRight.
Android — LayoutDirection и Drawable
android:supportsRtl="true" в манифесте — обязательно, но недостаточно. start/end вместо left/right в XML-атрибутах (paddingStart, layout_marginEnd) — Lint поймает не всё, особенно в программно создаваемых LayoutParams.
VectorDrawable с autoMirrored="true" — правильный способ зеркалить иконки. Но BitmapDrawable и PNG из mipmap не зеркалятся автоматически. Нужно либо rotationY = 180f через код, либо отдельный ресурс в drawable-ldrtl-*.
В Jetpack Compose Modifier.padding(start = 16.dp) корректно зеркалится при RTL, а Modifier.offset(x = 16.dp) — нет. Разница неочевидна, пока не запустишь на арабской локали. Alignment.Start и Alignment.End работают правильно; Alignment.Left — нет.
Анимации. SlideInHorizontally в Compose принимает лямбду initialOffsetX: для LTR это -fullWidth, для RTL — +fullWidth. Без проверки LocalLayoutDirection.current анимация едет с неправильной стороны.
Как мы это реализуем
Аудит начинается с запуска приложения на арабской локали через adb shell setprop persist.sys.locale ar-AE (Android) и Settings → General → Language & Region (iOS Simulator) — это выявляет 80% проблем ещё до правок.
На iOS работаем с UIView.appearance(whenContainedInInstancesOf:) для системного наследования semanticContentAttribute, переписываем кастомные draw() под conditional flipping, проверяем все NSTextAlignment.natural vs .left в старых XIB-файлах (.natural зеркалится, .left — нет).
На Android проводим lint-аудит на hardcoded-left/right атрибуты, заменяем LayoutParams programmatically через MarginLayoutParamsCompat.setMarginStart/End, добавляем autoMirrored к векторным иконкам направления.
Flutter требует отдельного внимания: TextDirection.rtl прокидывается через Directionality widget. Но CustomPainter рисует в абсолютных координатах Canvas — зеркалирование через canvas.scale(-1, 1) с canvas.translate(-size.width, 0). Row с textDirection: TextDirection.rtl и стандартные виджеты Material работают корректно; проблемы — в кастомных компонентах и анимациях через AnimationController с hardcoded offset.
React Native — I18nManager.isRTL для условной логики, flexDirection: 'row' зеркалится при RTL автоматически на iOS, на Android — только с React Native 0.63+. Для старых версий нужен I18nManager.forceRTL(true) с перезапуском приложения.
Типичные ошибки, которые пропускают в ревью
- Жёстко заданный
textAlign: 'left'в кастомных компонентах вместо'auto'или'start' -
transform: [{scaleX: -1}]применён к иконке без проверкиI18nManager.isRTL— иконка зеркалится всегда - Числа в арабском тексте:
١٢٣(eastern arabic numerals) vs123— зависит от контекста отображения, нужно явно задаватьNSLocale/Locale - Бидиректциональный текст (арабский + английское название):
NSAttributedStringс явнымNSWritingDirectionAttributeNameиначе порядок слов ломается
Этапы работы
- Аудит — запуск на арабской/ивритской локали, скриншоты всех экранов, составление списка артефактов
- Классификация — layout issues, icon mirroring, text alignment, animation direction
- Правки — итеративно по компонентам, начиная с навигации и основных экранов
- Тестирование — на реальных устройствах (Samsung с One UI меняет поведение части компонентов относительно AOSP)
- RTL-скриншот-тесты — добавляем в CI, чтобы регрессии ловились автоматически
Полный RTL-аудит и реализация для приложения среднего масштаба: 3-5 дней. Стоимость зависит от количества экранов и платформ.







