Asp .Net Core 系列:国际化多语言配置

文章目录

    • 概述
      • 术语
    • 本地化器
      • IStringLocalizer
        • 在服务类中使用本地化
      • IStringLocalizerFactory
      • IHtmlLocalizer
      • IViewLocalizer
    • 资源文件
      • 区域性回退
    • 配置 CultureProvider
      • 内置的 RequestCultureProvider
      • 实现自定义 RequestCultureProvider
      • 使用 Json 资源文件
    • 设计原理
      • IStringLocalizerFactory & IStringLocalizer
      • DataAnnotationsLocalization
      • RequestLocalizationMiddleware
    • 总结
    • 问题

参考:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/localization?view=aspnetcore-8.0

概述

术语

  • 全球化 (G11N):使应用支持不同语言和区域的过程。 缩写来自第一个和最后一个字母以及它们之间的字母数。
  • 本地化 (L10N):针对特定语言和区域自定义全球化应用的过程。
  • 国际化 (I18N):包括全球化和本地化。
  • 区域性:一种语言和/或区域。
  • 非特定区域性:具有指定语言但不具有区域的区域性(例如“en”、“es”)。
  • 特定区域性:具有指定语言但和区域的区域性(例如“en-US”、“en-GB”、“es-CL”)。
  • 父区域性:包含特定区域性的非特定区域性。(例如,“en”是“en-US”和“en-GB”的父区域性)。
  • 区域设置:区域设置与区域性相同。

本地化器

  • IStringLocalizer
  • IStringLocalizerFactory
  • IHtmlLocalizer
  • IViewLocalizer

IStringLocalizer

IStringLocalizerIStringLocalizer<>可以在运行时提供区域性资源,使用非常简单,就像操作字典一样,提供一个 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
{
}
在服务类中使用本地化
  1. 首先,注入本地化服务,并启用中间件
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注册了IStringLocalizerFactoryIStringLocalizer<>,并指定了资源的根目录为“Resources”。

然后,我们又通过UseRequestLocalization启用了中间件RequestLocalizationMiddleware。默认情况下,该中间件支持的区域文化仅为当前区域文化,即CultureInfo.CurrentCultureCultureInfo.CurrentUICulture,我们可以通过AddSupportedCulturesAddSupportedUICultures自定义设置多个支持的区域文化:

  • Culture:影响日期、时间、数字或货币的展示格式
  • UICulture:影响查找哪些区域文化资源(如.resx、json文件等),也就是说,如果这里未添加某区域文化A,即使添加了对应区域文化A的资源文件,也无发生效。一般 Culture 和 UICulture 保持一致。

另外,当我们的服务接收到一个请求时,如果该请求未指明当前的区域文化,就会使用默认的,这里我们通过SetDefaultCulture指定了默认区域文化为 zh-CN

最后,通过设置ApplyCurrentCultureToResponseHeaderstrue,将当前区域信息设置到Http响应头的Content-Language中。

  1. 接下来,我们新建“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
名称
CurrentTimeCurrent Time:
  • SharedResource.zh-CN.resx
名称
CurrentTime当前时间:
  • HomeController.en-US.resx
名称
HelloWorldHello, World!
  • HomeController.zh-CN.resx
名称
HelloWorld你好,世界!

这些文件默认为“嵌入的资源”

  1. 为了优雅地使用共享资源,我们在项目根目录下创建SharedResource伪类,用来代理共享资源。
public class SharedResource
{// 里面是空的
}
  1. 最后,我们在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,获取结果如下:

highlighter- awk

当前区域文化:zh-CN
你好,世界!
当前时间:2023/6/2 11:19:08

此时查看响应头信息,可以发现

Content-Language: zh-CN

下面,我们通过 url 传递参数culture,指定区域文化为en-US,访问{your-host}/home/getstring?culture=en-US,获取结果如下:

highlighter- awk

当前区域文化:en-US
Hello, World!
Current Time:6/2/2023 11:47:50 AM

此时的响应头信息:

Content-Language: en-US

如果你的本地化果并不是预期的,并且当前区域文化没问题的情况下,可以通过SearchedLocation查看资源搜索位置(如 _localizer["HelloWord"].SearchedLocation),检查资源放置位置是否有误。

IStringLocalizerFactory

有时,我们可能想要使用一些没有代理类或代理类无法使用的区域资源,无法直接通过IStringLocalizer<>进行注入,那IStringLocalizerFactory就可以帮助我们获取对应的IStringLocalizer,该接口结构如下:

