多言語対応をしよう

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

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

真面目な話、ソフトウェアで最低限英語対応すると、ターゲットが英語圏の人にまで届く可能性が高まります。

もちろん、ドキュメントも英語化が必要ですけど、今ならDeepLとか使えば便利にそれっぽく翻訳してくれますからね。 さらにChatGPTで添削とかさせれば、ニュアンス踏まえて指摘してくれますよ!

今回は、WPFにおいてViewとViewModelだけを使った、極めてシンプルな多言語対応サンプルを示したいと思います。

プロジェクト作成

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

そして開かれた MainWindow.xaml を以下のようにしましょう。 「Language」の中に「Japanese」「English」「Russian」として、ウィンドウ内全域にボタンを貼り付けているだけです。

<Window x:Class="MultiLanguageTest.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MultiLanguageTest"
        mc:Ignorable="d"
        Title="MultiLanguageTest" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="Language">
                <MenuItem Header="Japanese" Command="{Binding ToJapanese}"/>
                <MenuItem Header="English" Command="{Binding ToEnglish}"/>
                <MenuItem Header="Russian" Command="{Binding ToRussian}"/>
            </MenuItem>
        </Menu>
        <Button Grid.Row="1" Margin="5" Content="{Binding Greetings}" Command="{Binding ExecuteGreetings}"/>
    </Grid>
</Window>

シンプルなテストなので、UIに関しては最低限の実装になっています。画面とかは本題じゃないからいいよね?

下準備に CommunityToolkit.Mvvm の導入

利便性のため、NuGetで CommunityToolkit.Mvvm を導入します。

メニューの「ツール」-「NuGetパッケージマネージャ」-「ソリューションのNuGetパッケージの管理」から、 「参照」タブをクリックしてCommunityToolkit.Mvvm を検索してプロジェクトにインストールします。

ライセンスが不安かも知れませんが CommunityToolkit.Mvvm はMITライセンスという、オープンソースライセンスの中で最も緩いライセンスです。

CommunityToolkit.Mvvm 自体を配布しないのであれば、商用とかクローズドソースもOKなので心配は要らないでしょう。 ほら、あの Xamarin で使われていた(もう過去形なんだよな…) Mono と同じライセンスです。

っていうか、これは CommunityToolkit.Mvvm 覚え書きに書くべき内容だったか…?

自分の目で確認したい方は.NET Community Toolkitをどうぞ。

また、CommunityToolkit.Mvvm に自信がない方は、拙作カップ麺シリーズ:WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(1) 簡単な依存性注入とデータバインディングっs

ViewModelの作成とバインディング

ソリューションエクスプローラーの「ソリューション MultiLanguageTest」内にある MultiLanguageTest を右クリックします。 そして「追加」-「クラス」で「名前」に MainWindowViewModel と入れて「追加」を押します。

MainWindowViewModel.cs が開かれるはずなので、ひとまず内容をこうして、ボタンに「あいさつ」と表示します。

using CommunityToolkit.Mvvm.ComponentModel;

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

そして、忘れちゃいけない VIewModel と View の結びつけです。MainWindow.xaml.cs を開き、以下のように変更します。

using System.Windows;

namespace MultiLanguageTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }
}

これで、画面中央のボタンに「あいさつ」と表示されました。

ここからが本番です!

各言語のリソースファイル作成

プロジェクト直下にリソースファイルを置くことは、基本的にプロジェクト規模からして適切ではないので、その前提で書きます。

なので MultiLanguageTest プロジェクトを右クリックして「追加」-「新しいフォルダー」として、名前を Resources にします。 これでフォルダが作られたので、この内部に3つのリソースファイルを作成します。

Resources フォルダを右クリックして「追加」-「新しい項目」-「リソース ファイル」を選び、まずは Resource.resx として追加します。 同様に Resource.ja.resxResource.ru.resx を作成します。

