前回の記事では、メインプロジェクトの国際化について簡単に紹介しました。これはリソース辞書(XAML)を使用して実現されていました。最近、Prismモジュール(Module)をいくつか追加したところ、サブモジュールでリソース辞書を使って国際化とローカライズを実装するのは難しく、良い参考記事も見つからなかったため、別の方法としてリソースファイルを使用することにしました。

一. 本記事の概要
サブモジュールの国際化とローカライズの要件:
- 各モジュールは独自の言語ファイルを持つ必要がある。
- メインプロジェクトで言語を動的に切り替えるとき、サブモジュールも追随して切り替わる必要がある。
- Prismを使用してモジュール化フレームワークを実装しており、メインプロジェクトと各サブモジュール間に参照関係があってはならない(疎結合)。メインプロジェクトから直接サブモジュールの言語ファイルを切り替えることはできない。
上記の要件に基づき、各モジュールにも言語ファイル(XAML)を定義してみましたが、メインウィンドウで言語を切り替えるときに「対応するリソース辞書ファイルが存在しない」というエラーが頻発し、非常に悩みました。最終的に「Accelerider.Windows」の国際化方式を参考にして、リソースファイルを使ってローカライズと国際化を実現しました。XAML方式にはこだわらないことにしました。

以下が修正後の効果です:

前回のバージョンとの違い:
タイトルバーの国際化に変更はありません。テキストバインディングの方法を変えただけで、実現効果は同じです。
左側に3つのサブモジュール(Home/Client/Server)を追加しました。Prismを使用して動的に読み込んでおり、メインプロジェクトのメインウィンドウの言語切り替えに追従して言語が切り替わります。
以下では、モジュールの作成方法と、メインウィンドウおよびモジュールの国際化の実装方法を簡単に紹介します。本当に簡単な紹介ですので、具体的な実装はコードをご確認ください。

二. 3つのPrismモジュール(Module)の追加
Prismテンプレートをインストールし、モジュールプロジェクトを迅速に作成できます。もちろん、手動で.NET Coreプロジェクトを作成することも可能ですが、いくつかの手順が増えるだけです(NuGetでPrism.Wpfパッケージ(7.2.0.1422)をインストールする必要があります)。私はPrismテンプレートを使って迅速に作成しました。
2.1 モジュール作成前の準備

上図のPrismテンプレートをダウンロードし、Visual Studioを再起動すると自動的にインストールされます。新しいプロジェクトを作成すると、Prismモジュールテンプレートが選択できるようになります:

.NET Core 3のバージョンを選択する必要があります。なぜなら、.NET CoreでWPFプロジェクトを作成しているからです。
2.2 モジュールの作成
以下は、作成済みの3つのモジュールプロジェクトのスクリーンショットです:

現在、3つのモジュールのファイル構成は類似しています:
- I18nResources:リソースフォルダ。3つの言語リソースファイルと1つのT4テンプレートファイル(言語キーを参照するため)を配置。T4テンプレートファイルは3つのモジュールとメインプロジェクトで同じ内容です(詳細はGitHubからソースコードをダウンロードして確認してください)。
- Views:ビューファイルを配置。現在はメインプロジェクトのメインウィンドウに表示するTabItemビューのみを使用。すなわち、MainTabItem.xamlで、TabItemを継承しています。
- xxxxModule.cs:Prismテンプレート定義ファイル。Prismがモジュールを検出するために使用。
3つのモジュールの重要な注意点:
- モジュールのプロジェクトファイルを編集し、モジュールファイルの出力先を変更:
// 一部のコードを省略。次の行をFalseに設定すると、出力先に.NET Coreバージョン情報が含まれなくなります。
<AppendTargetFrameworkToOutputPath>Flase</AppendTargetFrameworkToOutputPath>
// 一部のコードを省略。DebugとReleaseのビルド出力先を変更し、メインプロジェクトでモジュールを統一して読み込めるようにします。
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\Build\Debug\Modules</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\Build\Release\Modules</OutputPath>
</PropertyGroup>
// 一部のコードを省略
- XXXModuleのコンストラクタで、リソースファイルのResourceManager参照を別のライブラリに追加し、言語切り替え時に使用できるようにします。たとえば、HomeModuleのコンストラクタに次のコードを追加するだけで、モジュールの国際化とローカライズは完了です:
I18nManager.Instance.Add(TerminalMACS.Home.I18nResources.UiResource.ResourceManager);
- XXXModuleのRegisterTypesメソッドで、ビュー"MainTabItem"を"RegionNames.MainTabRegion"に登録します。メインウィンドウは"RegionNames.MainTabRegion"を使用してモジュールのビューを表示・読み込みます。
_regionManager.RegisterViewWithRegion(RegionNames.MainTabRegion, typeof(MainTabItem));
- UIコントロールの国際化テキストバインディングに使用するmarkupは、オープンソースライブラリの名前空間です(後述のリンクを参照)。このプロジェクトでは、このライブラリをソリューションに直接組み込んでいます。i18NResources:LanguageはT4テンプレートファイルから生成されたクラスで、テキスト翻訳のキーに関連付けられます。テキストバインディングのコード例:
<TextBlock Grid.Row="2" Text="{markup:I18n {x:Static i18NResources:Language.MainTabItm_Header}}" />
三. メインプロジェクト
メインプロジェクトのディレクトリ構成は以下の通りです:

3.1 Prismモジュールの動的読み込み
3つのモジュールを読み込むための主要なコードはApp.xaml.csファイルにあります。上記のコードでは、3つのモジュールをModulesディレクトリに出力し、メインプロジェクトはこのディレクトリを直接読み込んでいます。その他の読み込み方法(構成ファイルを使用するなど)については、Prism公式のサンプルを参照してください(文末にリンクあり):
protected override IModuleCatalog CreateModuleCatalog()
{
string modulePath = @".\Modules";
if (!Directory.Exists(modulePath))
{
Directory.CreateDirectory(modulePath);
}
return new DirectoryModuleCatalog() { ModulePath = modulePath };
}
メインウィンドウはサブモジュールで登録されたTabItemビューを表示します。prism:RegionManager.RegionNameは各サブモジュールで登録されたリージョン文字列で、モジュールのTabItemビューに関連付けられます。コードは以下の通りです:
<TabControl Grid.ColumnSpan="2" SelectedIndex="0"
Style="{StaticResource MainTabControlStyle}"
ItemContainerStyle="{StaticResource MainTabItemStyle}"
prism:RegionManager.RegionName="{x:Static ui:RegionNames.MainTabRegion}"/>
メインウィンドウはTabControlコントロールの形式でサブモジュールのビューを表示します:

メインプロジェクトがサブモジュールを正常に読み込むためには、メインプロジェクトのプロジェクトファイルも出力先を変更する必要があります:
// 一部のコードを省略。次の行をFalseに設定すると、出力先に.NET Coreバージョン情報が含まれなくなります。
<AppendTargetFrameworkToOutputPath>Flase</AppendTargetFrameworkToOutputPath>
// 一部のコードを省略。DebugとReleaseのビルド出力先を変更し、メインプロジェクトでモジュールを統一して読み込めるようにします。
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\Build\Debug</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\Build\Release</OutputPath>
</PropertyGroup>
// 一部のコードを省略
3.2 言語ファイル形式の変更
既存のXAML言語ファイルを削除し、resxリソースファイルに置き換えました。3つのモジュールのリソースファイルタイプと同様です。以下はメインプロジェクトのリソースファイルです:

リソースファイルに置き換えたことで、XAMLファイルよりも編集が便利になりました。当初はリソースファイルで国際化を実装することを検討していましたが、あえてXAMLファイルを試してみたかったのです。

3.3 言語切り替えのコアコード
動的な言語切り替えの主要コードは以下のように変更しました:
public static void SetLanguage(string language = "")
{
if (string.IsNullOrWhiteSpace(language))
{
language = ConfigHelper.ReadKey(KEY_OF_LANGUAGE);
if (string.IsNullOrWhiteSpace(language))
{
language = System.Globalization.CultureInfo.CurrentCulture.ToString();
}
}
ConfigHelper.SetKey(KEY_OF_LANGUAGE, language);
_lastLanguage = language;
var culture = new System.Globalization.CultureInfo(language);
I18nManager.Instance.CurrentUICulture = culture;
}
言語切り替えのコアは最後の一文です。詳細は省略しますが、ソリューションにライブラリとソースコードが含まれています:
I18nManager.Instance.CurrentUICulture = culture;
四. ソースコード
- ソースコードのアドレス、スターを歓迎します:https://github.com/dotnet9/TerminalMACS/tree/master/src/TerminalMACS.Manager/TerminalMACS.ManagerForWPF
五. 参考資料
- Prism Template Pack(Prismテンプレート):https://marketplace.visualstudio.com/items?itemName=BrianLagunas.PrismTemplatePack
- WPF国際化オープンソース補助ライブラリ:https://github.com/DingpingZhang/WpfExtensions
- Accelerider.Windows(サブモジュール読み込みの参考オープンソースプロジェクト):https://github.com/Accelerider/Accelerider.Windows
- Prism-Samples-Wpf(公式デモ):https://github.com/PrismLibrary/Prism-Samples-Wpf