csharp

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

IHtmlLocalizer

相对于IStringLocalizerIHtmlLocalizerIHtmlLocalizer<>中的资源可以包含 HTML 代码,并使其能够在前端页面中正常渲染出来。

通常情况下,我们仅仅需要本地化文本内容,而不会包含 HTML。不过这里还是简单介绍一下。

  1. 首先调用AddViewLocalization注册服务
builder.Services.AddControllersWithViews().AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

此处我们注册了IHtmlLocalizerFactoryIHtmlLocalizer<>,以及接下来要讲的IViewLocalizer共3个服务,并且通过LanguageViewLocationExpanderFormat.Suffix指定了视图(View)语言资源命名格式为后缀,即 <view-name>.<language>.resx

  1. 接着在 SharedResource 的资源文件中添加以下内容:
  • SharedResource.en-US.resx
名称
WelcomeWelcome {0}!
  • SharedResource.zh-CN.resx
名称
Welcome欢迎 {0}!
  1. 最后自己可以在视图中看一下效果,文本确实被加粗了:
@inject IHtmlLocalizer<SharedResource> HtmlSharedResource<div class="text-center">@HtmlSharedResource["Welcome", "jjj"]
</div>

IViewLocalizer

IViewLocalizer是专门服务于视图的,他没有泛型版本,也没有工厂类,所以它只能用来获取当前视图资源文件中的资源,如果想要使用其他资源,可以使用IStringLocalizerIHtmlLocalizer

它继承自IHtmlLocalizer,所以它也支持资源中包含 HTML 代码:

public interface IViewLocalizer : IHtmlLocalizer { }

下面我们在Views/Home/Index.cshtml中演示一下效果。

上面我们已经通过AddViewLocalizationIViewLocalizer服务注册到容器中了。

  1. 首先在Resources/Views/Home目录下增加以下两个资源文件,并设置内容:
  • Index.en-US.resx
名称
WelcomeWelcome {0} !!!
  • Index.zh-CN
名称
Welcome欢迎 {0} !!!
  1. 在视图中使用并查看效果
@inject IViewLocalizer L <div class="text-center"><h1>@L["Welcome", "jjj"]</h1>
</div>

资源文件

资源文件是将可本地化的字符串与代码分离的有用机制。 非默认语言的转换字符串在 .resx 资源文件中单独显示。 例如,你可能想要创建一个包含转换字符串、名为 Controllers.HomeController.es.resx 的西班牙语资源文件。 “es”是西班牙语的语言代码。

资源文件命名

资源名称是类的完整类型名称减去程序集名称。 例如,类 LocalizationWebsite.Web.Startup 的主要程序集为 LocalizationWebsite.Web.dll 的项目中的法语资源将命名为 Startup.fr.resx。 类 LocalizationWebsite.Web.Controllers.HomeController 的资源将命名为 Controllers.HomeController.fr.resx。 如果目标类的命名空间与将需要完整类型名称的程序集名称不同。 例如,在示例项目中,类型 ExtraNamespace.Tools 的资源将命名为 ExtraNamespace.Tools.fr.resx。

在示例项目中,ConfigureServices 方法将 ResourcesPath 设置为“资源”,因此主控制器的法语资源文件的项目相对路径是 Resources/Controllers.HomeController.fr.resx。 或者,你可以使用文件夹组织资源文件。 对于主控制器,该路径将为 Resources/Controllers/HomeController.fr.resx。 如果不使用 ResourcesPath 选项,.resx 文件将转到项目的基目录中。 HomeController 的资源文件将命名为 Controllers.HomeController.fr.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>

区域性回退

当请求的区域资源未找到时,会回退到该区域的父区域资源,例如档区域文化为 zh-CN 时,HomeController资源文件查找顺序如下:

  • HomeController.zh-CN.resx
  • HomeController.zh.resx
  • HomeController.resx

如果都没找到,则会返回资源 Key 本身。

配置 CultureProvider

内置的 RequestCultureProvider

  1. QueryStringRequestCultureProvider:通过在 Query 中设置"culture"、"ui-culture"的值,例如 ?culture=zh-CN&ui-culture=zh-CN
  2. CookieRequestCultureProvider:通过Cookie中设置名为 “.AspNetCore.Culture” Key 的值,值形如 c=zh-CN|uic=zh-CN
  3. AcceptLanguageHeaderRequestCultureProvider:从请求头中设置 “Accept-Language” 的值

