Привет! Меня зовут Сергей Балалаев, я руковожу отделом разработки мобильного приложения «Пункт Ozon». Это то самое приложение, которым сотрудники пунктов выдачи заказов сканируют штрихкод, чтобы выдать товар получателю. Оно внутреннее, для сотрудников. iOS-версией постоянно пользуются 12 тыс. человек, поэтому при постановке задачи нас не просили делать мультиязычную версию. Но мы с самого начала разработки решили поддерживать несколько языков — когда возникнет необходимость локализации, справимся в спокойном режиме и без проблем, свойственных проектам, в которых локализацию не закладывали. Я хочу рассказать, как мы побороли типичные проблемы локализации для наших iOS-проектов, зачем собрали свой линтер для локализации и как это всё помогло упростить и автоматизировать процесс.   Локализация — та задача, с которой рано или поздно сталкивается большинство мобильных разработчиков. Штука вроде бы тривиальная, но только на первый взгляд. Если посмотреть чуть вглубь и задуматься не просто про то, как «сделать локализацию», а про то, как обеспечить стабильно высокое качество:  снизить количество случайных удалений или ошибок при мерджах,  избежать дубликатов или мусорных ключей,  и обеспечить удобство для сторонних переводчиков, …то процесс может доставлять массу проблем. Более того, это довольно сложно протестировать.  В разработке iOS-приложения мы используем SwiftUI 2.0 и стараемся использовать наиболее простые и стандартные решения, предоставляемые SDK iOS. В Android для локализации используется библиотека R, а R.Swift в iOS — это попытка её повторить. Когда работаешь только с iOS, то постепенно привыкаешь к особенностям локализации на платформе. Но если работаешь одновременно с двумя платформами, то на контрасте становится понятно, что в iOS-локализации есть проблемы и мало готовых инструментов для их решения.   Локализация сама по себе несложная задача, сложно её внедрять на этапе готового приложения. Мы решили сократить затраты времени в будущем, и заложили возможность локализации на старте разработки. Это добавило трудов по поддержке локализации, ревью кода и тестированию. Однако, разработанное мной и внедренное в проекте решение (Localinter) свело эти затраты на нет. Как именно и почему — я расскажу дальше. Сейчас мы поддерживаем два языка: русский и английский. Можно было бы оставить всего один язык, так, например, поступила команда Android. Но у нас в команде много энтузиастов, изучающих английский язык, и, кстати, в ближайшем ПВЗ я наблюдаю таких же энтузиастов среди его сотрудников, короче пусть будет.  Для iOS-проектов в самом простом случае локализация выглядит так:  добавляем нужное количество языков в Xcode; создаём файл локализации (Localizable.strings); заполняем его нужными значениями для всех локалей; профит! Чтобы приложение выглядело красиво, нужно учесть плюрализм. Это позиция, в соответствии с которой имеется не одна (монизм), не две (дуализм), а множество сущностей, или — в нашем контексте — значений перевода. Для английского языка задача решается довольно просто, для русского— чуть сложнее, но в целом, особых сложностей тут не возникает.  Как видите, в мире iOS-разработки всё довольно просто, но коварство в том, что эта простота порождает множество проблем. Давайте пробежимся по ним в порядке важности для приложения.  Бывает, что в файле локализации нет нужного ключа. Чаще всего такое случается при банальных опечатках или случайных удалениях во время мёрджа. Это приводит к тому, что в приложении в том месте, где нет локализации, отображается вместо «пупсик» какое нибудь “NAME_OF_KEY_BY_DEVELOPER”. Выглядит плохо, тестировать сложно, увеличивает time-to-market. Например, когда несколько разработчиков одновременно добавили несколько своих переводов под одним ключом. Проблема возникает чаще всего в результате того, что разработчики не читают чужие ключи и могут вписать свой, не посмотрев, что такой уже есть. В таком случае, например, вместо лейбла «пупсик» пользователь увидит «отменить нежности все». Все мы люди, и иногда разработчики вместо добавления ключа в файл локализации просто хардкодят нужное значение. Такое бывает, когда торопишься. В результате все варианты перевода сводятся к тому, что захардкодил разработчик. Это похожая на предыдущую проблема: бывает, что вместо правильного перевода в файл локализации просто копируют значения из других языков. Тоже результат спешки и небрежности, но имеет место.  По мере роста приложения накапливаются ключи, которые больше не используются — например, когда старая фича становится не нужна или изменилась. Это не совсем проблема, пользователь не заметит разницы. Но это влияет на сложность поддержки кода и размер приложения. Протестировать это невозможно — как чёрный ящик.  Зачастую в требованиях не описаны все кейсы, когда понадобится перевод. В таких случаях разработчики обычно переводят какие-то простые элементы самостоятельно, чтобы ускорить процесс. Это не совсем правильно, и было бы здорово иметь возможность удобно отдать локализацию на ревью стороннему переводчику. Такого инструмента тоже нет в SDK iOS. Редактура и корректура — отдельная история.  Конкретно для нашего приложения процесса внешней вычитки и редактуры нет, у нас не было бизнес-требования к качеству локализации, это скорее инициатива команды разработки. Но такое требование (и, соответственно, процесс) есть в приложении покупателя (основное приложение Ozon). Поэтому мы пошли по простому пути, без привлечения внешних сервисов перевода, но в статье рассмотрим и такие сервисы.   Это всё разные проблемы. Есть критичные, которые ломают приложение и отображают текст с ошибками; есть некритичные, которые ухудшают поддержку и увеличивают объём приложения. Отдельно неприятно то, что их очень неудобно отслеживать и править. Тестировать локализованное приложение чуть сложнее: нужно переключать локаль и несколько раз проверять одни и те же фичи. Отдельного процесса тестирования локализации — когда тестируем только её, без проверки новой фичи — у нас нет. Это ещё больше увеличило бы time-to-market, который для нас важен, и усложнило бы процессы.  А в процессе ревью практически невозможно отследить те проблемы, о которых я написал выше. Например, когда просто смотришь на код и оцениваешь его работоспособность, крайне сложно заметить случайную опечатку или символ из другой локали (и такие вещи были тоже). Утомляло ещё то, что на ревью находились одни и те же ошибки. А там где есть рутина – становится понятно, что процесс можно автоматизировать и не тратить на него время.  Моя задача, как руководителя разработки, — наладить процесс, когда мы выпускаем нужную функциональность с нужным качеством в нужные сроки. И проблемы с качеством или задержками релизов из-за проверки локализации сильно этому мешали бы. Поэтому, в поиске баланса между скоростью разработки и возможностью получить качественную локализацию, не усложняя при этом процесс, я встал на путь автоматизации.  В первую очередь мы с командой попробовали найти готовое решение, которое закрыло бы наши потребности. Попробовали несколько разных, но ни одно по отдельности не решало полностью наши задачи.  Есть два подхода: использовать внешние сервисы, либо встраивать в свой процесс инструменты кодогенерации для локализации. Забегая вперёд, скажу, что иногда можно использовать сразу оба подхода; они могут дополнять друг друга.  Главная особенность сервиса в том, что он приспособлен для решения конкретных задач и не лезет к вам в код приложения; зато им могут пользоваться внешние специалисты (например, профессиональные переводчики). Инструмент для кодогенерации встраивается в процесс разработки и призван облегчить для разработчика локализацию и тестирование.  Кроме того, мы поглядели на опыт наших коллег из веба и других команд мобильной разработки, о чём расскажу дальше. На текущий момент есть два основных инструмента для кодогенерации — SwiftGen и R.swift. Остальные решения уступают им в функциональности.  В целом, кодогенераторы для локализации устроены так, что превращают разные ресурсы (строки для локализации, изображения, цвета, файлы) в обычные конструкции языка Swift, и вместо конструкторов с текстовыми ключами вы в работе используете константы (или функции в случае плюрализма). Это позволяет решать некоторые проблемы на этапе компиляции.  SwiftGen хорош тем, что у него высокая скорость генерации и есть шаблонирование; но проблема в том, что нет проверки всех языков. Дело в том, что когда вы настраиваете SwiftGen, вы указываете ему файл перевода одного (основного) языка, а остальные он игнорирует. R.swift решает задачу проверки нескольких языков (видит все файлы переводов), но в остальном уступает: шаблонирования нет, и скорость генерации значительно ниже.  Расскажу чуть подробнее, как выглядит локализация с использованием R.swift: Например, для решения проблемы с плюрализмом достаточно указать нужное количество item для значения, и все нужные формы автоматически подставятся.  Если искусственно внедрить в перевод баг (например, сделать опечатку), то R.swift при компиляции предупредит, что изменился ключ, используемый в приложении. Аналогично, предупреждения мы увидим и при отсутствии ключа (нужного значения для перевода).  В случае с ключом-дубликатом R.swift на этапе компиляции создаст две одинаковые константы и компилятор не соберёт приложение с ошибкой.  Итого: R.swift справляется с проблемой отсутствия ключей, но не решает остальные важные для нас задачи. Ищем дальше. В поисках инструмента, который решит наши задачи, мы рассматривали и внешние сервисы, например, lokalise.com. Что он из себя представляет? На первый взгляд, это веб-интерфейс для создания локализации прямо на сайте сервиса. Вы создаёте ключи, значения, редактируете перевод, и на выходе получаете автоматически сгенерированные файлы локализации со строками, которые можно добавлять в свой проект.  Кроме того, у сервиса есть свой SDK, и у него есть киллер-фича: с помощью некоторых манипуляций можно сделать «горячую замену локали» прямо на пользовательских устройствах, без необходимости перезагрузки приложения. Также эта фича позволяет не перевыпускать приложение, чтобы поправить ошибку локализации.  Кроме того, с помощью сервиса можно локализовать приложение на iOS и Android. Это тоже помогает поддерживать актуальную и единообразную локализацию.  Таким образом, lokalise.com решает проблему неверного языка перевода с помощью встроенного механизма проверки и позволяет подключать в процесс перевода внешних экспертов — профессиональных переводчиков и корректоров. Но проблемы с отсутствующими и дублирующимися ключами или прописанными в коде строками он не решает, а это для нас было самым важным. Ещё один недостаток этого сервиса — стоимость. Довольно высокая (от $120 в месяц), особенно для небольших приложений. Есть 14-дневный пробный период — можно даже успеть локализовать приложение и зарелизить.  В общем, резюмируя: готовые инструменты есть, они закрывают лишь часть наших задач, но не все. Я решил, что нужно искать дальше.  Для веб-версии сайта Ozon.ru у нас в компании уже было решение, позволяющее управлять локализацией. Им воспользовалась сначала команда мобильного Покупателя (Buyer), пообтесав под мобилку, и затем коллеги из разработки мобильного приложения для продовца (Seller). Нам такой сервис не подошёл по ряду причин; в основном, потому что он излишне усложнил бы наши процессы.  Принцип использования сервиса в Buyer и Seller является компиляцией рассмотренных выше решений:  Заводим переводы в сервисе; можно подключить переводчика или редактора, всё вычитать и поправить в вебе. Генерируем файлы аналогично localise.com. С помощью SwiftGen генерируем код для использования ресурсов через константы и функции. Это помогло коллегам решить практически все проблемы, кроме захардкоженных строк и мусорных ключей, они всё ещё доставляли неудобства. Для нас такой сервис просто увеличил бы время разработки и решал бы задачу, которая на данный момент не стоит — улучшал качество перевода. Поэтому мы пришли к менее трудозатратному решению.  Хотелось получить решение, которое будет решать вообще все наши задачи. Изучив вышеописанные варианты, пришли к тому, что лучше всего использовать комбинацию из инструментов и написать свой линтер, который проверит то, с чем не справляются другие. Я назвал его 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.  Хочется подвести некий итог, не только про инструменты, но и в целом про выводы из многолетнего опыта и практики.  Это не подойдёт стартапам, но если вы разрабатываете сколько-то серьёзное приложение, то старайтесь делать локализацию (хотя бы с одним языком) максимально рано. Чем позже придёте к локализации, тем сложнее это будет сделать. Это подойдёт проектам, у которых есть потребность в повышении качества перевода. У нас в этом не было необходимости. Помогает тратить меньше времени на непрофильные задачи. Если проект небольшой, то присмотритесь к R.swift; на 10 экранах он проще и работает быстро. Если проект больше — то советую SwiftGen.  Даты, валюты, другие единицы измерения — не забывайте о них и используйте форматтеры:  Локализованные даты — используются разные форматы и разделители. Применяйте DateFormatter. Для корректной работы с валютами пригодится NumberFormatter. И MeasurementFormatter, чтобы правильно отображать единицы измерения. Локализуйте в том числе название приложения и системные сообщения. Это делается через файл info.plist: просто добавьте переводы для нужных сообщений. Обратите внимание, что при использовании Localinter с файлом info.plist нужно добавить небольшие настройки — так как там есть неиспользуемые в приложении ключи, его нужно исключить из обработки линтером.  Скорее всего, ваши тексты и картинки (а может, и другие ресурсы) будут подтягиваться с бэкенда, поэтому расскажите им про планы на локализацию заранее и договоритесь. И не забывайте обновлять сервисы, когда создаёте пакет локализации.  Он помог нам, может оказаться полезным и другим. Благодаря своей простоте он подойдёт для большинства проектов, а ещё его можно модифицировать под свои нужды. Присылайте идеи и замечания в pull requests, а если знаете другие способы ускорить и обезопасить процесс локализации — рассказывайте тут в комментариях к статье! А запись моего выступления по этой теме вы можете найти здесь:  МибилРук