Karakuri.com

Fintechではたらくアプリケーションエンジニアの技術録

WPFアプリケーションを多言語対応する方法のまとめ

WPFアプリケーション開発において多言語対応というのは悩みの種です。Androidはstrings.xmlでテキストは管理され、各国語のstrings.xmlを作成すればOSが使い分けてくれるのですが、WPFにもそんな便利機能はあるのでしょうか。

1. LocBamlツールを使った方法

どうやらLocBamlツールというものを使う方法がMicrosoftの標準推奨だそうです。が、ググってみれば分かりますが誰も使っていません。MSDNを読んでみても数秒でコレジャナイ感でいっぱいになります。
方法 : アプリケーションをローカライズする

2. Resources.resxを各国語用意する方法

これが冒頭で述べたAndroidに似た方法のようです。Resource.resxファイルでアプリ内で使用するテキストを管理できるのですが、Resources.<カルチャ>.resxのようにカルチャをファイル名に付加するとOSのカルチャを見て読み出すresxファイルを切り替えてくれるようです。
WPF アプリの国際化 (多言語対応) と、実行中の動的な言語切り替え | grabacr.nét
しかしこの方法だとアプリケーション実行中に言語を切り替えるのがやや面倒です。例えば店舗据え置き型のアプリケーションを開発している場合、来店のお客さまが言語を切り替えるような仕様などの対応が面倒です。また、resxファイルが独自形式なので翻訳を外注するのも面倒になります。resxってVisual Studioでも扱いづらいんですよね…。
f:id:hazakurakeita:20170918233508p:plain
とはいえ、この方法が最も人気のある方法のようです。

3. 独自の言語管理機能をつくる

前いた会社では独自の言語管理機能を作っていました。xmlでテキストを管理し、それを設定に応じて読み出しコードで取り出せるようになっていました。そのときはWindows.Formsアプリケーションだったので、UIもコードで記述しているケースも多く、なんとかそれでなっていました。また、xmlでテキストを管理しているため、翻訳会社に外注しやすいというメリットがありました。ただし、バグを作りこんでしまうリスクと開発保守のコストがかかるのは当然ながらデメリットですね。

4. ResourceDictionaryでリソース用のxamlを読み込む方法

様々な方法を調べて検討してみた結果、最終的にはこの方法が最も優れているという結論になりました。この方法ではxamlでテキストを管理し、xamlではStaticResourceまたがDynamicResourceを使って呼び出すというものです。

Japanese.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfLocalizationSample.Resources"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="National">日本</system:String>
    <system:String x:Key="Hello">こんにちは世界!</system:String>
</ResourceDictionary>

English.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfLocalizationSample.Resources"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="National">English</system:String>
    <system:String x:Key="Hello">Hello World!</system:String>
</ResourceDictionary>

このように言語ごとにxamlでテキストを管理します。そして利用するアプリケーションのApp.xamlでこのxamlを追加します。追加するのは日本語のxamlだけでいいです。

<Application x:Class="WpfLocalizationSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfLocalizationSample"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Resources;component/Resources/Japanese.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

これでアプリケーションのプロジェクトのデザイナーがStaticResourceやDynamicResourceを使って見えるようになります。言語の切り替えはApp.xaml.csで行います。

public partial class App : Application
{
   public App()
   {
      InitializeComponent();

      var language = ConfigurationManager.AppSettings["Language"];
      var dictionary = new ResourceDictionary();

      language = string.IsNullOrEmpty(language) ? "Japanese" : language;
      dictionary.Source = new Uri("/Resources;component/Resources/" + language + ".xaml", UriKind.Relative);
      Resources.MergedDictionaries[0] = dictionary;
   }
}

これはアプリケーションのコンフィグレーションファイルに言語設定を保存した場合の方法です。App.exe.configファイルのAppSettingsから日本語か英語かを取得し、そのResourceDictionaryを作成、アプリケーションのMergedDictionariesに設定したテキストのxamlを入れます。これで起動したアプリケーションは設定した言語とリンクされることになります。

サンプルコード

上記のコードを含むサンプルコードをGitHubにアップしています。
GitHub - HazakuraKeita/WpfLocalizationSample: Language localization infrastructure for Windows Presentation Foundation (WPF).
サンプルコードを実際に動かしてみた動画はこちらです。
youtu.be
ご覧いただけると分かるのですが、上部のテキストがStaticResource、下部がDynamicResourceです。言語の切り替えは下記のようにMergedDictionariesを上書きするだけでDynamicResourceは切り替わります。つまりはBindingされているということなので、PCのリソースはStaticResourceよりは使ってしまいます。

LanguageChangeCommand = new DelegateCommand((parameter) =>
{
    var language = parameter as string;
    var dictionary = new ResourceDictionary();
    var configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

    // Language config setting change
    configuration.AppSettings.Settings["Language"].Value = language;
    configuration.Save();
                
   // Current resource change
    language = string.IsNullOrEmpty(language) ? "Japanese" : language;
    dictionary.Source = new Uri("/Resources;component/Resources/" + language + ".xaml", UriKind.Relative);
    Application.Current.Resources.MergedDictionaries[0] = dictionary;
});

Application.Current.Resourcesを使うのがポイントですね。これを使うという情報はググってもあまり見つかりませんでした。
また、動画ではダイアログのテキストの言語が切り替わる様子も映っています。しかしこちらはStaticResourceも変わっています。これはMergedDictionariesを上書きしたあとに新規でWindowが作成されたからです。WindowやPageの作成の際にStaticResourceは読み込み処理が走るので、ダイアログの言語は両方変わるわけですね。Pageも再作成すれば切り替わります。