Безопасная локализация строк в iOS: Localinter

Привет! Меня зовут Сергей Балалаев, я руковожу отделом разработки мобильного приложения для ПВЗ в Ozon. Это то самое приложение, которым сотрудники пунктов выдачи сканируют штрихкод, чтобы выдать товар получателю. Оно внутреннее, для сотрудников. iOS-версией постоянно пользуются 12 тыс. человек, поэтому при постановке задачи нас не просили делать мультиязычную версию. Но мы с самого начала разработки решили поддерживать несколько языков — когда возникнет необходимость локализации, справимся в спокойном режиме и без проблем, свойственных проектам, в которых локализацию не закладывали. Я хочу рассказать, как мы побороли типичные проблемы локализации для наших iOS-проектов, зачем собрали свой линтер для локализации и как это всё помогло упростить и автоматизировать процесс.  

3de0edf1b9296a238542d2190bc6bda4.png

Локализация — та задача, с которой рано или поздно сталкивается большинство мобильных разработчиков. Штука вроде бы тривиальная, но только на первый взгляд. Если посмотреть чуть вглубь и задуматься не просто про то, как «сделать локализацию», а про то, как обеспечить стабильно высокое качество:  

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

  • избежать дубликатов или мусорных ключей,  

  • и обеспечить удобство для сторонних переводчиков,

…то процесс может доставлять массу проблем. Более того, это довольно сложно протестировать. 

В разработке 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 редактируем для языков

InfoPlist.strings редактируем для языков

Обратите внимание, что при использовании Localinter с файлом info.plist нужно добавить небольшие настройки — так как там есть неиспользуемые в приложении ключи, его нужно исключить из обработки линтером. 

Ещё немного настроек Localinter

Ещё немного настроек Localinter

Учитывайте интеграцию с бэкендом

Скорее всего, ваши тексты и картинки (а может, и другие ресурсы) будут подтягиваться с бэкенда, поэтому расскажите им про планы на локализацию заранее и договоритесь. И не забывайте обновлять сервисы, когда создаёте пакет локализации. 

Подключайте Localinter

Он помог нам, может оказаться полезным и другим. Благодаря своей простоте он подойдёт для большинства проектов, а ещё его можно модифицировать под свои нужды. Присылайте идеи и замечания в pull requests, а если знаете другие способы ускорить и обезопасить процесс локализации — рассказывайте тут в комментариях к статье!

А запись моего выступления по этой теме вы можете найти здесь:

Приглашаем на Ozon Tech Community Mobile Meetup

UPD: Добавили записи выступлений и слайды Мы делаем приложения для всех — для покупателей, продавцов…

habr.com

© Habrahabr.ru