理解ASP.NET Core - 全球化&本地化&多语言(Globalization and Localization)

这篇具有很好参考价值的文章主要介绍了理解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”的父区域性。

区域代码查询:https://www.venea.net/web/culture_code

实现本地化

一般情况下,统一使用英文作为多语言的字典Key,在 Web 刚进入开发阶段时,最好就支持多语言,否则后续改造的工作量会比较大。当然,你可以选择使用中文作为 Key,不过并不太推荐,毕竟你总不能要求懂阿拉伯语的人要懂中文。

本地化器

ASP.NET Core 提供了多种本地化工具:

  • 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
名称
CurrentTime Current Time:
  • SharedResource.zh-CN.resx
名称
CurrentTime 当前时间:
  • HomeController.en-US.resx
名称
HelloWorld Hello, 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,获取结果如下:

当前区域文化: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),检查资源放置位置是否有误。

在模型验证中使用本地化

好了,我们已经掌握了本地化在服务类中的使用方法,接下来,一起来看下在模型验证中如何使用本地化。

  1. 首先通过调用AddDataAnnotationsLocalization注册数据注解本地化服务:
builder.Services
    .AddControllersWithViews()
    .AddDataAnnotationsLocalization();
  1. 接着在 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 密码长度不能小于
UserNameIsRequired 请输入用户名
  1. 最后在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

相对于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
名称
Welcome <b>Welcome {0}!</b>
  • SharedResource.zh-CN.resx
名称
Welcome <b>欢迎 {0}!</b>
  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
名称
Welcome Welcome {0} !!!
  • Index.zh-CN
名称
Welcome 欢迎 {0} !!!
  1. 在视图中使用并查看效果
@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,分别为:

  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" 的值

如果只传了 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

  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-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<>));
    }
}

一共注册了两个实现,分别是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.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,它接收两个参数,TypeIStringLocalizerFactory,返回一个IStringLocalizer。从这里我们就可以看出来,它的本地化就是通过IStringLocalizer来实现的。

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

IViewLocalizer & IHtmlLocalizer

IViewLocalizerIHtmlLocalizerIHtmlLocalizer<>这里就不再深入了,毕竟现在前端更多的是用三大前端框架,等用到的时候再去了解吧。

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;
    }
}

总结

通过以上内容,我们可以总结出以下核心知识点:文章来源地址https://www.toymoban.com/news/detail-479740.html

  • 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 文件。

到了这里,关于理解ASP.NET Core - 全球化&本地化&多语言(Globalization and Localization)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ASP.NET Core 3.1系列(13)——本地缓存MemoryCache的使用

    在实际开发过程中,缓存( Cache )是一项重要技术。有时候为了缓解数据库访问的压力,我们可以将一些需要经常读取但又几乎不会变化的数据存在缓存里,以此加快数据的访问速度。在 ASP.NET Core 中,缓存一般分为本地缓存和分布式缓存。相较于分布式缓存( Redis ),本地

    2024年02月05日
    浏览(50)
  • 安全机密管理:Asp.Net Core中的本地敏感数据保护技巧

    在我们开发过程中基本上不可或缺的用到一些敏感机密数据,比如 SQL 服务器的连接串或者是 OAuth2 的 Secret 等,这些敏感数据在代码中是不太安全的,我们不应该在源代码中存储密码和其他的敏感数据,一种推荐的方式是通过 Asp.Net Core 的 机密管理器 。 在 ASP.NET Core 中,机密

    2024年04月25日
    浏览(40)
  • asp.net core 项目从本地部署到远程云服务器

    今天向大家详细介绍一下,如何将一个.net core项目部署到远程云服务器,我的服务器镜像为 window server 2019. 1.安装IIS 首先在服务器上安装IIS(我的是最新的iis10),我已经安装完毕,这个安装操作比较简单,自行谷歌。 安装完成后会看到下面的界面: 2.安装web deploy程序 该程序的功能: 它

    2023年04月22日
    浏览(47)
  • 网站本地化翻译、建设助力企业拓展全球市场 安睿杰翻译

    企业开展海外业务,建立面向目标地区或国家的海外站或独立站,离不开网站的本地化。 网站本地化分为:网站本地化翻译、网站本地化建设。 【网站本地化翻译】是指将网站的文本、图片、音频、视频等内容中的语言转换成目标地区用户的母语。 【网站本地化建设】是指

    2024年01月25日
    浏览(77)
  • ASP.NET Core教程:ASP.NET Core 程序部署到Windows系统

    本篇文章介绍如何将一个ASP.NET Core Web程序部署到Windows系统上。这里以ASP.NET Core WebApi为例进行讲解。首先创建一个ASP.NET Core WebApi项目,使用默认的Values控制器,这里使用Visual Studio 2019创建一个ASP.NET Core 3.1d的WebApi项目。 创建新项目的时候选项ASP.NET Core Web应用程序,如下图所

    2023年04月08日
    浏览(52)
  • ASP.NET Core MVC -- 将视图添加到 ASP.NET Core MVC 应用

    右键单击“视图”文件夹,然后单击“添加”“新文件夹”,并将文件夹命名为“HelloWorld”。 右键单击“Views/HelloWorld”文件夹,然后单击“添加”“新项”。 在“添加新项 - MvcMovie”对话框中: 在右上角的搜索框中,输入“视图” 选择“Razor 视图 - 空” 保持“名称”框的

    2024年02月13日
    浏览(85)
  • ASP.NET和ASP.NET Core的区别

    ASP.NET和ASP.NET Core是两个不同的Web应用程序框架,它们都是由Microsoft开发的。ASP.NET是Microsoft推出的第一个Web应用程序框架,而ASP.NET Core是其最新版本。本文将介绍ASP.NET和ASP.NET Core的简介和区别。 ASP.NET的简介 ASP.NET是一个基于.NET框架的Web应用程序框架,它是Microsoft推出的第一

    2024年02月16日
    浏览(72)
  • Asp.Net VS ASP.NET Core 请求管道

    参考链接 ASP.NET CORE 启动过程及源码解读 请求进入Asp.Net工作进程后,由进程创建HttpWorkRequest对象,封装此次请求有关的所有信息,然后进入HttpRuntime类进行进一步处理。HttpRuntime通过请求信息创建HttpContext上下文对象,此对象将贯穿整个管道,直到响应结束。同时创建或从应用

    2024年02月04日
    浏览(78)
  • 【ASP.NET Core 基础知识】--最佳实践和进阶主题--设计模式在ASP.NET Core中的应用

    一、设计模式概述 1.1 什么是设计模式 设计模式是在软件设计过程中反复出现的、经过验证的、可重用的解决问题的方法。它们是针对特定问题的通用解决方案,提供了一种在软件开发中可靠的指导和标准化方法。设计模式通常描述了一种在特定情景下的解决方案,包括了问

    2024年02月21日
    浏览(133)
  • ASP.NET Core SingleR Core:WebApi + .net 客户端开发

    我之前稍微研究了一下SignalR Core。用起来还行。简单来说SignalR就是用来解决实时通讯的问题的。 ASP.NET Core SingleR:初次体验和简单项目搭建 SignalR支持三种客户端,C#,Java,JavaScirpt。基本够用了。本身就是微软开发的,肯定支持自己的语言。因为是Websocket的上层封装,所以也要支

    2024年01月20日
    浏览(117)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包