Я — Денис, Android-разработчик в «Лайв Тайпинге». В этой статье я расскажу о том как адаптировать интерфейс в мобильном приложении под RTL в XML и Jetpack Compose. Вы узнаете, об особенностях RTL интерфейсов, отличиях поддержки RTL в XML и Jetpack Compose, и о том как поддержка RTL может привлечь новых пользователей.
Что такое RTL
RTL (right-to-left) — чтение справа налево встречается в языках ближнего востока и южной азии. Так читают, к примеру арабы и персы. LTR (left-to-right) — напротив чтение слева направо, так читаем мы и ещё миллиарды людей.
RTL контент в интернете занимает на начало 2024 года — 3%. Подробнее ниже в таблице.
Позиция | Язык | Распространение |
---|---|---|
1 | Английский | 51.7% |
4 | Русский | 4.5% |
12 | Персидский | 1.5% |
18 | Арабский | 0.6% |
Для 660 млн. человек — чтение справа налево привычно. На первый взгляд, чтобы адаптировать интерфейс под RTL нужно: перевести текст, выровнять его по правому краю и готово. На деле это не так.
LTR vs RTL интерфейс
Текст
Важно, перевести и зеркально отразить текст для чтения справа налево. Слова или фразы, которые остаются на оригинальном языке без зеркального отражения.
?? Три способа чтения:
LTR — слева направо, как в большинстве языках;
RTL — справа налево, в арабском, иврите, персидских языках;
BIDI — текст в направлении как слева направо, так и справа налево. Часто такое происходит, когда совмещаются символы из разных алфавитов.
Последовательность символов внутри строки
Физически в строке символы расположены последовательно, но за итоговое отображение этой последовательности на экране отвечает unicode bidirectional algorithm.
Вкратце:
для каждого символа в строке вычисляется направленность;
строка бьётся на блоки одинаковой направленности;
блоки выстраиваются в порядке, заданном базовым направлением.
На направленность каждого символа влияет тип и направленность соседних символов.
1) Сильно направленные. Направление определено заранее: для большинства символов это LTR, для арабских и ивритских символов — RTL.

2) Нейтральные — знаки пунктуации или пробелы. Направление не определено заранее, они принимают направление соседних сильно направленных символов.
Запятая между символами, направленными слева направо, "o" и "w" в строке "Hello, world", принимает направление этих символов, как в LTR, так и в RTL.

Нейтральный символ располагается между двумя сильно направленными символами разной направленности, он станет нейтрально направленным.

То же самое случается с нейтральными символами в конце строки.

3) Слабо направленные — не влияют на окружающие символы.
Непрерывные последовательности цифр упорядочиваются слева направо, однако два числа подряд, разделенные нейтральным символом, будут идти справа налево, если задано базовое направление RTL.

4) Зеркальные символы — меняют форму в зависимости от контекста. Открывающая скобка в RTL будет выглядеть как закрывающая в LTR и наоборот.
В большинстве случаев это не вызывает проблем, однако если скобки случайно имеют разное направление, визуально они будут обращены в одну сторону, особенно если скобка находится в конце строки.

Шрифт
Нужно проверить, что шрифт поддерживает RTL. В Apple это шрифт SF Arabic, а у Google Noto.
Текст кнопок и заголовков может выглядеть слишком мелким в арабском и иврите. Так как заглавные буквы не применяются — увеличьте размер шрифта на 10%.
Числа
Западные цифры не нужно переворачивать — многим арабам удобно ими пользоваться. Цифры на арабском языке идут справа налево. Это важно учитывать в полях для ввода номера телефона или суммы денег, где правильный порядок цифр играет роль.
Европейские цифры | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
Арабские цифры | ٠ | ١ | ٢ | ٣ | ٤ | ٥ | ٦ | ٧ | ٨ | ٩ |

Элементы, которые отображают прогресс располагаем по направлению чтения. При этом сами цифры переворачивать не нужно.

