Как мы приложение Додо Пиццы на арабский переводили

Что вы знаете о том, как добавить поддержку языков, которые пишутся справа налево (Right to Left, RTL), в iOS-приложение? Нужно использовать leading и trailing вместо left и right, а ещё… Вот и мы больше ничего не знали, но пришлось разобраться.

Мы готовим приложение Додо Пиццы к локализации на арабский язык. В статье хотим поделиться находками и рассказать, зачем нам поддержка RTL в приложении, почему не достаточно просто адаптировать вёрстку в коде для поддержки RTL, зачем мы перерисовывали иллюстрации и чем отличается арабский знак процента от европейского. Ещё покажем много скриншотов и поделимся шпаргалками по поддержке RTL в коде.

Breaking news! Додо открыли пиццерию в Дубае. Для разработчиков это значит, что нужно подготовить приложение для запуска в новой стране. Обычно бóльшая часть работы здесь ложится на бэкендеров, а нам в приложениях нужно только новый язык добавить. 

Локализация-то у нас уже давно настроена, но запуск в Дубае особенный. Локализация приложения для Дубая отличается от локализации для всех предыдущих стран, потому что в арабском языке буквы пишутся справа налево. А это значит, что люди читают справа налево. И весь контент они воспринимают справа налево. У них меняется направление взгляда. А значит и в приложении…

Вначале включаем RTL

Мы начали с того, что погуглили, а как это вообще делать исследования проблемы и поиска вариантов решения нашей задачи.

У Apple есть хорошие материалы по теме, можно начать с них:

Затем решили прочувствовать RTL на себе и пошли тыкать наше приложение. Если вы захотите посмотреть, как будет выглядеть ваше приложение с включенным RTL, то вот как это можно сделать:

В Xcode перейти в Edit Scheme (option+click по текущему таргету) → Перейти на вкладку Options → В App Language выбрать Right-to-Left Pseudolanguage вместо System Language → Перезапустить приложение

В выпадающем меню Xcode будет два pseudolanguage: Right-to-Left и Right-to-Left With Right-to-Left Strings. 

Первый просто выровняет все локальные строки приложения направо. Второй перевернёт в них буквы.

e0055ebb6470e38aa15d7bb62ce8b5f0.png

Обратите внимание, что при выборе Right-to-Left With Right-to-Left Strings изменятся только локальные строки. Строки, которые приходят с бэка, будут показываться в том виде, в котором он их присылает.

Смотреть только на своё приложение — плохой вариант, потому что нет насмотренности на RTL-приложения. Чтобы эту насмотренность развить, мы смотрели разные адаптированные приложения и сайты.

А после того, как мы немного привыкли к RTL и всё рассмотрели в нашем приложении, мы приступили к составлению документа со скриншотами всех экранов. Делали по два скриншота каждого экрана (обычный и с включённым RTL), а потом для каждого экрана описывали, что работает не так, как должно. Когда мы это сделали, стало понятно, что многие проблемы повторяются от экрана к экрану. Мы их сгруппировали. Дальше расскажем про каждую группу проблем.

Удивляемся работе Xcode с RTL-строками

Бонус-раздел. Зацените, как Xcode ведёт себя с RTL-строками:

На видео всегда нажимаем стрелочку вправо.

Для сравнения, в Android Studio поведение отличается. В студии курсор всегда движется в том направлении, какую стрелочку ты нажимаешь на клавиатуре. Может, это настраивается где-то, мы не смотрели.

Смотрим, что там сломалось в приложении

Давайте пройдёмся по группам проблем.

Вёрстка вьюшек и ячеек

При адаптации приложения под RTL-языки iOS делает бóльшую часть работы за нас. В приложении есть экраны, которые уже выглядят так, как и должны, когда включаешь RTL.

Самая большая группа проблем — это когда вьюшки свёрстаны так, что iOS не может сама их правильно перевернуть. 

В каждую из таких вьюшек нужно зайти, разобраться, почему возникает проблема, и поправить её руками.

