前言

之前就写过 2 篇, 只是写的很乱, 这篇作为整理版.

Asp.net core (学习笔记 路由和语言 route & language)

Asp.net core 学习笔记之 globalization & localization 复习篇

我的项目只是做语言而已, 没有做区域, 也没有 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<RequestLocalizationOptions>(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 Page</h1>
@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<RequestLocalizationOptions>(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 匹配哦. 请参考: ASP.NET Core – Razor Pages Routing

Shared Resource

上面提到的都是 1 个 .cshtml 对应 1 个 .resx. 但有时候内容一样想做抽象怎么办呢?

创建一个空的 class 和 .resx.

namespace TestLocalization.Pages;
public class SharedResource { }

然后, 在 .cshtml 把注入换成 IHtmlLocalizer<ClassName> 就可以了

@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<IndexModel> _stringLocalizer;
private readonly IHtmlLocalizer<IndexModel> _htmlLocalizer; public IndexModel(
IStringLocalizer<IndexModel> stringLocalizer,
IHtmlLocalizer<IndexModel> 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 获取翻译后的值。

IHtmlLocalizer 则内容可以包含 HTML。使用时 _htmlLocalizer["Key"] 返回一个对象,通过 WriteTo(writer, encoder) 获取翻译后的值,这里和 string 的 .Value 不同哦

看看例子

代码

var value1 = _stringLocalizer["Hello World {0}", "123"].Value; // "哈喽世界 123 <span>test</span>"
var value2 = _htmlLocalizer["Hello World {0}", "123"].Value; // "哈喽世界 {0} <span>test</span>"

结果是不同的,_htmlLocalizer.Value 拿到的是还没有处理的值

正确的获取方式是

using var ms = new MemoryStream();
using var sw = new StreamWriter(ms, Encoding.UTF8);
_htmlLocalizer["Hello World {0}", "test"].WriteTo(sw, htmlEncoder);
await sw.FlushAsync();
ms.Seek(0, SeekOrigin.Begin);
using var streamReader = new StreamReader(ms, Encoding.UTF8);
var text = await streamReader.ReadToEndAsync(); // "哈喽世界 test <span>test</span>"

Tips:在 Razor Pages(.cshtml)要拿 htmlEncoder 可以直接用 this.HtmlEncoder

.rexs 的位置

仔细看, IStringLocalizer<IndexModel> 的泛型是 IndexModel class, 也就是当前的 class.

直觉会认为它应该和 View 用同一个 resx.

但其实不是, 上面有提到 SharedResource, 只要是 class 就是 namespace + class = forlder + file name, 所以是 IndexModel.zh-Hans.resx

也是醉了...因此我建议当需要这样搞时, 做一个 shared class 让 view 也统一使用 IHtmlLocalizer 会更好.

Localizer with specify culture

上面我们提到,Localization 是通过 middleware 拦截 request,然后通过逻辑判断 HttpContext 最终决定整个 request 使用什么语言。

那如果我们想在某个地方特别指定某种语言去翻译一段文字,而不是 follow request culture 可以吗?

可以。我们先看看源码,了解一下 Localization 是怎样工作的。

翻看源码 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");
}

set CultureInfo.CurrentUICulture 是 ASP.NET Core 5.0 之后的唯一方法,以前有一个叫 ResourceManagerWithCultureStringLocalizer 的可以更简单的做到,但是因为一些原因被拿掉了,参考:Github Issue

CurrentCulture 的作用域

参考:

Docs – 区域性和基于任务的异步操作

Stack Overflow – Keep CurrentCulture in async/await

Stack Overflow – ASP.NET MVC (Async) CurrentCulture is not shared between Controller and View

Index.cshtml.cs、Index.cshtml、_Layout.cshtml、Templated delegates

这几个地方都是独立的作用域,需要分别 set CultureInfo.CurrentCulture,超级麻烦。

比如 templated delegates

@{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
CultureInfo.CurrentUICulture = new CultureInfo("zh-Hans"); Func<dynamic?, object> template = @<h1>@Localizer["Hello World"]</h1>;
} <h1>@Localizer["Hello World"]</h1> @* 结果是: 哈喽世界 *@
@template(null) @*结果是: Hello World *@

template 的内容依然是英文。

要这样才行

@{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
CultureInfo.CurrentUICulture = new CultureInfo("zh-Hans"); Func<dynamic?, object> template = @<h1>@{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
CultureInfo.CurrentUICulture = new CultureInfo("zh-Hans");
@Localizer["Hello World"]
}</h1>;
} <h1>@Localizer["Hello World"]</h1> @* 结果是: 哈喽世界 *@
@template(null) @* 结果是: 哈喽世界 *@

再比如:我们在 Index.cshtml.cs

public class IndexModel : PageModel
{
public void OnGet()
{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
}
}

在它的 View Index.cshtml 去拿会发现,没有 set 到

@using System.Globalization
@{
var culture = CultureInfo.CurrentCulture.DisplayName; // 还是 English
}

CurrentCulture 的作用域原理

为什么我们 set CultureInfo.CurrentCulture 不代表 request 级别的 culture?

但 Localization Middleware set 的却代表 request 级别的 culture 呢?

我们来模拟一下它的过程

public static async Task Main()
{
Console.WriteLine("Start: " + CultureInfo.CurrentCulture.DisplayName);
DoMiddleware();
await DoControllerAsync();
await DoViewAsync();
} public static void DoMiddleware()
{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
Console.WriteLine("Set culture in middleware");
} public static async Task DoControllerAsync()
{
Console.WriteLine("Controller: " + CultureInfo.CurrentCulture.DisplayName);
} public static async Task DoViewAsync()
{
Console.WriteLine("View" + CultureInfo.CurrentCulture.DisplayName);
}

输出

正确。那我们尝试在 controller set culture 看看。

public static async Task DoControllerAsync()
{
CultureInfo.CurrentCulture = new CultureInfo("ja");
Console.WriteLine("Controller: " + CultureInfo.CurrentCulture.DisplayName);
}

输出

View 没有拿到 Controller set 的日语,它只拿到了 middleware set 的中文。

Why?!问题出在 async Task。

在 async Task 里面设置 CultureInfo.CurrentCulture 离开 async Task 就没了,这是 CultureInfo.CurrentCulture 的规则,想知道细节可以看上面的参考链接。

middleware 不是 async Task 所以它 set 就变成了 request 级别,而 Controller 或者 View 都是 async Task,所以 set 了只能在自己小小的区域玩。

所以,如果想 page 级别的 culture,不能在 PageModel 里,也不能在 View 里。最少需要在 PageFilter。

这里给一个例子

利用 IPageFilter 和 FilterAttribute 拦截并且设置 culture。

public class PageCultureFilter : IPageFilter
{
public void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
var pageCultureAttr = context.ActionDescriptor.DeclaredModelTypeInfo?.GetCustomAttribute<PageCultureAttribute>();
if (pageCultureAttr != null)
{
CultureInfo.CurrentCulture = new CultureInfo(pageCultureAttr.Culture);
CultureInfo.CurrentUICulture = new CultureInfo(pageCultureAttr.Culture);
}
} public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
} public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
{
}
}

