WPF でサクッと多言語化したい、しかも動的に切り替えたい

## はじめに

WPF アプリケーションにおける動的な多言語対応について、サードパーティーの [WPFLocalizeExtension](https://www.nuget.org/packages/WPFLocalizeExtension/) を用いた実装をまとめます。

結論だけご覧になりたい方は[こちら](#冗長な記述を取り除きたい今回書きたかったこと)

## 本文

### シングルトンと変更通知を利用した方法

多言語化はすでに多くのブログ等で紹介されており、アプリの実行中に動的に切り替える方法も[ぐらばく](https://twitter.com/Grabacr07)様がまとめておられます。
ざっくりとですが、Properties.Resources をラップする変更通知を有したシングルトンを仲介させることで実現されています。

[WPF アプリの国際化 (多言語対応) と、実行中の動的な言語切り替え](http://grabacr.net/archives/1647)

###### Resources.resx
| 名前 |  値  | コメント |
| ---- | ---- | ---- |
| HelloWorld | Hello, world! | |

###### Resources.ja-JP.resx
| 名前 |  値  | コメント |
| ---- | ---- | ---- |
| HelloWorld | こんにちは、世界！ | |

　

```cs:ResourceService
namespace Sample
{
    public class ResourceService : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private static readonly ResourceService _current = new ResourceService();
        public static ResourceService Current => this._current;

        private readonly MultilingualApp.Properties.Resources _resources = new Resources();
        public MultilingualApp.Properties.Resources Resources => this._resources;

        protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
            => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        public void ChangeCulture(string name)
        {
            Resources.Culture = CultureInfo.GetCultureInfo(name);
            this.RaisePropertyChanged(nameof(Resources));
        }
    }
}
```

```xml:View
<Window x:Class="Sample.MainWindow"
        (中略)
        xmlns:app="clr-namespace:Sample">

   <TextBlock Text="{Binding Resources.HelloWorld, Source={x:Static app:ResourceService.Current}, Mode=OneWay}"/>

</Window>
```


### WPFLocalizeExtension を用いた方法

私も以前は上記のシングルトンを挟んだ方法で多言語化を行っていましたが、新たに WPFLocalizeExtension を導入することで、実装の自由度が落ちるものの、仲介クラスが不要になり、Xaml をより短く記述できるようになりました。

[NuGet: WPFLocalizeExtension](https://www.nuget.org/packages/WPFLocalizeExtension/)
[Prism を使用したComposite Application の多言語対応](https://feynman.hatenablog.com/entry/2019/03/04/191827)

```xml:View
<Window x:Class="Sample.MainWindow"
        (中略)
        xmlns:lex="http://wpflocalizeextension.codeplex.com" 
        lex:ResxLocalizationProvider.DefaultAssembly="Sample"
        lex:ResxLocalizationProvider.DefaultDictionary="Resources">

   <TextBlock Text="{lex:Loc HelloWorld}"/>

</Window>
```

このままでも良いのですが、添付プロパティの DefaultAssembly と DefaultDictionary は、言語リソースを使用するすべての View で指定が必要になり、やや冗長な印象です。

---


### 冗長な記述を取り除きたい（今回書きたかったこと！）

実は、上記の Default 設定を C# 側から指定するためのメソッドが用意されています。
これを応用して、Prism を導入しかつ AutoWireViewModel の有効化が前提になりますが、一か所の記述ですべての View に反映させることができます。
[NuGet: Prism.Unity](https://www.nuget.org/packages/Prism.Unity/)

View のインスタンスを受け取り ViewModel のインスタンスを生成する処理をフックできる SetDefaultViewModelFactory() を利用すれば、View の表示前に横断的に割り込みができるため、このタイミングで設定を済ませてしまいましょう。

```cs:App
namespace Sample
{
    public partial class App : PrismApplication
    {
        (中略)

        protected override void ConfigureViewModelLocator()
        {
            // base.ConfigureViewModelLocator() で既定のファクトリが設定されるので、これを上書きします。
            // 仮引数で View のインスタンスを取得できるため、ここに対応を一元化させます。
            ViewModelLocationProvider.SetDefaultViewModelFactory(
                (view, viewModelType) =>
                {
                    if (view is DependencyObject d)
                    {
                        ResxLocalizationProvider.SetDefaultAssembly(d, d.GetType().Assembly.GetName().Name);
                        ResxLocalizationProvider.SetDefaultDictionary(d, "Resources");
                    }
                    return this.Container.Resolve(viewModelType);
                }
            );
        }
    }
}
```

これによって、View の DefaultAssembly と DefaultDictionary の記述は不要になります。
（Prism はみなさん使ってるのであってもいいですよね、、）

```xml:View
<Window x:Class="Sample.MainWindow"
        (中略)
        xmlns:lex="http://wpflocalizeextension.codeplex.com"
        xmlns:p="http://prismlibrary.com/"
        p:ViewModelLocator.AutoWireViewModel="True">

   <TextBlock Text="{lex:Loc HelloWorld}"/>

</Window>
```

### 備忘

言語を切り替える際は以下のように指定します。

```cs
    LocalizeDictionary.Instance.Culture = CultureInfo.GetCultureInfo("ja-JP");
```

　
以上