我们也可以在这3个的基础上进行自定义配置,例如通过在 Query 中设置"lang"的值来设置区域:

options.AddInitialRequestCultureProvider(new QueryStringRequestCultureProvider() { QueryStringKey = "lang" });

AddInitialRequestCultureProvider默认将新添加的 Provider 放置在首位。

内置的还有一个RouteDataRequestCultureProvider,不过它并没有被默认添加到提供器列表中。它默认可以通过在路由中设置 culture 的值来设置区域,就像微软官方文档一样。需要注意的是,一定要在 app.UseRouting() 之后再调用 app.UseRequestLocalization()

实现自定义 RequestCultureProvider

实现自定义RequestCultureProvider的方式有两种,分别是通过委托和继承抽象类RequestCultureProvider

下面,我们实现一个从自定义 Header 中获取区域文化信息的自定义RequestCultureProvider

  1. 通过委托实现自定义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

  1. 通过继承抽象类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-Languagepublic 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<>));}
}

一共注册了两个实现,分别是ResourceManagerStringLocalizerFactoryStringLocalizer<>,先来看一下工厂:

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.xxxIResourceNamesCache 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,就是当 资源名 不存在时,不会抛出异常,而是返回 nullprotected string? GetStringSafely(string name, CultureInfo? culture){var keyCulture = culture ?? CultureInfo.CurrentUICulture;var cacheKey = $"name={name}&culture={keyCulture.Name}";// 资源已缓存为不存在,直接返回 nullif (_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,它接收两个参数,TypeIStringLocalizerFactory,返回一个IStringLocalizer。从这里我们就可以看出来,它的本地化就是通过IStringLocalizer来实现的。

默认情况下,它的本地化器指向当前模型类资源,我上面提到过,可以将其自定义为从共享资源中获取,这下你就理解为啥所有模类都会受影响了吧。

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;}}// 是否允许回退到父区域文化,默认值:truepublic bool FallBackToParentCultures { get; set; } = true;// 是否允许回退到父UI区域文化,默认值:truepublic bool FallBackToParentUICultures { get; set; } = true;// 是否要将当前请求的区域文化设置到响应头 Content-Language 中,默认值:falsepublic 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){// 检查区域文化(可能有多个)是否支持,如果不支持则返回 nullcultureInfo = GetCultureInfo(cultures,_options.SupportedCultures,_options.FallBackToParentCultures);}if (_options.SupportedUICultures != null){// 检查UI区域文化(可能有多个)是否支持,如果不支持则返回 nulluiCultureInfo = 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种本地化器:

    • IStringLocalizerIStringLocalizer<>:文本本地化器,是最常用的,可以通过依赖注入获取,也可以通过IStringLocalizerFactory来获取。IStringLocalizer<>是对IStringLocalizer的一层包装。
    • IHtmlLocalizerIHtmlLocalizer<>:HTML本地化器,顾名思义,可以本地化HTML文本而不会对其编码。可以通过依赖注入获取,也可以通过IHtmlLocalizerFactory来获取。
    • IViewLocalizer:视图本地化器,用于前端视图的本地化。
  • 通过AddLocalization设置资源根目录,并注册本地化服务IStringLocalizer<>IStringLocalizerFactory

  • 通过AddDataAnnotationsLocalization注册数据注解本地化服务,主要是设置DataAnnotationLocalizerProvider委托

  • 通过AddViewLocalization注册视图本地化服务IViewLocalizerIHtmlLocalizer<>IHtmlLocalizerFactory

  • 通过UseRequestLocalization

    启用请求本地化中间件 RequestLocalizationMiddleware,它可以从请求中解析出当前请求的区域文化信息并设置到当前的处理线程中。

    • 通过AddSupportedCulturesAddSupportedUICultures配置受支持的 Cultures 和 UICultures
    • 通过SetDefaultCulture配置默认 Culture
    • 默认提供了三种 RequestCultureProvider:
      • QueryStringRequestCultureProvider:通过在 Query 中设置"culture"、"ui-culture"的值,例如 ?culture=zh-CN&ui-culture=zh-CN
      • CookieRequestCultureProvider:通过Cookie中设置名为 “.AspNetCore.Culture” Key 的值,值形如 c=zh-CN|uic=zh-CN
      • AcceptLanguageHeaderRequestCultureProvider:从请求头中设置 “Accept-Language” 的值
    • 通过AddInitialRequestCultureProvider添加自定义RequestCultureProvider,可以通过委托传入解析逻辑,也可以继承RequestCultureProvider抽象类来编写更复杂的逻辑。
  • 可以通过 Nuget 包My.Extensions.Localization.Json将资源文件(.resx)更换为 Json 文件。

