Во время одного из релизных спринтов в ветке разработки появилось более десятка новых строк интерфейса. Архивация завершилась успешно, однако после переключения языка тестировщики обнаружили откат к английскому тексту и пустые кнопки. Причина была не в компиляторе: конвейер никогда не рассматривал полноту локализации как проверку, способную завершить сборку с ошибкой. Для постоянно работающего облачного Mac эффективнее не ждать ручной проверки всех экранов, а проверять .xcstrings перед каждой сборкой.
Сначала определим, что именно должна блокировать проверка
String Catalog представляет собой JSON-файл, но возможность его разобрать ещё не означает готовность к выпуску. Практическая проверка должна выявлять как минимум три типа проблем: отсутствие обязательного целевого языка, состояние перевода, отличное от translated, и пустую итоговую строку. Формы множественного числа, варианты для разных устройств и другие подобные данные находятся внутри variations, поэтому проверка только первого уровня stringUnit приведёт к пропущенным ошибкам.
Критерии приёмки следует хранить в репозитории, а не во временной конфигурации сборочного узла. Предположим, что исходный язык проекта — английский, а обязательные целевые языки — упрощённый китайский, японский и французский. Их можно передавать скрипту в качестве параметров.
| Проверка | Условие ошибки | Действие |
|---|---|---|
| Целевой язык | В localizations отсутствует соответствующий ключ |
Остановить сборку |
| Состояние перевода | Хотя бы один конечный узел имеет состояние, отличное от translated |
Вывести конкретный ключ и язык |
| Содержимое строки | value пусто или содержит только пробельные символы |
Остановить сборку |
| Строка не переводится | shouldTranslate имеет значение false |
Явно пропустить |
Эта проверка определяет только полноту ресурсов локализации в каталоге. Она не заменяет ручную проверку обрезанного текста в интерфейсе, динамических параметров и смысловой точности перевода.
Создаём валидатор без внешних зависимостей
Сохраните скрипт как Scripts/check_xcstrings.py. Он использует только среду Python, доступную в macOS. Если команда закрепила отдельный путь к Python, его следует явно указать в конфигурации сборки, чтобы различия в PATH между интерактивной оболочкой и CI не влияли на результат.
#!/usr/bin/env python3
import argparse
import json
import pathlib
import sys
def units(node):
found = []
if isinstance(node, dict):
unit = node.get("stringUnit")
if isinstance(unit, dict):
found.append(unit)
for key, value in node.items():
if key != "stringUnit":
found.extend(units(value))
elif isinstance(node, list):
for value in node:
found.extend(units(value))
return found
parser = argparse.ArgumentParser()
parser.add_argument("root")
parser.add_argument("--locale", action="append", required=True)
args = parser.parse_args()
failures = []
files = sorted(pathlib.Path(args.root).rglob("*.xcstrings"))
if not files:
failures.append("no .xcstrings files found")
for path in files:
with path.open(encoding="utf-8") as handle:
catalog = json.load(handle)
for key, entry in catalog.get("strings", {}).items():
if entry.get("shouldTranslate") is False:
continue
localizations = entry.get("localizations", {})
for locale in args.locale:
localized = localizations.get(locale)
if localized is None:
failures.append(f"{path}:{key}:{locale}:missing locale")
continue
leaves = units(localized)
if not leaves:
failures.append(f"{path}:{key}:{locale}:missing stringUnit")
continue
for index, unit in enumerate(leaves):
state = unit.get("state")
value = unit.get("value", "")
if state != "translated":
failures.append(
f"{path}:{key}:{locale}:{index}:state={state}"
)
if not value.strip():
failures.append(
f"{path}:{key}:{locale}:{index}:empty value"
)
for failure in failures:
print(failure, file=sys.stderr)
sys.exit(1 if failures else 0)
Выполните в корневом каталоге репозитория:
python3 Scripts/check_xcstrings.py . \
--locale zh-Hans \
--locale ja \
--locale fr
Если скрипт завершится с ненулевым кодом, любой распространённый оркестратор задач сможет остановить последующие этапы. В выводе указаны файл, ключ строки, язык и номер конечного узла, поэтому для исправления ошибки не потребуется сначала просматривать весь журнал сборки.
Подключаем проверку к конвейеру на облачном Mac
Рекомендуется запускать проверку после разрешения зависимостей, но до xcodebuild build или archive. Подготовка зависимостей подтверждает, что содержимое репозитория полностью размещено на диске, а ранняя проверка позволяет не тратить время на компиляцию, тестирование и архивацию заведомо непригодного коммита.
set -euo pipefail
mkdir -p build/reports
python3 Scripts/check_xcstrings.py Sources \
--locale zh-Hans \
--locale ja \
--locale fr \
2> build/reports/localization-errors.txt
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
build
Параметр set -o pipefail особенно важен. Если позднее вывод будет передаваться в tee, без него конвейер может увидеть только успешное завершение последней команды и проигнорировать ошибку валидатора. Каталог отчётов также следует пересоздавать в начале задачи, чтобы файлы от предыдущего запуска на облачном Mac не были ошибочно приняты за результаты текущей проверки.
Добавляем ручную проверку экспортированных локализаций
Непосредственный разбор .xcstrings удобен для автоматического контроля, но перед выпуском также следует экспортировать пакеты локализации и проверить полноту языка разработки, комментариев и контекста. Для целевых языков можно выполнить:
rm -rf build/xcloc
mkdir -p build/xcloc
xcodebuild -exportLocalizations \
-project App.xcodeproj \
-localizationPath build/xcloc \
-exportLanguage zh-Hans
xcodebuild -exportLocalizations \
-project App.xcodeproj \
-localizationPath build/xcloc \
-exportLanguage ja
Если экспорт завершился ошибкой, не следует сразу повторять его с перезаписью диагностических данных. Сначала сохраните полный стандартный поток ошибок и проверьте scheme, путь к проекту и идентификатор целевого языка. Код языка должен точно совпадать с ключом в Catalog: например, zh-Hans и zh-Hant — два независимых целевых языка, и заменять их общим префиксом нельзя.
Экспортированный .xcloc удобно передать ответственному за локализацию для выборочной проверки, однако результаты сборки не рекомендуется добавлять в основную ветку. В репозитории должны храниться исходные .xcstrings, скрипт проверки и список целевых языков, а каталог экспорта следует оставлять артефактом конкретного запуска.
Устраняем типичные ложные срабатывания и проводим приёмку
Первый тип ложных срабатываний связан с техническими строками, которые не требуется переводить. Не следует произвольно исключать их в скрипте по префиксу ключа. Вместо этого явно задайте в Catalog shouldTranslate: false, чтобы правило хранилось вместе с самим ресурсом. Второй тип возникает, когда для множественного числа переведена только ветвь one, а other пропущена. Рекурсивное чтение каждого stringUnit позволяет точно обнаружить такую ошибку.
Третий тип проблемы возникает, когда разработчик добавляет язык, но забывает обновить параметры CI. У списка целевых языков должен быть единый источник — например, массив в скрипте проекта или переменная конвейера, — чтобы разные задачи не поддерживали отдельные копии. Четвёртый тип связан с пробелами, переводами строк или заполнителями, ошибочно принятыми за полноценный перевод. Текущий скрипт отклоняет строки, состоящие только из пробельных символов. Для параметров формата %@, %d и подобных можно также добавить сравнение наборов заполнителей в исходной и целевой строках, но блокирующую проверку следует включать только после поддержки экранирования и позиционных параметров.
Перед слиянием выполните приёмку в следующем порядке:
- Намеренно удалите один целевой язык и убедитесь, что задача завершается ошибкой.
- Замените один перевод пробелом и убедитесь, что в отчёте указан конкретный ключ.
- Создайте незавершённую ветвь множественного числа и убедитесь, что рекурсивная проверка её обнаруживает.
- Восстановите ресурсы, запустите проверку повторно и убедитесь, что старый отчёт не влияет на результат.
- В завершение выполните сборку в конфигурации Release и проверьте, что контрольный этап и основная задача используют одну и ту же извлечённую копию репозитория.
В результате получается не разовое сканирование переводов, а стабильно воспроизводимое на облачном Mac инженерное ограничение: при неполных ресурсах процесс завершается ошибкой как можно раньше, а компиляция, тестирование и архивация начинаются только после их исправления.
Часто задаваемые вопросы
На каком этапе CI запускать проверку String Catalog?
Запускайте её после подготовки зависимостей, но до компиляции и архивирования. При ошибке задание остановится до наиболее затратных этапов сборки.
Достаточно ли проверять только состояние translated?
Нет. Нужно также проверять наличие каждого целевого языка, пустые значения и все вложенные stringUnit внутри вариантов, включая формы множественного числа.
Запустите следующую сборку в облачном Mac.
Сравните три конфигурации Apple Silicon и выберите подходящий для текущего рабочего процесса регион среди пяти зарубежных узлов.