Посмотрите, например, на фиолетовый виджет активного заказа:

Заголовок и основной текст нужно выровнять по правому краю, картинку и основной текст — поменять местами.

В чём ошибка: виджет активного заказа свёрстан на фреймах, которые не учитывают RTL-ориентацию. Мы предпочитаем не использовать вёрстку на фреймах, но в данном случае это обосновано требованиями к виджету.

Чтобы виджет корректно отображался, нужно узнать текущую ориентацию с помощью свойства view.effectiveUserInterfaceLayoutDirection и пересчитать фреймы для RTL-ориентации.

Окна с пищевой ценностью и с ингредиентами, которые можно исключить, совсем поплыли:

5b3d62521ff5b3c7b67eb8eff0083f41.png2a53e2ed90d2e7c648a8127176df120c.png

В чём ошибки:

  • в окне с КБЖУ у всех названий стояло выравнивание слева, у всех значений — справа. Нужно учитывать RTL при выставлении textAlignment;

  • в окне с ингредиентами у кнопки «Закрыть» установлен alignment = .left, а список ингредиентов свёрстан на фреймах.

    Адаптация приложения под RTL стала отличным поводом заглянуть на все экраны и убедиться, что используемые компоненты соответствуют дизайн-системе. Так, например, мы полностью переделали диалог удаления ингредиентов.

Чекаут тоже выглядит не очень хорошо:

109e7f58483d225decbec1d73a20bd44.png

В чём ошибка: кажется, что экран совсем рассыпался, но на самом деле всё не страшно. Просто стоял неправильный textAlignment для UILabel, а картинки не были отзеркалены.

На списке стран вначале должен отображаться флаг, потом название страны. Значит, в RTL флаг должен быть справа от страны.

7888c3215fb95435b8b65bb18fc16099.png

В чём ошибка: флаг — это эмодзи. В коде было так:

var nameWithFlag: String { 	
    "(isoCode.emoji) (name)"
}

Обратите внимание на кнопку «Показать ближайшие пиццерии»:

В чём ошибка: у кнопки были захардкожены left и right imageEdgeInsets на сториборде. Переделали на NSDirectionalEdgeInsets, которым можно задать leading и trailing инсеты, вместо left и right.

Также в коде встретилось огромное количество мест с повторяющимися ошибками:

  • у UILabel установлен textAlignment = .left или textAlignment = .right. В зависимости от сценария нужно использовать textAlignment = .natural или устанавливать textAlignment в зависимости от значения effectiveUserInterfaceLayoutDirection. В случае использования .natural, если у пользователя установлена LTR-ориентация, то текст будет выравниваться по левому краю, а если RTL, то по правому;

  • кое-где использовались констрейнты left и right вместо leading и trailing;

  • парочка экранов была свёрстана с использованием ​​PinLayout и не поддерживала RTL. Откуда у нас в проекте вообще взялся PinLayout? Это уже совсем другая история, однажды расскажем её в Telegram-канале Dodo Mobile;) 

Кастомные UI-элементы

Бывает, что разработчики всё сделали как положено: использовали leading и trailing, чтобы, если вдруг будет RTL, всё работало чётко. В нескольких местах проекта мы столкнулись с ошибками, связанными с этим. Так получилось с полем ввода номера телефона:

В чём ошибка: iOS отрабатывала правильно — автоматически переворачивала поле для ввода номера, потому что в вёрстке использовались leading и trailing констрейнты. Но нам нужно, чтобы поле всегда отображалось слева направо (как в России), потому что номера телефонов всегда пишутся слева направо. Проще всего было зафиксировать поле с помощью semanticContentAttribute = .forceLeftToRight.

Забавно сломался инпут для ввода кода из СМС. При вводе цифры в нём появляются как обычно (слева направо), а точки на фоне исчезают справа налево. На скриншоте введены три цифры и осталась видна одна точка — под единицей.

