DIを使いながら、国際化対応(多言語対応)をしよう

今やグローバル時代!世界を相手にしなければ勝てない!

そう、東京弁だけでなく大阪弁、名古屋弁、博多弁など…ごめんなさい調子に乗りました、単純に「日本語」「英語」「ロシア語」対応について語ります。(天丼)

今回は総集編として カップ麺シリーズ:一から学べる、WPFでの多言語対応方法 を、DI込みの記事として公開します。

最初にプロジェクトを作ろう

今回は「WPFアプリケーション」で DIMultiLanguageTest というプロジェクトを作りましょう。フレームワークは「.NET 9.0」を利用します。

プロジェクトに ViewsViewModel フォルダを作ります。 そして開かれた MainWindow.xamlViews フォルダに移動して、以下のようにします。

<Window
    x:Class="DIMultiLanguageTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:DIMultiLanguageTest"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="DIMultiLanguageTest"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="Language">
                <MenuItem Command="{Binding ToJapaneseCommand}" Header="Japanese" />
                <MenuItem Command="{Binding ToEnglishCommand}" Header="English" />
                <MenuItem Command="{Binding ToRussianCommand}" Header="Russian" />
            </MenuItem>
        </Menu>
        <Button
            Grid.Row="1"
            Margin="5"
            Command="{Binding ExecuteGreetingsCommand}"
            Content="{Binding Greetings}" />
    </Grid>
</Window>

MainWindow.xaml のフォルダを移動したので、このままでは例外で落ちるため、App.xaml を以下のように書き換えます。

<Application
    x:Class="DIMultiLanguageTest.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:DIMultiLanguageTest"
    StartupUri="Views/MainWindow.xaml">
    <Application.Resources />
</Application>

わかると思いますが、MainWindow.xaml を移動した Views\StartupUri に書き加えたわけですね。

そして、CommunityToolkit.Mvvm を NuGet で導入しましょう。 分かる人は当然 dotnet コマンドからでいいですよ?

次に、Microsoft.Extensions.DependencyInjection も NuGetで導入しましょう。分かる人は(略)

CommunityToolkit.Mvvm でDIの設定をしよう

App.xaml.cs に以下のコードを適切にコピペしましょう、これでDIの最低限の準備ができます。

ちなみに DI のテンプレートとしてどこかに保存しておくのがいいでしょう。定型文くらいの立ち位置ですから。

public partial class App : Application
{
    /// <summary>
    /// サービスの登録をします
    /// </summary>
    public App()
    {
        Services = ConfigureServices();
        Ioc.Default.ConfigureServices(Services);
    }

    /// <summary>
    /// 現在の App インスタンスを取得します
    /// </summary>
    public new static App Current => (App)Application.Current;

    /// <summary>
    /// サービスプロバイダです
    /// </summary>
    public IServiceProvider Services { get; }

    /// <summary>
    /// サービスを登録します
    /// </summary>
    /// <returns></returns>
    private static ServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<IMessenger, WeakReferenceMessenger>();
        services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();

        return services.BuildServiceProvider();
    }
}

ここで必要なディレクティブ

using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

が追加されていると思いますが、無いのであれば自分で書き加えましょう。

この時点では IMainWindowViewModelMainWindowViewModel にエラーが出ます。

MainWindow に依存性注入

Views\MainWindow.xaml.cs のコンストラクタを以下のように書き換えます。

public MainWindow()
{
    InitializeComponent();
    DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
}

次に IMainWindowViewModelMainWindowViewModel を作ります。 ViewModelフォルダに MainWindowViewModel.cs を作成して以下のようにします。

using CommunityToolkit.Mvvm.ComponentModel;

namespace DIMultiLanguageTest.ViewModels
{
    public interface IMainWindowViewModel { }

    public class MainWindowViewModel : ObservableObject, IMainWindowViewModel
    {
        /// <summary>
        /// 挨拶をするボタンのテキスト
        /// </summary>
        public string Greetings
        { get => "あいさつ"; }
    }
}

※ここで static にマークできるというメッセージは無視してください。

そして、App.xaml.cs と MainWindow.xaml.cs に以下のディレクティブを追記します。

using DIMultiLanguageTest.ViewModels;

