GNU gettextによる国際化機能で英語アプリケーションを日本語対応させる方法を記述する。
我々としては逆に日本語アプリケーションを開発してから必要に応じ英語対応させたいが、文字コードに起因するトラブル予測が難しく推奨されない。
英語文字列を真の表示文字列を検索するメッセージIDと割り切れば、少しは気持ちが楽になるかもしれない。
本記事においてソースコードファイルおよびインクルードファイルは全てASCIIで記述されているものとする。コメントに日本語を混在させる場合はコンパイラオプションに適切な入力文字コードを設定すれば良いが、ここは覚悟を決めて英語に統一しよう。
翻訳ファイルの作成
GNU gettextはバイナリ形式翻訳ファイルを事前作成し、実行ファイルが実行時にこれを参照して翻訳された文字列に置き換える。以下に本サイトが標準とする翻訳ファイル作成ワークフローを示す。ターゲットの実行形式ファイル名をProject1.exeと仮定する。
- ソースコードファイルからxgettextで翻訳テンプレートファイルmessages.potを作成する。
- messages.potからmsginitでテキスト形式翻訳ファイルProject1.poを作成する。
- Project1.poをエディタで開き、翻訳文字列を定義する。
- 編集されたProject1.poからmsgfmtでバイナリ形式翻訳ファイルProject1.moを作成する。
ソースコードファイルが変更されたら以下の手順で翻訳ファイルを更新する。
- ソースコードファイルからxgettextでmessages.potを作成する。
- messages.potをmsgmergeでProject1.poへマージする。
- Project1.poをエディタで開き、変更追加される翻訳文字列を定義する。
- 編集されたProject1.poからmsgfmtでProject1.moを作成する。
本サイトのxgettext、msginit、msgmerge、msgfmtはMSYS2が導入したものでPOSIXサブシステムからはそのまま利用できるが、ウィンドウズ環境からは以下の環境変数を必要とする。環境変数LANGの必要性はC:\msys64\usr\share\gettext\ABOUT-NLSに記述されている。GNU gettextはmingw32/mingw64サブシステムにも存在するが本サイトは気付かずmsys2サブシステムを利用した。
LANG=ja_JP.UTF-8
PATH=C:\msys64\usr\bin;%PATH%
煩雑なのでProject1.poのエディタ編集を除く各手順をバッチファイル化し、プロジェクト毎にカスタマイズする。なおPoeditでProject1.poを編集すれば自動的にProject1.moも作成するのでmsgfmtは使わずにすむ。
コンソールアプリケーション
コンソールアプリケーションは英語文字列をシフトJIS日本語文字列に翻訳する。以降においてターゲットの実行形式ファイル名をConsole1.exeと仮定する。
バッチファイル
k_xgettext.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
xgettext.exe *.cpp -k_ -o bin\messages.pot
このサンプルではソースコードファイル(*.cpp)のファイルからのみ文字列を検索するが、例えばインクルードファイルも検索に加えるならxgettextの実引数に*.hを加える。その場合プロジェクトがインクルードファイルを持たないとxgettextは失敗するが、これはMSYS2でビルドされた実行形式のグロッビング(ワイルドカードによるファイル名補完)機能制約による。
k_msginit.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
mkdir bin\ja\LC_MESSAGES
msginit.exe -i bin\messages.pot -o bin\ja\LC_MESSAGES\Console1.po -l ja_JP.SJIS
翻訳はlibintlライブラリを使う。ライブラリは翻訳ファイルの文字コードからターゲット文字コードに変換するため、-lオプションの一部で指定する文字コードはシフトJIS(SJIS)に拘る必要は無い。例えば-lオプションをja_JP.UTF-8としても問題なく翻訳できる。
- 覚え書き
- ところで翻訳ファイルを検索する文字列(翻訳前の文字列またはメッセージID)の方は文字コード変換にあずからない。ライブラリは翻訳文字列を発見できないと検索文字列をターゲット文字コードを無視してそのまま返すため、検索文字列はASCIIに限定することが推奨される(GNU gettext utilities 11.2.4 How to specify the output character set gettext uses)。本サイトは英語アプリケーションを作成して日本語翻訳ファイルを添付せよと主張するが、実はこれを最大の理由とする。
k_msgmerge.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
msgmerge.exe bin\ja\LC_MESSAGES\Console1.po bin\messages.pot -U --backup=numbered
k_msgfmt.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
msgfmt.exe bin\ja\LC_MESSAGES\Console1.po -o bin\ja\LC_MESSAGES\Console1.mo
翻訳ファイルの配置
統合開発環境Code::Blocksでの運用を前提とし、翻訳ファイルの配置を開発時と配布時で変える。さもなくば開発時に複数の翻訳ファイルをメンテナンスする事になる。
開発時にバッチファイルは翻訳ファイルを以下に配置する。テキスト形式翻訳ファイルの文字コードはシフトJISとして扱う。
ディレクトリ | ファイル | 内容 |
[プロジェクトディレクトリ] | Console1.cbp | Code::Blocksプロジェクト |
│ | *.cpp, (*.h) | ソースコード、(インクルード) |
│ | k_xgettext.bat | xgettext実行バッチ |
│ | k_msginit.bat | msginit実行バッチ |
│ | k_msgmerge.bat | msgmerge実行バッチ |
│ | k_msgfmt.bat | msgfmt実行バッチ |
├ bin | messages.pot | 翻訳テンプレート |
│├ Debug32 | Console1.exe | 32ビット実行形式デバッグ版 |
│├ Release32 | Console1.exe | 32ビット実行形式リリース版 |
│├ Debug64 | Console1.exe | 64ビット実行形式デバッグ版 |
│├ Release64 | Console1.exe | 64ビット実行形式リリース版 |
│└ ja | | |
│ └ LC_MESSAGES | Console1.po | 日本語テキスト形式翻訳 |
│ | Console1.mo | 日本語バイナリ形式翻訳 |
└ obj | | |
├ Debug32 | *.o | 32ビットオブジェクトデバッグ版 |
├ Release32 | *.o | 32ビットオブジェクトリリース版 |
├ Debug64 | *.o | 64ビットオブジェクトデバッグ版 |
└ Release64 | *.o | 64ビットオブジェクトリリース版 |
配布時に必要なのはバイナリ形式翻訳ファイルのみで、インストーラが(あるいは手動で)以下に配置する。
ディレクトリ | ファイル | 内容 |
[インストールディレクトリ] | Console1.exe | 実行形式 |
└ ja | | |
└ LC_MESSAGES | Console1.mo | 日本語バイナリ形式翻訳 |
ソースコードの修正
コンソールアプリケーションは翻訳ファイルを利用する国際化機能ライブラリとしてGNU gettextのlibintlライブラリを用いる。例として動作確認(1)で作成したConsole1プロジェクトに国際化機能を加える。プロジェクトに以下のライブラリを追加する。
シフトJISのリテラル文字列はどこにも存在せず、コンパイルオプション-fexec-charset=CP923は必要ない。main.cppを以下に修正してビルドする。
main.cpp
- libintl.hをインクルードし、_マクロなどを定義する。
- 翻訳対象となる英語文字列を_マクロで囲む。
- libintlライブラリ関数のコールをmain関数に追加する。
翻訳ファイルの配置を開発時と配布時で変える目的でTMyAppDirクラスを追加する。
- TMyAppDirが必要とするC++標準ライブラリcstringとalgorithmをインクルードする。
- TMyAppDirクラスをmain関数の前に定義する。
- libintlライブラリbindtextdomain関数へ与える翻訳ファイルディレクトリをTMyAppDir::Ignoreメンバ関数で指定する。
ロケールをウィンドウズAPIで設定する。これをsetlocale関数で行わない理由は後述する。
- windows.hをインクルードする。
- libintlライブラリ関数コールより前にSetThreadLocale(LOCALE_USER_DEFAULT)でユーザーデフォルトロケールを選択する。これをLOCALE_SYSTEM_DEFAULTでコールすればシステムロケールを選択する。
#include <iostream>
#include <windows.h>
#include <libintl.h>
#include <cstring>
#include <algorithm>
#define _(String) gettext(String)
#define gettext_noop(String) (String)
#define N_(String) gettext_noop(String)
class TMyAppDir
{
private:
const char* const p0_;
const char* pX_;
const char* pY_;
private:
bool Match(const char* pattern,const char* pXX) const
{
if (*pattern==0||pXX==pY_-1) {return *pattern==0&&pXX==pY_-1;}
if (*pattern=='*')
{
for (;pXX!=pY_-1;++pXX) {if (Match(pattern+1,pXX)) {return true;}}
return Match(pattern+1,pXX);
}
if (*pattern!='?'&&*pattern!=*pXX) {return false;}
return Match(pattern+1,pXX+1);
}
std::string IgnoreImpl() const {return {p0_,pY_-1};}
template<typename... Args> std::string IgnoreImpl
(const char* pattern,Args&&... args) const
{
if (Match(pattern,pX_)) {return {p0_,std::max(p0_,pX_-1)};}
else {return IgnoreImpl(std::forward<Args>(args)...);}
}
public:
TMyAppDir():p0_{_pgmptr},pX_{},pY_{}
{
auto rp0=std::make_reverse_iterator(p0_);
auto rpY=std::find(rp0-std::strlen(p0_),rp0,'\\');
if (rpY==rp0)
{
throw std::runtime_error
("Strange, _pgmptr has a path containing no delimiter.");
}
pX_=std::find(rpY+1,rp0,'\\').base();
pY_=rpY.base();
}
template<typename... Args> std::string Ignore(Args&&... args) const
{return IgnoreImpl(std::forward<Args>(args)...);}
};
using namespace std;
int main()
{
::SetThreadLocale(LOCALE_USER_DEFAULT);
bindtextdomain("Console1",TMyAppDir{}.Ignore("Debug*","Release*").c_str());
textdomain("Console1");
cout << _("Hello world!") << endl;
return 0;
}
プロジェクトが上記以外のソースコード/インクルードファイルを含む場合は、全てにおいて_マクロ定義して翻訳対象となる英語文字列を_マクロで囲む。ただし翻訳ファイル設定がmain関数内で行われるため、静的ストレージ(主にグローバル変数)に置かれた文字列を翻訳することはできない。
実行
ユーザーデフォルトロケール(LOCALE_USER_DEFAULT)はウィンドウズスタートメニューから[設定|時刻と言語|地域|日付、時刻、地域の追加設定|地域|形式|形式]で設定する。システムロケール(LOCALE_SYSTEM_DEFAULT)なら[設定|時刻と言語|地域|日付、時刻、地域の追加設定|地域|管理|システムロケールの変更]で設定する。設定したロケールが日本語(日本)であれば翻訳された日本語が表示され、それ以外なら翻訳前の英語が表示される。LANGなどの環境変数はロケールの設定に優先する。例えばフランス語の翻訳ファイルが存在するとして、ロケールが日本語(日本)としてもLANG=fr_FRとすればフランス語へ翻訳する。
翻訳文字列はターゲット文字コードに変換して出力する。変換に失敗する場合は検索文字列(翻訳前の文字列またはメッセージID)をそのまま出力する。例えばフランス語翻訳文字列はシステムロケール日本語(日本)の場合に対応文字コードへ変換できず、検索文字列をそのまま表示する。
ロケールの検討
ロケールは言語や地域を定義するパラメータセットとして定義される。本サイトはcodecvtの考察で一般的なプログラミングの観点で議論したが、本記事はlibintlライブラリによる翻訳のコマンドプロンプト出力に議論を絞る。詳細はソースコードに譲りここでは結論のみを述べる。
ドキュメント(GNU gettext utilities 4.2 Triggering gettext Operations)はsetlocale(LC_ALL,"")でC言語ロケールを設定せよとするが、これはロケールを環境変数から取得するPOSIX互換環境の話でウィンドウズ環境はコマンドプロンプト出力に失敗する。LC_CTYPEが"C"ロケール以外で失敗するためで理由はソースコードで詳説する。ウィンドウズ環境はユーザーデフォルトロケールあるいはシステムロケールを用い、本サイトは設定としてウィンドウズAPI関数SetThreadLocaleの使用を提案する。通常はデフォルトのままで十分で不要だが、ユーザー意図と異なってしまう場合が存在し明示的にコールする。
この場合でも環境変数がユーザーデフォルトロケール/システムロケールに優先し、以下にロケール取得の優先順位を示す。通常のウィンドウズ環境はこれらの環境変数を設定しない。なおsetlocale(LC_MESSAGES,NULL)の返すロケールは何の影響も与えない。
- LC_ALL環境変数が存在して"C"または"POSIX"が設定されていれば、"C"ロケールとして取得する(翻訳は行わない)。
- LC_MESSAGES環境変数が存在して"C"または"POSIX"が設定されていれば、"C"ロケールとして取得する(翻訳は行わない)。
- LANG環境変数が存在して"C"または"POSIX"が設定されていれば、"C"ロケールとして取得する(翻訳は行わない)。
- LANGUAGE環境変数が存在していれば、そのまま取得する。
- LC_ALL環境変数が存在してウィンドウズ準拠名ロケールが設定されていれば、POSIX準拠名に変換して取得する。
- LC_ALL環境変数が存在していれば、そのまま取得する。
- LC_MESSAGES環境変数が存在してウィンドウズ準拠名ロケールが設定されていれば、POSIX準拠名に変換して取得する。
- LC_MESSAGES環境変数が存在していれば、そのまま取得する。
- LANG環境変数が存在してウィンドウズ準拠名ロケールが設定されていれば、POSIX準拠名に変換して取得する。
- LANG環境変数が存在していれば、そのまま取得する。
- ユーザーデフォルトロケールあるいはシステムロケールを取得する。
環境変数が設定されてなければユーザーデフォルトロケールかシステムロケールのどちらかで明示的に指定した方が間違いない。ユーザーデフォルトロケールとするならSetThreadLocale(LOCALE_USER_DEFAULT)をコールする。
- ウィンドウズスタートメニューから[設定|時刻と言語|地域|日付、時刻、地域の追加設定|地域|形式|形式]で設定する。
- ユーザーアカウント毎の設定で再起動を必要としない。
システムロケールとするならSetThreadLocale(LOCALE_SYSTEM_DEFAULT)をコールする。
- ウィンドウズスタートメニューから[設定|時刻と言語|地域|日付、時刻、地域の追加設定|地域|管理|システムロケールの変更]で設定する。
- 全ユーザーアカウントに影響する設定で再起動を必要とする。
翻訳文字列出力の文字コードは以下の優先順位で取得する。OUTPUT_CHARSET環境変数による設定は不必要で、さらに不都合となる場合があり行わない方が良い。
- OUTPUT_CHARSET環境変数が存在して文字コードiconv準拠名が設定されていれば、それを取得する。
- setlocale(LC_CTYPE,NULL)が返すロケールがコードページ(整数値)を指定していれば、それに対応する文字コードを取得する。
- システムロケールの"ANSI"コードページ(整数値)に対応する文字コードを取得する
コマンドプロンプトのデフォルトはシステムロケールの"OEM"コードページ(整数値)でCHCPコマンドは任意に変更できる。これが文字列出力と一致しない場合はソースコードで詳説した。
ウィンドウズデスクトップアプリケーション
wxWidgetsライブラリによるウィンドウズデスクトップアプリケーションについて説明する。ウィンドウズデスクトップアプリケーションは英語文字列をUTF-8日本語文字列に翻訳してから、全てwxWidgetsライブラリのwxString文字列型に変換する。以降においてターゲットの実行形式ファイル名をDesktop1.exeと仮定する。
バッチファイル
k_xgettext.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
xgettext.exe *.cpp *.h -k_ -o bin\messages.pot
k_msginit.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
mkdir bin\ja
msginit.exe -i bin\messages.pot -o bin\ja\Desktop1.po -l ja_JP.UTF-8
k_msgmerge.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
msgmerge.exe bin\ja\Desktop1.po bin\messages.pot -U --backup=numbered
k_msgfmt.bat
@SETLOCAL
@SET LANG=ja_JP.UTF-8
@SET PATH=C:\msys64\usr\bin;%PATH%
msgfmt.exe bin\ja\Desktop1.po -o bin\ja\Desktop1.mo
翻訳ファイルの配置
開発時に各バッチファイルは翻訳ファイルを以下に配置する。テキスト形式翻訳ファイルの文字コードはBOMを持たないUTF-8として扱う。例えばウィンドウズメモ帳はUTF-8を編集できるが保存時に勝手にBOMを付加してしまいmsgfmtがエラーとなる。Poeditならそのような心配は無い。
ディレクトリ | ファイル | 内容 |
[プロジェクトディレクトリ] | Desktop1.cbp | Code::Blocksプロジェクト |
│ | *.cpp, *.h | ソースコード、インクルード |
│ | resource.rc | ウィンドウズリソース |
│ | k_xgettext.bat | xgettext実行バッチ |
│ | k_msginit.bat | msginit実行バッチ |
│ | k_msgmerge.bat | msgmerge実行バッチ |
│ | k_msgfmt.bat | msgfmt実行バッチ |
├ bin | messages.pot | 翻訳テンプレート |
│├ Debug32 | Desktop1.exe | 32ビット実行形式デバッグ版 |
│├ Release32 | Desktop1.exe | 32ビット実行形式リリース版 |
│├ Debug64 | Desktop1.exe | 64ビット実行形式デバッグ版 |
│├ Release64 | Desktop1.exe | 64ビット実行形式リリース版 |
│└ ja | Desktop1.po | 日本語テキスト形式翻訳 |
│ | Desktop1.mo | 日本語バイナリ形式翻訳 |
├ obj | | |
│├ Debug32 | *.o | 32ビットオブジェクトデバッグ版 |
│├ Release32 | *.o | 32ビットオブジェクトリリース版 |
│├ Debug64 | *.o | 64ビットオブジェクトデバッグ版 |
│└ Release64 | *.o | 64ビットオブジェクトリリース版 |
└ wxsmith | *.wxs | wxSmithリソース |
配布時はインストーラが以下に配置する。
ディレクトリ | ファイル | 内容 |
[インストールディレクトリ] | Desktop1.exe | 実行形式 |
└ ja | Desktop1.mo | 日本語バイナリ形式翻訳 |
ソースコードの修正
ウィンドウズデスクトップアプリケーションは翻訳ファイルを利用する国際化機能の実装にwxWidgetsライブラリのwxLocaleクラスを利用する。例として動作確認(1)で作成したDesktop1プロジェクトに国際化機能を加える。Desktop1Appクラス(Desktop1App.hとDeskotp1App.cpp)を以下に修正してビルドする。
Desktop1App.h
- 翻訳対象となる英語文字列を_マクロで囲む。
- Desktop1Appクラスのプライベートメンバ変数にwxLocale型のlocale_を追加する。
#ifndef DESKTOP1APP_H
#define DESKTOP1APP_H
#include <wx/app.h>
class Desktop1App : public wxApp
{
private:
wxLocale locale_;
public:
virtual bool OnInit();
};
#endif
Desktop1App.cpp
- 翻訳対象となる英語文字列を_マクロで囲む。
- Desktop1App::OnInitメンバ関数でlocale_メンバ変数のメンバ関数コールを追加する。
#include "wx_pch.h"
#include "Desktop1App.h"
#include <wx/stdpaths.h>
#include "Desktop1Frame.h"
#include <wx/image.h>
IMPLEMENT_APP(Desktop1App);
bool Desktop1App::OnInit()
{
wxStandardPaths::Get().IgnoreAppSubDir("Debug*");
wxStandardPaths::Get().IgnoreAppSubDir("Release*");
locale_.Init(wxLANGUAGE_DEFAULT,wxLOCALE_DONT_LOAD_DEFAULT);
locale_.AddCatalog("Desktop1");
locale_.AddCatalog("wxstd");
bool wxsOK = true;
wxInitAllImageHandlers();
if ( wxsOK )
{
Desktop1Frame* Frame = new Desktop1Frame(0);
Frame->Show();
SetTopWindow(Frame);
}
return wxsOK;
}
プロジェクトが上記以外のソースコード/インクルードファイルを含む場合は、全てにおいて翻訳対象となる英語文字列を_マクロで囲む。ただし翻訳ファイル設定がDesktop1App::OnInitメンバ関数内で行われるため、静的ストレージ(主にグローバル変数)に置かれた文字列を翻訳することはできない。Code::Blocksに付属するwxSmithで入力された文字列は自動的に_マクロで囲まれてソースコードへ挿入される。コンソールアプリケーションと同様に翻訳ファイル配置を開発時と配布時で変えるが、wxStandardPaths::IgnoreAppSubDirメンバ関数があらかじめ対応し加える修正は無い。
実行
ウィンドウズスタートメニューから[設定|時刻と言語|言語|Windowsの表示言語]が日本語であれば翻訳された日本語が表示され、それ以外であれば翻訳前の英語が表示される。