В чём ошибка: точки-плейсхолдеры лежат в UIStackView. Стек автоматически переворачивается при включении RTL, но ему тоже можно установить semanticContentMode = .forceLeftToRight.

На пиццах из половинок скруглённые индикаторы прокрутки отзеркалились и теперь находятся по центру пиццы:

88d51a95a71beeb17f2a06722a16cbbf.png

В чём ошибка: к полосам прокрутки стояли констрейнты leading и trailing. iOS их переворачивала. Но экран пиццы из половинок универсальный для RTL и LTR, поэтому на нём ничего не нужно переворачивать. Поменяли констрейнты на left и right.

У всплывающей подсказки стрелочка должна переместиться налево:

3e4bd214dbf53100d36f69a07189c284.png

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

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

813bf03dd86d129b8228681df95f3ea6.png

В чём ошибка: для каждого сегмента рассчитывались фреймы. Добавили альтернативные расчёты для RTL.

Коллекции

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

24f73583b7445ecce19905ca078bfe94.png

Некоторые коллекции, например, категории в меню, отзеркалились (стали выровнены справа). Но элементы в них располагаются не в правильном порядке. На скриншоте категория «Пицца» должна быть первой, то есть самой правой.

3497c0d5c78ba956c180d245f1a8c422.png

На экране с продуктами, которые можно приобрести за додокоины, элементы в коллекции стоят на нужных местах, но по умолчанию показывается последний элемент. В этой коллекции первая категория товаров — это напитки. Если на скриншоте проскролить категории вбок, то напитки будут стоять на нужном месте.

73b4bdb8c0627b019d5b6dc8ac15e830.png

Для всех проблем с коллекциями нам помогло одно решение — использовать UICollectionViewFlowLayout, который поддерживает RTL. Чтобы UICollectionViewFlowLayout отзеркаливался, следует создать класс, наследующийся от UICollectionViewFlowLayout, и переопределить свойство flipsHorizontallyInOppositeLayoutDirection. По умолчанию оно false, а должно быть true. Тогда коллекции с таким лейаутом будут отзеркаливаться.

class RTLSupportedCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override var flipsHorizontallyInOppositeLayoutDirection: Bool {
        true
    }
}

Иконки, которые имеют направление

Отдельная тема — это картинки. Некоторые из них нужно отзеркалить, а некоторые — нет. Это целая наука!

Посмотрим на экран выбора способа доставки. Курьера на первой иконке нужно отзеркалить, потому что иконка показывает направление движения. То есть курьер должен идти навстречу пользователю, а не от него.

452a293a996b383106a0f60bc8b5d749.png

Нож и вилку не надо зеркалить. Мы узнавали, в арабских странах держат нож в правой руке.

На следующем скриншоте около адреса доставки стрелочка не перевернулась. Её нужно перевернуть.

34eb4a633cc1b58aeb662cbef859abed.png

Звёздочку на виджете активного заказа тоже нужно перевернуть, потому что люди, у которых выбран арабский язык, будут смотреть на виджет справа налево. Звёздочка должна встречать взгляд пользователей лицом, а не спиной.

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

А вот бейджик »2в1» на пицце нужно переместить в другой угол.

На этом этапе нужно рассмотреть каждую картинку в приложении и решить, какие из них перевернуть, а какие нет.

Для тех иконок, которые нужно перевернуть, можно выставить специальную настройку:

let image = UIImage(named: "some-image") 
imageView.image = image?.imageFlippedForRightToLeftLayoutDirection()

Этот код проверяет выбранную ориентацию и говорит imageView перевернуть картинку.

В документации Apple встречается код, который через NSAffineTransform отзеркаливает саму картинку. Но мы не стали использовать этот вариант.

Не каждую картинку можно автоматически отзеркалить. Например, у нас есть картинки с логотипом Додо (оранжевой буквой D и вписанной в неё птичкой). Если их отзеркалить, то логотип будет перевернут — так делать неправильно.

