パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
多言語対応アプリケーションをどのように開発するか

MSYS2/mingw-w64/wxWidgetsでの多言語対応(マルチリンガル)アプリケーション開発を考察する。

日本語アプリケーション開発の検討から本サイトはウィンドウズデスクトップアプリケーションを英語アプリケーションとして開発し、GNU gettext翻訳ファイルを用いて実行時に日本語メッセージへ変換する。英語が不得意な我々としては不本意だが、そのまま多言語対応が可能というメリットがある。サイト作成者は多言語対応アプリケーションを開発した経験も開発する予定も無いが、本項目はその手段について考察する。本サイトの多言語対応アプリケーションは以下と定義する。

  • 英語とその他一つ以上の言語に対応する(対応言語)。
  • オプションでシステム言語(デフォルト)あるいは対応言語の一つを選択する。
  • システム言語を選択すればウィンドウズスタートメニュー[設定|時刻と言語|言語|Windowsの表示言語]に従う。[...|Windowsの表示言語]が対応言語に無い場合は英語となる。
  • アプリケーション表示文字列とウィンドウズHTMLヘルプファイル言語はオプション設定に従う。オプション設定される言語にHTMLヘルプファイルが無い場合は英語のそれを選択する。
  • インストーラ表示文字列は対応言語のうちInno Setupにインストールされている言語から選択できる。選択のデフォルトは[...|Windowsの表示言語]に従う。

wxWidgetsプロジェクトウィザード(K2)で新たにDesktopMLプロジェクトを生成し多言語(例えば英語、フランス語、日本語、韓国語)対応に修正してみよう。プロジェクト生成は動作確認(4)に従うものとし、面倒ならそのDesktop3を修正しても構わない。

アプリケーション

ウィザード生成のソースコードに直接書き込まれている表示文字列は英語だが、表示文字列をウィンドウズ[...|Windowsの表示言語]に合わせる機能は既に実装されていて、日本語バイナリ形式翻訳ファイルを供給すれば[...|Windowsの表示言語]が日本語の場合は日本語表示、それ以外の場合は英語表示となっている。

新たにオプションで日本語以外の表示言語も選択できるようにソースコードを修正する。またオプションに従い適切なHTMLヘルプファイルを選択させる。ソースコード修正は動作確認(3)が参考になろう。

メニュー項目の追加

wxSmithで言語オプション設定するための[Tools|Language]メニュー項目をメインウィンドウ(DesktopMLFrame)メニューに追加する。[Tools]メニューは[File]と[Help]の間に配置する。[Tools|Language]メニュー項目ハンドラとしてOnLanguageを定義する。

アプリケーションクラスの修正

アプリケーションクラス(DeskotpMLApp)OnInitメンバ関数は国際化機能を実装するwxLocaleクラスインスタンスのlocale_メンバ変数をwxLocale::Initメンバ関数で初期化する。生成ソースコードは第一実引数にwxLanguage列挙値のwxLANGUAGE_DEFAULTを与えるが、これをウィンドウズレジストリから読み込むlangに修正する。レジストリに無い場合はデフォルトとしてwxLANGUAGE_DEFAULTとして、かつその値をレジストリに保存する。langは後述TMyLanguageDialogクラスインスタンスの[Select language to use]ダイアログが変更してレジストリに保存する。レジストリ操作を行うので<wx/msw/registry.h>をインクルードする。

DesktopMLApp.cpp(抜粋)

...
#include <wx/msw/registry.h>
...
bool DesktopMLApp::OnInit()
{
...
wxRegKey regKey{wxRegKey::HKCU
,"Software\\" MYAPPINFO_PUBLISHER "\\" MYAPPINFO_NAME};
auto lang=long{wxLANGUAGE_DEFAULT};
if (!(regKey.Exists()
&&regKey.HasValue("Language")&&regKey.QueryValue("Language",&lang)))
{
regKey.Create();
regKey.SetValue("Language",lang);
}
locale_.Init(lang,wxLOCALE_DONT_LOAD_DEFAULT);
locale_.AddCatalog(MYAPPINFO_NAME);
locale_.AddCatalog("wxstd");
...
return wxsOK;
}

メインウィンドウクラスの修正(HTMLヘルプの切り替え)

メインウィンドウクラス(DesktopMLFrame)はコンストラクタでHTMLヘルプファイルを管理するwxHelpControllerクラスインスタンスのhelpController_メンバ変数をwxHelpController::Initializeメンバ関数で初期化する。生成ソースコードはHTMLヘルプファイル名にDesktopML.chmを与えるが、これをレジストリから読み込むwxLagnguage列挙値langで適当なものに切り替える。切り替えるファイル名は2文字言語コードlangNameを取得してDesktopML_<ISO 639-1:two-letter code>.chmとし、例えば韓国語ならDeskotpML_ko.chmとなる。HTLMヘルプファイルが見つからない場合は英語DesktopML_en.chmを選択する。2文字言語コードはGNU gettextツールが翻訳ファイルを配置するディレクトリ名でもある。レジストリ操作を行うので<wx/msw/registry.h>をインクルードする。