Иконки
Иконки, где есть движение, нужно отразить. Так как арабы читают справа налево, то и движение воспринимают тем же образом. Например, иконки велосипедистов, машин в LTR движутся вправо, в RTL должны влево.

Иконки с текстом, текстовые баблы, — отражаем, и обязательно адаптируем под язык пользователя.

Иконки «вперёд» и «назад» меняются местами. Если в LTR дизайне кнопка «назад» показывает влево, в RTL она смотрит вправо.

Элементы управления: свитчи, ползунки громкости, кнопки перелистывания треков — не отражаем. Ниже пример интерфейса плеера в музыкальном приложении.

Изображения переворачиваем, за единственным исключением когда страдает визуал. Объекты, предметы из оффлайн мира – отражать не следует. Например, движущийся велосипедист — это иконка движения, значит отражаем. А наушники или телефон — это предметы, оставляем неизменными.
Категорически нельзя переворачивать иконки брендов и значков по типу «✅». Так только запутаем пользователя.
Ниже приведу ещё несколько примеров того как правильно адаптировать иконки под RTL.

Косая черта в LTR может указывать на выключенное состояние как для языков LTR, так и для языков RTL. Поэтому в RTL такие иконки отражать не нужно.

Слайдер увелечения громкости звука в RTL нужно отразить.

Текст внутри иконок нужно адаптировать.
Настройка проекта
Android поддерживает RTL из «коробки» с API 17. Чтобы RTL заработал — укажите в Manifest android:supportRtl=”true”
. Библиотеки Facebook, Google, уже включают внутри себя поддержку RTL. Поэтому поддержка RTL будет «смёржена» автоматически внутрь Manifest, если эти библиотеки добавлены в проект.
Аналогично флагу в Manifest, добавить RTL можно и в рантайме:
override fun attachBaseContext(base: Context) {
super.attachBaseContext(object: ContextWrapper(base) {
override fun getApplicationInfo(): ApplicationInfo {
val origin = super.getApplicationInfo()
origin.flags = origin.flags or ApplicationInfo.FLAG_SUPPORTS_RTL
return origin
}
})
}
Особенности реализации в XML и Compose
XML
Если мобильное приложение изначально не разрабатывалось с поддержкой RTL, начните с автоматического рефакторинга в Android Studio. Для этого перейдите в Refactor -> Add Right-to-Left RTL Support
.

Рефакторинг заменит в XML файлах атрибуты, которые не подходят для RTL:
MarginLayoutParams.marginStart
вместоMarginLayoutParams.marginLeft
;android:paddingEnd
вместоandroid:PaddingRight
;android:marginStart
вместоandroid:marginLeft
;app:layout_constraintStart_toStartOf
вместоapp:layout_constraintLeft_toLeftOf
;android:gravity="end"
вместоandroid:gravity="right"
;android:drawableStart
вместоandroid:drawableLeft
;Gravity.START
вместоGravity.LEFT
;android:textAlignment
например наandroid:textAlignment="viewStart"
.
В рантайме можно принудительно заставить отображать View в RTL или LTR режиме. Это нужно для элементов, которые строго LTR или RTL.
layoutDirection = View.LAYOUT_DIRECTION_LTR
val View.isRtl: Boolean get() = layoutDirection = View.LAYOUT_DIRECTION_RTL
val Context.isConfigurationRtl: Boolean
get() = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
direction == if (isConfigurationRtl) View.FOCUS_RIGHT else View.FOCUS_LEFT
Compose
Jetpack Compose — RTL friendly фреймворк. Ничего настраивать не нужно. Но тем не менее для Compose работают те же принципы и подходы из XML. Не смотря на то, что Compose RTL friendly, — это молодой фреймворк и допускает ошибки.
В Compose также можно принудительно заставить отображать View в RTL или LTR режиме:
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
content()
}
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Учтите, что у CompositionLocalProvider
присутствует риск модификации элементов в дереве View. Для Row стоит использовать Arrangement для случаев, когда нужно только отзеркалить layout.
Особенности работы с картинками
если нужно добавить картинку для RTL — укажите это в спецификации ресурса;
auto-mirroring
— кнопка доступна при импортировании картинок в Android Studio. Нажмите, если хотите автоматически отзеркалить в режиме RTL.