Картинки, которые нельзя автоматически перевернуть, нужно перерисовать и положить в проект. Для такого ассета можно настроить локализацию:

Бейджики на фото в меню

3815d75ed281a538dc54c0603d19b040.png

Про бейджики на фото уже упоминалось выше. В RTL они должны располагаться в левом верхнем углу фото.

Сейчас бейджики — часть фотографии. В планах сделать так, чтобы они рисовались в приложении. Тогда мы сможем автоматически менять их расположение при включении RTL. А пока это ещё одна из причин не переворачивать фото.

Анимации

Все анимации тоже стоит перепроверить, у нас их немного, но и те, что есть, в RTL работают неправильно.

Анимация добавления в корзину

Одна из вещей, которая меняет своё направление при включении RTL, — это UITabBar. Самая первая вкладка будет располагаться справа, а последняя — слева.

Зацените, как ведёт себя анимация добавления товара в корзину. Товар всегда улетает в крайнюю правую вкладку таббара. А должен улетать в корзину.

Анимация статуса заказа

Анимация загрузки должна происходить в другую сторону.

Third party

28d5f6661a3dcbeff5ff9553db5d7c88.png

Не забываем проверить сторонние сервисы. Вот как обстоит ситуация у нас.

На трансляции из Ivideon есть текст. У этого текста выравнивание по центру, а сам текст можно поменять в настройках трансляции. Значит, проблем нет.

А вот чат и капча совсем не адаптированы под RTL. Самостоятельно мы это исправить не сможем, значит, придётся обсуждать потенциальные доработки с разработчиками SDK.

WebView

1730b6a2d025150bdfa10a70bc496c93.png

У нас есть WebView в проекте. Они открывают ссылки на наш сайт, гугл-документы, социальные сети и так далее.

С самими WebView ничего делать не нужно, но нужно не забыть прицепить ссылки на локализованные страницы.

Разбираемся, где баг, а где фича

Когда мы первый раз включили RTL в приложении, то наши мозги сломались. 

Это очень тяжело и непривычно воспринимать. Всё выглядит дико и ты не понимаешь: это выглядит плохо, потому что непривычно или потому что это сломано.

А что-то выглядит нормально, и ты не понимаешь: ты уже привык к RTL или это выглядит нормально, потому что элемент отображается неправильно и выровнен слева направо.

Была пара мест, в которых мы вначале напряглись, а потом оказалось, что всё нормально. В этом разделе расскажем про такие места.

Поля ввода

Первое — это поля ввода. 

Мы переключили приложение в RTL и начали тыкать по полям ввода, чтобы что-то написать. Текст появлялся не с той стороны от каретки, а сама каретка не двигалась. Мы расстроились, что это придётся фиксить.

Оказалось, что если поле ввода находится в режиме RTL и ты начинаешь вводить русские/английские буквы, то они появляются слева от каретки, а сама каретка остаётся на месте. 

Зато если ты начинаешь вводить арабские буквы, то они появляются справа от каретки, а сама каретка смещается влево. 

Так и должно быть.

Поле ввода промокода

bbe73281cddba7bff98926fd00b25546.png

Промокоды у нас содержат только английские буквы. Английские буквы пишутся слева направо. Значит, поле ввода должно быть выровнено слева.

Plural-строки (.stringsdict)

9adcb0b61c91d9f37e1d350cfbda0a87.png

Ещё у нас сломались все множественные формы строк в приложении.

Мы в самом начале статьи рассказывали про два вида pseudolanguage в проекте. Оказалось, что тот, который переворачивает буквы, ломает plural-строки.

Ошибка возникает только при отладке, на проде с арабскими строками всё работает корректно.

Радуемся, что не всё сломалось