langがwxLANGUAGE_DEFAULTの場合の特別な処理は次項でまとめて説明する。

DesktopMLFrame.cpp(抜粋)

...
#include <wx/msw/registry.h>
...
DesktopMLFrame::DesktopMLFrame(wxWindow* parent,wxWindowID id)
{
...
wxRegKey regKey{wxRegKey::HKCU
,"Software\\" MYAPPINFO_PUBLISHER "\\" MYAPPINFO_NAME};
assert(regKey.Exists()&&regKey.HasValue("Language"));
auto lang=long{wxLANGUAGE_DEFAULT};
regKey.QueryValue("Language",&lang);
auto langName=(lang==wxLANGUAGE_DEFAULT
?TMyLocaleInfo{LOCALE_CUSTOM_UI_DEFAULT}.GetName()
:wxLocale::GetLanguageCanonicalName(lang)).Left(2);
auto getChmFileName=[](auto lName)
{
return wxFileName{wxStandardPaths::Get().GetDataDir()
,wxString::Format(MYAPPINFO_NAME "_%s.chm",lName)};
};
auto chmFileName=getChmFileName(langName);
if (!chmFileName.FileExists()) {chmFileName=getChmFileName("en");}
helpController_.Initialize(chmFileName.GetFullPath());
}

メインウィンドウクラスの修正(言語選択ダイアログ)

三つのクラスを導入する。TMyLocaleInfoクラスはウィンドウズAPI関数GetLocaleInfoラッパーでウィンドウズロケールID(LCID)で構築し言語コード(LOCALE_SNAME)とネイティブ言語名(LOCALE_SNATIVELANGNAME)を取得する。LCIDにLOCALE_SYSTEM_DEFAULTを与えればシステムデフォルト言語、LOCALE_CUSTOM_UI_DEFAULTを与えればスタートメニュー[設定|時刻と言語|言語|Windowsの表示言語]が選択される。wxLocaleクラスインスタンスのLCIDはwxLocale::FindLanguageInfoで得られるwxLanguageInfoインスタンスポインタからwxLanguageInfo::GetLCID関数で取得する。

TMyLanguageChoiceクラスはドロップダウンリスト選択コントロールwxChoiceを継承する。コンストラクタでシステム言語(正確には[...|Windowsの表示言語])を選択リスト先頭に与え、バイナリ形式翻訳ファイルが配置されたディレクトリを順次探索しその言語を選択リストに追加する。パブリックメンバ関数SetLanguage/GetLanguageはwxLagnguage列挙値による選択を設定/取得する。TMyLanguageDialogクラスはwxDialogを継承する。TMyLanguageChoiceクラスインスタンスのchoice_メンバ変数を持ちパブリックメンバ関数SetLanguage/GetLanguageはchoice_のメンバ関数をコールする。

覚え書き
ダイアログはwxSmithエディタで作成するのが通常だがコード縮約のため直書きとした。

[Tools|Language]メニュー項目ハンドラのOnLanguageメンバ関数はレジストリからwxLagnguage列挙値langを読み込みTMyLanguageDialogダイアログをモーダル表示する。langを変更したらレジストリに保存しアプリケーション再起動が必要とのメッセージを表示する。

レジストリ操作<wx/msw/registry.h>に加えGetLocaleInfoコールのバッファ操作に<vector>をインクルードする。wxLocaleのウィンドウズ実装はwxLANGUAGE_DEFAULTの扱いに不具合があり、wxLANGUAGE_DEFAULTで初期化するとシステムデフォルト言語に設定される一方で翻訳ファイルはスタートメニュー[設定|時刻と言語|言語|Windowsの表示言語]に従い探索される。デフォルトでは同じものだが後者を設定変更すると言語名と翻訳が食い違う。対処としてwxLANGUAGE_DEFAULTの場合の言語名はwxLocaleメンバ関数によらずGetLocaleInfoにLOCALE_CUSTOM_UI_DEFAULTを与えて取得する。すなわち本アプリケーションの"システム言語"はシステムデフォルト言語ではなく[...|Windowsの表示言語]とする。

DesktopMLFrame.cpp(抜粋)

