Привет! Меня зовут Сергей Балалаев, я руковожу отделом разработки мобильного приложения «Пункт 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 у нас в компании уже было решение, позволяющее управлять локализацией. Им воспользовалась сначала команда мобильного Покупателя (Buyer), пообтесав под мобилку, и затем коллеги из разработки мобильного приложения для продовца (Seller). Нам такой сервис не подошёл по ряду причин; в основном, потому что он излишне усложнил бы наши процессы. 

Принцип использования сервиса в 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, а если знаете другие способы ускорить и обезопасить процесс локализации — рассказывайте тут в комментариях к статье!


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

Комментарии (2)


  1. storoj
    18.05.2023 14:04
    +1

    Зачем в работающем приложении на горячую обновлять локализацию? Никто не умрёт, если новая локализация применится при следующем перезапуске. Зато сколько жизни можно сэкономить не поддерживая такую маргинальную фичу.


    1. SofBix Автор
      18.05.2023 14:04

      Вы правы, так спасти жизни можно, фича действительно моргинальная, не сколь своим явлением, сколь реализацией. Если залезть под капот SDK Localise, то там обнаруживается Swizzling, работа с Notification Center, обязывание разработчиков обновлять текстовые сообщения в специальном методе. Хочется такой SDK не использовать.