问题

Only the invariant culture is supported in globalization-invariant mode.

报错显示:这个问题是由于.NET Core应用程序在全球化不变的模式下运行,而该模式只支持不变的文化,而不支持其他任何文化。

解决方法:

    将.csproj中的 <InvariantGlobalization>true</InvariantGlobalization>里面的内容改成false即可。

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/830135.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二证券|股息分红和送股哪个好?

在股票投资中&#xff0c;股息分红和分红送股是两种常见的股票分红方法。关于股息分红和送股哪个好&#xff0c;第二证券下面就为我们具体介绍一下。 股息分红和送股哪个好并没有一个绝对的答案&#xff0c;它们只是上市公司向股东分配利润的不同方法。股息分红的好处是投资者…

武汉星起航:挂牌上海股权中心,开启资本新篇章助力跨境电商飞跃

2023年10月30日&#xff0c;武汉星起航电子商务有限公司在上海股权托管交易中心成功挂牌展示&#xff0c;标志着这家在跨境电商领域拥有卓越声誉的企业正式迈入了资本市场的大门。对于武汉星起航来说&#xff0c;这不仅是其发展历程中的一个重要里程碑&#xff0c;更是对其业务…

STM32-TIM定时器与PWM输出

学习目标&#xff1a; 1. 熟练掌握 TIM 的参数配置。 2. 掌握通道的参数配置。 3. 深刻理解 PWM 与功率的关系。 4. 理解 PWM 的原理示意。 一 什么是 PWM 输出 PWM &#xff08; pulse width modulation &#xff09;一种脉冲宽度调节技术。 PWM 的效果是什么样子&#xf…

AOMEI Partition Assistant傲梅分区助手技术员版:专业级的硬盘分区利器

在数字化时代&#xff0c;数据存储和管理变得愈发重要。对于电脑技术员而言&#xff0c;一款功能强大、操作简便的分区工具无疑是提高工作效率的得力助手。而傲梅分区助手技术员版&#xff08;AOMEI Partition Assistant&#xff09;正是这样一款备受赞誉的专业级硬盘分区软件。…

Day23.一刷数据结构算法(C语言版) 39组合总和;40组合总和II;131分割回文串

一、39组合总和 本题是集合里元素可以用无数次&#xff0c;那么和组合问题的差别&#xff0c;其实仅在于对startIndex上的控制 题目链接&#xff1a;组合总和 文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;带你学透回溯算法-组合总和 &#xff08;39.组合总和&#xff…

从零开始构建大语言模型(MEAP)

原文&#xff1a;annas-archive.org/md5/c19a4ef8ab1664a3c5a59d52651430e2 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 一、理解大型语言模型 本章包括 大型语言模型&#xff08;LLM&#xff09;背后的基本概念的高层次解释 探索 ChatGPT 类 LLM 源自的 Transfo…

workminer之dht通信部分

workminer是通过SSH爆破传播的挖矿木马&#xff0c;感染后会释放xmrig挖矿程序利用主机的CPU挖取北方门罗币。该样本能够执行特定的指令&#xff0c;指令保存在一个配置文件config中&#xff0c;config文件类似于xml文件&#xff0c;里面有要执行的指令和参数&#xff0c;样本中…

spring cloud eureka 初始化报错(A bean with that name has already been defined)

报错内容 The bean ‘eurekaRegistration’, defined in class path resource [org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration E u r e k a C l i e n t C o n f i g u r a t i o n . c l a s s ] , c o u l d n o t b e r e g i s t e r e d . A …

回归预测 | Matlab实现NGO-ESN北方苍鹰算法优化回声状态网络多输入单输出回归预测

回归预测 | Matlab实现NGO-ESN北方苍鹰算法优化回声状态网络多输入单输出回归预测 目录 回归预测 | Matlab实现NGO-ESN北方苍鹰算法优化回声状态网络多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现NGO-ESN北方苍鹰算法优化回声状态网络…

《QT实用小工具·四十五》可以在界面上游泳的小鱼