...
#include <wx/msw/registry.h>
#include <vector>
...
class TMyLocaleInfo
{
private:
const LCID lcid_;
wxString Get(LCTYPE lcType) const
{
auto buf=std::vector<TCHAR>(::GetLocaleInfo(lcid_,lcType,0,0));
::GetLocaleInfo(lcid_,lcType,buf.data(),buf.size());
return buf.data();
}
public:
TMyLocaleInfo(LCID lcid):lcid_{lcid} {}
wxString GetName() const {return Get(LOCALE_SNAME);}
wxString GetNativeLangName() const {return Get(LOCALE_SNATIVELANGNAME);}
};
class TMyLanguageChoice:public wxChoice
{
public:
TMyLanguageChoice():wxChoice{} {}
template<typename... Args> TMyLanguageChoice(Args&&... args)
:TMyLanguageChoice{} {Create(std::forward<Args>(args)...);}
bool Create(wxWindow *parent,wxWindowID id
,const wxPoint& pos=wxDefaultPosition,const wxSize& size=wxDefaultSize)
{
if (wxChoice::Create(parent,id,pos))
{
Append(wxString::Format(_T("System (%s)")
,TMyLocaleInfo{LOCALE_CUSTOM_UI_DEFAULT}.GetNativeLangName())
,(void*)(intptr_t)(wxLANGUAGE_DEFAULT));
for (auto file=wxFindFirstFile
(wxStandardPaths::Get().GetDataDir()+"\\*",wxDIR)
;!file.IsEmpty();file=wxFindNextFile())
{
if (wxFileExists(file+"\\"+MYAPPINFO_NAME+".mo"))
{
if (auto* info
=wxLocale::FindLanguageInfo(wxFileName{file}.GetName()))
{
Append
(TMyLocaleInfo{info->GetLCID()}.GetNativeLangName()
,(void*)(intptr_t)(info->Language));
}
}
}
return true;
}
return false;
}
void SetLanguage(int lang)
{
auto i=(unsigned int){0};
for (;i<GetCount()&&lang!=(int)(intptr_t)(GetClientData(i));++i) {}
SetSelection(i<GetCount()?i:0);
}
int GetLanguage() const
{return (int)(intptr_t)(GetClientData(GetSelection()));}
};
class TMyLanguageDialog:public wxDialog
{
private:
TMyLanguageChoice* choice_;
public:
TMyLanguageDialog(wxWindow* parent)
:wxDialog{parent,wxID_ANY,_("Select language to use")}
,choice_{new TMyLanguageChoice{this,wxID_ANY}}
{
auto* btnSizer=new wxStdDialogButtonSizer{};
btnSizer->AddButton(new wxButton{this,wxID_OK});
btnSizer->AddButton(new wxButton{this,wxID_CANCEL});
btnSizer->Realize();
auto* sizer=new wxBoxSizer{wxVERTICAL};
sizer->Add(new wxStaticText{this,wxID_ANY
,_("Language (Requires restart)")},0,wxALL|wxEXPAND,5);
sizer->Add(choice_,0,wxALL|wxEXPAND,5);
sizer->Add(btnSizer,0
,wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL,5);
SetSizer(sizer);
sizer->SetSizeHints(this);
}
void SetLanguage(int lang) {choice_->SetLanguage(lang);}
int GetLanguage() const {return choice_->GetLanguage();}
};
DesktopMLFrame::DesktopMLFrame(wxWindow* parent,wxWindowID id)
{
...
}
...
void DesktopMLFrame::OnLanguage(wxCommandEvent& event)
{
wxRegKey regKey{wxRegKey::HKCU
,"Software\\" MYAPPINFO_PUBLISHER "\\" MYAPPINFO_NAME};
assert(regKey.Exists()&&regKey.HasValue("Language"));
auto lang=long{wxLANGUAGE_DEFAULT};
regKey.QueryValue("Language",&lang);
TMyLanguageDialog dlg{this};
dlg.SetLanguage(lang);
if (dlg.ShowModal()==wxID_OK)
{
regKey.SetValue("Language",dlg.GetLanguage());
wxMessageBox(_("Restart application to change language.")
,MYAPPINFO_NAME);
}
}
覚え書き
レジストリはHKCUキー階層下に作成する。本サイトの作成するインストーラがデフォルトで管理するレジストリはHKLMキー階層下で違いは留意しておく必要がある。いずれにせよ動作確認後は削除しておく。

翻訳ファイル

GNU gettext/Poedit

GNU gettext/Poeditで様々な言語の翻訳ファイルを作成できる。バッチファイルk_xgettext.batk_msginit.batk_msgmerge.batk_msgfmt.batはターゲット言語依存部分を書き換える。韓国語ならja_JP.UTF-8をko_KR.UTF-8に、bin\jaをbin\koに書き換える。Poeditはテキスト形式翻訳ファイル(*.po)の言語に合わせた候補文字列を自動的に表示する(有料版を購入しないと候補数は制限されるが)。

Code::Blocksのカスタマイズ

多言語対応アプリケーション開発が常態なら[Tools]メニューに登録すべきだろう。英語、フランス語、韓国語への翻訳ファイルを作成するとして、以下を[Tools|Configure Tools]で追加する。全て自作ツールKGetTextをターゲット言語を明示してコールする。

