Notes C API & C++11+ & ReactiveX & Qt #7 ~ Qtの翻訳システム(コーディング編)

ソースコードサイトの変更

まず最初に、1つご連絡があります。前回までの公開ソースコードを、訳あって以下のサイトに変更しました。

github.com

前回までの解説に使った改訂前のv0.0.1と、このサイトのv0.0.1は、原則合わせてありますので、連載上は問題ないかと思います。そのv0.0.1の説明も、今回が最後になります。

今回の目標「Qtの翻訳システム」

前々回の#5にて、Qtの翻訳システムについては概略を説明しました。今回はもう少し細かいところも説明して、v0.0.1の締めくくりにしたいと思います。

Qtの翻訳システムは、おおよそ次のような手順で行います。

  1. プロジェクトファイルで翻訳編集ファイル名を宣言します。
  2. ソースコード上にあるハードコーディングされたANCI形式のC文字列に印を付けます。
  3. ツールを使って翻訳編集ファイルとして抜き出します。
  4. 各文字列に翻訳を付けます。
  5. 実行時に変換可能な形にします。
  6. 実行形式ファイルの内部、または外部にバンドルします。

それでは、前出のv0.0.1のソースコードを元に順に説明していきます。

プロジェクトファイルで翻訳ファイル名を宣言

Qtアプリケーションやライブラリに使用されるQtプロジェクト(.proファイル)には、翻訳編集ファイル(.tsファイル)を指定する変数TRANSLATIONSがあります。

TRANSLATIONS += ncl.ja_JP.ts

演算子+=は、略式足し算としてよく使いますが、.proファイルでは変数に複数の値を指定するときに使います。このTRANSLATIONS変数も複数の値を指定できます。多言語対応する場合には、ここで国や言語に個別に対応するファイル名を指定することが可能です。複数のファイル名を指定する場合は空白文字で区切ります。

前述の例では、ファイル名nclと、拡張子.tsの間に、.ja_JP(日本語_日本)としています。これは、QtのQLocaleクラスが返すロケール文字列に合うようにしているためです。この箇所を、他の言語や国別の文字列に変えて指定すれば、他言語の翻訳ファイルを作ることができるようになります。

翻訳を実行するコードを書く

Qtを使えば勝手に翻訳してくれるわけではなく、コードを書く必要があります。次に、プログラム上でどのように翻訳する仕組みを動作させるのかを見てみます。