还有

public class PageCultureAttribute : ResultFilterAttribute
{
public string Culture { get; set; } public PageCultureAttribute(string culture)
{
Culture = culture;
} public override void OnResultExecuting(ResultExecutingContext context)
{
CultureInfo.CurrentCulture = new CultureInfo(Culture);
CultureInfo.CurrentUICulture = new CultureInfo(Culture);
base.OnResultExecuting(context);
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
}
}

这样 Index.cshtml.cs、Index.cshtml、_Layout.cshtml、Templated delegates 就都拿到相同的 culture 了。

获取语言相关信息

public class IndexModel : PageModel
{
private readonly RequestLocalizationOptions _requestLocalizationOptions;
public IndexModel(
IOptionsSnapshot<RequestLocalizationOptions> _requestLocalizationOptionsAccessor
)
{
_requestLocalizationOptions = _requestLocalizationOptionsAccessor.Value;
}
public void OnGet()
{
var languageDisplayName = HttpContext.Features.Get<IRequestCultureFeature>()!.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)"]
}
}

关于 New Line

有时候 placeholder 需要 new line

<textarea placeholder="@Localizer["Hi\r\nI love you"]" rows="5"></textarea>

注意,它是 \r\n 而不是 \n 哦。

.resx 长这样

  <data name="Hi
I love you" xml:space="preserve">
<value>嗨
我爱你</value>
</data>

就可以 enter 去下一行。

Without DI & ResourceManager

IViewLocalizer、IStringLocalizer、IHtmlLocalizer 都需要 DI 注入。

如果在静态 class 方法内也想 localizer 怎么办呢?

可以用比较底层的 ResourceManger。它的规则和 IStringLocalizer 非常相似,只是调用有点不同而已。

public static class Program
{
public static async Task Main()
{
CultureInfo.CurrentCulture = new CultureInfo("zh-Hans");
CultureInfo.CurrentUICulture = new CultureInfo("zh-Hans");
var resourceManager = new ResourceManager(typeof(MyResource).FullName!, Assembly.GetExecutingAssembly());
var value1 = resourceManager.GetString("I love you"); // 用 CurrentCulture
var value2 = resourceManager.GetString("I love you", culture: new CultureInfo("ms")); // 指定 culture
Console.WriteLine(value1); // 我爱你
}
}

MyResource class 的 namespace

.resx 的 folder and file

和上面我们提过的 Shared Resource 查找规则是一样的。

Could not find the resource "CSharp11.Parent.Child.MyResource.resources" among the resources

如果找不到 .resx 或者 file 里面 match 不到 key 是会报错的哦。如果想像 IStringLocalizer 那样,拿不到 resource 就拿 key 当作 value,需要自己额外处理。

