Регрессия псевдолокализации iOS на облачном Mac

Регрессия псевдолокализации iOS на облачном Mac

Английская надпись на кнопке может состоять всего из шести букв в среде разработки, но после перевода на немецкий занять всю строку. А кнопка возврата, расположенная слева, в интерфейсе с направлением справа налево может по-прежнему указывать не в ту сторону. Если обнаружить такие проблемы только после завершения перевода, исправления затронут вёрстку, тестирование и сроки выпуска. Надёжнее при каждой проверке слияния на облачном Mac принудительно удлинять строки, менять направление интерфейса и сразу завершать проверку с ошибкой, если элементы управления не видны или недоступны для взаимодействия.

Сначала определите, что должен выявлять регрессионный барьер

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

Барьер должен в первую очередь выявлять четыре типа проблем: видимой области текстового элемента явно недостаточно; основная кнопка отсутствует, недоступна для нажатия или находится за пределами окна; в интерфейсе появляется жёстко заданный текст, который не прошёл через слой локализации; навигация, горизонтальные списки и направленные значки не меняются при включении RTL.

Псевдолокализация выявляет структурные проблемы, но не определяет точность перевода. Проверка на реальных языках по-прежнему необходима, однако на неё не следует перекладывать затраты на поиск таких базовых дефектов, как фиксированная ширина.

Создайте управляемый слой преобразования строк

Не обходите представления в тестах и не изменяйте их метки. Такой подход минует реальный путь получения строк приложением и не позволяет обнаружить жёстко заданный текст. Надёжнее добавить к уже существующей точке входа локализации преобразователь, который работает только в отладочных сборках.

enum PseudoMode: String {
    case expanded
    case rtl
}

#if DEBUG
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
    guard let mode else { return value }

    switch mode {
    case .expanded:
        let padding = String(repeating: "·", count: max(2, value.count * 2 / 5))
        return "[\(value)\(padding)]"
    case .rtl:
        return "⁧\(value)⁩"
    }
}
#else
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
    value
}
#endif

При запуске приложение должно читать PSEUDO_LOCALE и принимать только явно заданные значения перечисления. Все пользовательские строки должны проходить через единую точку входа, например L10n.text("checkout.confirm"), и только затем передаваться преобразователю. Если у какого-либо заголовка не появились граничные маркеры, значит, он обошёл слой локализации.

В режиме RTL необходимо также задать семантическое направление в корневом представлении приложения. В UIKit для тестового окна можно установить semanticContentAttribute = .forceRightToLeft; в SwiftUI значение layoutDirection можно внедрить в корневое представление. Не отражайте содержимое изображений и не пытайтесь принудительно разворачивать числа, пути или фрагменты кода.

Проверяйте видимость и обрезку с помощью UI-тестов

В тестах не нужно сравнивать каждый пиксель. Сначала охватите основные сценарии: главный экран после входа, списки, подробные представления, формы и состояния ошибок. Для ключевых элементов управления задайте стабильные accessibility identifier. Тестовый процесс должен запускать нужный режим через переменную окружения:

func launchApp(mode: String) -> XCUIApplication {
    let app = XCUIApplication()
    app.launchEnvironment["PSEUDO_LOCALE"] = mode
    app.launchArguments += ["-UI_TESTING", "1"]
    app.launch()
    return app
}

func testExpandedCheckoutLayout() {
    let app = launchApp(mode: "expanded")
    let confirm = app.buttons["checkout.confirm"]

    XCTAssertTrue(confirm.waitForExistence(timeout: 8))
    XCTAssertTrue(confirm.isHittable)
    XCTAssertFalse(confirm.frame.isEmpty)
    XCTAssertTrue(app.windows.firstMatch.frame.contains(confirm.frame))
}

isHittable полезнее простой проверки exists: кнопка может присутствовать в дереве доступности, но быть перекрыта модальным окном или находиться за пределами экрана. Для текстовых элементов можно получить frame по identifier и проверить, что он не выходит за границы своего контейнера. Не пытайтесь определять обрезку текста с помощью OCR. Для критически важных компонентов лучше в отладочной сборке предоставлять разницу между intrinsicContentSize и фактическим frame.

Оформите область проверки в виде списка