Resource.resx が英語(標準)、Resource.ja.resx が日本語、Resource.ru.resx がロシア語のリソースファイルですね。 Resource. の後にカルチャーコードを付けるのが命名規則となっています。カルチャーコードというのは ja とか ru などです。

早速 Resource.resx を開いてみましょう、リソースエクスプローラーが開かれます。 ここで、リソースエクスプローラーのリストビュー上部にある、右から二つ目のアイコンColumns 「Columns」を押してみましょう。

メニュー内に「ja」「ru」があるかと思います。両方にチェックを付けると、リソースエクスプローラーに内容が表示され、編集できるようになります。

では、リソース「Greetings」を作ってみましょう。AddResources 「リソースの作成」を押して、開かれたウィンドウの「名前」に Greetings と入力します。

ニュートラル値は一般的には英語 なので、ニュートラル値には英語で Greetings と入れましょう。 すると、リソースエディタの Greetingsjaru の所が赤い枠で囲まれます。 ja には あいさつru には Приветствие と入力します。

多言語対応の準備

いよいよ、お待ちかねの多言語対応の準備です。

MultiLanguageTest プロジェクトを右クリックして「追加」-「クラス」として、名前を ResourceService.cs として作成します。内容は以下の通り。

using System.Globalization;
using System.Resources;

namespace MultiLanguageTest
{
    public static class ResourceService
    {
        private static ResourceManager resourceManager = new("MultiLanguageTest.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;
        }
    }
}

コメントが日本語なのは、とりあえず理解を促すためですから、多言語対応の際には各自書き換えてくださいね…

ここで "MultiLanguageTest.Resources.Resource" という引数を使っていますが、 これは MultiLanguageTest プロジェクトにある Resources フォルダ内の Resource にアクセスする、という意味になっています。

プロジェクト名が変わる場合は MultiLanguageTest を書き換え、フォルダ名が違う場合は Resources を書き換え、 リソースファイル名が Resource.resxResource.ja.resx などではない場合 Resource を書き換えてください。

多言語対応の開始

まずは、最初の MainWindow.xaml で書いたけど、まだバインディングしていない諸々にバインディングします。

MainWindowViewModel.cs を以下のように書き換えてください。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MultiLanguageTest
{
    public class MainWindowViewModel : ObservableObject
    {
        /// <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>
        public RelayCommand ToJapanese { get; set; }

        /// <summary>
        /// 英語にする
        /// </summary>
        public RelayCommand ToEnglish { get; set; }

        /// <summary>
        /// ロシア語にする
        /// </summary>
        public RelayCommand ToRussian { get; set; }

        public MainWindowViewModel()
        {
            ToEnglish = new RelayCommand(() => ChangeGreetingsCulture("en"));
            ToJapanese = new RelayCommand(() => ChangeGreetingsCulture("ja"));
            ToRussian = new RelayCommand(() => ChangeGreetingsCulture("ru"));
            ResourceService.ChangeCulture("en");
        }

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

ここで [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] を入れないと「staticにできますよ」って言ってくるんです。 ですが、そのメッセージ通りに static にしてしまうと悲惨なことに、言語切り替えがされなくなります。なので CA1822 を抑えるために必須です。

加えて [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression")] を入れないと「不要な抑制を削除します」って言ってくるんです。 なので、不要な抑制じゃないという事をこうして指示します。

これで Language から言語選択すれば「Greetings」が各言語に書き換わります

最後にプレゼント、完成品をGitHubに上げておきます。(※記事公開当初、リポジトリがPrivateになっていました、伏してお詫び申し上げます)

あなたへの宿題

今回はあなたに宿題を出します。実際に手を動かしてみると、意外と理解が深まりますよ。私も普段から手を動かしてます。

まだ、ExecuteGreetings コマンドが実装されていません。

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

この記事や CommunityToolkit.Mvvm 覚え書きを読んでいれば、そんなに難しくないと思うけど…どうしてもお手上げって人は、Mastodonかメールで質問してください。