Prism を使用したComposite Application の多言語対応

PrismのWPFアプリを多言語対応する方法の例を書きたいと思います。多言語化ライブラリーとして WPFLocalizationExtension を使用し、.resx ファイルを使用した切り替えを行います。使用している環境は Win10, Visual Studio Community 2017, Prism.WPF V7.1.0.431, .NET4.5.2, DIコンテナとしてUnity が使われています。

WPFサンプルソフトの準備

今回使用するPrismのWPFサンプルは
github.com
の14番目である 14-UsingEventAggregator です。
14-UsingEventAggregatorをダウンロードして、VisualStudio で開き、Nuget で必要ライブラリーを復元してBuild&実行が可能な状態までもっていきます。
f:id:feynman911:20190304164538j:plain
14-UsingEventAggregator は、UsingEventAggregator というメインのプロジェクトと ModuleA、ModuleB という2つのプラグイン用のクラスライブラリーがあります。
UsingEventAggregator の MainWindow には LeftRegion と RightRegion という2つの ContentControl エリアが設定されており、そこにModuleA の MessageView と ModuleB の MessageList がプラグインされることになります。

<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <ContentControl prism:RegionManager.RegionName="LeftRegion" />
        <ContentControl Grid.Column="1" 
             prism:RegionManager.RegionName="RightRegion" />
    </Grid></span>

プラグインしているのは UsingEventAggregator の App.xaml.cs に書かれている

        protected override void ConfigureModuleCatalog
              (IModuleCatalog moduleCatalog)
        {
            moduleCatalog.AddModule<ModuleA.ModuleAModule>();
            moduleCatalog.AddModule<ModuleB.ModuleBModule>();
        }

になります。
ModuleA、ModuleBを名前指定でプラグインしていますが、特定のフォルダーを探索して組み込む場合には、サンプルソフトの 07-Modules – Directory にあるように、

        protected override IModuleCatalog CreateModuleCatalog()
        {
            return new DirectoryModuleCatalog() 
            { ModulePath = @".\Modules" };
        }

とするとModulesフォルダーにあるDLLをプラグインするようになりますから、その時にはModuleA、ModuleBをBuildしたDLLをModulesフォルダーにコピーしておく必要があります。

プラグインされる場所は、それぞれのモジュールのModuleAModule.cs 、ModuleBModule.cs で regionManager に登録されています。

ModuleAModule.cs
        public void OnInitialized(IContainerProvider containerProvider)
        {
            var regionManager = 
                         containerProvider.Resolve<IRegionManager>();
            regionManager.RegisterViewWithRegion
                        ("LeftRegion", typeof(MessageView));
        }
ModuleBModule.cs
        public void OnInitialized(IContainerProvider containerProvider)
        {
            var regionManager = 
                       containerProvider.Resolve<IRegionManager>();
            regionManager.RegisterViewWithRegion
                      ("RightRegion", typeof(MessageList));
        }

左側に ModuleA、右側にModuleBがプラグインされ、SendMessageボタンを押すたびにModuleBのListボックスに文字が追加されていくというアプリケーションになります.
f:id:feynman911:20190304170238j:plain

WPFLocalizationExtension の導入

.resxファイルを言語ごとに用意し、表示言語を切り替えるために、 WPFLocalizationExtensionを導入します。
Visual Studio 2017の NuGetの管理からWPFLocalizationExtension を UsingEventAggregator、ModuleA、ModuleB にインストールします。
github.com

resxファイルの追加

Propertiesに言語毎にResources ファイルを用意します。Porpertiesで右クリックし、追加、新しい項目からリソースファイルを追加します。その時のファイル名は言語名を含めたものします。日本語は Resources.ja-JP.resx、米語は Resources.en-US.resx となります。もともとあった Resources.resx は対応する言語のresxが無い時に使用されるので、通常は英語で書いておけばいいでしょう。
もともとのresxファイルを開いて、上部のタブにあるアクセス修飾子を Public にする必要があります。
例として、 UsingEventAggregator に resxファイルを追加し、次のような文字列を設定します。

ファイル			名前			値
Resources.resx		TITLE	Prism Unity Application(default) 	
Resources.ja-JP.resx	TITLE	Prism Unity Application(ja-JP) 
Resources.en-US.resx	TITLE	Prism Unity Application(en-US) 

それぞれのファイルのプロパティーの所はデフォルトのままで良いです。
ビルドアクション:埋め込みリソース
出力ディレクトリにコピー:コピーしない

ModuleA、ModuleB にも同様にして言語毎のresxファイルを作っておきます。ModuleA の resx にボタンの名称と、ModuleAからModuleBへ送る言葉を追加しておきます。

ファイル			名前			値
Resources.resx		MESSAGE2SEND	MESSAGE TO SEND
 			SENDMESSAGE	SEND MESSAGE
Resources.ja-JP.resx	MESSAGE2SEND	送信メッセージ
			SENDMESSAGE	メッセージ送信