Тестирование
Для тестирования RTL интерфейса можно не переключать язык на арабский или иврит. Перейдите в настройки разработчика — «Отразить интерфейс». Либо воспользуйтесь фичей per-app language.
В Jetpack Compose протестировать поведение RTL можно указав в параметр Preview локаль, которой присущ RTL. Например, @Preview(locale = "ar")
.
RTL в Webview
На отображение Webview мобильные разработчики повлиять не могут.
Возможные проблемы
Ошибки пользовательского опыта
Копирование
Что будет, если попытаться редактировать двунаправленный текст? Или хотя бы выделить и скопировать его часть? И то же самое при правке кода в редакторе и код-ревью — боль.
Landmarks: دبي مارينا مول — 600 m, داماك العقارية — 1.2 km
Форматирование
Представим, что приложение получило с бэкенда адрес и динамически подставило в строку. В RTL интерфейсе — это будет выглядеть некорректно. Ниже вариант до подстановки.

Строчка в арабском варианте ниже неправильно отображается.

Ниже правильный вариант.

Это можно исправить используя Bodi форматтер:
val suggestion = "6 Krasina Street, Omsk"
val bidiFormmater = BidiFormatter.getInstance()
String.format(
getString(R.string.did_you_mean),
bidiFormatter.unicodeWrap(suggestion),
)
Каверзный функционал
Календари
Направление дней недели, дат, месяцев, годов в RTL «обратное». Нажимаешь на кнопку перелистывания месяца вправо, а отображается предыдущий месяц. Это нужно правильно обработать.

Читалки
При чтении книги справа налево, важно учесть изменение направление навигации по страницам, главам и расположение элементов управления. В Bookmate и других приложениях Яндекс интерфейс адаптирован под RTL в 90% случаев.

Реализация RTL friendly проекта
Реализуем RTL friendly проект и посмотрим с какими сложностями мы столкнёмся во время адаптации реального экрана:
Ссылка на репозиторий — https://github.com/DenisPopkov/RTL_Support_App.

