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 的泛型是 IndexModel class, 也就是当前的 class.

直觉会认为它应该和 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)"]
    }
}

相关