.Netでの「Globalization」と「Localization」を試す

C#

.Netで複数言語対応を検討する機会がありましたので、調べた内容をまとめます。

イメージ

まずは、どのようなことを実現したいのかのイメージを付けるために以下をご覧ください。ユーザーがロケール(言語)を選択することで、そのロケールに合わせた表示に変更することをゴールとします。

前提

以下の環境で実行しています。

oswindows11
dotnet8.0.304
UIBlazor(interactive SSR)
IDEVisual Studio 2022 Community

あらかじめ、Interactive SSRでBlazorのプロジェクトを作成してください。バージョンによって多少の差異がある可能性がありますが、基本的にInteractive SSRのプロジェクトの構成を前提としています。

本記事に記載しているソースコードは説明に必要な個所のみ抜粋していること、ご了承ください。

事前準備(言語切り替えドロップダウン実装)

初めにご覧いただいた通り、ユーザーは、「アメリア」、「日本」、「フランス」のいずれかをヘッダーのドロップダウンから選択します。そして選択した国に応じてUIの表示内容を変化させます。

ドロップダウンによる国の切り替え機能は、一部カスタマイズしていますが、基本的に以下のマイクロソフトのドキュメント通りに作成しています。詳細についてはそちらをご確認いただければと思いますが、自分の理解を深めるためにも1つづ実装内容を見ていきたいと思います。

「国切り替え機能」を実装するために、以下のファイルを作成・編集します。

  • Program.cs
  • Components/App.razor
  • Components/Controllers/CultureController.cs
  • Components/Pages/CultureSelector.razor
  • Components/Layout/MainLayout.razor

Program.csファイルの修正

多言語化対応で使用するLocalizationサービスを追加します。必要に応じで「Microsoft.Extensions.Localization」パッケージをNugetで追加しておいてください。

builder.Services.AddLocalization();

次に、今回対応させるロケール情報を設定します。ロケール情報は、標準のロケールコードに従って指定します。

app.UseRequestLocalization(new RequestLocalizationOptions()
	.SetDefaultCulture("en-US")
		.AddSupportedCultures(new[] { "en-US", "ja-JP", "fr-FR" })
		.AddSupportedUICultures(new[] { "en-US", "ja-JP", "fr-FR" })
	);

最後に、この後追加するControllerを利用するために、以下の設定を追加します。

builder.Services.AddControllers();

app.MapControllers();

Components/App.razor

App.razorファイルでは、アプリ全体でのレンダリングモードの指定と、現在のロケール情報をクッキーに設定するコードを追加します。

// _imports.razorでもOK
@using System.Globalization
@using Microsoft.AspNetCore.Localization

<body>
    @* レンダリングモード *@
    <Routes @rendermode="InteractiveServer"/>
</body>

@code{
    [CascadingParameter]
    public HttpContext? HttpContext { get; set; }

    protected override void OnInitialized()
    {
        HttpContext?.Response.Cookies.Append(
            CookieRequestCultureProvider.DefaultCookieName,
            CookieRequestCultureProvider.MakeCookieValue(
                new RequestCulture(
                    CultureInfo.CurrentCulture,
                    CultureInfo.CurrentUICulture
                )
            )
        );
    }
}

Components/Controllers/CultureController.cs

ドロップダウンを選択したときに、選択されたロケール情報をクッキーに保存して、元の画面にリダイレクトする処理を記載するコントローラーを作成します。

using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;

[Route("[controller]/[action]")]
public class CultureController : Controller
{
    public IActionResult Set(string culture, string redirectUri)
    {
        if (culture != null)
        {
            HttpContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(
                    new RequestCulture(culture, culture)));
        }

        return LocalRedirect(redirectUri);
    }
}

リダイレクトは、URLにクエリストリングを追加することで実装されることが良くあります。

https://test.com?redirect=/xxxxxx

攻撃者がリダイレクトURLを不正な値に置き換えることで、ユーザーは気づかずに悪意のあるサイトにリダイレクトしてしまい、秘密情報などが盗まれてしまいます。これはオープンリダイレクトという攻撃手法です。

https;//test.com?redirect=/bad.com/xxxxxxx

LocalRedirectメソッド」では、リダイレクト先がローカルアドレス出なかった場合に例外を発生させる仕組みになっており、オープンリダイレクトを防ぐことができます。

Components/Pages/CultureSelector.razor

ロケールを選択するドロップダウンコンポーネントを作成します。21行目付近のロケール情報は各自の要件に合わせて置き換えてください。また、43行目では、先ほど追加したコントローラーに遷移させつつ、リダイレクト先を指定しています。

@using System.Globalization
@inject IJSRuntime JS
@inject NavigationManager Navigation

<div>
    <label>
        Select your locale:
        <select @bind="selectedCulture" @bind:after="ApplySelectedCultureAsync">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@culture.DisplayName</option>
            }
        </select>
    </label>
</div>

@code
{
    private CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("ja-JP"),
        new CultureInfo("fr-FR")
    };

    private CultureInfo? selectedCulture;

    protected override void OnInitialized()
    {
        selectedCulture = CultureInfo.CurrentCulture;
    }

    private async Task ApplySelectedCultureAsync()
    {
        if (CultureInfo.CurrentCulture != selectedCulture)
        {
            var uri = new Uri(Navigation.Uri)
                .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
            var cultureEscaped = Uri.EscapeDataString(selectedCulture.Name);
            var uriEscaped = Uri.EscapeDataString(uri);

            Navigation.NavigateTo(
                $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
                forceLoad: true);
        }
    }
}

37行目では、Navigation.urlから現在のページのURLを取得できます。Navigationはデフォルトで組み込まれています。そのため、Program.csでサービスを登録しなくても、3行目のようにDIして利用できます。

マイクロソフトのドキュメントでは、ドロップダウン(select)を<p>で囲んでいます。ヘッダーに中央ぞろえで配置しようとすると、<p>だとうまくできなかったので、<div>に変更しています。

Components/Layout/MainLayout.razor

最後にロケール選択ドロップダウンをヘッダーに追加します。追加する場所はどこでも大丈夫です。

<div class="top-row px-4 d-flex align-items-center">
    @* ここを追加 *@
	<CultureSelector />
	<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>

以上で、ロケール選択ドロップダウンの作成は完了です。

UI表示設定

ここからは、ロケールを切り替えたときに画面に表示するメッセージなどの設定を行います。

リソースファイルの作成

リソースファイルとは拡張子が「resx」のファイルです。このファイルには、画面に表示する各言語に対応したメッセージなどを記載します。XMLファイルですが、Visual Studioでは専用のGUIで記載することができます。

リソースファイル用のフォルダ(Localization)を作成し、その中にリソースファイルを作成します。今回は、「ルートディレクトリ > Components > Localization」に作成しました。

フォルダを作成したら、最初に空のrazorファイルを作成します。ファイル名は任意ですが、ここで指定した名前をリソースファイルでも使用します。今回は、「CustomLocalization.razor」としています。

続いてリソースファイルを作成します。Localizationフォルダを右クリックして、新規アイテムの追加を選択後、「Resource File」を選択します。ファイル名は「CustomLocalization.resx」となります。

リソースファイルは言語ごとに作成します。同じ手順で日本語とフランス語用のリソースファイルも作成します。この時ファイル名はそれぞれ、「CustomLocalization.ja.resx」、「CustomLocalization.fr.resx」とします。命名規約の詳細は以下に記載されていますので、確認してみてください。

今回は英語をデフォルトとしますので、「CustomLocalization.resx」に英語のメッセージを記載します。

実際に日本語用のファイルは以下のようになります。英語とフランス語も同様に作成します。Nameはメッセージ取得のキーとなるので、どの言語でも同じ名前になります。Valueを国ごとに変更します。

リソースファイルの読み込み

razorページでリソースファイルに記載した内容を利用するための手順です。razorページではDIで受け取ったインスタンスをもとにリソースファイルの情報にアクセスしますので、「Program.cs」でLocalizationサービスを追加します。

builder.Services.AddLocalization();

続いて、razorページの上部に以下を追加します。今回はデフォルトで作成される「Components/Pages/Home.razor」を編集します。

@inject IStringLocalizer<CustomLocalization> Loc

CustomLocalization型」は、先ほど作成した空のrazorファイルの名前です。必要に応じて「_Imports.razor」ファイルでusingを追加しておいてください。

@using プロジェクト名.Components.Localization

リソースファイルに指定したメッセージは以下のどちらかの方法で取得します。ドロップダウンで言語を切り替えるたびに、その言語に対応したメッセージが取得されます。

// 日時(日本語の場合)
@Loc.GetString("Date")
@Loc["Date"]

日時の表示

日時(DateTime)は、選択されたロケールに合わせて自動的に表示が切り替わります。

<div>
    @now
    @* @now.ToString()でも同じ *@
</div>
@code{
    private DateTime now = DateTime.Now;
}

すべての日付や時刻表示では、明示的・暗黙的に「DateTimeFormatInfoオブジェクト」が使用されます。このオブジェクトは現在のロケールに合わせた日付や時刻のフォーマット情報が含まれています。razorファイル上でも、「@DateTimeFormatInfo.CurrentInfo」もしくは、「@CultureInfo.CurrentCulture.DateTimeFormat」から確認できます。

ロケールを「en-US」に指定したときの情報をウォッチで表示したものが以下になります。

値段

値段に関しては、言語によって通貨の単位、小数点の有無、「,(カンマ)」や「.(ピリオド)」の使い方が異なります。日本では金額に小数点は用いません。また3桁ごとにカンマを入れるのが一般的です。一方アメリカやフランスでは小数点表示があります。また、フランスでは3桁ごとにカンマではなくスペースで区切るようです。さらに小数点はピリオドではなくカンマを使うようです。「Tostring」メソッドに通貨を表す「C」を指定することで、通貨の単位などを表示できます。

<div>
    @price.ToString("C")
<div>

@code{
    private decimal price = 1122.50m;
}

数値に関しても日付と同様に、「NumberFormatInfoオブジェクト」にロケールに合わせったフォーマット設定が格納されています。以下はフランスのものです。

月や曜日

月や曜日も言語によって異なります。多言語対応するときに、これらもすべてリソースファイルに設定しないといけないかというと、そうではありません。これらの情報は先ほど挙げた「DateTimeFormatInfoオブジェクト」に格納されています。このオブジェクトから名前を取得するだけで、ロケールに合わせた名前を表示できます。

	<select>
		@foreach (var month in DateTimeFormatInfo.CurrentInfo.MonthNames)
		{
			<option value="@month">@month</option>
		}
	</select>
	<select>
		@foreach (var day in DateTimeFormatInfo.CurrentInfo.DayNames)
		{
			<option value="@day">@day</option>
		}
	</select>

最後に

今回のプロジェクト構造です。作成・編集したファイルだけを赤枠で囲んでいます。

コメント