СценарийПроверка expandedПроверка RTLДанные о сбое
Панель навигацииЗаголовок и действия не перекрываютсяНаправление возврата и порядок элементов верныИерархия интерфейса, frame элементов
ФормаМетки переносятся, сообщения об ошибках отображаются полностьюМетки и поля ввода выровнены по одной осиidentifier поля, текущее значение
СписокДлинный заголовок не вытесняет основное действиеВложенные элементы и стрелки расположены правильноИндекс строки со сбоем, снимок экрана
Модальное окноКнопки отображаются полностью и доступны для нажатияПорядок действий соответствует семантикеframe модального окна, дерево элементов

Организуйте среду выполнения на облачном Mac

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

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
export PSEUDO_LOCALE="${PSEUDO_LOCALE:-expanded}"
RESULT_DIR="$PWD/TestResults/$PSEUDO_LOCALE"
mkdir -p "$RESULT_DIR"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "platform=iOS Simulator,name=iPhone 16,OS=latest" \
  -resultBundlePath "$RESULT_DIR/LayoutTests.xcresult" \
  PSEUDO_LOCALE="$PSEUDO_LOCALE"

expanded и rtl следует запускать как два независимых задания, а не переключать в рамках одного сеанса симулятора. Так причину сбоя можно однозначно связать с конкретным режимом, а повторный запуск не унаследует направление интерфейса от предыдущего теста. Если на облачном Mac одновременно выполняется несколько заданий, каждому из них также нужен отдельный путь DerivedData, чтобы результаты тестов не перезаписывали друг друга.

Разделяйте типичные ложные срабатывания и реальные дефекты

Чаще всего ложные срабатывания возникают из-за незавершённой анимации, незакрытого системного модального окна или непостоянной длины тестовых данных. Решение заключается не в добавлении одинаковой задержки ко всем тестам, а в ожидании конкретного состояния: исчезновения индикатора загрузки, появления нужного элемента или стабилизации количества окон. Тестовые данные также должны быть фиксированными, особенно имена пользователей, даты и тексты серверных ошибок.

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

При обнаружении сбоя сохраните как минимум следующие данные:

  1. Текущий режим псевдоязыка, версии Xcode и симулятора.
  2. identifier, frame и label проблемного элемента, а также сведения о его доступности для нажатия.
  3. Иерархию доступности текущего экрана.
  4. Соответствующий xcresult и снимок экрана в момент сбоя.
  5. Фиксированные тестовые данные, необходимые для воспроизведения этого экрана.

Задайте порог слияния и предотвратите утечку тестовых ресурсов

Барьер должен блокировать проблемы, мешающие выполнить задачу: основное действие не видно или оно недоступно для нажатия, после обрезки текст теряет смысл, порядок элементов в RTL неверен, интерактивный элемент не имеет доступного имени. Незначительные изменения отступов можно записывать для последующей проверки — не каждое расхождение в пикселях должно блокировать слияние.

Наконец, добавьте проверку Release-сборки: просканируйте настройки сборки и ресурсы приложения, убедитесь в отсутствии тестовых файлов псевдоязыка и проверьте, что официальная сборка игнорирует PSEUDO_LOCALE. Также запустите набор дымовых тестов в обычном режиме, чтобы убедиться, что сам слой преобразования не изменяет строки.

Ценность этого процесса заключается не в создании странно выглядящего интерфейса, а в превращении ручной проверки многоязычной вёрстки перед выпуском в воспроизводимое инженерное ограничение для каждого изменения кода. Сначала охватите основные сценарии, а затем постепенно добавляйте состояния ошибок и редко используемые модальные окна — так барьер останется стабильным и удобным в сопровождении.

Часто задаваемые вопросы

Заменяет ли псевдолокализация проверку на реальных языках?

Нет. Она быстро находит фиксированную ширину, непереведённые строки и ошибки RTL, но финальная проверка нужна как минимум на одном многословном и одном RTL-языке.

Как не допустить тестовый преобразователь в рабочую сборку?

Поместите его под условие DEBUG и останавливайте Release-сборку, если обнаружена переменная PSEUDO_LOCALE или ресурс, предназначенный только для тестов.

Какие ошибки должны блокировать слияние?

Слияние следует блокировать, если основное действие скрыто или недоступно, текст теряет смысл из-за обрезки, нарушен RTL-порядок либо отсутствует метка доступности.

Выделенный физический узел

Перенесите следующую сборку в облачный Mac mini

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

Арендовать Mac mini