Пользовательский интерфейс в Qt

Совместное проектирование локализации и доступности

Введение

Локализация (поддержка нескольких языков, разобранная в основном цикле статей про CMake/Boost и Qt Linguist) и доступность на первый взгляд кажутся независимыми задачами, но на практике они тесно пересекаются: текст для screen reader должен переводиться так же, как и видимый текст интерфейса, направление текста (RTL для арабского/иврита) влияет на логику клавиатурной навигации и tab order, а длина переведённого текста на разных языках может радикально отличаться, что влияет на расчёты размеров, разобранные в предыдущей статье. В заключительной статье цикла разберём эти точки пересечения.

Концепция

Каждый вызов setAccessibleName()/setAccessibleDescription() с захардкоженной русской или английской строкой ломает локализацию приложения для других языков точно так же, как захардкоженный видимый текст интерфейса — эти строки должны проходить через тот же механизм перевода tr(), что и обычный видимый текст, и попадать в те же файлы .ts для переводчиков (статья 39 основного цикла). Для языков с направлением письма справа налево (RTL) Qt автоматически отражает большинство layout’ов через QGuiApplication::setLayoutDirection(), но порядок tab order и логика навигации стрелками в кастомных компонентах должны быть явно проверены и, возможно, скорректированы для этого случая, поскольку «следующий» логический элемент при RTL может физически располагаться слева, а не справа.

Пример кода: Совместное проектирование локализации

#include <QPushButton>
#include <QApplication>

// ПРОБЛЕМНО: accessibleName на фиксированном языке игнорирует локализацию,
// делая интерфейс недоступным для screen reader пользователей других языков
void createButtonBadPractice(QWidget *parent)
{
    auto *button = new QPushButton(QObject::tr("Сохранить"), parent);
    button->setAccessibleName("Сохранить текущий документ"); // не переведено!
}

// ЛУЧШЕ: accessibleName проходит через тот же механизм перевода tr(),
// что и видимый текст — попадает в тот же .ts файл для переводчиков
void createButtonGoodPractice(QWidget *parent)
{
    auto *button = new QPushButton(QObject::tr("Сохранить"), parent);
    button->setAccessibleName(QObject::tr("Сохранить текущий документ"));
}
// Учёт направления письма (RTL) при расчёте кастомной клавиатурной навигации
#include <QGuiApplication>
#include <QKeyEvent>

class CustomCarousel : public QWidget
{
    Q_OBJECT
protected:
    void keyPressEvent(QKeyEvent *event) override
    {
        const bool isRtl = QGuiApplication::layoutDirection() == Qt::RightToLeft;

        // Логическое значение "следующий элемент" зависит от направления письма:
        // при RTL клавиша "влево" должна перемещать к логически СЛЕДУЮЩЕМУ элементу,
        // а не к предыдущему, как было бы при LTR
        bool moveToNext = false;
        if (event->key() == Qt::Key_Right) {
            moveToNext = !isRtl;
        } else if (event->key() == Qt::Key_Left) {
            moveToNext = isRtl;
        } else {
            QWidget::keyPressEvent(event);
            return;
        }

        if (moveToNext) {
            selectNextItem();
        } else {
            selectPreviousItem();
        }
    }

private:
    void selectNextItem();
    void selectPreviousItem();
};
// QML-layout, автоматически адаптирующийся к RTL без ручной логики
import QtQuick

// LayoutMirroring автоматически отражает anchors (left/right) при RTL,
// что снимает необходимость в ручной проверке направления для большинства
// простых случаев — но НЕ для логики клавиатурной навигации, как в примере выше
Item {
    LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
    LayoutMirroring.childrenInherit: true

    Row {
        anchors.left: parent.left // автоматически становится "anchors.right" при RTL
        Text { text: qsTr("Имя пользователя") }
    }
}

Пояснения к коду

createButtonGoodPractice() оборачивает строку accessible name в тот же tr(), что используется для видимого текста кнопки — это гарантирует, что переводчики, работающие с файлами .ts (статья 39), увидят и переведут эту строку наравне с остальным видимым текстом интерфейса, и screen reader пользователь, работающий на другом языке, услышит корректно переведённое описание, а не оригинальный русский текст независимо от выбранного языка интерфейса.

CustomCarousel::keyPressEvent() явно проверяет QGuiApplication::layoutDirection() и инвертирует логику навигации стрелками для RTL-языков — это критично для кастомных компонентов, реализующих собственную логику навигации (в отличие от стандартных layout’ов и виджетов Qt, которые в большинстве случаев автоматически адаптируются к RTL через встроенные механизмы). QML-пример показывает LayoutMirroring — встроенный механизм автоматического зеркалирования простых анкоров без необходимости в ручной проверке направления, но явно отмечает, что такое автоматическое зеркалирование не покрывает кастомную императивную логику навигации, которую нужно проверять и адаптировать вручную, как в примере с CustomCarousel.

Подводные камни

  • Захардкоженные accessible-строки, забытые при первоначальной локализации приложения, обнаруживаются значительно позже, чем обычные пропуски в переводе видимого текста — поскольку они не видны зрячему тестировщику переключающему язык интерфейса визуально, такие пропуски пропускаются стандартным процессом QA и обнаруживаются только при специальном тестировании с screen reader на нерусском/неанглийском языке интерфейса.
  • Существенная разница в длине переведённого текста между языками, ломающая расчёты размеров элементов, рассчитанных под один конкретный язык (часто — английский или русский исходник) — немецкий перевод того же текста в среднем заметно длиннее английского оригинала, а перевод на китайский — заметно компактнее; жёстко зафиксированные размеры кнопок и полей, не учитывающие эту вариативность (см. предыдущую статью про масштабирование), могут одинаково плохо работать как для увеличенного шрифта, так и для просто более длинного перевода на другой язык интерфейса.
  • LayoutMirroring автоматически отражает геометрию, но не порядок логической навигации Tab/Focus между элементами в кастомных компонентах с собственноручно прописанной логикой обхода фокуса (например, через явный setTabOrder(), как разбиралось в статье 202) — простое включение LayoutMirroring.enabled не гарантирует автоматическую корректность tab order для RTL без отдельной проверки и, возможно, явной адаптации этой логики.
  • Тестирование RTL-поддержки только визуально, без проверки реального направления навигации с клавиатуры или screen reader. Интерфейс может выглядеть визуально корректно отражённым для RTL-языка (текст, иконки, layout зеркалированы правильно), но при этом клавиатурная навигация может остаться «залипшей» в LTR-логике (как было бы без явной проверки layoutDirection() в CustomCarousel), что незаметно при чисто визуальной проверке, но сразу проявляется при реальном использовании клавиатуры носителем RTL-языка.