動的な言語切り替え

この記事は 2010年の第一四半期に公開された [qt-quarterly] 33 から、Dynamic Translation を翻訳したものです。


動的な言語切り替え

ユーザーインターフェースの動的な言語の切り替えができるアプリケーションの作り方

執筆: Johan Thelin

ユーザーインターフェースの翻訳が簡単にできることは Qt の強みの一つです。最も基本的な部分からのユニコードのサポートと国際化のための簡単な仕組みにより、国際化されたアプリケーションを簡単に作ることができます。

翻訳するためのルールは単純です。エンドユーザーに表示する文字列を [qt QObject tr] 関数に渡し、その結果を表示する文字列として使用してください。tr 関数は単数形、複数形を扱うことが可能です。また、誤訳を避けるために、曖昧な言葉については説明を含めることもできます。

インデックスを指定して取得されるようなメッセージの文字列のリストのように、tr() が使用できない状況で文字列が使われることもあります。この場合は、 [qt "" QT_TR_NOOP l=qtglobal m=#QT_TR_NOOP], [qt "" QT_TRANSLATE_NOOP l=qtglobal m=#QT_TRANSLATE_NOOP]  もしくは [qt "" QT_TRANSLATE_NOOP3 l=qtglobal m=#QT_TRANSLATE_NOOP3] を使用することでその文字列が翻訳対象であることを指定できます。その文字列を tr() 関数に渡して得られた文字列を表示してください。

それから、全てのユーザーが目にする文字列を lupdate を使用して抽出します。それを Qt Linguist を使用して翻訳したものを lrelase を使用して実際に利用できる形式に変換します。ここまできたら翻訳したものを使用するのは簡単で、[qt QTranslator] を [qt QApplication] のオブジェクトにインストールするだけです。

新しい翻訳がインストールされる際には、QEvent::LanguageChange イベントがシステムから送られます。これによりアプリケーション起動中でも言葉の変更が可能になります。Qt Designer が生成したユーザーインターフェースの場合は Ui:: クラスに実装されている retranslateUi() 関数で対応します。Qt Creator を使用して Qt デザイナ フォーム クラスを作成すると、 retranslateUi() を実行する changeEvent() メソッドが自動的に生成されます。

完璧な世界を複雑にするもの

動的に更新されないもののひとつにモデルがあります。データに tr を使用するようなモデルを実装する方法や、tr を使用するようなデリゲートを実装する方法によって、再描画の際の翻訳が可能になります。ここではまた別の方法、tr を使用するプロキシモデル(TranslatorProxyModel)を作成してモデルのデータを翻訳しましょう。

翻訳をモデルに追加するのはデリゲートに追加するより便利です。モデルは QEvent::LanguageChange イベントを受け取ることができ、そこで reset() を呼ぶことで、ビューが全てのデータを再取得します。

実際のデータを提供するために tr を使用するモデルでは、tr を直接使用するのはほぼ問題ありません。しかし、翻訳は文脈によります。文脈は通常は tr を呼んでいるクラス自身になります。同じフレーズを異なる文で使用する場合には文脈に依存して翻訳することが可能です。これはプロキシモデルはそれ自体には文字列を保持しないので、文脈も外から与えられる必要があることを意味します。

このため、プロキシモデルのコンストラクタは文脈を引数としてとります。ここで渡した文脈は、tr の代わりに直接使用する [qt QCoreApplication translate] 関数の第一引数にそのまま渡されます。クラスの宣言を以下に示します。

class TranslatorProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit TranslatorProxyModel(const char *translatorContext,
QObject *parent = 0);
QVariant data(const QModelIndex &index,
int role=Qt::DisplayRole) const;
QVariant headerData(int section,
Qt::Orientation orientation,
int role=Qt::DisplayRole) const;

protected:
bool event(QEvent *e);

private:
QByteArray m_translatorContext;
};

