Приветствую, дорогие хабражители!
Сегодня я хочу поделиться интересным опытом в решении проблемы локализации. В 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 и добавляем туда только нужное для этого таргета.
- Если решили реанимировать старый таргет, то со строками вообще ни чего делать не надо, всё подтянется из базового файла.
- Скрипт не захламляет гит, результат работы остаётся локально и его можно безболезненно удалить.
→ Готовый скрипт можно взять тут
Не претендую на идеальность скрипта, пул-реквесты приветствуются.