1、概述 源码放在文章末尾 该项目实现了灵动的小鱼&#xff0c;可以在界面上跟随鼠标点击自由的游泳&#xff0c;项目demo演示如下所示&#xff1a; 项目部分代码如下所示&#xff1a; #include "magicfish.h" #include <QtMath> #include <QPainter>…

C++:map和set的封装

关于红黑树的模拟实现&#xff0c;大家不清楚的先去看看博主的博客再来看这篇文章&#xff0c;因为set和map的封装底层都是利用用的红黑树。所以这里不会过多介绍红黑树的相关内容&#xff0c;而更多的是去为了契合STL中的红黑树去进行改造&#xff0c;让封装的set和map能够去复…

CMake使用

一、CMake 是什么 CMake 是一个跨平台的自动化构建系统&#xff0c;它使用配置文件 CMakeLists.txt 来管理软件构建过程。CMake 基于 Makefile 做了二次开发。 二、单个文件目录 # CMake 最低版本号要求 cmake_minimum_required(VERSION 3.16.3)# 工程名 project(CMakeSingle)…

uniapp自定义返回事件(封装)

uniapp自定义返回事件 在我们使用uniapp时&#xff0c;我们导航栏一般都是自定义的&#xff0c;比如用uview框架的导航栏&#xff0c;那么返回事件通常会遇到以下几个问题 返回事件前需要做一些额外的处理 h5项目刷新页面后返回失效 返回按钮点击后到指定页面 如果只是监听返…

PhotosCollage for Mac:优雅且实用的照片拼贴软件

PhotosCollage for Mac是一款优雅且实用的照片拼贴软件&#xff0c;为Mac用户提供了一个便捷、高效的平台&#xff0c;以创建精美、个性化的照片拼贴作品。 PhotosCollage for Mac v1.4.1激活版下载 该软件界面简洁直观&#xff0c;操作便捷。用户只需将想要拼贴的照片拖入“照…

CSS基础:position定位的5个类型详解!

你好&#xff0c;我是云桃桃。 一个希望帮助更多朋友快速入门 WEB 前端的程序媛。 云桃桃-大专生&#xff0c;一枚程序媛&#xff0c;感谢关注。回复 “前端基础题”&#xff0c;可免费获得前端基础 100 题汇总&#xff0c;回复 “前端工具”&#xff0c;可获取 Web 开发工具合…

Andorid复习

组件 TextView 阴影 android:shadowColor"color/red" 阴影颜色android:shadowRadius"3.0" 阴影模糊度&#xff08;大小&#xff09;android:shadowDx"10.0" 横向偏移android:shadowDy"10.0" 跑马灯 这里用自定义控件 public cla…

日本极致产品力 | 源自内蒙古,日本99.7%的人都喝过都百年畅销饮料

​《极致产品力》日本深度研学是一个顾问式课程,可以帮助企业找产品、找方向、找方法,在日本终端市场考察中洞悉热销产品背后的成功逻辑,了解最新最前沿的产品趋势和机会。结合日本消费趋势中国转化的众多经验,从品牌、包装、卖点、技术和生产工艺等多方面寻找中国市场的解决方…

WIFI/BT中蓝牙的硬件资源是如何调度的 UART和PCM接口传输的是什么信号

安卓或IOS手机中&#xff0c;wifi/bt中的蓝牙是如何调度硬件资源的&#xff0c;尤其是UART和PCM是如何分配的。M.2 wifi/bt模块或其他形式的模块中&#xff0c;蓝牙是如何调度硬件资源的&#xff0c;尤其是UART和PCM是如何分配的。今天我们就图文并茂的解决这个问题。 蓝牙文件…

新买的设备自带的仪器校准证书,是否可以作为校准报告使用?

设备在刚刚购买时&#xff0c;有些商家会承诺&#xff0c;在交付设备的同时&#xff0c;还会交付设备的校准证书&#xff0c;这证书是附赠的&#xff0c;属于商家给客户的一种福利&#xff0c;面对附赠的仪器校准证书&#xff0c;很多客户也会有疑惑&#xff0c;这附赠的证书有…

UTONMOS:用区块链技术拓展商业边界在哪里?

引言 大约从 2021 年Web 3 这个新概念开始受到风险基金和科技圈的普遍关注。但如果你对过去几年区块链的发展历史足够了解&#xff0c;就应该已经意识到现在的 Web 3 并不是什么新技术&#xff0c;甚至不是旧技术的进步&#xff0c;它只是一个基于区块链技术的宏大构想。 我是…