はじめに
昨日は @hermit4 さんによる 2017年に追加された機能一覧 という記事でした。Qt 5.10 がリリース された当日にその内容まですべてカバーしてて素晴らしいですね。
というわけで、今回は Qt 5.10 で導入された「Qt Quick の再翻訳機能」を早速使ってみました。
QtWidgets の翻訳の仕組みのおさらい
Qt Quick の翻訳の前に、そもそも QtWidgets の翻訳の仕組みをおさらいしましょう。
基本的な流れ
- ソースコードの翻訳対象の文字列を tr() で囲む ... Writing Source Code for Translation
- プロジェクトファイルに
TRANSLATIONS = app_ja_JP.ts app_fr_FR.ts
と翻訳対象言語の .ts ファイルを宣言する ... Specifying Translation Sources in Qt Project Files - lupdate コマンドにより .pro ファイルから .ts ファイルを生成する ... Using lupdate
- Linguist ツールで .ts ファイルを翻訳する ... Qt Linguist Manual: Translators
- lrelease コマンドにより .ts ファイルから .qm ファイルを生成する ... Using lrelease
- アプリケーションのソースコードで QTranslator クラスでその .qm ファイルをロードし、アプリケーションにインストールする
詳細は Qt の国際化関連のドキュメント「Internationalization with Qt」を参照してください。
多言語対応のサンプルは「Hello tr() Example」を参照してください。
サンプルコード
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 アプリケーションでは言語の切り替え時にはアプリケーションを再起動するのが慣例となっていました。
Qt Quick の翻訳の仕組み
基本的な流れは C++ の時と同じですが、tr() の代わりに、qsTr() で文字列を囲みます。
Text {
id: txt1;
text: qsTr("Back");
}
Qt Quick の組み込み機器での採用が増えるにつれ、「アプリケーションの再起動なしに言語の切り替えをしたい」という要望をよく耳にするようになりました。
QML での翻訳言語の動的切り替え
Qt の Wiki にはこれを実現するためのトリックが書かれたページが存在します。
How to do dynamic translation in QML
詳細は割愛しますが、翻訳言語が変わった際(=QTranslator が再インストールされた際)に、あるオブジェクトのプロパティを変え(たことにし)て、バインディングの再評価処理を走らせ、新しい QTranslator を用いて qsTr() での翻訳を行うといったトリックを用いています。
Text {
text: qsTr("Hello") + rootItem.emptyString
}
これはソースコードの見た目があまりにもダサいので、私が先月 Automotive Grade Linux (通称 AGL)の デモアプリの一つにこのような対応を入れた 際には、以下のような記述にしてみました。
Translator {
id: translator
}
Text {
text: translator.translate(qsTr('LEFT FRONT TIRE'), translator.language)
}
少しだけ見た目が改善されたとは思いますが、仕組み的には同じような感じです。
Qt 5.10 で「再翻訳が可能になりました」
驚くべき事に、7年も前 に この機能についてのリクエスト が登録されています。
Qt 5.10 で QQmlEngine::retranslate() というスロットが追加され、これを実行することですべての翻訳の再評価が行われるようになりました。
(5.10 の時点では、(翻訳対象の)文字列以外のバインディングもすべて再評価されてしまうようですが、将来的に改善される予定です。)
このスロットは QQmlEngine のインスタンスが QEvent::LanguageChange
を受け取った際には自動的に呼び出されます。
ということは、QCoreApplication::installTranslator() を呼ぶと自動で QEvent::LanguageChange
が発生する ので、なにもせずとも自動で翻訳が切り替わる!!!
と思いたいところなのですが、トップレベルウィンドウに自動的に言語変更が伝搬する QtWidgets とは異なり、Qt Quick の場合は(少なくとも現時点では)なにもおこりません。
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));
}
}
...
}
bool QGuiApplication::event(QEvent *e)
{
if(e->type() == QEvent::LanguageChange) { // もうちょっとがんばれよ
setLayoutDirection(qt_detectRTLLanguage()?Qt::RightToLeft:Qt::LeftToRight);
}
return QCoreApplication::event(e);
}
というわけで、以下のように明示的にイベントを送る必要があるのですが、
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 に翻訳用のファイルを羅列しています。
#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 で読み込んで、エンジンの再翻訳スロットを実行しています。
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>
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 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" というパスで読み込めるようにしています。
<RCC>
<qresource prefix="/translations">
<file>ja_JP.qm</file>
</qresource>
</RCC>
おわりに
QQmlEngine
のインスタンスに QEvent::LanguageChange
を送りつけるか、retranslate()
メソッドを呼ぶことで、qsTr() で囲んだ文字列(以外もです)がすべて更新されるということで、素晴らしく簡単に動的な言語切り替えが可能になりました。
言語の切り替えの度に再起動がいらないなんて Qt 的には夢のようです。
ということで、組み込み機器で多言語対応をしたいみなさんは、これだけのためにでも Qt 5.10 にアップデートしましょうね!
明日は @sharkpp さんが参上してくれるようです。お楽しみに!