Qt/C++ による GUI ソフトウェア作成の練習 7:国際化

このページでは,GUI アプリケーションを国際化(ローケールに応じてメニューの言語を変更)する練習をします.
ここでは,実行ファイルに埋め込む,埋め込まないで起動時にロードする,という 2 方法を練習しています.

まず国際化前のコードを提示します.これは,練習 2 で作成したメニューバーのコードを少し改変したものです.
このコードに対して,Qt6 翻訳ツールを使いつつ翻訳用のソースファイルを作成します.
作成したソースファイルを使い,まず翻訳ファイルを実行ファイルに埋め込む方法を試します.
次いでソースファイルを改変し,翻訳バイナリファイルを実行ファイルとは別に作成しておき,起動時にロードする方法を試します.

Before : 6:ピッキング

インフォメーション

ドキュメント

Qt のドキュメント Qt 6 のなかにあるポータルページを紹介しておきます,
これらのページと検索でヒットする解説記事を参考にすれば,国際化できました.


Internationalization with Qt:概論と関連トピックス
Qt Linguist Manual:翻訳ツール Linguist および lupdate の使用法や使用例

インストールした Debian パッケージ

パッケージ名(バージョン)
g++(12.2.0)GNU C++ コンパイラ
make (4.3)コンパイルを制御するユーティリティ
qt6-base-dev(6.4.2)Qt6 アプリケーションのビルドに使用されるヘッダ開発ファイル
qt6-l10n-tools(6.4.2) Qt6 翻訳ツール

目次(ページ内リンク)


国際化前のソースファイル
翻訳用のファイルを置くためのサブディレクトリの追加
プロジェクトファイルの改訂
翻訳作業
main() 関数の改訂
動作確認
別法:翻訳バイナリファイルを起動時にロード

国際化前のソースファイル

メニューバーで作成したコードを改変して材料とします.
下がそのコードで,メインウィンドウにメニューバーを載せただけです.メニューは "File(F)" のみであり,何も機能しません.
ここでは "File(F)" を国際化します.

ソースファイル

これらは,tmp というディレクトリに置くとします.

main_window.h


#include <QMainWindow>
#include <QMenuBar>

class Main_Window : public QMainWindow {
 Q_OBJECT  //メニューの国際化には Q_OBJECT を宣言する必要があります.これは,Qt の C++ 拡張をおこなうためのマクロです

 private:
  QMenuBar *menu_bar;
  QMenu *file_menu;

 public:
  Main_Window(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
};

main_window.cc

ソースコードを記述する方法は,Writing Source Code for Translation に記されています.

ここでは,赤字で記したように,国際化すべき部分を tr() 関数に渡すようにコーディングします.
tr() 関数は QObject クラスのメンバ関数で,戻り値は引数としたソーステキストの翻訳版です.
このページでは,翻訳版を作成するわけです.


#include "main_window.h"

Main_Window::Main_Window(QWidget *parent, Qt::WindowFlags f)
 : QMainWindow(parent, f),
   menu_bar(menuBar())
{
 file_menu = new QMenu(tr("File(&F)"), this);
 menu_bar->addMenu(file_menu);
}

main.cc


#include <QMainWindow>
#include <QApplication>
#include "main_window.h"

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

 Main_Window window(nullptr, Qt::Window);
 window.show();

 return app.exec();
}

tmp.pro

qmake のプロジェクトファイルです."tmp.pro" という名前にしています.
このページのレベルでは,名前を変更しても問題ありません.


TEMPLATE = app  #実行ファイルを作成(ライブラリを作成するのではありません)
TARGET = window  #実行ファイル名を window とする
QT += widgets
target.path = /usr/local/bin  #インストール先
INSTALLS += target

HEADERS += main_window.h
SOURCES += main.cc main_window.cc

ビルドと実行

国際化前のメインウィンドウ

実行ファイルを作成して起動してみます.インストールはしません.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ ./window &

画像は実行イメージです.
メニューバーに "File(F)" と表示されています.


翻訳用のファイルを置くためのサブディレクトリの追加

ここでは,ソースファイルを置いた tmp ディレクトリにサブディレクトリ i18n を追加します.
すなわち,tmp/i18n 内に翻訳用のファイルを置くわけです.
サブディレクトリは別の名前でも可能ですし,サブディレクトリを作成しないで,tmp ディレクトリ内に翻訳用のファイルを置くこともできます.

~/tmp$ mkdir i18n


プロジェクトファイルの改訂

qmake のプロジェクトファイル,"tmp.pro" に,3 行追加しました.赤字で示しています.