これで、DI を使うための最低限の準備が整いました。

リソースファイルの作成

ここでは、 英語リソースファイルを Resource.resx、 日本語リソースファイルを Resource.ja.resx、 ロシア語リソースファイルを Resource.ru.resx とします。

この jaru がカルチャーコードで、リソースファイルの命名規則になっています。

プロジェクト直下に Resources フォルダを作り、 「追加」-「新しい項目」-「リソースファイル」を選び、この三つのファイルを作成します。

リソースエディタの細かい使い方は カップ麺シリーズ:一から学べる、WPFでの多言語対応方法 を参考にしてください。

名前には「Greetings」、ja には「あいさつ」、ru には「Приветствие」とすると、リソースエディタは以下のようになるかと思います。

resource_editor.jpg

国際化対応(多言語対応)の準備

プロジェクト直下に Services フォルダを作り、その中に ResourceService.cs を作成します。

内容は以下の通りとなります。

using System.Globalization;
using System.Resources;

namespace DIMultiLanguageTest.Services
{
    public static class ResourceService
    {
        private static readonly ResourceManager resourceManager = new("DIMultiLanguageTest.Resources.Resource", typeof(ResourceService).Assembly);

        /// <summary>
        /// 言語を変更します。
        /// </summary>
        /// <param name="cultureCode">変更するカルチャーコード</param>
        public static void ChangeCulture(string cultureCode)
        {
            var culture = new CultureInfo(cultureCode);
            CultureInfo.CurrentUICulture = culture;
        }

        /// <summary>
        /// リソースファイルから名前をキーに、カルチャーコードに基づいた文字列を取得します。
        /// </summary>
        /// <param name="key">文字列を取得するリソースファイルキー</param>
        /// <returns>取得したい文字列</returns>
        public static string GetString(string key)
        {
            return resourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? string.Empty;
        }
    }
}

DIMultiLanguageTest.Resources.Resource は「プロジェクト名 - フォルダ名 - リソースファイル名」になっています。

国際化対応(多言語対応)の開始

ここは一気に行きます。MainWindowViewModel.cs を以下のようにしてください。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DIMultiLanguageTest.Services;

namespace DIMultiLanguageTest.ViewModels
{
    public interface IMainWindowViewModel { }

    public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
    {
        /// <summary>
        /// 挨拶をするボタンのテキスト
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression")]
        public string Greetings
        { get => ResourceService.GetString("Greetings"); }

        /// <summary>
        /// 挨拶のためのカルチャーコードを変更します。
        /// </summary>
        /// <param name="cultureCode">カルチャーコード</param>
        private void ChangeGreetingsCulture(string cultureCode)
        {
            ResourceService.ChangeCulture(cultureCode);
            OnPropertyChanged(nameof(Greetings));
        }

        [RelayCommand]
        private void ToEnglish()
        {
            ChangeGreetingsCulture("en");
        }

        [RelayCommand]
        private void ToJapanese()
        {
            ChangeGreetingsCulture("ja");
        }

        [RelayCommand]
        private void ToRussian()
        {
            ChangeGreetingsCulture("ru");
        }
    }
}

この部分はメッセージ抑制に過ぎないですが、エラー一覧でうるさくなると思うので追記しています。

[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression")]

補足しますと、XAMLで設定した ToEnglishCommand に対応するのが ToEnglish() となっています、他も同様です。

[RelaCommand] を使うと、末尾に Command を加えて ToEnglish()ToEnglishCommand() としてバインディングされます。

おわりに

この通りに作成すれば、メニュー Language から表示画面が切り替わります。

元記事と同様に…今回は名前が変更されていますが ExecuteGreetingsCommand が実装されていません。

宿題として、英語では「Hello」、日本語では「こんにちは」、ロシア語では「Здравствуйте」と、メッセージボックスを表示するコマンドを実装してくださいね。

CommunityToolkit.MvvmのDIと組み合わせた国際化対応(多言語対応)は、こんな感じになりますよという話でした。

「Services」フォルダはあまり聞き慣れないかもしれませんが、 例えば、ViewとViewModel、あるいはModelのどこに置けば適切か迷うような、層を横断するようなクラスの置き場とでも考えてください。

宿題に手をつけられる、完成品はGitHub に載せています。