概要
ASP.NET MVC で多言語対応をこころみてみます。多言語対応だけシンプルにこころみます。デモはこちら。
オライリー本を読んだので、前回より若干洗練されています。(だといいな)
ベリーシンプル多言語対応
前回は Accept-Language
に応じた言語切り替えを扱いましたが、今回は URL によって切り替わるパターンも扱います。
- /Home/Index を、 http リクエストの
Accept-Language
に応じて言語が切り替わるページとする - /Home/About を、 URL によって言語が切り替わるページとする
- /ja/Home/About のとき日本語、 /en/Home/About のとき英語、ということ
- /Home/About に直接アクセスすると、勝手に /.../Home/About へリダイレクト
- ページを日本語にする(/ja/ に遷移する)リンクと英語にする(/en/ に遷移する)リンクをつくる
興味のあるパターンだけ見てもらえればいいと思います。完成品のプロジェクトはここ↓においてあります。
デモはこちら。(デモは予告なく閉じることがあります。)
今回やることを図にして整理
/Home/Index についてはこう。
/Home/About と /Home/Contact についてはこう。
今回使うプロジェクト
今回使うプロジェクトは次のように New しました。
- ASP.NET Web Application (.NET Framework)
- .NET Framework 4.7.2
- MVC
- Authentication: No Authentication
- Place solution and project in the same directory のチェックは外す
各言語リソースファイル作成
リソースファイルは拡張子 resx のファイルですね。ここに言語ごとにテキストを書いておきます。リソースファイルは
[好きな名前].[culture の名前].resx
という名称でつくります。デフォルトの言語リソースと、多言語のリソースは同じ階層に並べましょう。リソースファイルのアクセス修飾子は忘れず Public
にします。(だいじ)
- I18n.resx: デフォルトで使うもの
- I18n.ja.resx: ja カルチャで使うもの
- I18n.en.resx: en カルチャで使うもの
場所は?
場所はどこでもよさそうです。私はたとえば /Home/Index 用のリソースは Resources/Home/Index/I18n.resx みたいなフォルダ階層をつくって置きます。実際に Razor ファイル(cshtml ファイル)で使うときの表記が @I18n.PageTitle
みたいになって見やすいからです。 I18n は internationalization の略です。
カルチャ?
カルチャというのは、 http リクエストヘッダ Accept-Language
の値のことみたいです。サイトの閲覧者さんが、何語を求めているかということですね。 C# 的には、 CurrentCulture
と CurrentUICulture
にそれをセットすれば、上で作った各リソースが自動で呼び出されます。
// CurrentCulture はフォーマット形式とかに使われる。
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en");
// CurrentUICulture はリソースを切り替えるのに使われる。
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en");
ふたつ挙げましたが、今回はリソースの切り替えしか扱わないので CurrentUICulture
しか関係ないです。
resx なくない?
ないですね。
ないときもあるみたいです。そういうときはリストにとらわれず resx の拡張子を手打ちしてファイルを作成すれば OK です。(えぇー?!
あるいはすでにある resx ファイルをどこかから見つけてきて、 Add Existing Item から追加することもできます。
リソースに書いたテキストを使うには?
String1 というテキストを用意したなら……
こういうふうに呼び出します。
@i18nApp.Resources.Home.Index.I18n.String1
あるいは
@using i18nApp.Resources.Home.Index
@I18n.String1
Accept-Language に応じた言語切り替え
/Home/Index を Accept-Language
によってリソースが切り替わるページにします。まず /Home/Index の View(cshtml) がリソースを使うようになっていないと話になりません。
@using i18nApp.Resources.Home.Index
<div class="jumbotron">
<h1>
@I18n.String1
</h1>
</div>
こう書けばとりあえずデフォルトのリソースを使ってくれます。 Accept-Language
に応じたリソースを使って欲しいときはトップディレクトリの Web.config の system.web タグ内に <globalization culture="auto" uiCulture="auto" />
を記述します。
<system.web>
<compilation debug="true" targetFramework="4.7.2" />
<httpRuntime targetFramework="4.7.2" />
<globalization culture="auto" uiCulture="auto" /> <!-- 追加 -->
</system.web>
リクエスト・ヘッダの Accept-Language
に応じて、自動で CurrentUICulture
が変わるようになります。 CurrentUICulture
に応じたリソースが呼び出されるので、これで /Home/Index は完成です。
Accept-Language はどう変更する?
- Chrome: メニュー > 設定 > 詳細設定 > 言語
- Firefox: メニュー > オプション > 言語
URL による言語切り替え
/Home/About を URL によってリソースが切り替わるページにします。 /Home/About の View(cshtml) を次のようにしておきます。リソースはあらかじめ用意してください。
@using i18nApp.Resources.Home.About
<div class="jumbotron">
<h1>
@I18n.String1
</h1>
</div>
{culture}/{controller}/{action} という URL を受け付けるようにする
そもそも ASP.NET MVC はデフォルトではそんな URL を受け付けません。 RouteConfig.cs を見ればわかるように、受け付けるのは {controller}/{action}
という形式だけです。 {culture}/{controller}/{action}
も受け付けるように、設定します。
// これを追加。
routes.MapRoute(
// 名前はてきとー。
name: "I18n",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional, culture = "ja" },
// {culture} には ja か en しか認めない、という設定。
constraints: new { culture = "ja|en" }
);
// 最初からあるやつ。
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
/en/Home/About みたいな URL でアクセスされたら、コントローラで culture = "en"
を受け取れるようになりました。受け取った culture を CurrentUICulture
にセットすれば、それに応じたリソースが呼ばれます。ヨシ!
URL の culture を CurrentUICulture にセット
で、どこでセットするかなんですが、コントローラの各アクションメソッド内ではやりたくないので、
// やりたくないやつ。
public ActionResult About(string culture)
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
return View();
}
アクション実行前に呼び出されるメソッド OnActionExecuting
にその処理を追加しましょう。
using System.Web.Mvc;
using System.Threading;
using System.Globalization;
namespace i18nApp.Helpers
{
public class I18nAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// culture が null だったら(/Home/About でアクセスされたということ)
// /ja/Home/About にリダイレクトしたいところだけどそれは後述。
string culture = filterContext.ActionParameters["culture"] as string;
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
}
}
}
そしてこの OnActionExecuting
を /Home/About に適用するには、アクションメソッドにこんなふうにくっつければ OK です。
using i18nApp.Helpers;
// これで↑の OnActionExecuting を事前に実行してくれる。
[I18nAttribute]
public ActionResult About(string culture)
{
return View();
}
- /ja あるいは /en/Home/About でアクセスされる
-
OnActionExecuting
メソッドでCurrentUICulture
を ja あるいは en にセットする - それに応じたリソースが View で表示される
これで /Home/About も完成です。デモページでは /Home/Contact も同様の実装にしてあります。
OnActionExecuting もりもりセット
実際のところ、アクションメソッド実行前にやってほしいことはもっとあります。
- /Home/About でアクセスされたら /ja/Home/About にリダイレクトしてほしい
- /en でアクセスされたら次からデフォルトの言語は en にしてほしい
そのへんを盛り込んでおきます。
public class I18nAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// NOTE:
// アクションパラメータを取得: filterContext.ActionParameters["culture"]
// GET クエリを取得: filterContext.HttpContext.Request.QueryString["key"]
// /{controller}/{action} のアクセスを /{culture}/{controller}/{action} へリダイレクト。
if (!(filterContext.ActionParameters["culture"] is string culture)) // culture が null だったら、という書き方
{
// リクエストされた URI。
Uri requestUri = filterContext.HttpContext.Request.Url;
var uriBuilder = new UriBuilder(requestUri);
// URI を変更。
// デフォルトは ja だけれど、クッキーに値が入っていればそれをデフォルトとする。
string initialDefaultCulture = "ja";
HttpCookie cookieCulture = filterContext.RequestContext.HttpContext.Request.Cookies["culture"];
if (cookieCulture != null && !string.IsNullOrEmpty(cookieCulture.Value))
{
initialDefaultCulture = cookieCulture.Value;
}
uriBuilder.Path = initialDefaultCulture + uriBuilder.Path;
// リダイレクト。
filterContext.Result = new RedirectResult(uriBuilder.ToString());
return;
}
// RouteConfig で culture を ja か en に絞っているので culure の値チェックは不要。
// NOTE: CurrentCulture is used to switch formats.
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
// NOTE: CurrentUICulture is used to switch resources.
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
// /ja や /en でアクセスされたらその値をクッキーに保存。
var cookie = new HttpCookie("culture")
{
Value = culture
};
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
}
URL を日本語版、あるいは英語版にするリンク
次のように Html.ActionLink
を書くことで、現在の URL に /ja や /en を付与したページへ遷移するリンクを作れます。
<li>
@Html.ActionLink(
"日本語",
this.ViewContext.RouteData.Values["action"].ToString(),
this.ViewContext.RouteData.Values["controller"].ToString(),
new { culture = "ja" },
null
)
</li>
<li>
@Html.ActionLink(
"English",
this.ViewContext.RouteData.Values["action"].ToString(),
this.ViewContext.RouteData.Values["controller"].ToString(),
new { culture = "en" },
null
)
</li>
もともとある Index, About, Contact へのリンクはそのままで OK です。
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
new { culture = "ja" }
とかをつけなくても自動で /ja/Home/About みたいなリンクになります。 ASP.NET MVC は現在の MapRoute
情報とアクションパラメータの値を保持するためです。
おしまい
ASP.NET Core が気になります。 Mac や Visual Studio Code で C# が書けるなんて素敵すぎる。