ASP.NET Core – Globalization & Localization
前言
之前就写过 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 的作用域
参考:
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的更多相关文章
- Asp.net core 2.x/3.x 的 Globalization 和 localization 的使用 (一) 使用方法
由于Api的接口需要返回多语言,因此参考了网上很多篇文章,,有些文章写的太过于理论,看起来比较费劲,今天下午搞了一个下午,总结了一下经验,, 做这个功能时,主要参考了两篇文章: https://blo ...
- 体验 ASP.NET Core 中的多语言支持(Localization)
首先在 Startup 的 ConfigureServices 中添加 AddLocalization 与 AddViewLocalization 以及配置 RequestLocalizationOp ...
- ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化
原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...
- Professional C# 6 and .NET Core 1.0 - 40 ASP.NET Core
本文内容为转载,重新排版以供学习研究.如有侵权,请联系作者删除. 转载请注明本文出处:Professional C# 6 and .NET Core 1.0 - 40 ASP.NET Core --- ...
- 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. ...
- asp.net core 实现支持多语言
asp.net core 实现支持多语言 Intro 最近有一个外国友人通过邮件联系我,想用我的活动室预约,但是还没支持多语言,基本上都是写死的中文,所以最近想支持一下更多语言,于是有了多语言方面的一 ...
- ASP.NET Core搭建多层网站架构【13-扩展之支持全球化和本地化多语言】
2020/02/03, ASP.NET Core 3.1, VS2019, ResXManager 摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构[13-扩展之支持全球化 ...
- 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 ...
- asp.net core输出中文乱码的问题
摘要 在学习asp.net core的时候,尝试在控制台,或者页面上输出中文,会出现乱码的问题. 问题重现 新建控制台和站点 public class Program { public static ...
- C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(上)
译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 40 章 ASP.NET Core(上)),不对的地方欢迎指出与交流. 章节出自<Professiona ...
随机推荐
- php.ini文件与php.d
`php.ini` 是 PHP 的主要配置文件,用于全局配置 PHP 的行为和功能.它包含了许多 PHP 的核心设置,如内存限制.错误报告级别.扩展加载等. `php.ini` 文件通常位于 PHP ...
- webpack4.15.1 学习笔记(七) — 懒加载(Lazy Loading)
懒加载或者按需加载,是一种很好的优化网页或应用的方式.实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或引用另外一些新的代码块.这样加快了应用的初始加载速度,减轻了它 ...
- PAT-1002 写出这个数 (20分) JavaScript(node)
读入一个正整数 n,计算其各位数字之和,用汉语拼音写出和的每一位数字. 输入格式: 每个测试输入包含 1 个测试用例,即给出自然数 n 的值.这里保证 n 小于 10100 . 输出格式: 在一行 ...
- [oeasy]python0131_[趣味拓展]各种符号_汉语拼音符号_中文全角英文字母_中文全角标点
各种符号 回忆上次内容 中文字符可以有各种分类方法 声母 拼音检字法 韵母 合辙押韵的分类 偏旁部首 实际上unicode的排序方法 添加图片注释,不超过 140 字(可选) ...
- .NET单元测试使用AutoFixture按需填充属性的几种方式,以及最佳实践
AutoFixture是一个.NET库,旨在简化单元测试中的数据设置过程.通过自动生成测试数据,它帮助开发者减少测试代码的编写量,使得单元测试更加简洁.易读和易维护.AutoFixture可以用于任何 ...
- SQL Server AdventureWorks示例数据库
SQL Server AdventureWorks2008R2 数据字典 AdventureWorks2008R2示例数据库下载 AdventureWorks2008R2数据字典(官网) Addres ...
- C# 网络编程:.NET 开发者的核心技能
前言 数字化时代,网络编程已成为软件开发中不可或缺的一环,尤其对于 .NET 开发者而言,掌握 C# 中的网络编程技巧是迈向更高层次的必经之路.无论是构建高性能的 Web 应用,还是实现复杂的分布式系 ...
- 安卓开发 StateListDrawable 应用
基础部份 StateListDrawable 安卓开发中,如果要做一个按扭按下改变背景,或获取焦点改变背景,最简单的方法是利用将背景指向一个资源,然后果在资源中配置事件,总共分为三步, 1) ...
- 断点续传:使用java对大文件进行分块与合并
通常我们下载上传的视频文件比较大.虽然https协议没有规定上传文件大小的限制,但是网络的质量,电脑硬件的参差不齐可能会导致大文件快要上传完成的时候突然断网了要重新上传,非常影响用户体验.以此我们引入 ...
- 【SpringMVC】01 快速上手
环境搭建 EvBuild 环境组成: - JDK 1.8 + - IDEA 2018 + - Maven 3.0 + - Tomcat 8.0 + 搭建步骤: 1.创建Maven - SpringMV ...