理解ASP.NET Core - 全球化&本地化&多语言(Globalization and Localization)
注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录
概述
在众多知名品牌的网站中,比如微软官网、YouTube等,我们经常可以见到“切换页面语言”的功能,我们可以选择最适合的语言浏览页面内容。毫无疑问,为网站提供多种语言,页面内容本地化,大大扩展了受众范围,提升了用户体验。
名词术语
为了更好地理解下面的内容,我们先来了解一下行业内通用的名词术语:
- Globalization (G11N):全球化,即使应用支持不同语言和区域的过程。G11N 是首字母、尾字母和它们之间字母的个数组成的,下同,不再赘述。
- Localization (L10N):本地化,即针对特定语言和区域自定义全球化应用的过程。
- Internationalization (I18N):国际化,又称为多语言,包含了全球化和本地化。
- Culture:区域性,即一种语言文化或区域。
- Neutral Culture:非特定区域性,即具有指定语言但不具有区域的区域性。例如“zh”、“en”,仅仅表示中文或英文,并没有包含指定地区,如大陆、香港、台湾等。
- Specific Culture: 特定区域性,即具有指定语言和区域的区域性。例如“zh-CN”、“zh-HK”。
- Parent Culture: 父区域性,例如“zh”就是“zh-CN”和“zh-HK”的父区域性。
实现本地化
一般情况下,统一使用英文作为多语言的字典Key,在 Web 刚进入开发阶段时,最好就支持多语言,否则后续改造的工作量会比较大。当然,你可以选择使用中文作为 Key,不过并不太推荐,毕竟你总不能要求懂阿拉伯语的人要懂中文。
本地化器
ASP.NET Core 提供了多种本地化工具:
- IStringLocalizer
- IStringLocalizerFactory
- IHtmlLocalizer
- IViewLocalizer
IStringLocalizer
IStringLocalizer和IStringLocalizer<>可以在运行时提供区域性资源,使用非常简单,就像操作字典一样,提供一个 Key,就能获取到指定区域的资源。另外,它还允许 Key 在资源中不存在,此时返回的就是 Key 自身。我们下面称这个 Key 为资源名。
下面是他们的结构定义:
public interface IStringLocalizer
{
// 通过资源名获取本地化文本,如果资源不存在,则返回 name 自身
LocalizedString this[string name] { get; }
// 通过资源名获取本地化文本,并允许将参数值填充到文本中,如果资源不存在,则返回 name 自身
LocalizedString this[string name, params object[] arguments] { get; }
// 获取所有的本地化资源文本
IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
}
public interface IStringLocalizer<out T> : IStringLocalizer
{
}
在服务类中使用本地化
- 首先,注入本地化服务,并启用中间件
var builder = WebApplication.CreateBuilder(args);
// 注册服务
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
var app = builder.Build();
// 启用中间件
app.UseRequestLocalization(options =>
{
var cultures = new[] { "zh-CN", "en-US", "zh-TW" };
options.AddSupportedCultures(cultures);
options.AddSupportedUICultures(cultures);
options.SetDefaultCulture(cultures[0]);
// 当Http响应时,将 当前区域信息 设置到 Response Header:Content-Language 中
options.ApplyCurrentCultureToResponseHeaders = true;
});
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
首先,我们通过AddLocalization注册了IStringLocalizerFactory和IStringLocalizer<>,并指定了资源的根目录为“Resources”。
然后,我们又通过UseRequestLocalization启用了中间件RequestLocalizationMiddleware。默认情况下,该中间件支持的区域文化仅为当前区域文化,即CultureInfo.CurrentCulture和CultureInfo.CurrentUICulture,我们可以通过AddSupportedCultures和AddSupportedUICultures自定义设置多个支持的区域文化:
Culture:影响日期、时间、数字或货币的展示格式UICulture:影响查找哪些区域文化资源(如.resx、json文件等),也就是说,如果这里未添加某区域文化A,即使添加了对应区域文化A的资源文件,也无发生效。一般 Culture 和 UICulture 保持一致。
另外,当我们的服务接收到一个请求时,如果该请求未指明当前的区域文化,就会使用默认的,这里我们通过SetDefaultCulture指定了默认区域文化为 zh-CN
最后,通过设置ApplyCurrentCultureToResponseHeaders为true,将当前区域信息设置到Http响应头的Content-Language中。
- 接下来,我们新建“Resources/Controllers”目录,在 Resources 目录下新建2个共享资源文件,在 Controllers 目录中新建2个
HomeController类的资源文件,目录结构如下:
- Resources
- Controllers
- HomeController.en-US.resx
- HomeController.zh-CN.resx
- SharedResource.en-US.resx
- SharedResource.zh-CN.resx
并填充内容如下:
- SharedResource.en-US.resx
| 名称 | 值 |
|---|---|
| CurrentTime | Current Time: |
- SharedResource.zh-CN.resx
| 名称 | 值 |
|---|---|
| CurrentTime | 当前时间: |
- HomeController.en-US.resx
| 名称 | 值 |
|---|---|
| HelloWorld | Hello, World! |
- HomeController.zh-CN.resx
| 名称 | 值 |
|---|---|
| HelloWorld | 你好,世界! |
这些文件默认为“嵌入的资源”
- 为了优雅地使用共享资源,我们在项目根目录下创建
SharedResource伪类,用来代理共享资源。
public class SharedResource
{
// 里面是空的
}
- 最后,我们在
HomeController中尝试一下效果
public class HomeController : Controller
{
// 用于提供 HomeController 的区域性资源
private readonly IStringLocalizer<HomeController> _localizer;
// 通过代理伪类提供共享资源
private readonly IStringLocalizer<SharedResource> _sharedLocalizer;
public HomeController(
IStringLocalizer<HomeController> localizer,
IStringLocalizer<SharedResource> sharedLocalizer
)
{
_localizer = localizer;
_sharedLocalizer = sharedLocalizer;
}
[HttpGet]
public IActionResult GetString()
{
var content = $"当前区域文化:{CultureInfo.CurrentCulture.Name}\n" +
$"{_localizer["HelloWorld"]}\n" +
$"{_sharedLocalizer["CurrentTime"]}{DateTime.Now.ToLocalTime()}\n";
return Content(content);
}
}
访问{your-host}/home/getstring,使用默认的区域文化zh-CN,获取结果如下:
当前区域文化:zh-CN
你好,世界!
当前时间:2023/6/2 11:19:08
此时查看响应头信息,可以发现
Content-Language: zh-CN
下面,我们通过 url 传递参数culture,指定区域文化为en-US,访问{your-host}/home/getstring?culture=en-US,获取结果如下:
当前区域文化:en-US
Hello, World!
Current Time:6/2/2023 11:47:50 AM
此时的响应头信息:
Content-Language: en-US
如果你的本地化果并不是预期的,并且当前区域文化没问题的情况下,可以通过
SearchedLocation查看资源搜索位置(如_localizer["HelloWord"].SearchedLocation),检查资源放置位置是否有误。
在模型验证中使用本地化
好了,我们已经掌握了本地化在服务类中的使用方法,接下来,一起来看下在模型验证中如何使用本地化。
- 首先通过调用
AddDataAnnotationsLocalization注册数据注解本地化服务:
builder.Services
.AddControllersWithViews()
.AddDataAnnotationsLocalization();
- 接着在 Dtos 目录下新建
RegisterDto模型类:
public class RegisterDto
{
[Required(ErrorMessage = "UserNameIsRequired")]
public string UserName { get; set; }
[Required(ErrorMessage = "PasswordIsRequired")]
[StringLength(8, ErrorMessage = "PasswordLeastCharactersLong", MinimumLength = 6)]
public string Password { get; set; }
[Compare("Password", ErrorMessage = "PasswordDoNotMatch")]
public string ConfirmPassword { get; set; }
}
其中 ErroMessage 赋值的均为本地化资源Key
3. 然后在“Resources/Dtos”目录下添加资源文件:
- RegisterDto.en-US.resx
| 名称 | 值 |
|---|---|
| PasswordDoNotMatch | The password and confirmation password do not match |
| PasswordIsRequired | The Password field is required |
| PasswordLeastCharactersLong | The Password must be at least {2} characters long |
| UserNameIsRequired | The UserName field is required |
- RegisterDto.zh-CN.resx
| 名称 | 值 |
|---|---|
| PasswordDoNotMatch | 两次密码输入不一致 |
| PasswordIsRequired | 请输入密码 |
| PasswordLeastCharactersLong | 密码长度不能小于 {2} |
| UserNameIsRequired | 请输入用户名 |
- 最后在
HomeController中添加一个Register方法:
[HttpPost]
public IActionResult Register([FromBody] RegisterDto dto)
{
if (!ModelState.IsValid)
{
return Content($"当前区域文化:{CultureInfo.CurrentCulture.Name}\n" +
"模型状态无效:" + Environment.NewLine +
string.Join(Environment.NewLine, ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))));
}
return Ok();
}
测试结果就不贴了,赶紧自己试一试吧!
另外,如果你觉得每一个模型类都要创建一个资源文件太麻烦了,可以通过DataAnnotationLocalizerProvider来手动指定IStringLocalizer实例,例如设置所有模型类仅从 SharedResource 中寻找本地化资源:
builder.Services
.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResource));
});
IStringLocalizerFactory
有时,我们可能想要使用一些没有代理类或代理类无法使用的区域资源,无法直接通过IStringLocalizer<>进行注入,那IStringLocalizerFactory就可以帮助我们获取对应的IStringLocalizer,该接口结构如下:
public interface IStringLocalizerFactory
{
IStringLocalizer Create(Type resourceSource);
IStringLocalizer Create(string baseName, string location);
}
下面我们通过IStringLocalizerFactory来获取HomeController资源实例:
public class HomeController : Controller
{
private readonly IStringLocalizer _localizer;
private readonly IStringLocalizer _localizer2;
public HomeController(IStringLocalizerFactory localizerFactory)
{
_localizer = localizerFactory.Create(typeof(HomeController));
_localizer2 = localizerFactory.Create("Controllers.HomeController", Assembly.GetExecutingAssembly().FullName);
}
[HttpGet]
public IActionResult GetString()
{
var content = $"当前区域文化:{CultureInfo.CurrentCulture.Name}\n" +
$"{_localizer["HelloWorld"]}\n" +
$"{_localizer2["HelloWorld"]}\n";
return Content(content);
}
}
这里演示了两种创建方式:
- 一个是通过类型来创建,一般我们不会手动通过该方式获取,而是直接注入对应的泛型版本
- 另一个是通过指定资源基础名称和所属程序集来创建,所谓资源基础名称,就是资源文件相对于资源根目录的相对路径+文件基础名称,例如对于 HomeController.XXX.resx 来说,资源根目录就是前面注册服务时设置的 Resources,相对路径为 Controllers,文件基础名为 HomeController,所以资源基础名称为 Controllers.HomeController
资源文件命名规则
是时候明确一下资源文件的命名规则了,很简单:类的资源名称 = 类的完整类型名 - 程序集名称。
还是拿HomeController来举例,假设所属程序集名称为LocalizationWeb.dll,默认根命名空间与程序集同名,那么它的全名称为LocalizationWeb.Controllers.HomeController,资源文件就需要命名为Controllers.HomeController.XXX.resx,而我们在注册本地化服务时,通过ResourcesPath指定了资源的根目录为 Resources,所以资源文件相对项目根目录的相对路径为Resources/Controllers.HomeController.XXX.resx。由于这样做可能会导致资源文件名字较长,并且不便于归类,所以我们可以将 Controllers 提取为目录,变为Resources/Controllers/HomeController.XXX.resx。
强烈建议程序的程序集名称与根命名空间保持一致,这样可以省很多事。如果不一致,当然也有解决办法,例如有个DifferentController,它位于Different.Controllers命名空间下,那么资源文件需要放置于Resources/Different/Controllers目录下。
最后,如果你愿意,可以把SharedResource类放到 Resources 文件夹下,让它和它的资源文件在一起,不过要注意它的命名空间,确保该类够按照上述规则对应到资源文件上。你可能还需要在.csproj文件中进行如下配置(二选一,具体原因参考此文档):
<PropertyGroup>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup>
或
<ItemGroup>
<EmbeddedResource Include="Resources/SharedResource.en-US.resx" DependentUpon="SharedResources" />
<EmbeddedResource Include="Resources/SharedResource.zh-CN.resx" DependentUpon="SharedResources" />
</ItemGroup>
IHtmlLocalizer
相对于IStringLocalizer, IHtmlLocalizer和IHtmlLocalizer<>中的资源可以包含 HTML 代码,并使其能够在前端页面中正常渲染出来。
通常情况下,我们仅仅需要本地化文本内容,而不会包含 HTML。不过这里还是简单介绍一下。
- 首先调用
AddViewLocalization注册服务
builder.Services
.AddControllersWithViews()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
此处我们注册了IHtmlLocalizerFactory、IHtmlLocalizer<>,以及接下来要讲的IViewLocalizer共3个服务,并且通过LanguageViewLocationExpanderFormat.Suffix指定了视图(View)语言资源命名格式为后缀,即 <view-name>.<language>.resx。
- 接着在 SharedResource 的资源文件中添加以下内容:
- SharedResource.en-US.resx
| 名称 | 值 |
|---|---|
| Welcome | <b>Welcome {0}!</b> |
- SharedResource.zh-CN.resx
| 名称 | 值 |
|---|---|
| Welcome | <b>欢迎 {0}!</b> |
- 最后自己可以在视图中看一下效果,文本确实被加粗了:
@inject IHtmlLocalizer<SharedResource> HtmlSharedResource
<div class="text-center">
@HtmlSharedResource["Welcome", "jjj"]
</div>
IViewLocalizer
IViewLocalizer是专门服务于视图的,他没有泛型版本,也没有工厂类,所以它只能用来获取当前视图资源文件中的资源,如果想要使用其他资源,可以使用IStringLocalizer或IHtmlLocalizer。
它继承自IHtmlLocalizer,所以它也支持资源中包含 HTML 代码:
public interface IViewLocalizer : IHtmlLocalizer { }
下面我们在Views/Home/Index.cshtml中演示一下效果。
上面我们已经通过
AddViewLocalization将IViewLocalizer服务注册到容器中了。
- 首先在
Resources/Views/Home目录下增加以下两个资源文件,并设置内容:
- Index.en-US.resx
| 名称 | 值 |
|---|---|
| Welcome | Welcome {0} !!! |
- Index.zh-CN
| 名称 | 值 |
|---|---|
| Welcome | 欢迎 {0} !!! |
- 在视图中使用并查看效果
@inject IViewLocalizer L
<div class="text-center">
<h1>@L["Welcome", "jjj"]</h1>
</div>
区域性回退
当请求的区域资源未找到时,会回退到该区域的父区域资源,例如档区域文化为 zh-CN 时,HomeController资源文件查找顺序如下:
- HomeController.zh-CN.resx
- HomeController.zh.resx
- HomeController.resx
如果都没找到,则会返回资源 Key 本身。
配置 CultureProvider
上面,我们通过在 url 中添加参数 culture 来设置当前请求的区域信息,实际上,ASP.NET Core 是通过IRequestCultureProvider接口来为我们提供区域的设置方式。
内置的 RequestCultureProvider
可以通过以下代码查看已添加的 Provider:
app.UseRequestLocalization(options =>
{
var cultureProviders = options.RequestCultureProviders;
}
可以看到,ASP.NET Core 框架默认添加了3种 Provider,分别为:
QueryStringRequestCultureProvider:通过在 Query 中设置"culture"、"ui-culture"的值,例如 ?culture=zh-CN&ui-culture=zh-CNCookieRequestCultureProvider:通过Cookie中设置名为 ".AspNetCore.Culture" Key 的值,值形如 c=zh-CN|uic=zh-CNAcceptLanguageHeaderRequestCultureProvider:从请求头中设置 "Accept-Language" 的值
如果只传了 culture 或 ui-culture,则会将该值同时赋值给 culture 或 ui-culture。我们可以通过以下代码查看
我们也可以在这3个的基础上进行自定义配置,例如通过在 Query 中设置"lang"的值来设置区域:
options.AddInitialRequestCultureProvider(new QueryStringRequestCultureProvider() { QueryStringKey = "lang" });
AddInitialRequestCultureProvider默认将新添加的 Provider 放置在首位。
内置的还有一个RouteDataRequestCultureProvider,不过它并没有被默认添加到提供器列表中。它默认可以通过在路由中设置 culture 的值来设置区域,就像微软官方文档一样。需要注意的是,一定要在 app.UseRouting() 之后再调用 app.UseRequestLocalization()。
实现自定义 RequestCultureProvider
实现自定义RequestCultureProvider的方式有两种,分别是通过委托和继承抽象类RequestCultureProvider。
下面,我们实现一个从自定义 Header 中获取区域文化信息的自定义RequestCultureProvider。
- 通过委托实现自定义
RequestCultureProviders
app.UseRequestLocalization(options =>
{
var cultures = new[] { "zh-CN", "en-US", "zh-TW" };
options.AddSupportedCultures(cultures);
options.AddSupportedUICultures(cultures);
options.SetDefaultCulture(cultures[0]);
options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(context =>
{
ArgumentException.ThrowIfNullOrEmpty(nameof(context));
// 从请求头“X-Lang”中获取区域文化信息
var acceptLanguageHeader = context.Request.GetTypedHeaders().GetList<StringWithQualityHeaderValue>("X-Lang");
if (acceptLanguageHeader == null || acceptLanguageHeader.Count == 0)
{
return Task.FromResult(default(ProviderCultureResult));
}
var languages = acceptLanguageHeader.AsEnumerable();
// 如果值包含多,我们只取前3个
languages = languages.Take(3);
var orderedLanguages = languages.OrderByDescending(h => h, StringWithQualityHeaderValueComparer.QualityComparer)
.Select(x => x.Value).ToList();
if (orderedLanguages.Count > 0)
{
return Task.FromResult(new ProviderCultureResult(orderedLanguages));
}
return Task.FromResult(default(ProviderCultureResult));
}));
}
需要注意的是,当未获取到区域文化信息时,若想要接着让后面的RequestCultureProvider继续解析获取,则记得一定要返回default(ProviderCultureResult),否则建议直接返回默认区域文化,即new ProviderCultureResult(options.DefaultRequestCulture.Culture.Name。
- 通过继承抽象类
RequestCultureProvider
public interface IRequestCultureProvider
{
// 确定当前请求的区域性,我们要实现这个接口
Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext);
}
public abstract class RequestCultureProvider : IRequestCultureProvider
{
// 指代空区域性结果
protected static readonly Task<ProviderCultureResult?> NullProviderCultureResult = Task.FromResult(default(ProviderCultureResult));
// 中间件 RequestLocalizationMiddleware 的选项
public RequestLocalizationOptions? Options { get; set; }
public abstract Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext);
}
public class CustomHeaderRequestCultureProvider : RequestCultureProvider
{
// Header 名称,默认为 Accept-Language
public string HeaderName { get; set; } = HeaderNames.AcceptLanguage;
// 当 Header 值有多个时,最多取前 n 个
public int MaximumHeaderValuesToTry { get; set; } = 3;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
ArgumentException.ThrowIfNullOrEmpty(nameof(httpContext));
ArgumentException.ThrowIfNullOrEmpty(nameof(HeaderName));
var acceptLanguageHeader = httpContext.Request.GetTypedHeaders().GetList<StringWithQualityHeaderValue>(HeaderName);
if (acceptLanguageHeader == null || acceptLanguageHeader.Count == 0)
{
return NullProviderCultureResult;
}
var languages = acceptLanguageHeader.AsEnumerable();
if (MaximumHeaderValuesToTry > 0)
{
languages = languages.Take(MaximumHeaderValuesToTry);
}
var orderedLanguages = languages.OrderByDescending(h => h, StringWithQualityHeaderValueComparer.QualityComparer)
.Select(x => x.Value).ToList();
if (orderedLanguages.Count > 0)
{
return Task.FromResult(new ProviderCultureResult(orderedLanguages));
}
return NullProviderCultureResult;
}
}
app.UseRequestLocalization(options =>
{
var cultures = new[] { "zh-CN", "en-US", "zh-TW" };
options.AddSupportedCultures(cultures);
options.AddSupportedUICultures(cultures);
options.SetDefaultCulture(cultures[0]);
options.RequestCultureProviders.Insert(0, new CustomHeaderRequestCultureProvider { HeaderName = "X-Lang" });
}
使用 Json 资源文件
你可能和我一样,不太喜欢 .resx 资源文件,想要将多语言配置到 json 文件中,虽然微软并没有提供完整地实现,但是社区已经有大佬根据接口规范为我们写好了,这里推荐使用My.Extensions.Localization.Json。
ASP.NET Core 也支持 PO 文件,如果有兴趣,请自行了解。
只需要将AddLocalization替换为AddJsonLocalization即可:
builder.Services.AddJsonLocalization(options => options.ResourcesPath = "JsonResources");
后面就是在 json 文件中配置多语言了,例如:
- HomeController.en-US.json
{
"HelloWorld": "Hello,World!"
}
- HomeController.zh-CN.json
{
"HelloWorld": "你好,世界!"
}
设计原理
现在,基础用法我们已经了解了,接下来就一起学习一下它背后的原理吧。
鉴于涉及到的源码较多,所以为了控制文章长度,下面只列举核心代码。
IStringLocalizerFactory & IStringLocalizer
先来看下AddLocalization中注册的默认实现:
public static class LocalizationServiceCollectionExtensions
{
internal static void AddLocalizationServices(IServiceCollection services)
{
services.TryAddSingleton<IStringLocalizerFactory, ResourceManagerStringLocalizerFactory>();
services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
}
}
一共注册了两个实现,分别是ResourceManagerStringLocalizerFactory和StringLocalizer<>,先来看一下工厂:
public interface IStringLocalizerFactory
{
IStringLocalizer Create(Type resourceSource);
IStringLocalizer Create(string baseName, string location);
}
public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory
{
private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache();
private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer> _localizerCache =
new ConcurrentDictionary<string, ResourceManagerStringLocalizer>();
private readonly string _resourcesRelativePath;
public ResourceManagerStringLocalizerFactory(
IOptions<LocalizationOptions> localizationOptions)
{
_resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
if (!string.IsNullOrEmpty(_resourcesRelativePath))
{
// 将目录分隔符“/”和“\”全部替换为“.”
_resourcesRelativePath = _resourcesRelativePath.Replace(Path.AltDirectorySeparatorChar, '.')
.Replace(Path.DirectorySeparatorChar, '.') + ".";
}
}
protected virtual string GetResourcePrefix(TypeInfo typeInfo)
{
// 代码不列了,直接说一下逻辑吧:
// 1. 如果资源根路径(_resourcesRelativePath)为空,即项目的根目录,那么直接返回 typeInfo.FullName
// 2. 如果资源根路径(_resourcesRelativePath)不为空,那么需要将资源根目录拼接在 typeInfo.FullName 中间, 按照如下格式拼接(注意里面的是减号):"{RootNamespace}.{ResourceLocation}.{FullTypeName - RootNamespace}"
}
protected virtual string GetResourcePrefix(string baseResourceName, string baseNamespace)
{
// 逻辑同上
}
public IStringLocalizer Create(Type resourceSource)
{
var typeInfo = resourceSource.GetTypeInfo();
var baseName = GetResourcePrefix(typeInfo);
var assembly = typeInfo.Assembly;
return _localizerCache.GetOrAdd(baseName, _ => CreateResourceManagerStringLocalizer(assembly, baseName));
}
public IStringLocalizer Create(string baseName, string location)
{
return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ =>
{
var assemblyName = new AssemblyName(location);
var assembly = Assembly.Load(assemblyName);
baseName = GetResourcePrefix(baseName, location);
return CreateResourceManagerStringLocalizer(assembly, baseName);
});
}
protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(
Assembly assembly,
string baseName)
{
return new ResourceManagerStringLocalizer(
new ResourceManager(baseName, assembly), // 指定了资源的基础名和所属程序集
assembly,
baseName,
_resourceNamesCache);
}
}
可以看到,Create(Type resourceSource)和Create(string baseName, string location)的实现都是通过CreateResourceManagerStringLocalizer来创建的,并且实例类型就是ResourceManagerStringLocalizer。另外,还通过_localizerCache将已创建的资源实例缓存了下来,避免了重复创建的开销,只不过由于缓存 Key 的构造规则不同,两者创建的实例并不能共享。
如果你现在就想要验证一下 HomeController 中的 Localizer 是否是相同的,你会发现通过构造函数直接注入的 IStringLocalizer<>._localizer 才是真正干活,你可以参考这段代码来获取它:
typeof(Microsoft.Extensions.Localization.StringLocalizer<GlobalizationAndLocalization.SharedResource>).GetField("_localizer", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance).GetValue(mySharedLocalizer)
接着看ResourceManagerStringLocalizer的实现细节:
public interface IStringLocalizer
{
LocalizedString this[string name] { get; }
LocalizedString this[string name, params object[] arguments] { get; }
IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
}
public class ResourceManagerStringLocalizer : IStringLocalizer
{
// 将不存在的资源 Key 进行缓存
private readonly ConcurrentDictionary<string, object?> _missingManifestCache = new ConcurrentDictionary<string, object?>();
// 用于操作 .resx 资源文件
private readonly ResourceManager _resourceManager;
private readonly IResourceStringProvider _resourceStringProvider;
private readonly string _resourceBaseName;
public ResourceManagerStringLocalizer(
ResourceManager resourceManager,
Assembly resourceAssembly,
string baseName, // 资源的基础名称,类似于 xxx.xxx.xxx
IResourceNamesCache resourceNamesCache)
: this(
resourceManager,
new AssemblyWrapper(resourceAssembly),
baseName,
resourceNamesCache)
{
}
internal ResourceManagerStringLocalizer(
ResourceManager resourceManager,
AssemblyWrapper resourceAssemblyWrapper,
string baseName,
IResourceNamesCache resourceNamesCache
: this(
resourceManager,
new ResourceManagerStringProvider(
resourceNamesCache,
resourceManager,
resourceAssemblyWrapper.Assembly,
baseName),
baseName,
resourceNamesCache)
{
}
internal ResourceManagerStringLocalizer(
ResourceManager resourceManager,
IResourceStringProvider resourceStringProvider,
string baseName,
IResourceNamesCache resourceNamesCache)
{
_resourceStringProvider = resourceStringProvider;
_resourceManager = resourceManager;
_resourceBaseName = baseName;
_resourceNamesCache = resourceNamesCache;
}
public virtual LocalizedString this[string name]
{
get
{
var value = GetStringSafely(name, culture: null);
// LocalizedString 包含了 资源名、资源值、资源是否不存在、资源搜索位 等信息
return new LocalizedString(name, value ?? name, resourceNotFound: value == null, searchedLocation: _resourceBaseName);
}
}
public virtual LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetStringSafely(name, culture: null);
var value = string.Format(CultureInfo.CurrentCulture, format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null, searchedLocation: _resourceBaseName);
}
}
public virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>
GetAllStrings(includeParentCultures, CultureInfo.CurrentUICulture);
protected IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures, CultureInfo culture)
{
// 通过 culture 获取所有资源,原理与通过资源名获取类似
// 需要注意的是,它是通过 yield return 返回的
}
// 所谓 Safely,就是当 资源名 不存在时,不会抛出异常,而是返回 null
protected string? GetStringSafely(string name, CultureInfo? culture)
{
var keyCulture = culture ?? CultureInfo.CurrentUICulture;
var cacheKey = $"name={name}&culture={keyCulture.Name}";
// 资源已缓存为不存在,直接返回 null
if (_missingManifestCache.ContainsKey(cacheKey))
{
return null;
}
try
{
// 通过 ResourceManager 获取资源
return _resourceManager.GetString(name, culture);
}
catch (MissingManifestResourceException)
{
// 若资源不存在,则缓存
_missingManifestCache.TryAdd(cacheKey, null);
return null;
}
}
}
好了,资源的加载流程我们已经清楚了,还有一个StringLocalizer<>需要看一下:
public interface IStringLocalizer<out T> : IStringLocalizer
{
}
public class StringLocalizer<TResourceSource> : IStringLocalizer<TResourceSource>
{
private readonly IStringLocalizer _localizer;
public StringLocalizer(IStringLocalizerFactory factory)
{
_localizer = factory.Create(typeof(TResourceSource));
}
public virtual LocalizedString this[string name] => _localizer[name];
public virtual LocalizedString this[string name, params object[] arguments] => _localizer[name, arguments];
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>
_localizer.GetAllStrings(includeParentCultures);
}
其实很简单,本质上还是通过工厂创建的本地化实例,真正干活的其实是它的私有变量_localizer,泛型只是一层包装。
DataAnnotationsLocalization
现在StringLocalizer的原理我们已经搞清楚了,但是数据注解本地化是如何实现的呢?它啊,其实也是通过StringLocalizer实现的,看:
public static IMvcCoreBuilder AddDataAnnotationsLocalization(
this IMvcCoreBuilder builder,
Action<MvcDataAnnotationsLocalizationOptions>? setupAction)
{
AddDataAnnotationsLocalizationServices(services, setupAction);
return builder;
}
public static void AddDataAnnotationsLocalizationServices(
IServiceCollection services,
Action<MvcDataAnnotationsLocalizationOptions>? setupAction)
{
services.AddLocalization();
// 如果传入的 setup 委托不为空则使用该委托配置 MvcDataAnnotationsLocalizationOptions,
if (setupAction != null)
{
services.Configure(setupAction);
}
// 否则使用默认的 MvcDataAnnotationsLocalizationOptionsSetup 进行配置
else
{
services.TryAddEnumerable(
ServiceDescriptor.Transient
<IConfigureOptions<MvcDataAnnotationsLocalizationOptions>,
MvcDataAnnotationsLocalizationOptionsSetup>());
}
}
internal class MvcDataAnnotationsLocalizationOptionsSetup : IConfigureOptions<MvcDataAnnotationsLocalizationOptions>
{
public void Configure(MvcDataAnnotationsLocalizationOptions options)
{
options.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) =>
stringLocalizerFactory.Create(modelType);
}
}
可以看到,MvcDataAnnotationsLocalizationOptions提供了一个委托DataAnnotationLocalizerProvider,它接收两个参数,Type和IStringLocalizerFactory,返回一个IStringLocalizer。从这里我们就可以看出来,它的本地化就是通过IStringLocalizer来实现的。
默认情况下,它的本地化器指向当前模型类资源,我上面提到过,可以将其自定义为从共享资源中获取,这下你就理解为啥所有模类都会受影响了吧。
IViewLocalizer & IHtmlLocalizer
IViewLocalizer、IHtmlLocalizer和IHtmlLocalizer<>这里就不再深入了,毕竟现在前端更多的是用三大前端框架,等用到的时候再去了解吧。
RequestLocalizationMiddleware
RequestLocalizationMiddleware的作用主要是解析并设置当前请求的区域文化,以便于本地化器可以正常工作。
我们可以通过RequestLocalizationOptions对该中间件进行配置,可配置项如下:
public class RequestLocalizationOptions
{
private RequestCulture _defaultRequestCulture =
new RequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture);
public RequestLocalizationOptions()
{
RequestCultureProviders = new List<IRequestCultureProvider>
{
new QueryStringRequestCultureProvider { Options = this },
new CookieRequestCultureProvider { Options = this },
new AcceptLanguageHeaderRequestCultureProvider { Options = this }
};
}
// 默认请求区域文化,默认值:当前区域文化
public RequestCulture DefaultRequestCulture
{
get => _defaultRequestCulture;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_defaultRequestCulture = value;
}
}
// 是否允许回退到父区域文化,默认值:true
public bool FallBackToParentCultures { get; set; } = true;
// 是否允许回退到父UI区域文化,默认值:true
public bool FallBackToParentUICultures { get; set; } = true;
// 是否要将当前请求的区域文化设置到响应头 Content-Language 中,默认值:false
public bool ApplyCurrentCultureToResponseHeaders { get; set; }
// 受支持的区域文化列表,默认仅支持当前区域文化
public IList<CultureInfo>? SupportedCultures { get; set; } = new List<CultureInfo> { CultureInfo.CurrentCulture };
// 受支持的UI区域文化列表,默认仅支持当前UI区域文化
public IList<CultureInfo>? SupportedUICultures { get; set; } = new List<CultureInfo> { CultureInfo.CurrentUICulture };
// 请求区域文化提供器列表
public IList<IRequestCultureProvider> RequestCultureProviders { get; set; }
// 设置受支持的区域文化(注意,它的行为是 Set,而不是 Add)
public RequestLocalizationOptions AddSupportedCultures(params string[] cultures)
{
var supportedCultures = new List<CultureInfo>(cultures.Length);
foreach (var culture in cultures)
{
supportedCultures.Add(new CultureInfo(culture));
}
SupportedCultures = supportedCultures;
return this;
}
// 设置受支持的UI区域文化(注意,它的行为是 Set,而不是 Add)
public RequestLocalizationOptions AddSupportedUICultures(params string[] uiCultures)
{
var supportedUICultures = new List<CultureInfo>(uiCultures.Length);
foreach (var culture in uiCultures)
{
supportedUICultures.Add(new CultureInfo(culture));
}
SupportedUICultures = supportedUICultures;
return this;
}
// 设置默认区域文化
public RequestLocalizationOptions SetDefaultCulture(string defaultCulture)
{
DefaultRequestCulture = new RequestCulture(defaultCulture);
return this;
}
}
下面看一下RequestLocalizationMiddleware中间件的实现:
public class RequestLocalizationMiddleware
{
// 区域文化回退最大深度,5 层已经很足够了
private const int MaxCultureFallbackDepth = 5;
private readonly RequestDelegate _next;
private readonly RequestLocalizationOptions _options;
public RequestLocalizationMiddleware(RequestDelegate next, IOptions<RequestLocalizationOptions> options)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_options = options.Value;
}
public async Task Invoke(HttpContext context)
{
// 默认当前请求区域文化为 options 中配置的默认值
var requestCulture = _options.DefaultRequestCulture;
IRequestCultureProvider? winningProvider = null;
// 如果存在 Provider,则通过 Provider 解析当前请求中设置的区域文化
if (_options.RequestCultureProviders != null)
{
foreach (var provider in _options.RequestCultureProviders)
{
var providerResultCulture = await provider.DetermineProviderCultureResult(context);
// 如果解析出来为 null,则继续让后续的 Provider 继续解析
if (providerResultCulture == null)
{
continue;
}
var cultures = providerResultCulture.Cultures;
var uiCultures = providerResultCulture.UICultures;
CultureInfo? cultureInfo = null;
CultureInfo? uiCultureInfo = null;
if (_options.SupportedCultures != null)
{
// 检查区域文化(可能有多个)是否支持,如果不支持则返回 null
cultureInfo = GetCultureInfo(
cultures,
_options.SupportedCultures,
_options.FallBackToParentCultures);
}
if (_options.SupportedUICultures != null)
{
// 检查UI区域文化(可能有多个)是否支持,如果不支持则返回 null
uiCultureInfo = GetCultureInfo(
uiCultures,
_options.SupportedUICultures,
_options.FallBackToParentUICultures);
}
// 如果区域文化和UI区域文化均不受支持,则视为解析失败,继续让下一个 Provider 解析
if (cultureInfo == null && uiCultureInfo == null)
{
continue;
}
// 两种区域文化若有为 null 的,则赋 options 中设置的默认值
// 注意:我们上面讲 Provider 时提到过,如果只传了 culture 和 ui-culture 其中的一个值,会将该值赋值到两者,这个行为是 Provider 中执行的,不要搞混咯
cultureInfo ??= _options.DefaultRequestCulture.Culture;
uiCultureInfo ??= _options.DefaultRequestCulture.UICulture;
var result = new RequestCulture(cultureInfo, uiCultureInfo);
requestCulture = result;
winningProvider = provider;
// 解析成功,直接跳出
break;
}
}
context.Features.Set<IRequestCultureFeature>(new RequestCultureFeature(requestCulture, winningProvider));
// 将当前区域文化信息设置到当前请求的线程,便于后续本地化器读取
SetCurrentThreadCulture(requestCulture);
if (_options.ApplyCurrentCultureToResponseHeaders)
{
var headers = context.Response.Headers;
headers.ContentLanguage = requestCulture.UICulture.Name;
}
await _next(context);
}
private static void SetCurrentThreadCulture(RequestCulture requestCulture)
{
CultureInfo.CurrentCulture = requestCulture.Culture;
CultureInfo.CurrentUICulture = requestCulture.UICulture;
}
private static CultureInfo? GetCultureInfo(
IList<StringSegment> cultureNames,
IList<CultureInfo> supportedCultures,
bool fallbackToParentCultures)
{
foreach (var cultureName in cultureNames)
{
if (cultureName != null)
{
// 里面通过递归查找支持的区域文化(包括回退的)
var cultureInfo = GetCultureInfo(cultureName, supportedCultures, fallbackToParentCultures, currentDepth: 0);
if (cultureInfo != null)
{
return cultureInfo;
}
}
}
return null;
}
}
总结
通过以上内容,我们可以总结出以下核心知识点:
- ASP.NET Core 提供了3种本地化器:
IStringLocalizer或IStringLocalizer<>:文本本地化器,是最常用的,可以通过依赖注入获取,也可以通过IStringLocalizerFactory来获取。IStringLocalizer<>是对IStringLocalizer的一层包装。IHtmlLocalizer或IHtmlLocalizer<>:HTML本地化器,顾名思义,可以本地化HTML文本而不会对其编码。可以通过依赖注入获取,也可以通过IHtmlLocalizerFactory来获取。IViewLocalizer:视图本地化器,用于前端视图的本地化。
- 通过
AddLocalization设置资源根目录,并注册本地化服务IStringLocalizer<>和IStringLocalizerFactory - 通过
AddDataAnnotationsLocalization注册数据注解本地化服务,主要是设置DataAnnotationLocalizerProvider委托 - 通过
AddViewLocalization注册视图本地化服务IViewLocalizer、IHtmlLocalizer<>和IHtmlLocalizerFactory - 通过
UseRequestLocalization启用请求本地化中间件RequestLocalizationMiddleware,它可以从请求中解析出当前请求的区域文化信息并设置到当前的处理线程中。- 通过
AddSupportedCultures和AddSupportedUICultures配置受支持的 Cultures 和 UICultures - 通过
SetDefaultCulture配置默认 Culture - 默认提供了三种
RequestCultureProvider:QueryStringRequestCultureProvider:通过在 Query 中设置"culture"、"ui-culture"的值,例如 ?culture=zh-CN&ui-culture=zh-CNCookieRequestCultureProvider:通过Cookie中设置名为 ".AspNetCore.Culture" Key 的值,值形如 c=zh-CN|uic=zh-CNAcceptLanguageHeaderRequestCultureProvider:从请求头中设置 "Accept-Language" 的值
- 通过
AddInitialRequestCultureProvider添加自定义RequestCultureProvider,可以通过委托传入解析逻辑,也可以继承RequestCultureProvider抽象类来编写更复杂的逻辑。
- 通过
- 可以通过 Nuget 包
My.Extensions.Localization.Json将资源文件(.resx)更换为 Json 文件。
理解ASP.NET Core - 全球化&本地化&多语言(Globalization and Localization)的更多相关文章
- 体验 ASP.NET Core 中的多语言支持(Localization)
首先在 Startup 的 ConfigureServices 中添加 AddLocalization 与 AddViewLocalization 以及配置 RequestLocalizationOp ...
- asp.net core 实现支持多语言
asp.net core 实现支持多语言 Intro 最近有一个外国友人通过邮件联系我,想用我的活动室预约,但是还没支持多语言,基本上都是写死的中文,所以最近想支持一下更多语言,于是有了多语言方面的一 ...
- 理解ASP.NET Core - 模型绑定&验证(Model Binding and Validation)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 模型绑定 什么是模型绑定?简单说就是将HTTP请求参数绑定到程序方法入参上,该变量可以是简单类 ...
- 理解 ASP.NET Core: 处理管道
理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...
- 理解ASP.NET Core - [01] Startup
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...
- 目录-理解ASP.NET Core
<理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...
- 理解ASP.NET Core - [02] Middleware
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 中间件 先借用微软官方文档的一张图: 可以看到,中间件实际上是一种配置在HTTP请求管道中,用 ...
- 理解ASP.NET Core - [03] Dependency Injection
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接 ...
- 理解ASP.NET Core - [04] Host
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 本文会涉及部分 Host 相关的源码,并会附上 github 源码地址,不过为了降低篇幅,我会 ...
- 理解ASP.NET Core - 配置(Configuration)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...
随机推荐
- 使用requests发送post请求登录
post请求 语法结构 requests.post(url,data=None,json=None) 参数说明 url:需要爬取的网址 data:请求数据 json:json格式的数据 案例:登录小说 ...
- import tensorflow出现ImportError: DLL load failed: 找不到指定的模块的问题(亲测可用)
错误如下图所示: 在很长时间的查找后,网上的很多办法都不能很好的解决问题,但是基本上指向了一个问题--版本问题,所以接下来我安装了与python环境对应的tensorflow包. 首先用以下命令查找对 ...
- Install Ansible on CentOS 8
环境准备: 1.至少俩台linux主机,一台是控制节点,一台是受控节点 2.控制节点和受控节点都需要安装Python36 3.控制节点需要安装ansible 4.控制节点需要获得受控节点的普通用户或r ...
- 计网学习笔记五 wireless && mobile networks
老师把无线网络用一节课一遍过了-感觉没能学透,便课后自己总结,看书,找资料补充,把无线网络大概摸了个七七八八.虽然不算精细,但还能看!内容包括WLAN总概,WiFi-WLAN的实现,802.11规定的 ...
- 全网最详细中英文ChatGPT-GPT-4示例文档-智能AI写作从0到1快速入门——官网推荐的48种最佳应用场景(附python/node.js/curl命令源代码,小白也能学)
目录 Introduce 简介 setting 设置 Prompt 提示 Sample response 回复样本 API request 接口请求 python接口请求示例 node.js接口请求示 ...
- [MySQL]set autocommit=0与start transaction的区别[转载]
set autocommit=0指事务非自动提交,自此句执行以后,每个SQL语句或者语句块所在的事务都需要显示"commit"才能提交事务. 1.不管autocommit 是1还是 ...
- Disruptor-简单使用
前言 Disruptor是一个高性能的无锁并发框架,其主要应用场景是在高并发.低延迟的系统中,如金融领域的交易系统,游戏服务器等.其优点就是非常快,号称能支撑每秒600万订单.需要注意的是,Disru ...
- Seal AppManager发布:基于平台工程理念的全新应用部署管理体验
4月12日,数澈软件Seal(以下简称"Seal")宣布推出新一代应用统一部署管理平台 Seal AppManager,采用平台工程的理念,降低基础设施操作的复杂度为研发和运维团队 ...
- 【LeetCode动态规划#07】01背包问题一维写法(状态压缩)实战,其二(目标和、零一和)
目标和(放满背包的方法有几种) 力扣题目链接(opens new window) 难度:中等 给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S.现在你有两个符号 + 和 -.对 ...
- 在Jupyter Notebook,沉浸式体验ChatGPT
大家好,我是章北海mlpy 写代码,修Bug是 ChatGPT 目前最擅长的领域之一 今天向大家推荐一个刚刚开源的Python包 安装后可以直接在IPython和Jupyter Notebook中直接 ...