Безопасная локализация строк в iOS: Localinter
Привет! Меня зовут Сергей Балалаев, я руковожу отделом разработки мобильного приложения для ПВЗ в Ozon. Это то самое приложение, которым сотрудники пунктов выдачи сканируют штрихкод, чтобы выдать товар получателю. Оно внутреннее, для сотрудников. iOS-версией постоянно пользуются 12 тыс. человек, поэтому при постановке задачи нас не просили делать мультиязычную версию. Но мы с самого начала разработки решили поддерживать несколько языков — когда возникнет необходимость локализации, справимся в спокойном режиме и без проблем, свойственных проектам, в которых локализацию не закладывали. Я хочу рассказать, как мы побороли типичные проблемы локализации для наших iOS-проектов, зачем собрали свой линтер для локализации и как это всё помогло упростить и автоматизировать процесс.
Локализация — та задача, с которой рано или поздно сталкивается большинство мобильных разработчиков. Штука вроде бы тривиальная, но только на первый взгляд. Если посмотреть чуть вглубь и задуматься не просто про то, как «сделать локализацию», а про то, как обеспечить стабильно высокое качество:
снизить количество случайных удалений или ошибок при мерджах,
избежать дубликатов или мусорных ключей,
и обеспечить удобство для сторонних переводчиков,
…то процесс может доставлять массу проблем. Более того, это довольно сложно протестировать.
В разработке iOS-приложения мы используем SwiftUI 2.0 и стараемся использовать наиболее простые и стандартные решения, предоставляемые SDK iOS. В Android для локализации используется библиотека R, а R.Swift в iOS — это попытка её повторить. Когда работаешь только с iOS, то постепенно привыкаешь к особенностям локализации на платформе. Но если работаешь одновременно с двумя платформами, то на контрасте становится понятно, что в iOS-локализации есть проблемы и мало готовых инструментов для их решения.
Как организован процесс локализации
Локализация сама по себе несложная задача, сложно её внедрять на этапе готового приложения. Мы решили сократить затраты времени в будущем, и заложили возможность локализации на старте разработки. Это добавило трудов по поддержке локализации, ревью кода и тестированию. Однако, разработанное мной и внедренное в проекте решение (Localinter) свело эти затраты на нет. Как именно и почему — я расскажу дальше. Сейчас мы поддерживаем два языка: русский и английский. Можно было бы оставить всего один язык, так, например, поступила команда Android. Но у нас в команде много энтузиастов, изучающих английский язык, и, кстати, в ближайшем ПВЗ я наблюдаю таких же энтузиастов среди его сотрудников, короче пусть будет.
Для iOS-проектов в самом простом случае локализация выглядит так:
добавляем нужное количество языков в Xcode;
создаём файл локализации (Localizable.strings);
заполняем его нужными значениями для всех локалей;
профит!
Чтобы приложение выглядело красиво, нужно учесть плюрализм. Это позиция, в соответствии с которой имеется не одна (монизм), не две (дуализм), а множество сущностей, или — в нашем контексте — значений перевода.
Для английского языка задача решается довольно просто, для русского— чуть сложнее, но в целом, особых сложностей тут не возникает.
Меняем английский
Меняем русский
Типичные проблемы локализации iOS-проекта
Как видите, в мире iOS-разработки всё довольно просто, но коварство в том, что эта простота порождает множество проблем. Давайте пробежимся по ним в порядке важности для приложения.
Отсутствующие ключи
Бывает, что в файле локализации нет нужного ключа. Чаще всего такое случается при банальных опечатках или случайных удалениях во время мёрджа. Это приводит к тому, что в приложении в том месте, где нет локализации, отображается вместо «пупсик» какое нибудь "NAME_OF_KEY_BY_DEVELOPER”.
Выглядит плохо, тестировать сложно, увеличивает time-to-market.
Дубликаты ключей
Например, когда несколько разработчиков одновременно добавили несколько своих переводов под одним ключом. Проблема возникает чаще всего в результате того, что разработчики не читают чужие ключи и могут вписать свой, не посмотрев, что такой уже есть. В таком случае, например, вместо лейбла «пупсик» пользователь увидит «отменить нежности все».
Hardcode-строки
Все мы люди, и иногда разработчики вместо добавления ключа в файл локализации просто хардкодят нужное значение. Такое бывает, когда торопишься. В результате все варианты перевода сводятся к тому, что захардкодил разработчик.
Неверный язык перевода
Это похожая на предыдущую проблема: бывает, что вместо правильного перевода в файл локализации просто копируют значения из других языков. Тоже результат спешки и небрежности, но имеет место.
Мусорные ключи
По мере роста приложения накапливаются ключи, которые больше не используются — например, когда старая фича становится не нужна или изменилась. Это не совсем проблема, пользователь не заметит разницы. Но это влияет на сложность поддержки кода и размер приложения. Протестировать это невозможно — как чёрный ящик.
Сложности с профессиональным переводом
Зачастую в требованиях не описаны все кейсы, когда понадобится перевод. В таких случаях разработчики обычно переводят какие-то простые элементы самостоятельно, чтобы ускорить процесс. Это не совсем правильно, и было бы здорово иметь возможность удобно отдать локализацию на ревью стороннему переводчику. Такого инструмента тоже нет в SDK iOS. Редактура и корректура — отдельная история.
Конкретно для нашего приложения процесса внешней вычитки и редактуры нет, у нас не было бизнес-требования к качеству локализации, это скорее инициатива команды разработки. Но такое требование (и, соответственно, процесс) есть в приложении покупателя (основное приложение Ozon). Поэтому мы пошли по простому пути, без привлечения внешних сервисов перевода, но в статье рассмотрим и такие сервисы.
Это всё разные проблемы. Есть критичные, которые ломают приложение и отображают текст с ошибками; есть некритичные, которые ухудшают поддержку и увеличивают объём приложения. Отдельно неприятно то, что их очень неудобно отслеживать и править.
Тестировать локализованное приложение чуть сложнее: нужно переключать локаль и несколько раз проверять одни и те же фичи. Отдельного процесса тестирования локализации — когда тестируем только её, без проверки новой фичи — у нас нет. Это ещё больше увеличило бы time-to-market, который для нас важен, и усложнило бы процессы.
А в процессе ревью практически невозможно отследить те проблемы, о которых я написал выше. Например, когда просто смотришь на код и оцениваешь его работоспособность, крайне сложно заметить случайную опечатку или символ из другой локали (и такие вещи были тоже). Утомляло ещё то, что на ревью находились одни и те же ошибки. А там где есть рутина — становится понятно, что процесс можно автоматизировать и не тратить на него время.
Моя задача, как руководителя разработки, — наладить процесс, когда мы выпускаем нужную функциональность с нужным качеством в нужные сроки. И проблемы с качеством или задержками релизов из-за проверки локализации сильно этому мешали бы. Поэтому, в поиске баланса между скоростью разработки и возможностью получить качественную локализацию, не усложняя при этом процесс, я встал на путь автоматизации.
Инструменты решения проблем локализации
В первую очередь мы с командой попробовали найти готовое решение, которое закрыло бы наши потребности. Попробовали несколько разных, но ни одно по отдельности не решало полностью наши задачи.
Есть два подхода: использовать внешние сервисы, либо встраивать в свой процесс инструменты кодогенерации для локализации. Забегая вперёд, скажу, что иногда можно использовать сразу оба подхода; они могут дополнять друг друга.
Главная особенность сервиса в том, что он приспособлен для решения конкретных задач и не лезет к вам в код приложения; зато им могут пользоваться внешние специалисты (например, профессиональные переводчики). Инструмент для кодогенерации встраивается в процесс разработки и призван облегчить для разработчика локализацию и тестирование.
Кроме того, мы поглядели на опыт наших коллег из веба и других команд мобильной разработки, о чём расскажу дальше.
Кодогенерация
На текущий момент есть два основных инструмента для кодогенерации — SwiftGen и R.swift. Остальные решения уступают им в функциональности.
В целом, кодогенераторы для локализации устроены так, что превращают разные ресурсы (строки для локализации, изображения, цвета, файлы) в обычные конструкции языка Swift, и вместо конструкторов с текстовыми ключами вы в работе используете константы (или функции в случае плюрализма). Это позволяет решать некоторые проблемы на этапе компиляции.
SwiftGen хорош тем, что у него высокая скорость генерации и есть шаблонирование;, но проблема в том, что нет проверки всех языков. Дело в том, что когда вы настраиваете SwiftGen, вы указываете ему файл перевода одного (основного) языка, а остальные он игнорирует.
R.swift решает задачу проверки нескольких языков (видит все файлы переводов), но в остальном уступает: шаблонирования нет, и скорость генерации значительно ниже.
Расскажу чуть подробнее, как выглядит локализация с использованием R.swift:
Получаем генерируемый файл
Например, для решения проблемы с плюрализмом достаточно указать нужное количество item для значения, и все нужные формы автоматически подставятся.
Используем полученную константу/функцию в коде
Как R.swift помогает решить проблему переводов?
Если искусственно внедрить в перевод баг (например, сделать опечатку), то R.swift при компиляции предупредит, что изменился ключ, используемый в приложении. Аналогично, предупреждения мы увидим и при отсутствии ключа (нужного значения для перевода).
В случае с ключом-дубликатом R.swift на этапе компиляции создаст две одинаковые константы и компилятор не соберёт приложение с ошибкой.
Итого: R.swift справляется с проблемой отсутствия ключей, но не решает остальные важные для нас задачи. Ищем дальше.
Внешние сервисы
В поисках инструмента, который решит наши задачи, мы рассматривали и внешние сервисы, например, lokalise.com. Что он из себя представляет? На первый взгляд, это веб-интерфейс для создания локализации прямо на сайте сервиса. Вы создаёте ключи, значения, редактируете перевод, и на выходе получаете автоматически сгенерированные файлы локализации со строками, которые можно добавлять в свой проект.
Кроме того, у сервиса есть свой SDK, и у него есть киллер-фича: с помощью некоторых манипуляций можно сделать «горячую замену локали» прямо на пользовательских устройствах, без необходимости перезагрузки приложения. Также эта фича позволяет не перевыпускать приложение, чтобы поправить ошибку локализации.
Кроме того, с помощью сервиса можно локализовать приложение на iOS и Android. Это тоже помогает поддерживать актуальную и единообразную локализацию.
Обновление переводов на этапе исполнения программы
Таким образом, lokalise.com решает проблему неверного языка перевода с помощью встроенного механизма проверки и позволяет подключать в процесс перевода внешних экспертов — профессиональных переводчиков и корректоров. Но проблемы с отсутствующими и дублирующимися ключами или прописанными в коде строками он не решает, а это для нас было самым важным.
Ещё один недостаток этого сервиса — стоимость. Довольно высокая (от $120 в месяц), особенно для небольших приложений. Есть 14-дневный пробный период — можно даже успеть локализовать приложение и зарелизить.
В общем, резюмируя: готовые инструменты есть, они закрывают лишь часть наших задач, но не все. Я решил, что нужно искать дальше.
Сервис Ozon для локализации
Для веб-версии сайта Ozon.ru у нас в компании уже было решение, позволяющее управлять локализацией. Им воспользовалась сначала команда мобильного Продавца (Seller), пообтесав под мобилку, и затем коллеги из разработки мобильного приложения для покупателя (Buyer). Нам такой сервис не подошёл по ряду причин; в основном, потому что он излишне усложнил бы наши процессы.
Принцип использования сервиса в Buyer и Seller является компиляцией рассмотренных выше решений:
Заводим переводы в сервисе; можно подключить переводчика или редактора, всё вычитать и поправить в вебе.
Генерируем файлы аналогично localise.com.
С помощью SwiftGen генерируем код для использования ресурсов через константы и функции.
Это помогло коллегам решить практически все проблемы, кроме захардкоженных строк и мусорных ключей, они всё ещё доставляли неудобства. Для нас такой сервис просто увеличил бы время разработки и решал бы задачу, которая на данный момент не стоит — улучшал качество перевода. Поэтому мы пришли к менее трудозатратному решению.
Вишенка на торте — Localinter
Хотелось получить решение, которое будет решать вообще все наши задачи. Изучив вышеописанные варианты, пришли к тому, что лучше всего использовать комбинацию из инструментов и написать свой линтер, который проверит то, с чем не справляются другие. Я назвал его Localinter и выложил в open source — ссылка на Github. По сути, это простой скриптовый Swift, не требующий подключения внешних библиотек или зависимостей, подключается и настраивается очень просто.
Localinter может работать в связке, например со SwiftGen (и в нашем проекте мы используем его именно так). Без него он тоже может использоваться и будет решать те же задачи, но мы и так используем SwiftGen для других целей. При необходимости вы сможете использовать и более сложные связки, например, с тем же lokalise.com или другим сервисом переводов, но нам хватает и такой интеграции.
Localinter анализирует исходники с помощью регулярных выражений и проверяет ресурсные строки на наличие контента, его корректность и названия строк. В основе решения лежат предустановленные регулярные выражения, которые мы написали. Они подходят для стандартного использования cо SwiftGen и L10n. И, конечно, можно дописать собственные регулярки.
Инструмент поддерживает два формата строковых файлов — обычный и через плюрализм.
Процесс локализации для нас сильно упростился. Как правило, все возможные проблемы решаются на этапе разработки. Например, если ключ не определён в одной из локалей, то разработчик увидит ошибку в IDE на этапе компиляции. Приложение не скомпилируется, и Localinter прямо в XCode подсветит файл, в котором не хватает ключа. Если есть дублирование ключей, то XCode подсветит нужную строку. И так далее по списку проблем.
Есть и нюансы: например, неиспользуемый ключ — это всего лишь warning, так как мусор в исходном коде не мешает правильной работе приложения.
Благодаря тому, что Localinter работает как привычный линтер и подсвечивает ошибки, исправить их получается гораздо быстрее, чем отправлять на тестирование, находить там, возвращать в разработку и так далее по процессу. Это ускоряет тестирование, снижает количество ошибок, минимизирует сложности с ревью.
Мы решили все проблемы, о которых я говорил: отсутствие и дубликаты ключей, hardcode-строки, неверный язык перевода и мусорные ключи. Более того, в нашем проекте Localinter выполняется за 60 мс. А ещё крутая штука — это понятное и стандартное решение, с которым разберётся практически любой iOS-разработчик, потому что оно написано на Swift.
Рекомендации по локализации для iOS
Хочется подвести некий итог, не только про инструменты, но и в целом про выводы из многолетнего опыта и практики.
Закладывайте локализацию приложения на старте
Это не подойдёт стартапам, но если вы разрабатываете сколько-то серьёзное приложение, то старайтесь делать локализацию (хотя бы с одним языком) максимально рано. Чем позже придёте к локализации, тем сложнее это будет сделать.
Используйте сервисы переводов
Это подойдёт проектам, у которых есть потребность в повышении качества перевода. У нас в этом не было необходимости. Помогает тратить меньше времени на непрофильные задачи.
Используйте кодогенерацию
Если проект небольшой, то присмотритесь к R.swift; на 10 экранах он проще и работает быстро. Если проект больше — то советую SwiftGen.
Учитывайте региональные различия
Даты, валюты, другие единицы измерения — не забывайте о них и используйте форматтеры:
Локализованные даты — используются разные форматы и разделители. Применяйте DateFormatter.
Для корректной работы с валютами пригодится NumberFormatter.
И MeasurementFormatter, чтобы правильно отображать единицы измерения.
Не забывайте про info.plist
Локализуйте в том числе название приложения и системные сообщения. Это делается через файл info.plist: просто добавьте переводы для нужных сообщений.
InfoPlist.strings редактируем для языков
Обратите внимание, что при использовании Localinter с файлом info.plist нужно добавить небольшие настройки — так как там есть неиспользуемые в приложении ключи, его нужно исключить из обработки линтером.
Ещё немного настроек Localinter
Учитывайте интеграцию с бэкендом
Скорее всего, ваши тексты и картинки (а может, и другие ресурсы) будут подтягиваться с бэкенда, поэтому расскажите им про планы на локализацию заранее и договоритесь. И не забывайте обновлять сервисы, когда создаёте пакет локализации.
Подключайте Localinter
Он помог нам, может оказаться полезным и другим. Благодаря своей простоте он подойдёт для большинства проектов, а ещё его можно модифицировать под свои нужды. Присылайте идеи и замечания в pull requests, а если знаете другие способы ускорить и обезопасить процесс локализации — рассказывайте тут в комментариях к статье!
А запись моего выступления по этой теме вы можете найти здесь:
Приглашаем на Ozon Tech Community Mobile Meetup
UPD: Добавили записи выступлений и слайды Мы делаем приложения для всех — для покупателей, продавцов…
habr.com