Resources.en-US.resx	MESSAGE2SEND	Message to Send
			SENDMESSAGE	Send Message

ResX Manager

上記では1ファイル毎に文言を設定しましたが、ResX Manager という拡張機能を使用するとresxファイルを横断的に設定する事ができます。
Visual Studio 2017 の 拡張機能と更新プログラム から ResXManagerをダウンロードして組み込みます。インストールされると、ツールに Resx Manager が追加さされています。これを使用すると、エクセルファイルへの Export、inport 、外部サイトを使った翻訳機能もあります。
github.com

ResX Manager を起動すると、次のように複数の言語を同時に編集できます。
f:id:feynman911:20190304171556j:plain

文字列のバインド

WPFLocalizationExtension を使用するために viewのXAMLに下記を追加します。

     xmlns:lex="http://wpflocalizeextension.codeplex.com" 
     lex:LocalizeDictionary.DesignCulture="en-US"    
     lex:ResxLocalizationProvider.DefaultAssembly="ModuleA"    
     lex:ResxLocalizationProvider.DefaultDictionary="Resources"

DesignCultureの値に応じて、デザイン時の表示言語が切り替えられます。
DefaultAssemblyにはそのプロジェクトのアッセンブリを指定します。上記はModuleAのものです。 UsingEventAggregator と ModuleB にも追加しておきましょう。
UsingEventAggregator のMainWindowでは次のようになります。

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        mc:Ignorable="d" 
        x:Class="UsingEventAggregator.Views.MainWindow"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:lex="http://wpflocalizeextension.codeplex.com" 
        lex:LocalizeDictionary.DesignCulture="en-US"    
        lex:ResxLocalizationProvider.DefaultAssembly="UsingEventAggregator"    
        lex:ResxLocalizationProvider.DefaultDictionary="Resources"
        Height="350" Width="500" MinWidth="500" MinHeight="350"
        Title="{lex:Loc TITLE}" >

上記では MainWindow のタイトルのバインドもしています。バインドは {lex:Loc 名前}で行う事ができます。
MainWindow に次のようなコンボボックスを追加しておくと、それだけで言語の選択ができます。

<ComboBox ItemsSource="{Binding MergedAvailableCultures, 
        Source={x:Static lex:LocalizeDictionary.Instance}}" SelectedItem= 
       "{Binding Culture, Source={x:Static lex:LocalizeDictionary.Instance}}" 
       DisplayMemberPath="NativeName" />

ModuleA の MessageView のボタンの名前を MESSAGE2SEND にバインドします。

<Button Command="{Binding SendMessageCommand}" 
        Content="{lex:Loc MESSAGE2SEND}" Margin="5"/>

ボタンが押された時には MessageViewの SendMessageCommand が呼ばれるので、そこの ModuleBへの転送文字列をresxから取り出した文字列に変更します。
その為に、値を取り出すstatic class を定義して置いて使用します。

    //Resourcesから値を取り出すスタティッククラス
    public static class LocalizationProvider
    {
        public static T GetLocalizedValue<T>(string key)
        {
            return LocExtension.GetLocalizedValue<T>
                (Assembly.GetCallingAssembly().GetName().Name 
                             + ":Resources:" + key);
        }
    }

SendMessageに値を読み出す処理を追加します。
using WPFLocalizeExtension.Extensions; して

    private void SendMessage()
    {
        //現在のcultureのresourcesを抜き出す
        Message = LocalizationProvider.GetLocalizedValue<string>
                      ("MESSAGE2SEND");
        _ea.GetEvent<MessageSentEvent>().Publish(Message);
    }

アプリの動作

これで、言語切り替えのコンボボックスで言語を選択すると、瞬時にバインドした文字列がその言語に切り替わります。(下記はregionの位置が分かるように、他にも少し修正しています。)
f:id:feynman911:20190304172957j:plain
f:id:feynman911:20190304173021j:plain
コードでresxから対応する言語の文字列を抜き出すのは、ログファイルへの書き込み言語の切り替え等で使えるでしょう。

まとめ

  • WPFLocalizationExtensionを使う事で、モジュールを含めて簡単に言語切り替えができます。
  • 言語毎のresxファイルの作成にはResXManagerを使うと便利です。
  • コードでresxから文字列を抜き出すこともできます。

ソースコード
github.com

補足

1. View の InitializeComponent(); でエラーが出ることがあるかもしれません。その場合には、Resources.Designer.cs のコンストラクター

        internal Resources() {
        }

の internal を public にして build すると解決する事があります。

2. XAMLデザイナーを使用してコマンドのバインドを行う為には ViewModel に引数無しのコンストラクターが必要

3. コードでカルチャー切り替えを行う為に、カルチャリストを取得するのは下記。
ObservableCollection cultureInfos =LocalizeDictionary.Instance.MergedAvailableCultures;

4. バインドで key を省略すると 名前+"_Content" が選択される。
名前が MyButton で key が省略された場合({lex:Loc} は {lex:Loc MyButton_Content} と同じ。