Windows Installer手引書 Part.24 ローカライズ

[**前の記事へ**](https://qiita.com/tohshima/items/4e9f8133c5a14800c622)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/e31e686f52873446497b)

## MSI単独で扱えるのは単一言語だけ

MSIファイル単独では一つの言語しか扱うことができません。MSIのテーブルの構造上、複数の言語を扱う仕組みは入っていません。だからといって多言語対応のMSIを作ることができないわけではなく、MSIファイルをハンドリングするmsiexec.exeの助けで多言語対応を実現可能です。

## MSIの言語サポートとtransform

先に、MSIの言語サポートを理解する上で重要なtransformについて説明します。複数の言語に対応したインストーラーを作る際、言語ごとにインストーラーを用意したとしても、それぞれの違いは表示する文字列の違い程度のものです。そこで、MSIファイル間の違いを抽出したり、抽出した違いを適用して異なるインストーラーを作り出す、transformというWindows Installerの機能が用意されています。これにより、重複する情報を配布する必要がなくなり、効率的なインストーラーを構成できるようになります。transformは、MSIの多言語化のためだけに用意された機能ではありませんが、transformを利用する機会のほとんどはMSIの多言語化でしょう。

[Microsoft Windows SDK for Windows 7 and .NET Framework 4](https://www.microsoft.com/en-us/download/details.aspx?id=8279)に同梱されている、MsiTran.exe（ほとんどの環境では"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin"にあります）を利用してMSIファイルを[transform](https://docs.microsoft.com/en-us/windows/win32/msi/msitran-exe)することができます。WiX Toolsetでインストーラーを開発する場合は、MsiTranにあたる機能は存在しないので、MSIを多言語化するには、Windows SDKが必携になります[^1]。

- 差分抽出
**MsiTran -g BASE.msi TARGET.msi DIFF.mst**
この処理は、BASE.msiとTARGET.msiを比較して、違いのある部分のうち、TARGET.msi側のデータを抜き出し、DIFF.mstに書き出します。

- 差分適用
**MsiTran -a DIFF.mst TARGET.msi ef**
この処理は、DIFF.mstのデータをTARGET.msiに上書きします。MsiTranコマンドで差分を適用するには、多少のコツが必要です。細かい説明は[マイクロソフトのドキュメント](https://docs.microsoft.com/en-us/windows/win32/msi/msitran-exe)に譲りますが、コマンドラインオプションの最後にどのような処理を許可するか、アルファベットで指定する必要があります。上記の例ではefの2つを指定していますが、eは「既存のテーブルの変更を許可する」で、fは「Code Pageの変更を許可する」です。多言語対応の際には、コマンドラインオプションの他にも差分を適用できる条件がありますが、それは[後述](#summary-informationのlanguage-code)します。実際の開発現場で、MsiTranコマンドを差分適用に使用する機会はまずありません。なぜなら、差分の適用は、インストール先環境で行うものであり、msiexec.exeに差分を適用する機能が含まれているからです。

- インストール時の差分適用
**msiexec /i BASE.msi TRANSFORMS=DIFF.mst**
TRANSFORMSプロパティに差分ファイルを指定します。インストール時にtransformすると、アンインストール時にも自動的にtransformが適用されます。この方法で多言語対応のインストーラーを実行する場合は、インストール先のOSの言語を検出して、適切なmstファイルをコマンドラインに渡すための「セットアップランチャー」となるプログラムをMSIファイルとは別に作成する必要があります。msiexecを使って差分適用する方法には、コマンドラインにmstファイルの指定がいらないもう一つの方法があります。それは後述します。

## Code Pageとは

昔ながらの非Unicodeアプリでは基本的に1文字を8bitで表し、0～127はどの言語でもASCIIコードと言われるコード体系を使いますが、128以降は言語ごとに異なるコード体系を持っています。例えば、日本語Windowsでは「Shift JIS」を使用します。このコード体系を、Code Pageと呼ぶIDで区別します。ただし、言語が違っていても同じCode Pageを持つ場合もあります。WindowsにおけるCode Pageの扱いは下記のページに解説があります。

**Code Pages**
https://docs.microsoft.com/en-us/windows/win32/intl/code-pages

MSIファイルに格納する文字情報は非Unicodeです[^2]ので、一つのMSIファイルは一つのCode Pageを持っています。欧米の言語は「1252」であることが多く、日本語（Shift-JIS）の場合は「932」が使われます。以下のページに一覧があります。

**Code Page Bitfields**
https://docs.microsoft.com/en-us/windows/win32/intl/code-page-bitfields

MSIで指定するCode Pageは、UITextテーブルやPropertyテーブルに格納する文字列と一致させます。WiX ToolsetのWixUI Dialog Libraryを使用する場合は、ライブラリ側でCode Pageが指定されている（拡張子.wxlファイルの先頭にあります）ので、改めて自分でCode Pageを指定する必要はありません。

## Language Codeとは

Windowsは色々な地域や言語向けにローカライズされた版がリリースされています。そして、版ごとに別々のLanguage Codeが割り振られています。例えば日本語版のWindowsでは、Language Codeは「1041」が割り当てられています。英語は多くの国で使われていますが、米国版と英国版のWindowsが別々に存在し、それぞれ異なるLanguage Code（前者は「1033」、後者は「2057」）が割り当てられています。以下のページに一覧があります。

**2.1.1906 Part 4 Section 7.6.2.39, LCID (Locale ID)**
https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a

MSIにとってのLanguage Codeは、色々な言語のMSIファイルを作成した際に、「どのMSIファイルがどの版のWindowsをターゲットにしているか」を示すものです。また、インストール先環境とMSIファイルが持つLanguage Codeが異なっていても、そのままインストールの実行は可能で、Code Pageが正しく設定されていればどの言語のWindowsで実行しても文字化けなく正しく表示されます。

MSIファイルではLanguage Codeが2か所で使用されています。１つは[Summary InformationのLanguages](https://docs.microsoft.com/en-us/windows/win32/msi/template-summary)、もう１つは[PropertyテーブルのProductLanguageプロパティ](https://docs.microsoft.com/en-us/windows/win32/msi/productlanguage)です。

## Summary InformationのLanguage Code

[マイクロソフトのドキュメント](https://docs.microsoft.com/en-us/windows/win32/msi/template-summary)によると、Summary InformationのLanguage Codeは、どのLanguage CodeのMSIにtransform可能かを示している、と書かれています。実際、MsiTranコマンドで差分を適用するには、この通りにしないとエラーが起きて適用できません。しかし、WiX Toolsetでは、Summary InformationのLanguagesとProductLanguageプロパティに同じ値が設定されるようになっており、2つ以上のLanguage Codeを設定できません。こうしたことから、「多言語化されたMSIファイルを作成するには、WiX Toolset単独では実現できない」、ということになります。

## PropertyテーブルのProductLanguageプロパティ

ProductLanguageプロパティには、どの版（言語）のWindowsをターゲットにしているか示すLanguage Codeを１つだけ設定します。WiX Toolsetで設定するLanguage Codeは、実質このプロパティを設定していると考えて良いと思います。

## Visual Studioで色々な言語のインストーラーを作る

では実際にWix Toolsetを使ってインストーラーを作ります。VisualStudioで作成する場合、UIの言語設定に関わる設定は、以下の３か所です。以下の例は、日本語に設定する場合です。

1.UIExtentionの追加
![Part24_sln.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/1eac044c-89ed-d996-bcee-43cb82da00c4.png)
2.Language Codeの設定とUIセットの選択
![Part24_wxs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/2454f0ee-bb47-74a7-ce5a-1a5ad9678508.png)
3.リンクする文字情報の言語選択
![Part24_prj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/4af21ad3-f03c-9e45-81ed-7409104e6552.png)

Culture to buildのExampleを見れば分かるように、ここに複数の言語を指定して複数のMSIファイルを得ることができます。注意が必要なのは、このようにして得られたMSIファイルは、どれもProductLanguageプロパティの内容が同じになってしまうことです。例えば、表示は日本語なのにProductLanguageプロパティは英語、などという“ちぐはぐ”なMSIファイルが何の警告もなく吐き出されます。それと引き換えに得られるメリットは、ビルド時間の短縮です。通常、WiX Toolsetで複数のMSIファイルを得るには、candleコマンドとlightコマンドを言語数だけ実行する必要があります。しかしこの方法を使うと、candleコマンドは1回だけ実行され、その後言語数だけlightコマンドが実行されます。

## MSIファイルのProductLanguageプロパティを変更する

ダイアログで表示する言語とProductLanguageプロパティが示す言語を一致させるために、ビルド後のMSIファイルを直接編集することを考えてみます。準備なしでできる、もっとも簡単な方法は、Orcaで変更する方法です。下図の項目を変更すれば済みます。
![Part24_ProductLanguageProperty.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/0d4f36fa-f320-ac54-f8ea-1626e8a5dab0.png)
ただ開発の現場で何度もOrcaでMSIファイルを開いてPropertyを変更して保存する、というやり方は現実的ではありません。そこで、WSH(Windows Scripting Host)を使ってスクリプトで処理する方法をお勧めします。

スクリプトでMSIファイルのテーブルを書き換える場合、SQL文を使う必要があります。マイクロソフトによる下記の記事が参考になります。

SQL Syntax
https://docs.microsoft.com/en-us/windows/win32/msi/sql-syntax

Examples of Database Queries Using SQL and Script
https://docs.microsoft.com/en-us/windows/win32/msi/examples-of-database-queries-using-sql-and-script

Windows InstallerはCOM(Component Object Model)のインターフェースを持っているので、WSHから簡単にこの機能を利用することが可能です。[Microsoft Windows SDK for Windows 7 and .NET Framework 4](https://www.microsoft.com/en-us/download/details.aspx?id=8279)をインストールしているなら、
C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts
に置かれている「WiRunSQL.vbs」を利用するのが簡単です。使い方の説明は[こちら](https://docs.microsoft.com/en-us/windows/win32/msi/execute-sql-statements)にあります。下記のようにすれば、ProductLanguageプロパティを書き換えられます。

```bat
cscript "C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts\WiRunSQL.vbs" Part24_MULTI.msi "UPDATE `Property` SET `Property`.`Value`='1041' WHERE `Property`.`Property`='ProductLanguage'"
```

上記のスクリプトはVBScriptで書かれていますが、JScriptでも変更できます。

```Javascript
var installer = new ActiveXObject("WindowsInstaller.Installer");
var database = installer.OpenDatabase("Part24_MULTI.msi", 1);
var view = database.OpenView("UPDATE `Property` SET `Property`.`Value`='1041' WHERE `Property`.`Property`='ProductLanguage'");
view.Execute();
database.Commit();
```

## 多言語に対応したインストーラーを作る

多言語に対応したインストーラーを作る方法は、いくつか考えられます。最も安直な方法は、

> 言語ごとに個別のMSIファイルを配布する

でしょう。昔、フリーのOffice系のソフトで言語ごとにインストーラーが分かれていたのを思い出します。言語ごとにダウンロードサイトを構築できるなら選択肢の一つになるかもしれませんが、メンテナンスの面で苦労をしょい込むことになると思います。次善の策としては

> MSIを起動するセットアップランチャーでインストール先の言語を判別して、適切なmstファイルを適用する

という方法が考えられます。セットアップランチャーはC/C++言語等でWin32アプリを作るよりは、例えばInno SetupのようなMSIを使わないインストーラー作成ツールを使うのが便利です。インストーラー向けの機能を簡単に利用できるようになっていますし、MSIではどうしても解決できない問題をセットアップランチャー側で解決できる安心感もあります。実は、多言語化にフォーカスした場合は、もっとシンプルな方法が存在します。

> MSIのストレージにLanguage Codeでフォルダを作り、その中にmstファイルを格納する

このようにすると、msiexecに言語判定させて自動でmstファイルを適用することができます。コマンドラインオプションも不要なので、MSIファイルをダブルクリックしてインストールすることができます。ここから、この方法について説明していきます。


## MSIの内部構造

用語の説明が先に必要なので、MSIの内部構造について説明しておきます。MSIファイルにはOrcaで見ることができるテーブルやSummary Informationを保持するだけでなく、ファイルを保持するためのストレージ領域を持っています。このMSIのファイル形式を[構造化ストレージ（Structured Storage)](https://docs.microsoft.com/ja-jp/windows/win32/stg/structured-storage-start-page?redirectedfrom=MSDN)[^3]と呼びます。構造化ストレージは、物理的なディスク上に構築されたファイルシステムの様に、フォルダを作りその下にファイルを格納することもできます。このフォルダをサブストレージと呼びます。

## 多言語インストーラーの構築

先にVisual StudioのCulture to buildを「en-US;ja-JP;de-DE;fr-FR」のように設定して、４か国語のMSIファイルをビルドします。そして、下記のようなバッチファイルをwxsファイルと同じフォルダに置いて実行すれば、日英独仏の４か国語に対応したMSIファイルが作成されます。

```bat
SET MSI_NAME=Part24_MULTI.msi
SET MY_DIR=%~dp0
SET OUT_DIR=%MY_DIR%bin\Debug\
SET MSI_TRAN="C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\MsiTran.Exe"
SET RUN_SQL=cscript //nologo "C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts\WiRunSQL.vbs"
SET SUB_STG=cscript //nologo "C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts\WiSubStg.vbs"
SET LANG_ID=cscript //nologo "C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts\WiLangId.vbs"
rem 念のためバッチファイルを置いたフォルダに移動
cd "%MY_DIR%"
rem MSIファイル内のProductLanguageプロパティを修正
%RUN_SQL% "%OUT_DIR%ja-JP\%MSI_NAME%" "UPDATE `Property` SET `Property`.`Value`='1041' WHERE `Property`.`Property`='ProductLanguage'"
%RUN_SQL% "%OUT_DIR%de-DE\%MSI_NAME%" "UPDATE `Property` SET `Property`.`Value`='1031' WHERE `Property`.`Property`='ProductLanguage'"
%RUN_SQL% "%OUT_DIR%fr-FR\%MSI_NAME%" "UPDATE `Property` SET `Property`.`Value`='1036' WHERE `Property`.`Property`='ProductLanguage'"
rem カレントディレクトリに多言語化のベースになる英語版をコピー
copy /Y "%OUT_DIR%en-US\%MSI_NAME%" .
rem mstファイル作成
%MSI_TRAN% -g "%MSI_NAME%" "%OUT_DIR%ja-JP\%MSI_NAME%" 1041.mst
%MSI_TRAN% -g "%MSI_NAME%" "%OUT_DIR%de-DE\%MSI_NAME%" 1031.mst
%MSI_TRAN% -g "%MSI_NAME%" "%OUT_DIR%fr-FR\%MSI_NAME%" 1036.mst
rem 言語ごとのサブストレージに格納
%SUB_STG% "%MSI_NAME%" 1041.mst 1041
%SUB_STG% "%MSI_NAME%" 1031.mst 1031
%SUB_STG% "%MSI_NAME%" 1036.mst 1036
rem Summary InformationのLanguages設定
%LANG_ID% "%MSI_NAME%" Package 1033,1041,1031,1036
```

1. WiRunSQL.vbsスクリプトで、MSIファイル内のProductLanguageプロパティを修正します。これは実行しなくても動いてしまうようですが、ここでは正しいMSIファイルに仕立てておきます。
2. 言語ごとにMsiTranコマンドを使ってmstファイルを作成します。日英独仏の４か国語のインストーラーを作るので英語をベースに、日独仏の３つのmstファイルを作成します。
3. WiSubStg.vbsスクリプトで、言語ごとのサブストレージにmstファイルを格納します。
4. WiLangId.vbsスクリプトで、MSIファイルのSummary InformationのLanguagesに対応言語を列挙します。

MediaTemplateエレメントでEmbedCab属性にyesが設定されていれば、一つのMSIファイルで日英独仏の４か国語に対応したインストーラーが作成されます。ベースの言語を英語にしているので、対応言語以外のOSでは、英語が使われます。

[^1]: Windows SDKでは[VBScriptでtransform機能を実現するWiGenXfm.vbsが用意されています](https://docs.microsoft.com/en-us/windows/win32/msi/generate-a-transform)。
[^2]: WiX Toolsetのソースの文字コードはUTF8なので、混同しないようご注意を。
[^3]: かつてのWordのデータ形式.docやエクセル.xlsのファイル形式にも使われていました。現在では、XMLを利用したものに移行していますね。


[**前の記事へ**](https://qiita.com/tohshima/items/4e9f8133c5a14800c622)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/e31e686f52873446497b)