Хабрахабр

Локализация приложения и поддержка RTL. Доклад Яндекс.Такси

При локализации сервиса важно внимательно отнестись к согласованию переводов между собой. Руководитель группы клиентской Android-разработки Яндекс.Такси Александр Бонель рассказал, какие практики и инструменты упрощают локализацию. Во второй части доклада Саша поделился опытом поддержки языка RTL в приложении: что хорошо, а что не совсем работает у Андроида из коробки, какие проблемы возникают из-за поддержки RTL и как их минимизировать в будущем.

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

В первую очередь хотелось бы внести ясность в терминологию. Начну с локализации. И многое другое. По моим наблюдениям, большое количество людей считают, что локализация ограничена всего-навсего переводами, хотя на самом деле она также решает проблемы, связанные с форматированием чисел, дат, времени, работу с текстом и символами, правовые аспекты, подачу контента потребителю таким образом, что он имел для него смысловую ценность и не был воспринят неоднозначно. ред.) (Общение с залом о том, у кого сколько языков поддерживается в приложениях — прим.

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

По большей части, весь процесс был ручной. Мы тогда достаточно фривольно относились к процессу локализации и к переводам. И сыграл свою роль человеческий фактор. Обновление текстов в нужных XML-файлах, добавление новых переводов проводилось вручную. Это приводило к тому, что у пользователей с отличной от русского языка системной локалью, если они задавали подъезд, приложение по окончанию поездки крэшилось. Разработчик определил не в дефолтной локале очередной перевод. И люди не могли сделать еще один заказ.

И мы работали только в России в трех городах: Москве, Екатеринбурге, Санкт-Петербурге. На тот момент у нас в приложении поддерживалось два языка: русский и английский.

Я думаю, вы понимаете всю сугубость проблемы, если мы в таком состоянии находились на тот момент. Сейчас мы работаем в 16 странах и переведены на 18 языков. Мы уже тогда поняли, что нужно что-то менять.

У него очень удобный UI-интерфейс, есть возможность определить свой проект, задать набор ключей, набор языков, на которые необходимо переводить проект. В Яндексе бо́льшая часть разработчиков мобильных приложений для решения проблем, связанных с локализацией, пользуется разработанным внутри сервисом. У него также очень хорошая интеграция с нашими внутренними сервисами. И после того, как переводы появляются, их можно выгрузить в любом удобном формате. Но лично для меня как для разработчика самое главное то, что у него есть API, потому что это уже наводит на мысль о том, что вся ручная работа, связанная с переводами, может быть автоматизирована. Есть версионированность. Мы разработали плагин для Gradle. Что мы и сделали. По сути, актуализация перевода в приложении теперь сводится к тому, что разработчик выполняет один-единственный task: updateTranslations.

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

При очередном выполнении git status разработчик видит у себя в рабочей директории, какие файлы поменялись.

Также он может посмотреть в сгенерированном HTML-отчете о том, какой ключ на какое значение в каком языке поменялся.

И также он может посмотреть, какие проблемы на данный момент присутствуют в переводах его проекта.

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

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

Та проблема, с которой мы столкнулись в 2014 году — банальная, отсутствие перевода. Какие проблемы этот плагин сейчас анализирует? Теперь для того, чтобы добавить новую строчку, новый перевод в проект, разработчику недостаточно просто определить его в XML-файле.

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

Если же он определит ключ в дефолтном файле, но забудет это сделать в сервисе локализации, то при очередном выполнении таска updatetranslations он получит ошибку от плагина, которая скажет ему: «Этот ключ есть в вашем проекте, но он отсутствует в сервисе локализации, его там нужно завести».

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

Но при этом в каком-то из переводов забыли поменять чиселку на строку.

Если в переводе используются Unicode-символы, очень легко запутаться в позиционировании символов и вместо неразрывного пробела получить перенос на другую строку — то, от чего мы пытаемся избавиться. И четвертая проблема также обусловлена человеческим фактором, в основном при переводе.

Это всё про локализацию.

Мы запустились в 2018 году под брендом Yango в Израиле. Теперь я хочу рассказать про опыт внедрения поддержки отрисовки справа налево в приложении на примере Израиля. Они читают справа налево. Как оказалось, в Израиле люди читают не так, как мы с вами.

Что в Android в принципе поддержка отрисовки справа налево реализована из коробки достаточно хорошо. С чего хочется начать?

Но хочу вас обрадовать: если вы у себя в приложениях интегрируете Facebook SDK для социальной авторизации, заботливые разработчики Facebook это уже сделали за вас. Начинается она с того, что в манифесте приложения, в элементе Application вам необходимо объявить атрибут supportRtl=”true”. Это то, о чем можно задуматься. И если вы не валидируете манифест при мерджинге, у вас этот атрибут со значением true встанет в ваше приложение, и оно уже будет уметь в Rtl.

4. Думаю, что большинство из сидящих в аудитории наверняка минимально поддерживают версию Android 4. 0 и выше. Кому-то больше повезло — это 5. 0, или, упаси, господи, 2. Кому-то могло меньше повезти, и они поддерживают 4. В этом случае у вас будут большие проблемы, потому что полноценная поддержка Rtl в Android появилась начиная с версии 4. 3. Поэтому с minSdk 17-м вы на порядок упростите себе жизнь. 2.

Надо отдать должное, работает достаточно хорошо. Перевод проекта на работу с Rtl начинается с refactoring tool в Android Studio — Add RTL Support Where Possible. Если вы хотите посмотреть, как ваше приложение по умолчанию работает при отрисовке в режиме справа налево, вам необязательно иметь на руках контент с так называемыми RTL strong символами — на иврите или на арабском языке. Он проходит по XML-файлам ваших разметок, смотрит на наличие атрибутов со значениями left и right у gravity, у paddings, margins, и заменяет их на start и end. Вам достаточно в настройках разработчика включить опцию Force RTL Layout Direction.

Какие возможности для кастомизации UI и рендеринга текста Android предоставляет для RTL?

У нее всего четыре возможных значения. Первый момент — это атрибут layoutDirection, который использует описанная опция Force RTL Layout Direction. Можно сказать о том, чтобы layout отрисовывался, исходя из выбранной локале. То есть она по умолчанию наследуется от родителя. Можно сказать четко, чтобы он отрисовывался справа налево или слева направо.

Многие до сих пор продолжают использовать gravity. Второй элемент, которые многие наверняка по привычке обходят стороной, когда занимаются выравниванием текста — это textAlignment. Для выравнивания текста не используйте gravity, а используйте textAlignment.

У него достаточно гибкая настройка. Третий атрибут — это направление рендеринга текста, textDirection. Но если вы поддерживаете полноценно RTL в приложении, его не нужно обходить стороной, потому что при игнорировании происходит такой забавный артефакт. Она позволяет определить, как текст рендерится в зависимости от того, какой первый strong символ попался в тексте, будь то из латинского набора символов strong LTR или из иврита strong RTL.

Поэтому какое-никакое значение вы ему в любом случае должны задать. То ли это пасхалочка, то ли это такой side effect в реализации editText: у вас начинает hint у editText разъезжаться.

Ссылка со слайда

Если вам нужно соблюсти определенную отрисовку каких-то графических ресурсов или разметки в режиме RTL, то Android предоставляет возможность задать qualifier ldrtl в вашей папочке, в которую вы можете положить любой ресурс, и он в режиме отрисовки RTL будет отрисован так, как вы задали.

По сути, в text renderer это осуществляется за счет того, что текст обрамляется так называемыми Bidi Unicode control символами. Иногда бывает потребность в runtime приходящий с бэкэнда текст отрисовывать как есть, как он пришел, игнорируя то, в каком режиме сейчас работает ваше приложение. Он принимает СharSequence на вход, и возвращает вам строку, обрамленную этими символами. Логику для работы с этими символами в себе содержит класс BidiFormatter, предоставляя только один-единственный метод unicodeWrap. Там их исчерпывающее количество, и, в принципе, он должен решать бо́льшую часть ваших проблем.

Но если есть что-то такое специфическое, у w3.org есть хорошая статья на тему того, как этими символами пользоваться.

В первую очередь это конфликт стандартов локализации. Какие проблемы мы приобрели, когда начинали поддержку RTL в нашем приложении?

И дело в том, что есть проблема с классом locale Java. BCP47 — это более новый стандарт локализации, который в частности поменял коды некоторых локалей. То есть мы это увидели, когда на наш бэкэнд в заголовке Accept-Language вместо кода he стал уходить код iw. Он по умолчанию работает в режиме обратной совместимости, и приходящие ему locale коды из BCP конвертирует в ISO.

Ссылка со слайда

Если вы помните слайд с конфигурацией нашего плагина, remapping был нужен как раз таки для этого — для того, чтобы переводить контент из he в iw.

У них есть класс Globalization и реализация localeToBcp47Language. Если вам необходимо использовать BCP формат, это в полной мере реализовано в проекте Apache Cordova.

Здесь, благо, по сути, аффектятся только анимации по оси X, и, пожалуй, единственное решение или подход, который вам поможет избежать большего количества проблем — это возможность пересмотреть анимации абсолютные в стороны анимаций относительных. Еще один момент, который «доставляет» при поддержке RTL в приложении — это анимации.

Дело в том, что если hint задан в RTL strong символах на иврите, он будет четко выровнен по правому краю, даже несмотря на то, что контент, который вы вводите в edit text, может быть из латинских символов. Одна проблема, которая решается, пожалуй, только reflection, потому что она жестко реализована так в самом компоненте — это выравнивание hint у TextInputLayout. Если hint у вас на латинице, он будет четко выровнен по левому краю. И наоборот. Это решить можно только с помощью reflection.

Их всего четыре. В конечном итоге, после того, как вы внедрили RTL у вас в приложении, имеет смысл пересмотреть, severities для RTL-issues, которые анализирует Lint. Если вы не поддерживаете RTL в вашем приложении, но явно где-то используете RTL specific атрибуты, то это RtlEnabled (Lint вам об этом скажет).

Ссылка со слайда

Если вы поддерживаете RTL в приложении, но minSdkVersion вашего приложения ниже 17, то вы обязаны также продолжать использовать атрибуты типа left и right, вы не можете использовать только атрибуты start и end в вашей разметке. Это то, что анализирует RtlCompat. RtlSymmetry ругается, когда видит асимметричные paddings. Вообще, мое мнение такое, что aapt при процессинге ресурсов должен крэшиться, если он натыкается на симметричные paddings. Если вам нужны какие-то асимметричные доступы, пользуйтесь margins как полноценным layout параметром.

Это как раз то, что в атрибутах указаны только значения left и right. Наконец, четвертая проблема — захардкоженый RTL. Это всё грепается из Lint по вхождению RTL. По сути, это сводит на нет refactoring tool Add RTL Support Where Possible. То есть если вам фантазия позволяет, вы можете придумать что-то свое, подсмотрев то, как анализ проходит сейчас. Также реализация анализа доступна в AOSP в классе RtlDetector.

Я хочу привести несколько тезисов на тему выстраивания правильной локализации у нас в приложении, к которым мы со временем пришли. Мой доклад подходит к концу.

Я о ней толком не обмолвился. В первую очередь, на старте проекта задумайтесь об интернационализации. По сути, интернационализация делает возможной локализацию в приложении, в продукте. Это совсем не то же самое, что локализация. Пожалуй, единственное, что здесь может случиться — наша фиксация как разработчиков на тему того, что «Я приложение пишу только для русского, значит, я буду проверять его только с русским языком». Это у Java из коробки работает за счет того, что есть полноценная поддержка Unicode, а у Android — за счет богатой системы ресурсов. А когда вы начинаете использовать в приложении армянский, грузинский, иврит или арабский язык, картина выглядит совсем иначе и ломается об вашу привычную парадигму.

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

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

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

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

Наша задача как разработчиков — помочь соблюсти это восприятие. У нас очень разнообразная аудитория пользователей, каждый воспринимает окружающий мир и контент приложения по-своему. Спасибо большое! Мой доклад на этом окончен.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть