ASP.NET Core – Globalization & Localization
前言
之前就写过 2 篇, 只是写的很乱, 这篇作为整理版.
我的项目只是做语言而已, 没有做区域, 也没有 Data Annotation 的需求, 所以下面不会提到.
参考:
docs – Globalization and localization in ASP.NET Core
Razor Pages Localisation - SEO-friendly URLs
Using Resource Files In Razor Pages Localisation
基本用法:
Setup Program.cs
这篇只讲 Razor Pages 的使用, 不会讲到 MVC 和 Data Annotation.
builder.Services.AddRazorPages()
.AddViewLocalization();
Setup Options
builder.Services.Configure(options => { var supportedCultures = new[] { "en", "zh-Hans" }; options.SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); });
定义支持的语言, 默认语言. 我项目没有区域性, 所以是 en 而不是 en-US.
最后启动就可以了
app.UseRequestLocalization();
app.MapRazorPages();
.resx
这个文件的位置是挺讲究的. .cshtml 在哪里它就在旁边. 取一样的 file name, 配上指定的 language code
位置虽然是可以改的, 但我觉得默认就很好了, follow 它吧.
用 Visual Studio 打开 .resx
Name 其实是 Key, 但是为了方便, 一般上会直接放默认语言的值. 你要放 Key (代号) 也是可以的.
pure text, HTML 都支持. 也支持 string format 代号 {0}, 调用时传入 parameters.
.cshtml 调用
@using Microsoft.AspNetCore.Mvc.Localization @inject IViewLocalizer Localizer <div class="text-center"> <h1 class="display-4">About Pageh1> @Localizer["Hello World"] @Localizer["<h1>Hello World {0}h1>", "parameter1"] div>
注入 IViewLocalizer, 使用方式是 Localizer["Key"]
它会返回一个对象, 而不是一个值哦.
这个对象有一个方法叫 WriteTo. Razor Pages 在 render 的时候会调用它, 最后 encode 成 HTML.
另外, Localizer["Key"] 如果没有找到 .resx file 它会返回 Key. 这个是为了方便项目提前设计. 以后才支持语言. 非常方便.
访问
https://localhost:7078/About?culture=zh-Hans&ui-culture=zh-Hans
它是通过 query params 来选择语言的哦.
Use Path Segment as Language Selection
上面提到, 默认用 query params 作为语言的选择, 但是 SEO 不鼓励这样做.
通常是用第一个 path segment 作为语言: /zh-Hans/about-us
builder.Services.Configure(options => { var supportedCultures = new[] { "en", "zh-Hans" }; options.SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(httpContent => { // 这里写判断逻辑 (base on httpContent info), 最后返回指定的语言就可以了 return Task.FromResult(new ProviderCultureResult("zh-Hans"))!; })); });
通过 AddInitialRequestCultureProvider 就可以实现了.
题外话, 用 path first segment 作为语言, 需要调整 Razor Pages 的 routing 匹配哦. 请参考:
Shared Resource
上面提到的都是 1 个 .cshtml 对应 1 个 .resx. 但有时候内容一样想做抽象怎么办呢?
创建一个空的 class 和 .resx.
namespace TestLocalization.Pages; public class SharedResource { }
然后, 在 .cshtml 把注入换成 IHtmlLocalizer
@using Microsoft.AspNetCore.Mvc.Localization @* @inject IViewLocalizer Localizer *@ @inject IHtmlLocalizer<SharedResource> Localizer
注意: resx 的 file name 和位置也是有讲究的哦, 依据 class 的 namespace + class name
比如 namespace = ProjectName.Pages, class name = SharedResource.
那么 .resx 必须放在 /Pages/SharedResource.zh-Hans.resx
详解资料可以看这篇: Resources Search Strategy
在 Model.cs 使用 Localization
上面都是讲 View 如何使用 Localization. 想在 Model.cs 里面使用的话, 不可以注入 IViewLocalization
public class IndexModel : PageModel { private readonly IStringLocalizer_stringLocalizer; private readonly IHtmlLocalizer _htmlLocalizer; public IndexModel( IStringLocalizer stringLocalizer, IHtmlLocalizer htmlLocalizer ) { _stringLocalizer = stringLocalizer; _htmlLocalizer = htmlLocalizer; } public void OnGet() { var value1 = _stringLocalizer["Hello World"].Value; // var value2 = _htmlLocalizer["Hello World"].WriteTo(TextWriter writer, HtmlEncoder encoder); } }
注入 IStringLocalizer 或者 IHtmlLocalizer
两者的区别是, IString 内容应该是 pure text 不包含 HTML. 使用的时候 _stringLOcalizer["Key"] 返回一个对象, 通过 .Value 获取翻译后的值.
IHtml 的使用是 _htmlLocalizer["Key"] 返回一个对象, 通过 WriteTo(writer, encoder) 获取翻译后的值, 注意: 它不是用 .Value 哦.
.rexs 的位置
仔细看, IStringLocalizer
直觉会认为它应该和 View 用同一个 resx.
但其实不是, 上面有提到 SharedResource, 只要是 class 就是 namespace + class = forlder + file name, 所以是 IndexModel.zh-Hans.resx
也是醉了...因此我建议当需要这样搞时, 做一个 shared class 让 view 也统一使用 IHtmlLocalizer 会更好.
动态设定语言
上面有提到通过 AddInitialRequestCultureProvider 可以控制每一次请求选择的语言. 但如果想动态切换呢?
比如 path segment 是中文. 但是系统要发一个 enquiry 给网站负责人, 内容要用英文. 这时就需要动态修改它.
翻看源码 RequestLocalizationMiddleware.cs
在 middleware 它做了 2 件事
1. set IRequestCultureFeature (这类 Feature 属于 request 的全局变量, 可以通过 HttpContext 访问到)
2. set CultureInfo (这个是静态类来的, 也算是全局变量吧)
而在 ResourceManagerStringLocalizer.cs 里
翻译就是依据 CultureInfo 这个静态类去做的.
所以要动态修改语言的话, 就要 re-set 掉 CultureInfo
public void OnGet() { CultureInfo.CurrentCulture = new CultureInfo("zh-Hans"); CultureInfo.CurrentUICulture = new CultureInfo("zh-Hans"); var value1 = _stringLocalizer["Hello World"].Value; }
.cshtml
@using System.Globalization
@{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
CultureInfo.CurrentUICulture = new CultureInfo("zh-Hans");
}
之前有 Github Issue 讨论过这种 set 方式好不好, 但后来视乎大家都接受了.
获取语言相关信息
public class IndexModel : PageModel { private readonly RequestLocalizationOptions _requestLocalizationOptions; public IndexModel( IOptionsSnapshot_requestLocalizationOptionsAccessor ) { _requestLocalizationOptions = _requestLocalizationOptionsAccessor.Value; } public void OnGet() { var languageDisplayName = HttpContext.Features.Get ()!.RequestCulture.Culture.DisplayName; // "中文(简体)" var supportLanguageDisplayNames = _requestLocalizationOptions.SupportedCultures!.Select(s => s.DisplayName).ToList(); // base on current language ["英语", "中文(简体)"] var supportLanguageNativeNames = _requestLocalizationOptions.SupportedCultures!.Select(s => s.NativeName).ToList(); // ["English", "中文(简体)"] var supportLanguageEnglishNames = _requestLocalizationOptions.SupportedCultures!.Select(s => s.EnglishName).ToList(); // ["English", "Chinese (Simplified)"] } }