Qt Quick の翻訳と表示言語の動的切り替え

# はじめに

昨日は @hermit4 さんによる [2017年に追加された機能一覧](https://qiita.com/hermit4/items/dbea02551ba1ac4968a3) という記事でした。[Qt 5.10 がリリース](https://blog.qt.io/jp/2017/12/08/qt-5-10-released/) された当日にその内容まですべてカバーしてて素晴らしいですね。

というわけで、今回は Qt 5.10 で導入された「Qt Quick の再翻訳機能」を早速使ってみました。

# QtWidgets の翻訳の仕組みのおさらい

Qt Quick の翻訳の前に、そもそも QtWidgets の翻訳の仕組みをおさらいしましょう。

## 基本的な流れ

1. ソースコードの翻訳対象の文字列を [tr()](http://doc.qt.io/qt-5/qobject.html#tr) で囲む ... [Writing Source Code for Translation](http://doc.qt.io/qt-5/i18n-source-translation.html)
2. プロジェクトファイルに `TRANSLATIONS = app_ja_JP.ts app_fr_FR.ts` と翻訳対象言語の .ts ファイルを宣言する ... [Specifying Translation Sources in Qt Project Files](http://doc.qt.io/qt-5/linguist-programmers.html#specifying-translation-sources-in-qt-project-files)
3. lupdate コマンドにより .pro ファイルから .ts ファイルを生成する ... [Using lupdate](http://doc.qt.io/qt-5/linguist-manager.html#using-lupdate)
4. Linguist ツールで .ts ファイルを翻訳する ... [Qt Linguist Manual: Translators](http://doc.qt.io/qt-5/linguist-translators.html)
5. lrelease コマンドにより .ts ファイルから .qm ファイルを生成する ... [Using lrelease](http://doc.qt.io/qt-5/linguist-manager.html#using-lrelease)
6. アプリケーションのソースコードで [QTranslator](http://doc.qt.io/qt-5/qtranslator.html) クラスでその .qm ファイルをロードし、アプリケーションにインストールする

詳細は Qt の国際化関連のドキュメント「[Internationalization with Qt](http://doc.qt.io/qt-5/internationalization.html)」を参照してください。
多言語対応のサンプルは「[Hello tr() Example](http://doc.qt.io/qt-5/qtlinguist-hellotr-example.html)」を参照してください。

## サンプルコード

```cpp:main.cpp

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QTranslator qtTranslator;
    qtTranslator.load("qt_" + QLocale::system().name(),
            QLibraryInfo::location(QLibraryInfo::TranslationsPath));
    app.installTranslator(&qtTranslator);

    QTranslator myappTranslator;
    myappTranslator.load("myapp_" + QLocale::system().name());
    app.installTranslator(&myappTranslator);

    ...
    return app.exec();
}
```

このような形で、アプリケーションの初期化時に翻訳の設定をするのが一般的な作りでした。
そのため、PC 向けの Qt アプリケーションでは言語の切り替え時にはアプリケーションを再起動するのが慣例となっていました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/13867/1799fc5c-df3b-23c1-7727-0ec5f4a22ad7.png)

# Qt Quick の翻訳の仕組み

基本的な流れは C++ の時と同じですが、tr() の代わりに、[qsTr()](http://doc.qt.io/qt-5/qml-qtqml-qt.html#qsTr-method) で文字列を囲みます。

```qml
Text {
    id: txt1;
    text: qsTr("Back");
}
```

Qt Quick の組み込み機器での採用が増えるにつれ、「アプリケーションの再起動なしに言語の切り替えをしたい」という要望をよく耳にするようになりました。

# QML での翻訳言語の動的切り替え

Qt の Wiki にはこれを実現するためのトリックが書かれたページが存在します。
[How to do dynamic translation in QML](https://wiki.qt.io/How_to_do_dynamic_translation_in_QML) 
詳細は割愛しますが、翻訳言語が変わった際（＝QTranslator が再インストールされた際）に、あるオブジェクトのプロパティを変え（たことにし）て、バインディングの再評価処理を走らせ、新しい QTranslator を用いて qsTr() での翻訳を行うといったトリックを用いています。

```qml
Text {
    text: qsTr("Hello") + rootItem.emptyString
}
```

これはソースコードの見た目があまりにもダサいので、私が先月 [Automotive Grade Linux](http://automotivelinux.org/) (**通称 AGL**)の [デモアプリの一つにこのような対応を入れた](https://git.automotivelinux.org/apps/dashboard/commit/?id=464e6c7f6056cec8a1d150216338d58a827307e6) 際には、以下のような記述にしてみました。

```qml
Translator {
    id: translator
}
Text {
    text: translator.translate(qsTr('LEFT FRONT TIRE'), translator.language)
}
```

少しだけ見た目が改善されたとは思いますが、仕組み的には同じような感じです。

# Qt 5.10 で「再翻訳が可能になりました」

驚くべき事に、**７年も前** に [この機能についてのリクエスト](https://bugreports.qt.io/browse/QTBUG-15602) が登録されています。

Qt 5.10 で [QQmlEngine::retranslate()](http://doc.qt.io/qt-5/qqmlengine.html#retranslate) というスロットが追加され、これを実行することですべての翻訳の再評価が行われるようになりました。
（5.10 の時点では、（翻訳対象の）文字列**以外**のバインディングもすべて再評価されてしまうようですが、将来的に改善される予定です。）

このスロットは QQmlEngine のインスタンスが `QEvent::LanguageChange` を受け取った際には自動的に呼び出されます。

ということは、[QCoreApplication::installTranslator() を呼ぶと自動で `QEvent::LanguageChange` が発生する](http://doc.qt.io/qt-5/qcoreapplication.html#installTranslator) ので、なにもせずとも自動で翻訳が切り替わる！！！



と思いたいところなのですが、トップレベルウィンドウに自動的に言語変更が伝搬する QtWidgets とは異なり、Qt Quick の場合は（少なくとも現時点では）なにもおこりません。

```cpp:qapplication.cpp
bool QApplication::event(QEvent *e)
{
    ...
    if(e->type() == QEvent::LanguageChange) { // こいつはがんばってる
        const QWidgetList list = topLevelWidgets();
        for (auto *w : list) {
            if (!(w->windowType() == Qt::Desktop))
                postEvent(w, new QEvent(QEvent::LanguageChange));
        }
    }
    ...
}
```

```cpp:qguiapplication.cpp
bool QGuiApplication::event(QEvent *e)
{
    if(e->type() == QEvent::LanguageChange) { // もうちょっとがんばれよ
        setLayoutDirection(qt_detectRTLLanguage()?Qt::RightToLeft:Qt::LeftToRight);
    }
    return QCoreApplication::event(e);
}
```

というわけで、以下のように明示的にイベントを送る必要があるのですが、

```cpp:
QEvent ev(QEvent::LanguageChange);
QCoreApplication::sendEvent(&engine, &ev);
```

直接イベント送るくらいなら、`QQmlEngine::retranslate()` を呼んだ方がいいかなって感じです。

## サンプルコード

```プロジェクトファイル
TEMPLATE = app
QT = quick
SOURCES = main.cpp
RESOURCES = qml.qrc translations.qrc
TRANSLATIONS = ja_JP.ts
```

TRANSLATIONS に翻訳用のファイルを羅列しています。

```cpp:main.cpp
#include <QtCore/QTranslator>
#include <QtGui/QGuiApplication>
#include <QtQml/QQmlApplicationEngine>
#include <QtQml/QQmlContext>

class I18N : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString language MEMBER language NOTIFY languageChanged)
public:
    explicit I18N(QObject *parent = nullptr)
        : QObject(parent)
        , language(QStringLiteral("C"))
    {}

signals:
    void languageChanged(const QString &language);

private:
    QString language;
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    QTranslator translator;
    I18N i18n;
    engine.rootContext()->setContextProperty("i18n", &i18n);
    QObject::connect(&i18n, &I18N::languageChanged, [&](const QString &language) {
        QCoreApplication::removeTranslator(&translator);
        if (translator.load(QStringLiteral("%1.qm").arg(language), QStringLiteral(":/translations"))) {
            QCoreApplication::installTranslator(&translator);
        }
        engine.retranslate();
    });
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

#include "main.moc"
```

language プロパティをもつ I18N という QObject のサブクラスを作成し、そのインスタンスを "i18n" という名前で qml 側から使えるようにしています。

この i18n の language プロパティが変更された場合には、その言語の翻訳ファイルを QTranslator で読み込んで、エンジンの再翻訳スロットを実行しています。

```xml:qml.qrc
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
    </qresource>
</RCC>
```

```qml:main.qml
import QtQuick 2.9
import QtQuick.Window 2.2

Window {
    id: root
    visible: true
    width: 300
    height: 100
    title: qsTr("Hello World")

    Text {
        anchors.centerIn: parent
        text: root.title
    }

    MouseArea {
        anchors.fill: parent
        onClicked: i18n.language = (i18n.language === 'C' ? 'ja_JP' : 'C')
    }
}
```

QML 側は、翻訳対象の文字列を qsTr() で囲むだけです。

MouseArea を利用して言語を変えるための仕組みも雑に実装しています。

```xml:ja_JP.ts
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
    <name>main</name>
    <message>
        <location filename="main.qml" line="13"/>
        <source>Hello World</source>
        <translation>世界の皆さんこんにちは</translation>
    </message>
</context>
</TS>
```

lupdate コマンドで生成される上記のファイルを、翻訳し、lrelease コマンドで生成した .qm ファイルを以下のリソースに組み込んで、":/translations/ja_JP.qm" というパスで読み込めるようにしています。

```xml:translations.qrc
<RCC>
    <qresource prefix="/translations">
        <file>ja_JP.qm</file>
    </qresource>
</RCC>
```

# おわりに

`QQmlEngine` のインスタンスに `QEvent::LanguageChange` を送りつけるか、`retranslate()` メソッドを呼ぶことで、qsTr() で囲んだ文字列（以外もです）がすべて更新されるということで、素晴らしく簡単に動的な言語切り替えが可能になりました。

言語の切り替えの度に再起動がいらないなんて Qt 的には夢のようです。

ということで、組み込み機器で多言語対応をしたいみなさんは、これだけのためにでも Qt 5.10 にアップデートしましょうね！


明日は @sharkpp さんが参上してくれるようです。お楽しみに！