TEMPLATE = app
TARGET = window
QT += widgets
target.path = /usr/local/bin
INSTALLS += target

HEADERS += main_window.h
SOURCES += main.cc main_window.cc

TRANSLATIONS += i18n/trans_ja.ts  #上で作成したサブディレクトリに翻訳ファイルを作成
CONFIG += lrelease  #翻訳バイナリファイル作成ツール
CONFIG += embed_translations  #作成した翻訳バイナリファイルを実行ファイルに埋め込む

翻訳作業

翻訳バイナリファイルのソースファイル trans_ja.ts を作成します.

雛形の作成

プロジェクトファイルを引数にして lupdate コマンドを実行すると,trans_ja.ts が作成されます.
作成先は,プロジェクトファイル tmp.pro に記しておいたのでした.
すなわち,
~/tmp$ lupdate tmp.pro
とすると,tmp/i18n/trans_ja.ts というテキストファイルが生成しました.下がその内容です.
赤字で示した箇所を書き換えればよさそうです.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ja_JP">
<context>
    <name>Main_Window</name>
    <message>
        <location filename="../main_window.cc" line="7"/>
        <source>File(&amp;F)</source>
        <translation type="unfinished"></translation>
    </message>
</context>
</TS>

翻訳

私の環境では,trans_ja.ts をクリックすると Qt Linguist という翻訳用の GUI ツールが起動するようになっていました.
Qt Linguist を使ってもよいのですが,ここではテキストエディタで書き換えました.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ja_JP">
<context>
    <name>Main_Window</name>
    <message>
        <location filename="../main_window.cc" line="7"/>
        <source>File(&amp;F)</source>
        <translation>ファイル(&amp;F)</translation>
    </message>
</context>
</TS>

main() 関数の改訂

QTranslator を使えば,翻訳バイナリファイルを作成し,実行ファイルに組み込むことができます.
QTranslator のページに記されているサンプルコードでは,メンバ関数
bool load(const QLocale &locale, const QString &filename, const QString &prefix = QString(), const QString &directory = QString(), const QString &suffix = QString())
を使っています.

main.cc


#include <QMainWindow>
#include <QApplication>
#include <QTranslator>
#include "main_window.h"

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

 QTranslator translator;
 if(translator.load(QLocale(), QLatin1String("trans"), QLatin1String("_"), QLatin1String(":/i18n")))
  app.installTranslator(&translator);

 Main_Window window(nullptr, Qt::Window);
 window.show();

 return app.exec();
}

ドキュメントには,load() 関数は,引数を基にして.ファイル名 + プレフィックス + UI 言語名 + サフィックス (サフィックスが指定されていない場合は「.qm」) をロードすると書いてあります.
ちょっと分かりにくかったので,load() 関数の宣言での仮引数と main() 関数内での実引数を対応させて記してみました,

  1. const QLocale &locale ⇔ QLocale()
  2. const QString &filename ⇔ QLatin1String("trans")
  3. const QString &prefix = QString() ⇔ QLatin1String("_")
  4. const QString &directory = QString() ⇔ QLatin1String(":/i18n")
  5. const QString &suffix = QString() ⇔ 省略.なので,サフィックスは ".qm"

これらの引数を基にして,i18n/trans_ja.ts から作成される翻訳バイナリファイル i18n/trans_ja.qm がロードされ,実行ファイルに組み込まれるわけです.
UI 言語名というのは,ここでは "ja" のことだと判ります.


動作確認

ビルドとインストール

定番のコマンドを実行します.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ sudo make install
端末には以下の内容が表示されました.実行ファイル window のみがインストールされました.

/usr/bin/qmake6 -install qinstall -exe window /usr/local/bin/window
strip /usr/local/bin/window

ここに記した作業手順では,翻訳バイナリファイル i18n/trans_ja.qm は残っていませんでした.

実行

国際化後のメインウィンドウ

実行ファイルに国際化リソースが組み込まれていることを確認します.
作業中に i18n/trans_ja.qm が見当たらなかったので心配です.念のためディレクトリを変更して,インストールされた実行ファイルを起動します.
~/tmp$ cd
~$ window &
画像のように,メニューが日本語で表示されました.成功です.
作業ディレクトリに戻ってアンインストールします.
~/tmp$ cd tmp
~/tmp$ sudo make uninstall
ビルドの際に生成した中間ファイルを削除するなら,
~/tmp$ make distclean


別法:翻訳バイナリファイルを起動時にロード

翻訳バイナリファイル *.qm を作成してロードする方法を試してみました.
上で作成したコードとあまり変わらないので,それらを転用・改変する形で紹介しています.
翻訳リソースが大きくなると使う方法なのかもしれません.