Нам не потребовалось ничего дорабатывать в:

  • UINavigationBar (в Android, например, все стрелочки «Назад» в навбаре сломались);

  • UITabBar;

  • UIStackView;

  • UIPageViewController;

  • Swipe to delete в таблицах;

  • сепараторы в таблицах (у нас некоторые сепараторы имеют отступ от границы экрана только с одной стороны. Вот этот отступ и должен отзеркалиться. На Android это автоматически не заработало);

  • поля ввода (UITextField, UITextView, UISearchBar);

  • анимации UINavigationController (push/pop) и возвращение назад свайпом.

Все перечисленные элементы автоматически переворачиваются при переключении на RTL-язык: меняется порядок табов, элементов в стеке, направление анимаций UINavigationController и так далее. 

Мы были приятно удивлены, когда убедились, что это тоже не требует доработок:

Разбираемся с неочевидными штуками

Во время локализации приложения на арабский язык недостаточно адаптировать UI, чтобы он корректно отображался для RTL-языков. Важно учесть и другие особенности языка и культуры.

Восточные цифры

В арабском языке для обозначения чисел используются восточные арабские цифры. Они отличаются от тех, к которым мы привыкли:

В приложении есть много мест, где используются цифры: цены, граммовки, номера телефонов и так далее. Почти во всех местах нужно использовать восточные цифры, но бывают исключения. Например, в ценниках и номерах телефонов нужно использовать восточные цифры, а вот для номеров банковских карт используются только западные цифры.

С помощью форматтеров можно конвертировать одни цифры в другие:

extension String {
    
    var toLatnDigits: String? {
        let numberFormatter: NumberFormatter = NumberFormatter()
        numberFormatter.locale = Locale(identifier: "ru_RU")
        guard let latnNumber = numberFormatter.number(from: self) else { return nil }
        return numberFormatter.string(from: latnNumber)
    }
}

Сами же числа всегда пишутся слева направо, в том числе и номера телефонов.

Кстати, в некоторых случаях iOS самостоятельно конвертирует западные цифры в восточные. Например, если бы эта строка была на арабском:

"resendCodeInDSecWithParam" = "Если код не придет,\n можно получить новый через %d сек";

То в форматированную строку подставилось бы количество секунд, написанное с использованием восточных цифр:

let string = String.localizedStringWithFormat(
    NSLocalizedString(
        "resendCodeInDSecWithParam",
        bundle: bundle,
        comment: "Signup screen"
    ),
    Int(self.seconds)
)

Не забываем проверить поля ввода, предполагающие ввод цифр. У нас, например, в поле для сдачи на чекауте нельзя ввести ничего, кроме западных цифр.

6f8079517050871c2e09c81aaff63d5a.png

Начать использовать восточные цифры в приложении — большая задача, которая затрагивает не только приложения, но и бэкенд. Мы приняли решение не поддерживать восточные цифры в первом релизе приложения с поддержкой арабского языка. Чтобы зафиксировать в приложении использование западных цифр, используем особый формат локали: арабская с западными цифрами (ar-u-nu-latn).  

Что с какой стороны писать

Не забываем проверить, что с какой стороны от числа: единицы измерения пишутся слева (١٠٠ غ), а знак валюты пишется справа от числа (١٠٠$).

Если указываешь временной интервал, то слева пишется время конца, а справа — время начала. Например, если пиццерия работает с 10 до 18 часов, то это записывается следующим образом: 18:00 — 10:00.

Слова на другом языке

Если в арабском тексте встречаются иностранные слова, то их не нужно писать задом наперёд. Не нужно переворачивать названия на русском и английском языках. Например, PayPal, Apple Pay.

Пунктуация

В арабском отличаются некоторые знаки препинания и прочие символы. Например, в нём используется другой знак процента ٪. У нас в приложении есть картинка с процентом, которую мы используем на экранах со скидками и акциями. Это как раз случай, когда картинку нельзя просто развернуть, её нужно будет заменить другой, с другим знаком процента.

Европейский знак % будет понятен, но использование арабского приоритетнее.

Локализацию знака процента можно сделать через локализацию строк или с помощью форматтера:

Не так:

label.text = String(localized: "(percentComplete)% complete")

А так:

