QML

Локализация QML-приложений: qsTr, переключение языков на лету

Введение

Основной цикл статей касался интернационализации desktop-приложений на Qt Widgets — в этой статье разберём специфику локализации QML-приложений: функцию qsTr() для перевода текстовых строк и динамическое переключение языка интерфейса без перезапуска приложения.

Концепция

qsTr() (аналог tr() для C++ из основного цикла статей) помечает строковый литерал как подлежащий переводу — инструменты Qt Linguist (тот же инструментарий, что используется и для Qt Widgets приложений) извлекают все вызовы qsTr() из QML-файлов для создания файлов перевода (.ts), которые затем переводятся и компилируются в файлы .qm, загружаемые приложением во время выполнения. Для динамического переключения языка без перезапуска приложения нужно явно вызвать qmlEngine->retranslate() после загрузки нового файла перевода, и обеспечить, что все текстовые свойства, использующие qsTr(), повторно пересчитываются — что естественно работает для строк, использующих qsTr() непосредственно в привязке свойства, но требует более явного внимания для строк, вычисленных и сохранённых заранее.

Пример кода: Локализация QML-приложений

// Использование qsTr() для текстовых строк, подлежащих переводу
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    title: qsTr("Главное окно приложения")

    Column {
        Label { text: qsTr("Введите ваше имя:") }
        TextField { placeholderText: qsTr("Имя") }

        Button {
            text: qsTr("Сохранить")
            onClicked: console.log(qsTr("Данные сохранены"))
        }

        // Перевод с параметром через arg()
        Label { text: qsTr("Найдено %1 результатов").arg(searchResultsCount) }
    }
}
// Динамическое переключение языка во время выполнения C++ кода приложения
#include <QQmlApplicationEngine>
#include <QTranslator>

class LanguageManager : public QObject
{
    Q_OBJECT
public:
    LanguageManager(QQmlApplicationEngine *engine, QObject *parent = nullptr)
        : QObject(parent), m_engine(engine) {}

    Q_INVOKABLE void switchLanguage(const QString &languageCode)
    {
        qApp->removeTranslator(&m_translator);

        if (m_translator.load(":/translations/app_" + languageCode + ".qm")) {
            qApp->installTranslator(&m_translator);
        }

        m_engine->retranslate(); // обязательный вызов — заставляет ВСЕ qsTr() в QML пересчитаться заново
    }

private:
    QQmlApplicationEngine *m_engine;
    QTranslator m_translator;
};

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

qsTr("Найдено %1 результатов").arg(searchResultsCount) показывает паттерн перевода строк с динамическими параметрами — текст шаблона ("Найдено %1 результатов") переводится целиком, с сохранением позиции %1, что даёт переводчику возможность естественно переставить порядок слов в переводе при необходимости (некоторые языки требуют иного порядка слов относительно вставляемого числового значения по сравнению с исходным языком). engine->retranslate() — критичный, легко упускаемый вызов для динамического переключения языка — без него уже загруженные и отображённые QML-привязки, использующие qsTr(), не пересчитываются автоматически даже после загрузки нового файла перевода через installTranslator(), и пользователь продолжил бы видеть текст на предыдущем языке до перезапуска приложения или явного вызова retranslate().

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

  • Сохранение результата qsTr() в обычном, не являющемся привязкой свойстве (например, в JavaScript-переменной, вычисленной один раз при инициализации, а не в виде живой привязки text: qsTr(...)) — такое сохранённое значение не пересчитается автоматически при вызове retranslate(), поскольку retranslate() повторно вычисляет именно сами привязки, содержащие qsTr(), а не произвольный, ранее вычисленный и просто сохранённый результат вызова этой функции.
  • Конкатенация нескольких отдельных переведённых фрагментов строки вместо использования единого шаблона с параметрами (qsTr("Найдено ") + count + qsTr(" результатов") вместо qsTr("Найдено %1 результатов").arg(count)) — это, аналогично подводным камням интернационализации в основном цикле статей, лишает переводчика возможности естественно изменить порядок слов для языков, где это грамматически необходимо, и единый шаблон с позиционным параметром (%1) — более гибкий и корректный подход для подавляющего большинства языков перевода.
  • Забытое включение QML-файлов проекта в процесс сканирования инструментами Qt Linguist для извлечения строк, подлежащих переводу — при настройке процесса локализации проекта важно убедиться, что используемые инструменты (lupdate) действительно сканируют QML-файлы наравне с C++ файлами, а не только последние, иначе строки, помеченные qsTr() исключительно в QML-коде, не попадут в файл перевода вовсе.
  • Отсутствие явного механизма уведомления QML-кода о смене языка для логики, зависящей не от текста напрямую, а от выбранного языка (форматирование чисел/дат, направление текста для языков с письмом справа налево) — переключение языка может требовать более широких изменений интерфейса, чем просто перевод текстовых строк (выравнивание текста, направление расположения элементов для языков RTL), и приложение с серьёзными требованиями к полноценной локализации должно явно учитывать эти более широкие аспекты, а не ограничиваться исключительно переводом отдельных текстовых строк через qsTr().