Приветствую, дорогие хабражители!

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

Для тех, кто с этим пока не сталкивался, объясню проблему подробнее на примере.

Допустим, у нас есть большой проект, в котором 90% общего кода и 3 таргета: MyApp1, MyApp2, MyApp3, которые имеют некоторое количество специфичных экранов, а так же каждый имеет своё название и тексты. По сути таргет представляет из себя самостоятельное приложение. Каждый из них должен быть переведен на 10 языков. При этом мы НЕ хотим добавлять ключи локализации типа app1_localizable_key1, app2_localizable_key1 и т.д. Хотим, чтобы в коде всё было красиво и локализация происходила одной строчкой

NSLocalizedString(@"localizable_key1", nil)

Без всяких if и ifdef, чтобы при добавлении нового таргета нам не пришлось искать по всему коду огромного проекта места с NSLocalizedString и прописывать там новые ключи. Так же хотим, чтобы часть ключей была привязана к специфичным экранам таргета, т.е. были ключи app2_screen1_key, app3_screen2_key.

Штатными средствами Xcode сейчас можно сделать следующее:

  • Скопировать общую часть localizable.strings в каждый таргет, при этом мы получим 3 копии этих файлов.
  • Добавить в соответствующие localizable.strings ключи специфичные для конкретного таргета.

Каким проблемы мы получаем:

  • Добавить новый общий ключ в проект достаточно накладно. Число мест равняется числу таргетов помноженному на число языков. В нашем примере это 30 мест.
  • Есть вероятность ошибки, когда добавили строку в 1-2 текущих таргета, с которыми идёт активная работа, а через год решили воскресить еще один или несколько таргетов. Придется вручную синхронизировать между собой локализации, либо писать для этого скрипт. А если была проявлена некоторая неряшливость при добавлении или мерже веток, и общие ключи смешаны со специфичными, то тут будет самый настоящий квест.
  • Объём файлов локализации. Они все постоянно растут, это затрудняет работу с ними и увеличивает шансы конфликта при мерже веток.

Что хотелось бы:

  • Чтобы все общие ключи хранились в отдельном файле.
  • Для каждого таргета был файл, в котором хранились только специфичные для него ключи, а так же общие ключи со значениями для данного таргета.

Для нашего примера имея общий файл localizable.strings со строками

"shared_localizable_key1" = "MyApp title"
"shared_localizable_key2" = "MyApp description"
"shared_localizable_key3" = "Shared text1"
"shared_localizable_key4" = "Shared text2"

Хотелось бы иметь файл localizable_app2.strings, в котором были бы ключи

"shared_localizable_key1" = "MyApp2 another title"
"shared_localizable_key2" = "MyApp2 another description"
"app2_screen1_key" = "Profile screen title"

Т.е. организовать в файлах локализации принцип наследования.

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

Мы имеем проект с 18 таргетами и 12 языками. И это не шутка, проект действительно большой и такое количество таргетов там необходимо. Каждый раз, когда нам нужно добавить новый общий ключ для перевода, мы имеем дело с 216 файлами локализации. Это отнимает достаточно много времени. А добавление нового таргета приводит к тому, что нужно скопировать в него еще 12 localizable.strings. В общем в какой-то момент мы поняли, что так больше жить нельзя и нужно искать решение.

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

Итак, для начала нам нужно было найти все общие ключи. Это можно сделать с помощью скрипта, не буду вдаваться в подробности, это достаточно тривиальная задача.

Далее, когда мы получили общий (базовый) файл локализации, а точнее 12 физических файлов, а так же набор файлов для каждого таргета, идем в Xcode, добавляем туда все файлы. При этом не прикрепляем файлы к какому-либо таргету, т.е. в правой панели в разделе Target Membership не должно быть отметок.



Эти отметки мы поставим только для файла, который будет результатом работы скрипта по сборке файлов.

Далее начинается тот самый «велосипед»:

  • Создаём в корне папку Localization, там будет лежать скрипт build_localization.py.
  • Создаём рядом со скриптом папку Localizable. В неё скрипт будет генерировать файлы localizable.strings.
  • Копируем в папку Localizable базовую локализацию.



Она нам нужна просто для того, чтобы корректно добавить ссылку на файлы в проект, и чтобы Xcode правильно их распознал. Иначе он не будет их использовать для поиска ключей. Например, если создать папку Localizable с правильно разложенными файлами localizable.strings внутри, и добавить в проект как ссылку на папку (create folder references), то не смотря ни на что Xcode не поймет, что мы дали ему ключи локализации. По-этому берем папку Localizable, перетаскиваем как группу (create group) и снимаем галочку copy items if needed, чтобы получилось как на картинке ниже.



Удаляем папку Localizable и вносим её в исключения для гита. Потому что результат работы скрипта нам в гите не нужен, он будет меняться для каждого таргета и засорять коммиты.

Теперь нам нужно добавить скрипт в фазу сборки. Для этого в Build Phases нажимаем New Run Script Phase и прописываем наш скрипт с параметрами. ?

python3 ${SRCROOT}/Localization/build_localization.py -b “${SRCROOT}/BaseLocalization" -s "${SRCROOT}/Target1Localization" -d "${SRCROOT}/Localization/Localizable"
?
b — это папка с базовой локализацией, s — локализация текущего таргета, d — папка результата.

Перемещаем новую фазу вверх, она должна быть не ниже фазы Copy Bundle Resources. Т.е. сначала скрипт генерирует файлы, а уже потом они забираются в бандл.



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



Это нужно проделать для каждого таргета. Проще всего это сделать открыв проект с помощью текстового редактора, потому что Xcode не сумеет скопировать/вставить фазу между таргетами. Соответственно параметр скрипта -s для каждого таргета будет свой.

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

В целом получили то, что и планировалось при реализации механизма наследования:

  • Общие ключи лежат в одном файле и не мешаются в других. Время на процесс внесения новых ключей сокращено в 18! раз.
  • Ключи, относящиеся к конкретному таргету, лежат в соответствующем файле.
  • Размер файлов значительно снизился. Избавились от захламления повторяющимися строками.
  • Процесс добавления нового языка в проект так же значительно упрощён.
  • При создании нового таргета не нужно копировать локализацию с кучей ненужных строк. Создаём новый файл localizable.strings и добавляем туда только нужное для этого таргета.
  • Если решили реанимировать старый таргет, то со строками вообще ни чего делать не надо, всё подтянется из базового файла.
  • Скрипт не захламляет гит, результат работы остаётся локально и его можно безболезненно удалить.

> Готовый скрипт можно взять тут

Не претендую на идеальность скрипта, пул-реквесты приветствуются.