label.text = String(localized: "(percentComplete.formatted(.percent)) complete")

Также в арабском не принято использовать знаки, обозначающие номер (№ и #). Мы же, например, используем такие знаки, когда показываем номер заказа в приложении.

Привычные нам знаки переворачиваются: ، ؛ ⸮.

Арабские люди

Чтобы картинки, на которых изображены люди, работали так, как вы этого ожидаете, т.е. привлекали внимание или вызывали тёплые чувства, пользователи должны ассоциировать себя с персонажами на этих картинках.

Для того чтобы достичь нужного эффекта, стоит заменить людей типичной европейской внешности на людей более близких восточной аудитории. Например, стоит отказаться от использования персонажей в зимней одежде и шапках-ушанках.

Мы привыкли, что локализация — это адаптация приложения под конкретный язык. Недавно мы добавили в приложение Додо Пиццы возможность ручной смены языка. И пока мы обсуждали картинки для ОАЭ, пришли к выводу, что нам нужно уметь показывать разные иллюстрации в зависимости от выбранной страны, а не от выбранного языка. Так и сделали для ОАЭ.

Код, который подставляет нужную картинку в зависимости от текущей страны, можно переиспользовать и в других странах. Например, устанавливать зимние картинки там, где это актуально:

Решаем, как будем тестировать

Изменений получается много, все они интерфейсные. Тестировать RTL-ориентацию будем через снепшот-тесты. Для снепшот-тестов мы используем библиотеку SnapshotTesting. Там есть возможность сделать снепшот, установив нужную ориентацию через traits:

let rtlTrait = UITraitCollection(layoutDirection: .rightToLeft)
assertSnapshot(
    matching: sut,
    as: .image(traits: rtlTrait),
    testName: QuickSpec.current.name
 )

Для удобства написали хелпер, чтобы одним тестом проверять сразу несколько снепшотов для LTR и RTL:

itShouldSnapshot(
    configs: [.default, .rightToLeft],
    matching: sut,
    configuration: { sut in
        let product = OrderHistoryViewModel.OrderProduct(
            name: "Сырные палочки с Песто",
            size: "16 шт",
            imagePlaceholder: .imgPizzaGift,
            category: .pizza
        )

        var viewModel = OrderHistoryViewModel.PastOrderItem()
        viewModel.name = "Сырные палочки с Песто"
        viewModel.totalPriceString = "249 ₽"
        viewModel.products = [product]

        sut.configure(viewModel: viewModel)
    },
    perceptualPrecision: precision,
    size: { sut in
        sut.sizeFitting(width: 320)
    }
)

Чуть позже прокачали его, чтобы ещё проверять dynamic type и тёмную тему.

Делаем выводы

iOS сильно облегчает работу по локализации приложения под RTL-языки. Можно сказать, что всё, что нам нужно адаптировать в приложении — это техдолг, который выстрелил.

Выводы:

  • Если сразу правильно верстать, то всю остальную работу iOS сделает за вас.

  • Чем меньше кастомных элементов, тем лучше это выглядит в RTL.

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

  • Направление текста — это особенность выбранного языка, но есть вещи, которые зависят не от языка, а от страны. Например, иллюстрации в приложении.

  • Не все картинки можно отзеркалить автоматически. Что-то придётся перерисовать.

  • Мы очень привыкли к LTR, поэтому готовое адаптированное приложение лучше показать кому-нибудь из носителей арабского языка, чтобы точно ничего не пропустить.

  • Снепшот-тесты ускоряют разработку и помогают понять, что вы ничего не сломали в процессе.

  • Поддержка RTL — это не разовое мероприятие, а непрерывный процесс. Каждая новая фича должна поддерживать RTL. Значит, нужно рассказать команде, на что обращать внимание при проектировании, составить чек-листы для тестирования и инструкции для разработчиков.

  • ОАЭ — мультинациональная страна. Нельзя оставить приложение только на арабском, английский тоже нужно поддерживать.

© Habrahabr.ru