翻訳バイナリファイルの作成とインストール

ここでは練習ということで,手動で作成してインストールしました.

まず,翻訳バイナリファイル trans_ja.qm を作成します.
元になるファイルは trans_ja.ts で,この作成法は上と同じです.
~/tmp$ lrelease i18n/trans_ja.ts -qm i18n/trans_ja.qm

次にインストールです.
ここでは,/usr/local/bin/i18n というディレクトリを作成してコピーしました.
~/tmp$ sudo mkdir /usr/local/bin/i18n
~/tmp$ sudo cp i18n/trans_ja.qm /usr/local/bin/i18n/

プロジェクトファイルの改変

翻訳バイナリファイルを手動でインストールしたので,実行ファイルに組み込む指示は不要です.
すなわち,tmp.pro から,CONFIG += lrelease 行と CONFIG += embed_translations 行を削除しました.
(翻訳バイナリファイルを作成・インストールする指示を記述できそうな気がしますが,このページ作成時点では調べていません)


TEMPLATE = app
TARGET = window
QT += widgets
target.path = /usr/local/bin
INSTALLS += target

HEADERS += main_window.h
SOURCES += main.cc main_window.cc

TRANSLATIONS += i18n/trans_ja.ts

main() 関数の改変:ロードの指示

main.cc に記述した load() 関数を変更して,インストールされた翻訳バイナリファイルをロードするように記述します.例えば,


#include <QMainWindow>
#include <QApplication>
#include <QTranslator>
#include "main_window.h"

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

 QTranslator translator;
 if(translator.load("/usr/local/bin/i18n/trans_ja"))  //これでは日本語化.国際化は,ローケールを取得して適切な .qm ファイルをロードする
  app.installTranslator(&translator);

 Main_Window window(nullptr, Qt::Window);
 window.show();

 return app.exec();
}

main() 関数の改変:QLocale から情報取得

上のコードでは,無条件に trans_ja.qm をロードしています.これでは日本語化です.
国際化と称するためには,ローケールを取得して,適切な .qm ファイルをロードする必要があります.
ここでは,QLocale クラスをインスタンス化し,ローケール情報を取得してみます.

main() 関数を以下のように改変します.
コードでは,QLocale インスタンスから情報を取得し,端末に出力しています.情報を取得する関数は,QLocale Class ドキュメント に書いてあり,その一部を利用しました.


#include <QMainWindow>
#include <QApplication>
#include <QTranslator>
#include "main_window.h"
#include <iostream>

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

 QLocale locale;
 std::cout << locale.amText().toStdString() << std::endl;
 std::cout << locale.bcp47Name().toStdString() << std::endl;  //IETF(Internet Engineering Task Force) 言語タグ
 std::cout << locale.currencySymbol().toStdString() << std::endl;  //通貨記号
 std::cout << locale.dateFormat().toStdString() << std::endl;
 std::cout << locale.dateTimeFormat().toStdString() << std::endl;
 std::cout << locale.dayName(1).toStdString() << std::endl;
 std::cout << locale.language() << std::endl;  //戻り値は enum Qlocale::Language.日本語は 120

 QTranslator translator;
 if(locale.language() == QLocale::Japanese && translator.load("/usr/local/bin/i18n/trans_ja"))
  app.installTranslator(&translator);
 //if(locale.bcp47Name() == "ja" …… でもよさそう

 Main_Window window(nullptr, Qt::Window);
 window.show();

 return app.exec();
}

ビルドと実行

国際化後のメインウィンドウ

定番のコマンドを実行します.
~/tmp$ qmake6
~/tmp$ make
~/tmp$ sudo make install
インストールされた実行ファイルと翻訳バイナリファイルを確実に利用するために,カレントディレクトリにある実行ファイルと翻訳バイナリファイルを削除します.
~/tmp$ make distclean
~/tmp$ rm i18n/trans_ja.qm
実行
~/tmp$ window &
国際化されたウィンドウが表示(ウィンドウは同じなので,画像は流用しています)されるとともに,下記の内容が端末に出力されました.

午前
ja
¥
yyyy年M月d日dddd
yyyy年M月d日dddd H時mm分ss秒 t
月曜日
120

後片付け

作業ディレクトリとインストールしたファイル,ディレクトリを削除するだけです.
~/tmp$ sudo make uninstall(または sudo rm /usr/local/bin/window)
~/tmp$ sudo rm -rf /usr/local/bin/i18n
~/tmp$ cd
~/tmp$ rm -rf tmp