ご覧の通り、プロキシモデルは [qt "" data l=qabstractitemview m=#data] と [qt "" headerData l=qabstractitemview m=#headerData] をオーバーライドします。この二つはとても似ていて、ソースのモデルの DisplayRole のデータを翻訳するだけです。その他の Role のデータの翻訳や、文字以外のデータの翻訳を美しく実装するのは興味のある方のエクササイズのためにここでは行わないことにします。もし挑戦する場合は、以下の data() 関数の実装を参考にしてください。

QVariant TranslatorProxyModel::data(const QModelIndex &index,
int role) const
{
if(role == Qt::DisplayRole)
return QCoreApplication::translate(
m_translatorContext.constData(),
QSortFilterProxyModel::data(index, role)
.toString().toAscii().constData());
else
return QSortFilterProxyModel::data(index, role);
}

翻訳プロキシの典型的な使用方法は全てのデータを QT_TRANSLATE_NOOP を使用して定義し、プロキシモデルを実際のモデルとビューの間に挟むことです。プロキシモデル自体では QEvent::LanguageChange イベントを受け取って処理をするだけですが、これは非常に重要で、また今まで忘れられていたソリューションです。

QStandardItemModel *model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels( QStringList()
<< QT_TRANSLATE_NOOP("CapitalModel", "Country")
<< QT_TRANSLATE_NOOP("CapitalModel", "Capital"));
model->appendRow(QList<QStandardItem*>()
<< new QStandardItem(QT_TRANSLATE_NOOP("CapitalModel", "Denmark"))
<< new QStandardItem(QT_TRANSLATE_NOOP("CapitalModel", "Copenhagen")));

// Add more data here

TranslatorProxyModel *proxyModel =
new TranslatorProxyModel("CapitalModel", this);
proxyModel->setSourceModel(model);
QTableView *view = new QTableView();
view->setModel(proxyModel);

TranslatorProxyModel の残りのソースコードは ダウンロード 可能です。

自動的な動的な言語切り替え

言語の切り替えが動的にできることは色々な意味で非常に重要です。Qt Designer を使用すると動的な切り替えは簡単にできます。ユーザーインターフェースをソースコードで書く場合にはとても面倒です。

それは全ての翻訳が必要となる全てのウィジェットを管理しなければいけないからです。これには全ての小さなラベルやグループボックスなどの表示のみで使われるような、通常は Qt のメモリ管理システムのおかげで管理する必要のないものが含まれます。しかし、Qt 4.2 からは QObject のインスタンスでは動的プロパティが使用できます。これと再帰アルゴリズムを組み合わせることで動的な言語の切り替えが自動化できます。

次のような計画で進めましょう。各ウィジェットで originalText という名前の動的プロパティを使用します。このプロパティはウィジェットの text, title, windowTitle などに設定されている翻訳元のテキストを保持します。

動的な言語切り替えは dynamicRetranslateUi() で行い、この関数は QEvent::LanguageChange イベントを受け取った際に呼び出されるようにしましょう。この関数は DynamicTranslatorWidget クラスで実装されます。この方法を再利用して、新しいウィンドウを作成する際にはこのクラスを単に継承します。このクラスの changeEvent 関数ではその関数を適切なタイミングで呼び出すようにします。

その関数の実装は3つの構成に分けられます。再帰、翻訳、それからテキストを変更する部分です。再帰の部分は単に QObject の階層をたどり、[qt "" children l=qobject m=#children]と [qt "" isWidgetType l=qobject m=#isWidgetType] を使用して全てのウィジェットを探します。その方法はダウンロード可能なソースコードパッケージを参照してください。翻訳の部分についてはそれほど簡単にはいきません。

DynamicTranslatorWidget は実際に tr の呼び出しを行うクラスの基底クラスなので、このクラス自体は tr で使用される適切な文脈ではありません。そのかわりにメタオブジェクトの情報から文脈に使用する派生クラスのクラス名を取得します。以下にそのコードを示します。

QString text = QCoreApplication::translate(
this->metaObject()->className(),
w->property("originalText")
.toString().toAscii().constData());

もうひとつの方法としては TranslatorProxyModel のようにクラスをコンテキストと一緒に渡すこともできます。もしコンテキストが指定されなかった場合には、メタオブジェクトの方法を代わりに使用できます。興味のある方は是非お試しください。

文字列を翻訳する場合は、それがユーザーが目にする文字列型のプロパティに設定する必要があります。このプロパティは全てのウィジェットで同じではありません。ラベルや様々なボタンなどのように text プロパティであることがほとんどですが、グループボックスのように title のものや、format プロパティの場合もあります。全てのウィジェットはトップレベルのウィジェットにもなりうるため、windowTitle もその対象です。

ここで示す解決法では、text と title プロパティを処理しています。他のプロパティ名に対応するのも簡単でしょう。また、トップレベルウィジェットにはこれらのプロパティがないことを想定しています。もし全てのウィンドウの基底クラスが DynamicTranslatorWidget であれば、いずれかの条件に当てはまるでしょう。

if (w->metaObject()->indexOfProperty("text") != -1)
w->setProperty("text", text);
else if (w->metaObject()->indexOfProperty("title") != -1)
w->setProperty("title", text);
else if (w->isWindow())
w->setWindowTitle(text);

あるプロパティがあるかないかはメタオブジェクトを通して確認していることに気づきましたか?これにより title や text などの名前のプロパティが動的プロパティとして設定されている場合にはこの処理を回避することが可能です。

ソースコードパッケージに含まれる方法には、もう一つ動的な言語切り替えの機能が追加されています。textValue, textValue1, textValue2 などのプロパティを設定可能で、これらは翻訳される文字列中の %n, %1, %2 などに使用されます。詳細はコード中のコメントに書いてあるためここでは割愛しますが、%n のサポートにより単数形、複数形に対応できることには注目する価値があるでしょう。

手抜きが上手な人のための文字列の展開

全てのウィジェットに originalText プロパティを設定するコードを追加すること決して楽しい作業ではないでしょう。このことは私自身1プログラマーとしても感じるところです。そして楽しい作業でないにもかかわらず、うまく自動化するのは難しいです。しかし、方法はあります。

DynamicTranslatorWidget クラスは dynamicExtractStrings() という protected 関数を持っています。これは dynamicRetranslateUi() と同じ再帰の方法で、見つけられる全てのウィジェットのオリジナルのテキストを展開します。

展開のコード自体は面白くありません。単に text, title, windowTitle プロパティを見つけてそれらを originalText プロパティに設定するだけです。また、設定済みの originalText プロパティは上書きしないようになっています。これを使用するにあたっていくつかの注意点があります。

  • tr を使用したり、 QT_TR_NOOP を使用した文字を tr で置き換える場合、この関数を実行する時点で translator がインストールされていないこと
  • %n, %1, %2 などを使用したテキストには対応していません。%n は QT_TR_NOOP と共には使用できません。これらの場合は(lupdate に文字列を登録するための)ダミーの tr の呼び出しをした上で、originalText プロパティを手動で設定してください。
  • originalText プロパティに空文字列を設定することで、テキストが自動で展開されずウィジットが翻訳されないようにできます。これにより、自分のコードで翻訳を変更することができます。
  • 全てのウィジェットのセットアップが終わって、全ての文字列が展開された時には必ず dynamicRetranslateUi() を実行してください。

%n を使用する場合に手動で originalText を追加しなければなりませんが、この場合は textValue プロパティも設定しないといけないため、手間が大幅に増えることではないでしょう。

おわりに

DynamicTranslator クラスと TranslatorProxyModel をアプリケーションで使用するのは簡単です。これらのクラスは実装の確かさよりも技術的なデモを目的にしていたので、多少の手直しは必要かもしれません。しかし、最終的には動的な言語の切り替えができるアプリケーションを作成できるでしょう。

ソースコード

この記事で取り上げたサンプルのソースコードは Qt Quaterly のウェブサイトから入手可能です:qq33-dynamic-translation.zip

執筆者について

Johan Thelin は Apress から発行されている the Foundations of Qt Development (Qt 開発の基礎) という本の著者であり、組み込みシステムをこよなく愛しています。また、ソフトウェア開発や技術文書の作成とともに QtCentre でも活躍しています。


Blog Topics:

Comments