int main(int argc, char *argv[])
{
  QCoreApplication app(argc, argv);
  QTranslator translator; // <= (1)
  translator.load(QLocale(), "ncl", ".", ":/translations", ".qm"); // <= (2)
  app.installTranslator(&translator); // <= (3)

翻訳システムを動作させるには、QCoreApplication(またはそれを継承したクラス)オブジェクトを生成した後、できるだけ早い機会に、「翻訳データをロードしたトランスレータをアプリにインストールする」コードを書く必要があります。

  1. 翻訳オブジェクトを作成
  2. 翻訳ファイルをロード
  3. 翻訳オブジェクトをQCoreApplicationにインストール

ここまでやっておけば、ソースコード内の文字列は、Qtの翻訳システムが文字列を置き換えてくれます。

QTranslator::load

先程のコードで、(2)のところで翻訳データを読み込んでいます。このQTranslator::loadにはいくつかのオーバーロードが存在しますが、前述のメソッドのシグネチャは次のようになります。

bool QTranslator::load(const QLocale &locale, const QString &filename, const QString &prefix = QString(), const QString &directory = QString(), const QString &suffix = QString())

このように、翻訳ファイル名、ディレクトリ名、プレフィックスサフィックスをバラバラに指定することで、いろいろなパターンで翻訳ファイルの指定を支援してくれます。これについては、Qtのヘルプに掲載されている例が、とてもよくわかります。

http://doc.qt.io/qt-5/qtranslator.html#load-1

かいつまんで説明すると、スペイン語(es)、カナダのフランス語(fr-CA)、ドイツ語(de)という優先順位を持つ、Windowsなどのロケール環境で次のQTranslator::loadを呼び出したとします。

translator.load(QLocale(), "foo", ".", "/opt/foolib", ".qm");

すると、次のようなパターンで検索し、最初に見つかったファイルをロードしてくれます。

  1. /opt/foolib/foo.es.qm
  2. /opt/foolib/foo.es
  3. /opt/foolib/foo.fr_CA.qm
  4. /opt/foolib/foo.fr_CA
  5. /opt/foolib/foo.de.qm
  6. /opt/foolib/foo.de
  7. /opt/foolib/foo.fr.qm
  8. /opt/foolib/foo.fr
  9. /opt/foolib/foo.qm
  10. /opt/foolib/foo.
  11. /opt/foolib/foo

検索するファイル名、ディレクトリは変わりませんが、言語名、拡張子名を入れ替えて、ファイルの有無を確認します。

もちろん、この適用ルールに当てはめたくない場合は、他のloadメソッドを使って直接指定すればいいわけです。なお、QLocaleで使われる言語と国の省略形による識別には、ISO639(言語)、ISO3166(国)が使用されています。日本語-日本であればja_JPとなります。

翻訳したい箇所をマーク

ところで、.qmファイルというものが急に出てきました。これを説明するには、ソースコード上の翻訳したい文字列にどうやって印を付けるか、というところから説明します。

例えば、v0.0.1のmain.cppの70行目付近に、以下のようなコードがあります。

out << QObject::tr("Build version of '%1'").arg(pServer)

この中の、QObject::tr()で囲った部分が、翻訳対象の文字列になります。普通に書くより長ったらしいと感じるかもしれません。これは、QObjectクラスを継承していない環境では、こう書かざるを得ませんが、特にQtのGUIアプリケーションなどでは、QObjectを継承している環境下でコーディングすることになるので、短くtr("...")とするだけでよくなります。まあ、慣れですね。

tsファイルの作成(または更新)

このようにして、ソースコード内の文字列にQObject::trメソッドでマークを付けたら、次はlupdateコマンドを使います。lupdateは、.pro(プロジェクト)ファイルを元に、ソースコードを探索するファイルと、登録されている.tsファイル名を使って、.tsファイルをなければ生成、あれば既存のファイルを更新します。.tsファイルは抜き出した文字列と、対となる翻訳文字を管理するもので、中身は単なるXMLファイルです。

Qt Linguist

.tsファイルはXMLなので、テキストエディタでも編集できなくはないですが、ここはQt LinguistというGUIツールを使った方がいいでしょう。

f:id:takahide-kondoh:20181119202745p:plain

Qt Linguistは、単に翻訳文字列を書き込むだけでなく、ソースコードGUIプレビューを確認しながら、状況にあった翻訳を考えられるように、プレビューができるところが大きな利点です。

リリース(バイナリファイルの作成)

一通り翻訳を付け終えたら、.tsファイルそのものを保存し直して、「リリース」作業をします。

リリースとは、.tsファイルから.qmファイルを作成する作業のことをいいます。.qmファイルはバイナリで構成された翻訳データで、QTranslatorがロードするのもこのファイルです。リリース作業はlreleaseコマンドを使いますが、前出のQt Linguistのメニューコマンドにもリリース機能はあるので、Qt Linguistを使って作業をするのであれば、後者の方が便利でしょう。

.qmファイルの保存先-Qtリソースを使った場合

ここまで来ればあと一息です。

.qmファイルは、そのままアプリケーションとともに配布することもできますが、インストーラに組み込むのが面倒な場合もあります。そんな時にお手軽なのが「Qtリソースファイル」として同梱してしまうことです。「Qtリソースファイル」とは、EXEやDLLなどのバイナリファイル内にリソースファイルコンテナを設けて、登録したファイルをあたかもファイルシステムの一部のようにして取り出すことができる仕組みです。私の場合、よく使うのが翻訳ファイル以外に、アイコン画像を取り込んでおくことがよくあります。

「Qtリソースファイル」は.qrc拡張子で作成されるファイルで、これも中身はXMLファイルです。ディレクトリと同じような階層構造を持っています。

f:id:takahide-kondoh:20181119204820p:plain

このスクリーンショットでは、1つの.qmファイル以外にいくつかのアイコンファイル名が登録されています。Qtリソースファイルは、コンパイル、リンクを経てEXEやDLL内に取り込まれます。Qtリソースファイルに登録されたファイル名に、プログラム内からアクセスするには、頭に:を付け、以降はディレクトリと同じ要領でアクセスすることができます。図の中のbirch.ja_JP.qmファイルにアクセスするには、:/translations/birch.ja_JP.qmとすればアクセスできます。

int main(int argc, char *argv[])
{
  QCoreApplication app(argc, argv);
  QTranslator translator;
  translator.load(QLocale(), "ncl", ".", ":/translations", ".qm");
  app.installTranslator(&translator);

前出のコードの内、":/translations"という部分が、ディレクトリ指定になり、ディレクトリの行き先は、リソースファイル内のtranslationsフォルダ内ということになります。

まとめ

以上が、Qtアプリケーションで多言語化を簡単にする翻訳システムの、ソースコードやツールを使った作業の流れになります。QtはマルチプラットフォームGUIアプリケーションの開発支援ツールなので、OSごとにマルチバイト言語の扱いが異なるC++コーディングでは、ソースコードをシングルバイトで共通化しておき、マルチバイト文字列は「翻訳」というプロセスに外部化したのは、ごく自然の流れだったのかもしれません。

おさらい

#3~#7で、Notes C APIとQtを使ったコーディングの初歩を勉強してきました。簡単に振り返っておきます。

Notes C API

  1. APIのヘッダーファイルを読み込む前にOSを識別するシグネチャを宣言する(Windows 32bitならW W32 NT)
  2. APIのヘッダーファイルを読み込む場合、Windowsではアラインメントを1バイトにする。
  3. APIを使用する前に関数NotesInitExtended、終了前に関数NotesTermを実行する。
  4. API関数の大半がSTATUSを返す。この値で関数が正しく動作したかわかる。

Qt

  1. Qtはマルチプラットフォームのアプリケーションを1つのソースコードで開発できる。
  2. プラットフォームごとの差異は、.proファイル内の定義で吸収する。
  3. シングルバイトでソースを書き、翻訳システムでマルチバイト化する。

Notes/Dominoはマルチプラットフォームアプリケーションですから、Qtと相性がいいはずです。次回以降も、実践的なNotes C APIアプリ開発手法を紹介していきます。