Tool Name Command Line Working Directory Tools Menu Path Output to
Gettext/Get for/English cmd /c $(#codeblocks.bin)\KGetText --get --target $(PROJECT_NAME) --editor "C:\Program Files (x86)\Poedit\Poedit" --no-msgfmt --locale en_US.UTF-8 --gettext-envlang en_US.UTF-8 $(PROJECT_DIR) Gettext/Get for/English Tools Output Window
Gettext/Get for/French cmd /c $(#codeblocks.bin)\KGetText --get --target $(PROJECT_NAME) --editor "C:\Program Files (x86)\Poedit\Poedit" --no-msgfmt --locale fr_FR.UTF-8 --gettext-envlang fr_FR.UTF-8 $(PROJECT_DIR) Gettext/Get for/French Tools Output Window
Gettext/Get for/Korean cmd /c $(#codeblocks.bin)\KGetText --get --target $(PROJECT_NAME) --editor "C:\Program Files (x86)\Poedit\Poedit" --no-msgfmt --locale ko_KR.UTF-8 --gettext-envlang ko_KR.UTF-8 $(PROJECT_DIR) Gettext/Get for/Korean Tools Output Window
Gettext/Edit for/English cmd /c $(#codeblocks.bin)\KGetText --edit --target $(PROJECT_NAME) --editor "C:\Program Files (x86)\Poedit\Poedit" --no-msgfmt --locale en_US.UTF-8 --gettext-envlang en_US.UTF-8 $(PROJECT_DIR) Gettext/Edit for/English Tools Output Window
Gettext/Edit for/French cmd /c $(#codeblocks.bin)\KGetText --edit --target $(PROJECT_NAME) --editor "C:\Program Files (x86)\Poedit\Poedit" --no-msgfmt --locale fr_FR.UTF-8 --gettext-envlang fr_FR.UTF-8 $(PROJECT_DIR) Gettext/Edit for/French Tools Output Window
Gettext/Edit for/Korean cmd /c $(#codeblocks.bin)\KGetText --edit --target $(PROJECT_NAME) --editor "C:\Program Files (x86)\Poedit\Poedit" --no-msgfmt --locale ko_KR.UTF-8 --gettext-envlang ko_KR.UTF-8 $(PROJECT_DIR) Gettext/Edit for/Korean Tools Output Window

英語への翻訳ファイル

ソースコードに書き込まれる表示文字列は英語なので英語への翻訳ファイルは本来なら不要であろう。しかしTMyLanguageChoiceコントロールが言語選択リストを作成する際にバイナリ形式翻訳ファイル(*.mo)の存在を利用するため、この目的で英語への翻訳ファイルを作成しておく。GNU gettextツールmsginitは翻訳後文字列(msgstr)を空白としてテキスト形式翻訳ファイル(*.po)を作成するが、--localeオプションが英語(en、en_US.ASCII、en_US.UTF-8)の場合に限り翻訳前文字列(msgid)をmsgstrへコピーする。Poeditで開けば"Language of the translation is the same as source language."という警告メッセージと共に翻訳後文字列が既に設定されていて面食らう事になる。そのままPoeditを閉じればファイル保存されず*.moも作成されないため、[File|Comipile to MO]で作成する。あるいは結果に影響しない何らかの変更を加えてファイル保存すれば*.moは自動作成される。例えばSource text(翻訳前文字列)とTranslation(翻訳後文字列)をテーブル表示する左側最上部ペインで任意行数(全部が望ましい)を選択し[Edit|Clear translation]で翻訳後文字列を削除してファイル保存する。

覚え書き
--localeオプションenでmsginitがmsgidをmsgstrへコピーする事は他のGNU gettextツールmsgenドキュメントで間接的に記述されている。

そもそも英語への翻訳ファイルは空で構わないがGNU gettextツールmsgfmtあるいはPoeditの処理時にエラーとならない程度の体裁を必要とする。*.poは最低限POヘッダと呼ばれる部分が必要で以下にmsgfmt/Poeditがエラーとしないミニマム例を示す。これとこれから作成される*.moを英語翻訳ファイルディレクトリ(bin\en)に置けば十分であろう。

msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

HTMLヘルプファイル

HTMLヘルプコンパイラ

ウィンドウズHTMLヘルプファイルはHTMLヘルプコンパイラ(hhc.exe)で作成しHTMLヘルプビューア(hh.exe)で表示する。hhc.exeはコードページ(手法)アプリケーションでシステムロケール(ウィンドウズスタートメニューから[設定|時刻と言語|地域|日付、時刻、地域の追加設定|地域|管理|システムロケールの変更])に影響され、作成されるHTMLヘルプファイルのロケール(あるいはコードページ(整数値))は任意設定できるものの作成時システムロケールが一致しないとhh.exeでの検索が機能しない。一方でhh.exeは意外にもユニコード(手法)アプリケーションでシステムロケールに影響されず任意のロケールで作成されたHTMLヘルプファイルを正しく表示する。

例えば韓国語のHTMLヘルプファイルを作成するなら、原稿(doxygen\*.md)を韓国語に書き換え、Doxyfile_winchm言語設定を変更(OUTPUT_LANGUAGE=Korean、CHM_FILE=../../bin/DesktopML_ko.chm、CHM_INDEX_ENCODING=CP949)し、バッチファイルk_doxygen_winchm.batの言語設定を変更(Shift-JIS/CP932をcseuckr/CP949に置換)してシステムロケール韓国語(韓国)の下でdoxygenディレクトリで実行する。しかしこれでは複数言語対応に混乱を招く事に加え、都度[...|システムロケールの変更]でウィンドウズ再起動を必要とする。

覚え書き
ウィンドウズ10バージョン1803からベータ版ながらシステムロケールUTF-8を選択できるようになったが、この下にhhc.exeで文字コードUTF-8のHTMLヘルプファイルを作成しても残念ながらhh.exeの文字化けは解消しない。

SBAppLocale

[...|システムロケールの変更]のウィンドウズ再起動は面倒なので一時的にシステムロケールを変更するツールSBAppLocaleを導入する。zipファイルを解凍し適当なディレクトリに配置するだけだが、ここではディープなセットアップで作成したC:\Users\user\CodeBlocks\binに放り込む。

バッチファイル

複数言語に対応するため各言語のHTMLヘルプファイル用入力データをdoxygen\winchm_multilingual\<ISO 639-1:two-letter code>に配置し、バッチファイルで一括処理する。例えば韓国語なら言語依存設定をdoxygen\winchm_multilingual\koに、原稿ファイルはdoxygen\winchm_multilingual\ko\winchmに配置する。

ディレクトリ ファイル 内容
[プロジェクトディレクトリ] DesktopML.cbp Code::Blocksプロジェクト
... ...
├ bin DesktopML_en.chm 英語HTMLヘルプ
││ DesktopML_fr.chm フランス語HTMLヘルプ
││ DesktopML_ja.chm 日本語HTMLヘルプ
││ DesktopML_ko.chm 韓国語HTMLヘルプ
│├ Debug32 ... ...
│︙ ... ...
├ obj
│├ Debug32 ... ...
│︙ ... ...
├ wxsmith ... ...
└ doxygen Doxyfile ソースコード解析用Doxygen設定
 │ Doxyfile_winchm HTMLヘルプファイル用Doxygen設定
 │ my_header.html ヘッダHTML
 │ my_footer.html フッタHTML
 │ my_customstylesheet.css カスケードスタイルシート
 │ k_doxygen_winchm_multilingual.bat 多言語対応HTMLヘルプファイル用Doxygen実行バッチ
 │ ... ...
 ├ html *.html, *.js, *.cssなど ソースコード解析用HTML
 └ winchm_multilingual Doxyfile_winchm_multilingual_template HTMLヘルプファイル用Doxygen設定テンプレート
  ├ en k_set_language.bat 英語オプション設定バッチ
  │︙ ... 英語...
  ├ fr k_set_language.bat フランス語オプション設定バッチ
  │︙ ... フランス語...
  ├ ja k_set_language.bat 日本語オプション設定バッチ
  │︙ ... 日本語...
  └ ko k_set_language.bat 韓国語オプション設定バッチ
   │ my_layout.xml 韓国語レイアウト
   ├ html_winchm *.html, *.js, *.cssなど 韓国語HTMLヘルプファイル用HTML
   │ index.hhp, index.hhc, index.hhk 韓国語HTMLヘルプコンパイラ出力
   └ winchm *.md 韓国語HTMLヘルプファイル用原稿

バッチファイルk_doxygen_winchm_multilingual.batはdoxygenディレクトリで実行し、言語サブディレクトリを探索して順次以下の処理を行う。

  1. オプション設定バッチ(例えばdoxygen\winchm_multilingual\ko\k_set_language.bat)をコールして言語依存オプション(LANGUAGE、CHARSET、CHARCODE、LOCALENUM)を読み込む。
  2. MSYS2のsedツールでDoxyfile_winchm_multilingual_tempalateから各言語用Doxygen設定ファイルを一時作成する。
  3. Doxygenを実行して原稿ファイル(...\ko\winchm\*.md)からヘルプ用HTMLファイル(...\ko\html_winchm\*.htmlなど)、hhc.exe出力ファイル(...\ko\html_winchm\index.hhpなど)、UTF-8文字コードのHTMLヘルプファイル(bin\DesktopML_ko.chm)を出力する。
  4. 各言語用Doxygen設定ファイルを削除する。
  5. sedツールとiconvツールでヘルプ用HTMLファイル(...\ko\html_winchm\*.htmlなど)文字コードを各言語ロケール(コードページ)(韓国語ならcseuckr/CP949)に修正する。
  6. SBAppLocaleにウィンドウズロケールID(韓国語なら1042)を与えてhhc.exeを起動させ、HTMLヘルプファイル(bin\DesktopML_ko.chm)を文字コード修正版で上書きする

Doxygenカスタマイズファイルは以下を除きウィザード生成のものをそのまま用いる。

  • Doxygen設定ファイルは一時作成される各言語用を用いる。各言語用は@INCLUDEタグでウィザード生成Doxyfile_winchmを参照する。
  • レイアウトファイルは表示文字列を言語毎に変更するため言語サブディレクトリに配置(...\ko\my_layout.xml)する。

doxygen\k_doxygen_winchm_multilingual.bat

言語依存オプションLANGUAGE、CHARSET、CHARCODE、LOCALENUMはk_set_language.batをコールして環境変数として設定する。

@rem Use enabledelayedexpansion flag for setlocal to make variables in for loop
@rem delayed expansion.
@SETLOCAL enabledelayedexpansion
@SET PATH=C:\msys64\usr\bin;%PATH%
@CHCP 437
@set _SUBDIR=winchm_multilingual
@set _DOXYFILE=%_SUBDIR%\Doxyfile_winchm_multilingual
@for /d %%j in (%_SUBDIR%\*) do @(
set _LANGBATFILE=%%j\k_set_language.bat
if exist !_LANGBATFILE! @(
call !_LANGBATFILE!
sed -r -e "{s/#\{LANG\}/%%~nj/g;s/#\{LANGUAGE\}/!LANGUAGE!/g;s/#\{CHARCODE\}/!CHARCODE!/g}" %_DOXYFILE%_template > %_DOXYFILE%
C:\msys64\mingw64\bin\doxygen.exe %_DOXYFILE%
del %_DOXYFILE%
echo:
echo Processing %%~nj^(!LANGUAGE!,!CHARSET!,!CHARCODE!,!LOCALENUM!^)...
set _OUTPUTDIR=%%j\html_winchm
for %%i in (!_OUTPUTDIR!\*.html !_OUTPUTDIR!\*.js !_OUTPUTDIR!\*.css) do @(
echo Converting %%~nxi from UTF-8 to !CHARCODE!...
sed -e 's/UTF-8/!CHARSET!/gI' < %%i | iconv -f UTF-8 -t !CHARCODE! -c > temp.dat
move /Y temp.dat %%i
)
echo:
C:\Users\user\CodeBlocks\bin\SBAppLocale !LOCALENUM! "C:\Program Files (x86)\HTML Help Workshop\hhc.exe" !_OUTPUTDIR!\index.hhp
)
)

doxygen\winchm_multilingual\ko\k_set_language.bat

韓国語版で例示する。

@set LANGUAGE=Korean
@set CHARSET=cseuckr
@set CHARCODE=CP949
@set LOCALENUM=1042

いくつかの言語依存オプション設定を示す。

言語 LANGUAGE CHARSET CHARCODE LOCALENUM
英語 English iso8859-1 CP1252 1033
フランス語 French iso8859-1 CP1252 1036
日本語 Japanese Shift-JIS CP932 1041
韓国語 Korean cseuckr CP949 1042

doxygen\winchm_multilingual\Doxyfile_winchm_multilingual_template

#{LANG}、#{LANGUAGE}、#{CHARCODE}はk_doxygen_winchm_multilingual.batのsedツールが置換するプレースホルダーである。

@INCLUDE = Doxyfile_winchm
# CHM_FILE path is relative to "./winchm_multilingual/#{LANG}/html_winchm" where
# index.hhp is in. It is not relateve to "." where this file is in.
WARN_LOGFILE = doxygen_winchm_multilingual_#{LANG}.log
HTML_OUTPUT = ./winchm_multilingual/#{LANG}/html_winchm/
INPUT = ./winchm_multilingual/#{LANG}/winchm/
LAYOUT_FILE = ./winchm_multilingual/#{LANG}/my_layout.xml
OUTPUT_LANGUAGE = #{LANGUAGE}
CHM_FILE = ../../../../bin/MultilingualTest_#{LANG}.chm
CHM_INDEX_ENCODING = #{CHARCODE}

Doxyfileの修正

ソースコード解析用Doxygen設定ファイル(doxygen\Doxyfile)はソースコード解析用HTMLファイルにHTMLヘルプ用HTMLファイルコピーを参考用に含むが、多言語対応でその原稿位置が移動するため修正する。以下に日本語HTMLヘルプ用を含む設定を示す。

...
# Include pages for Windows HTML help
INPUT += ./winchm_multilingual/ja/winchm/
ALIASES += "mainpage_winchm=\page mainpage_winchm"
...

Code::Blocksのカスタマイズ

多言語対応アプリケーション開発が常態ならこれも[Tools]メニューに登録する。k_doxygen_winchm_multilingual.batをC:\Users\user\CodeBlocks\batchへ移動して以下を追加する。

Tool Name Command Line Working Directory Tools Menu Path Output to
Doxygen/Create HTML help (multilingual) cmd /c $(#codeblocks.batch)\k_doxygen_winchm_multilingual $(PROJECT_DIR)\doxygen Doxygen/Create HTML help (multlingual) Tools Output Window

Deployバーチャルターゲットの修正

ウィザード生成Deployバーチャルターゲットの利用する_DeployEndターゲットのプレビルドステップも必要なら修正する。

Target Step Execution steps
_DeployEnd Pre-build cmd /c ...
cmd /c cd doxygen & $(#codeblocks.batch)\k_doxygen_winchm_multilingual
cmd /c ...
cmd /c ...

インストーラ

表示言語の選択

Inno Setupインストーラ表示のダイアログ文字列も多言語対応する。インストーラはスクリプト[Languages]セクションにリストされる言語メッセージファイル(*.isl)からデフォルト言語を選択する。

  1. *.islの[LangOptions]に示される言語コードがスタートメニュー[設定|時刻と言語|言語|Windowsの表示言語]に一致するものを選択する。
  2. 一致するものが見つからない場合はリスト先頭の*.islを選択する。

[Setup]セクションShowLanguageDialogがyes(デフォルト)で[Languages]セクションに*.islが2個以上ある場合、デフォルト言語で[セットアップに使用する言語の選択]ダイアログ(英語表示なら[Select Setup Language]ダイアログ)を表示して以降のダイアログ文字列言語をリストから選択する。

スクリプトファイル

ウィザード生成のスクリプトファイル(innosetup\InnoSetup.iss)を多言語対応に拡張する。スクリプトは三つの#forディレクティブループで多言語対応を自動化する。

  • バイナリ翻訳ファイル(*.mo)を探索し[Files]セクションへ登録する(FindMoFileProcループ)。
  • HTMLヘルプファイル(*.chm)を探索し[Files]へ登録する(FindChmFileProcループ)。
  • 登録される*.moの言語に対応する言語メッセージファイル(*.isl)があれば[Languages]に登録する(FindLangProcループ)。

FindMoFileProcループはウィザード生成スクリプトファイルオリジナルに最初から実装済みで、FindChmFileProcループとFindLangProcループが今回の拡張になる。

innosetup\InnoSetup.iss

; Script initially generated by the Inno Setup Script Wizard, modified by Takeshi Kodama.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#include "..\version_macro.h"
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
#define MyAppId "{"+AUTOVERSION_STR_GUID
#define MyAppName MYAPPINFO_NAME
#define MyAppVersion AUTOVERSION_STR_VER
#define MyAppPublisher MYAPPINFO_PUBLISHER
;#define MyAppURL "http://www.example.com/"
#define MyAppExeName MyAppName+".exe"
#define MyAppCopyright "(C)"+AUTOVERSION_STR_YEAR+" "+MYAPPINFO_AUTHOR
#define MyDir "..\bin\"
#define MyAppExePath32 MyDir+"Release32\"+MyAppExeName
#define MyAppExePath64 MyDir+"Release64\"+MyAppExeName
#define MyAppChmPattern MyDir+MyAppName+"_??.chm"
#define MyAppLangPattern MyDir+"*"
#define MyAppMoPath(lang) MyDir+lang+"\"+MyAppName+".mo"
[Setup]
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
;AppPublisherURL={#MyAppURL}
;AppSupportURL={#MyAppURL}
;AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppPublisher}\{#MyAppName}
DefaultGroupName={#MyAppPublisher}
OutputDir=.
OutputBaseFilename={#MyAppName}-{#MyAppVersion}
Compression=lzma
SolidCompression=yes
AppCopyright={#MyAppCopyright}
VersionInfoVersion={#MyAppVersion}
DisableWelcomePage=False
ArchitecturesInstallIn64BitMode=x64
UninstallDisplayIcon={app}\{#MyAppExeName}
WizardStyle=modern
#if !FileExists(MyAppExePath32)
ArchitecturesAllowed=x64
#endif
[Files]
#if FileExists(MyAppExePath32)
#if FileExists(MyAppExePath64)
Source: "{#MyAppExePath32}"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode
Source: "{#MyAppExePath64}"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode
#else
Source: "{#MyAppExePath32}"; DestDir: "{app}"; Flags: ignoreversion
#endif
#else
#if FileExists(MyAppExePath64)
Source: "{#MyAppExePath64}"; DestDir: "{app}"; Flags: ignoreversion
#else
#error No execution files are found
#endif
#endif
; NOTE by Takeshi Kodama
; Defining and executing any user defined procedure by #sub/#endsub directives
; up to here causes a strange compiler error such as "Undeclared variable
; identifier: ..." in FindChmFileProc procedure. That's the reason why
; [Languages] section has been moved to after [Files]. To reproduce the
; problem uncomment the following lines.
;#sub DummyProc
;#endsub
;#expr DummyProc
#define MyFindHandle
#define MyFindResult
#sub FindChmFileProc
Source: "{#MyDir+FindGetFileName(MyFindHandle)}"; DestDir: "{app}"; Flags: ignoreversion
#endsub
#for {MyFindResult=MyFindHandle=FindFirst(MyAppChmPattern,0);MyFindResult;MyFindResult=FindNext(MyFindHandle)} FindChmFileProc
#if MyFindHandle
#expr FindClose(MyFindHandle)
#endif
#sub FindMoFileProc
#define MyLang FindGetFileName(MyFindHandle)
#if !(SameText(MyLang,".")||SameText(MyLang,".."))&&FileExists(MyAppMoPath(MyLang))
Source: "{#MyAppMoPath(MyLang)}"; DestDir: "{app}\{#MyLang}"; Flags: ignoreversion
#endif
#undef MyLang
#endsub
#for {MyFindResult=MyFindHandle=FindFirst(MyAppLangPattern,faDirectory);MyFindResult;MyFindResult=FindNext(MyFindHandle)} FindMoFileProc
#if MyFindHandle
#expr FindClose(MyFindHandle)
#endif
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Languages]
; NOTE by Takeshi Kodama
; Such an unreproducible error might take place in the FindLangProc procedure
; that the MyLang string is empty. If such the error takes place calling
; FindClose again like commented lines below might solve.
;#if MyFindHandle
; #expr FindClose(MyFindHandle)
;#endif
Name: "en"; MessagesFile: "compiler:Default.isl"
#sub FindLangProc
#define MyLang FindGetFileName(MyFindHandle)
#if !(SameText(MyLang,".")||SameText(MyLang,".."))&&FileExists(MyAppMoPath(MyLang))
#if MyLang=="en"
;#define MyMessagesFile "Default.isl"
#elif MyLang=="hy"
#define MyMessagesFile "Languages\Armenian.isl"
#elif MyLang=="ca"
#define MyMessagesFile "Languages\Catalan.isl"
#elif MyLang=="co"
#define MyMessagesFile "Languages\Corsican.isl"
#elif MyLang=="cs"
#define MyMessagesFile "Languages\Czech.isl"
#elif MyLang=="da"
#define MyMessagesFile "Languages\Danish.isl"
#elif MyLang=="nl"
#define MyMessagesFile "Languages\Dutch.isl"
#elif MyLang=="fi"
#define MyMessagesFile "Languages\Finnish.isl"
#elif MyLang=="fr"
#define MyMessagesFile "Languages\French.isl"
#elif MyLang=="de"
#define MyMessagesFile "Languages\German.isl"
#elif MyLang=="he"
#define MyMessagesFile "Languages\Hebrew.isl"
#elif MyLang=="is"
#define MyMessagesFile "Languages\Icelandic.isl"
#elif MyLang=="it"
#define MyMessagesFile "Languages\Italian.isl"
#elif MyLang=="it"
#define MyMessagesFile "Languages\Italian.isl"
#elif MyLang=="ja"
#define MyMessagesFile "Languages\Japanese.isl"
#elif MyLang=="no"
#define MyMessagesFile "Languages\Norwegian.isl"
#elif MyLang=="pl"
#define MyMessagesFile "Languages\Polish.isl"
#elif MyLang=="ru"
#define MyMessagesFile "Languages\Russian.isl"
#elif MyLang=="sl"
#define MyMessagesFile "Languages\Slovenian.isl"
#elif MyLang=="es"
#define MyMessagesFile "Languages\Spanish.isl"
#elif MyLang=="tr"
#define MyMessagesFile "Languages\Turkish.isl"
#elif MyLang=="uk"
#define MyMessagesFile "Languages\Ukrainian.isl"
#endif
#ifdef MyMessagesFile
Name: {#MyLang}; MessagesFile: "compiler:{#MyMessagesFile}"
#undef MyMessagesFile
#endif
#endif
#undef MyLang
#endsub
#for {MyFindResult=MyFindHandle=FindFirst(MyAppLangPattern,faDirectory);MyFindResult;MyFindResult=FindNext(MyFindHandle)} FindLangProc
#if MyFindHandle
#expr FindClose(MyFindHandle)
#endif
#undef MyFindResult
#undef MyFindHandle
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Registry]
Root: "HKA"; Subkey: "Software\{#MyAppPublisher}"; Flags: uninsdeletekeyifempty
Root: "HKA"; Subkey: "Software\{#MyAppPublisher}\{#MyAppName}"; Flags: uninsdeletekey
覚え書き
#forディレクティブループを多用したせいか不可解なエラーが生じる。サイト作成者の環境だけかもしれないがFindChmFileProcループ前の#sub/#exprディレクティブDummyProcをアンコメントするだけでエラーが発生する。より悪い事にFindLangProcループ中で取得するMyLang変数(2文字言語コード)がブランクとなるケースがスクリプト改変中まれに現れ直前ループのハンドルクローズをもう一度行う(FindClose(...))と解決したりする。FindCloseはマニュアル上は廃止でコール不要とされているのにコール有無で明らかに挙動が変わる。[Languages]と[Files]の順番がウィザード生成スクリプトオリジナルと逆転しているのはエラー回避のトライアンドエラーの結果でその理由は説明できない。こういった事は大抵望ましくない事態の前兆だが現時点では解明できていない。