它也是 wrap 一层来出的.

ASP.NET Core – Globalization & Localization的更多相关文章

  1. Asp.net core 2.x/3.x 的 Globalization 和 localization 的使用 (一) 使用方法

    由于Api的接口需要返回多语言,因此参考了网上很多篇文章,,有些文章写的太过于理论,看起来比较费劲,今天下午搞了一个下午,总结了一下经验,, 做这个功能时,主要参考了两篇文章: https://blo ...

  2. 体验 ASP.NET Core 中的多语言支持(Localization)

    首先在 Startup 的 ConfigureServices 中添加 AddLocalization 与 AddViewLocalization 以及配置 RequestLocalizationOp ...

  3. ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化

    原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...

  4. Professional C# 6 and .NET Core 1.0 - 40 ASP.NET Core

    本文内容为转载,重新排版以供学习研究.如有侵权,请联系作者删除. 转载请注明本文出处:Professional C# 6 and .NET Core 1.0 - 40 ASP.NET Core --- ...

  5. 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】

    Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...

  6. asp.net core 实现支持多语言

    asp.net core 实现支持多语言 Intro 最近有一个外国友人通过邮件联系我,想用我的活动室预约,但是还没支持多语言,基本上都是写死的中文,所以最近想支持一下更多语言,于是有了多语言方面的一 ...

  7. ASP.NET Core搭建多层网站架构【13-扩展之支持全球化和本地化多语言】

    2020/02/03, ASP.NET Core 3.1, VS2019, ResXManager 摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构[13-扩展之支持全球化 ...

  8. ASP.NET Core 1.1.0 Release Notes

    ASP.NET Core 1.1.0 Release Notes We are pleased to announce the release of ASP.NET Core 1.1.0! Antif ...

  9. asp.net core输出中文乱码的问题

    摘要 在学习asp.net core的时候,尝试在控制台,或者页面上输出中文,会出现乱码的问题. 问题重现 新建控制台和站点 public class Program { public static ...

  10. C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(上)

    译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 40 章  ASP.NET Core(上)),不对的地方欢迎指出与交流. 章节出自<Professiona ...

随机推荐

  1. SSRF结合Redis未授权的打法

    目录 SSRF + Redis未授权 案例 怎么构造 redis 数据包? Reference SSRF不难理解,服务器端请求伪造(英语:Server-side Request Forgery,简称S ...

  2. 玄机-第一章 应急响应-Linux日志分析

    目录 前言 简介 应急开始 准备工作 查看auth.log文件 grep -a 步骤 1 步骤 2 步骤 3 步骤 4 步骤 5 总结 前言 又花了一块rmb玩玄机...啥时候才能5金币拿下一个应急靶 ...

  3. [oeasy]python0081_[趣味拓展]ESC键进化历史_键盘演化过程_ANSI_控制序列_转义序列_CSI

    光标位置 回忆上次内容 上次了解了 新的转义模式 \033 逃逸控制字符 escape 这个字符 让字符串 退出标准输出流 进行控制信息的设置 可以设置 光标输出的位置       ​   添加图片注 ...

  4. oeasy教您玩转vim - 47 - # 使用标记

    ​ 使用标记 回忆上节课内容 有了这个range.address我们可以做很多事情 跳转:44 复制和剪切 1,3d 3,$y %d o 配合搜索 /oeasy/,$y 5;/oeasy/d 其实还有 ...

  5. Excel快速下拉填充序列至10000行

    问题:想要下拉输入的数据递增得到1.2.3--10000,但是手动下拉太累 解决: 1.如在A1单元格输入1,在A2单元格输入2 2.选中A2单元格,在上方名称框中填写A2:A1000,回车,此时将选 ...

  6. Docker PHP容器安装composer

    1.进入php容器docker exec -it cb6c1fe83bff(php容器ID) bash2.安装composerphp -r "copy('https://install.ph ...

  7. layout文本相关

    Textview t=findViewById(R.id.t); ONE设置文本内容: 在XML中android:text直接写 在java中setText()中修改 注意点1继承appcompata ...

  8. Jmeter函数助手36-P

    P函数用于获取jmeter属性值.类似property函数 属性名称:填入jmeter的属性名称 默认值:缺省值,当获取属性值为空时则返回该值 1.填入属性名称获取属性值${__P(language, ...

  9. 【FastDFS】环境搭建 01 跟踪器和存储节点

    FastDFS:分布式文件系统 它对文件进行管理,功能包括:文件存储.文件同步.文件访问(文件上传.文件下载)等,解决了大容量存储和负载均衡的问题. 特别适合以文件为载体的在线服务,如相册网站.视频网 ...

  10. 【Shiro】02 shiro.ini文件分析

    [什么是INI文件?] INI 全称:Initialization File 初始文件 Window系统文件扩展名 Shiro 使用时可以连接数据库,也可以不连接数据库. 当不使用数据库时,需要配置S ...