Предлагаю рассмотреть работу с RTL адаптацией с двух сторон: XML, Compose. Сперва переведём текстовые ресурсы на арабский язык.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">اشترك في LOGO العقل</string>
<string name="label_description">حدد التعريفة. سيتم خصم الأموال مباشرة بعد تأكيد الدفع. ويتم الشطب التالي في بداية كل فترة فوترة جديدة. يمكن إدارة اشتراكات LOGO من خلال قائمة ملف تعريف متجر التطبيقات.</string>
<string name="subscription_price">%s ₽</string>
<plurals name="month">
<item quantity="one">شهر</item>
<item quantity="two">%s أشهر</item>
<item quantity="few">%s أشهر</item>
<item quantity="many">%s اشهر</item>
<item quantity="other">%s اشهر</item>
<item quantity="zero">%s اشهر</item>
</plurals>
<string name="popular_label">شائع</string>
<plurals name="money_withdraw">
<item quantity="one">يتم خصم الأموال كل شهر</item>
<item quantity="two">يتم خصم الأموال كل %s أشهر</item>
<item quantity="few">يتم خصم الأموال كل %s أشهر</item>
<item quantity="many">يتم خصم الأموال كل %s اشهر</item>
<item quantity="other">يتم خصم الأموال كل %s اشهر</item>
<item quantity="zero">يتم خصم الأموال كل %s اشهر</item>
</plurals>
<string name="subscription_description_first_point">الوصول الكامل إلى جميع الدراسات الاستقصائية</string>
<string name="subscription_description_second_point">تفسير النتائج والتوصيات الشخصية</string>
<string name="subscription_description_third_point">تاريخ وديناميكية النتائج</string>
</resources>
Некоторые текстовые ресурсы пришлось вручную адаптировать под арабский язык, так как некоторые переносы строк не соответствовали дизайну или выглядели криво.
Для переноса строки в текстовых ресурсах используйте n\
вместо \n
.
Чтобы добиться в XML макете корректного поведения текстовых компонентов в RTL, добавьте атрибут:
android:textDirection="locale"
textDirection
— определит, как текст рендерится в зависимости от типа символа.
Параметры:
anyLTR
— если в тексте присутствуют сильно направленные RTL символы, то атрибут определяет, направление как RTL. В противном случае LTR, если таких символов нет;firstStrongLTR
— этот атрибут определяет направление текста по первому сильно направленному символу. В противном случае LTR, если таких символов нет;firstStrongRTL
— атрибут работает также как иfirstStrongLTR
. В противном случае RTL, если таких символов нет;locale
— направление будет соответствовать тому, что выставлено в системе;ltr
— направление слева направо;rtl
— направление справа налево.
Отдельный файл конкретно для RTL — не так эффективен, если разница между LTR интерфейсом не разительна. Так как придётся поддерживать изменения сразу в двух файлах, что обязательно приведёт к ошибкам.
Выше я рассказал, что числа важно правильно адаптировать. Если мы просто подставим числа в текстовый ресурс — они не будут адаптированы. Цену подписки и валюту, в которой она оформляется адаптируем, если есть на это требования. Вместо этого обернём их вот так:
NumberFormat.getInstance(Locale.getDefault()).format(monthAmount)
Теперь система в зависимости от языка, автоматически адаптирует числа под нужную локаль.
По итогу небольшого количества правок не адаптированного интерфейса экрана — получим такой результат в арабской версии.

Итог
В заключении приведу пример адаптации мобильного приложения для арабской аудитории. Yango music — Яндекс музыка для арабских пользователей.

Сервис работает в 18 странах, в том числе в ОАЭ, Израиле, Финляндии, Норвегии. В мае 2023 года сервис стал доступен в Пакистане. До этого работа тестировалась в Гватемале и Перу.
Опыт Яндекса показывает, что успех сервиса в родной стране, можно перенести на другую аудиторию. Яндекс музыка не единственное приложение, которое адаптировано для арабской аудитории. В сторах можно найти адаптацию Яндекс такси и Яндекс карт.
Очень сложно что-то сделать без знания языка
Вы будете думать, что всё готово, пока не покажете свой проект настоящему носителю языка. Разрабатывайте с точки зрения человека, не знающего никакого языка. Ведь даже такие очевидные для тебя слова, как, например, «Twitter», возможно, придётся переводить. И знаки препинания, оказывается, не на всей планете одинаковые.
Посмотреть, как изначально расположены символы в строке и почему они визуально расположились именно так, позволяет инструмент на сайте Юникода: http://unicode.org/cldr/utility/bidi.jsp.
За уточнениями лучше обращаться к людям, для которых вы локализуете продукт. Они подскажут удобно ли им, корректно ли это выглядит.
Итоговый рецепт
Сложно описать в одном списке всё, что нужно сделать, каждый продукт требует особых подходов и решений:
обязательно найдите носителя языка и покажи ему прототипы как можно раньше;
собирайте стили для LTR и RTL в разные файлы;
добавьте исключения для всего, что переворачивать не нужно и явно отразите то, что не перевернулось само;
не допускайте хардкода языковых конструкций (например, конкатенация строк через запятую), по возможности конфигурируй всё, включая знаки препинания. Это пригодится не только для RTL — к примеру, на греческом языке вопросительный знак — «;».
Если вы нашли неточности/ошибки в статье или просто хотите дополнить её своим мнением — то прошу в комментарии!