ABP Localize
ABP Localize
2023/6/1
➡️

asp.net core localize

配置服务

//增加 localize 服务,如果不定义 ResourcesPath ,默认的根路径是项目的根目录
//此方法内部也会加入核心服务
builder.Services.AddLocalization(options => { options.ResourcesPath = "resource";});

//配置本地 request 支持的多语言,如果不配做默认使用当前电脑的区域语言
builder.Services.AddRequestLocalization(options => {
  var cultures = new[] { "zh-CN", "en-US" };
  //Culture:影响日期、时间、数字或货币的展示格式
  options.AddSupportedCultures(cultures);
  //UICulture:影响查找哪些区域文化资源(如.resx、json文件等),
  //也就是说,如果这里未添加某区域文化A,
  //即使添加了对应区域文化A的资源文件,也无发生效。一般 Culture 和 UICulture 保持一致。
  options.AddSupportedUICultures(cultures);
  options.SetDefaultCulture(cultures[0]);
  // 当Http响应时,将 当前区域信息 设置到 Response Header:Content-Language 中
  options.ApplyCurrentCultureToResponseHeaders = true;
});

//配置中间件
app.UseRequestLocalization();

嵌入资源文件方式使用

asp.net core 默认的资源是嵌入的本地资源文件 后缀为 resx 文件

  1. controller 单独的资源

资源按照 controller 进行区分 ,每个单独的 controller 都有对应的资源文件

文件发现方式有来中 . 和 / ,

options.ResourcesPath = "resource"; 项目的根目录下找 resource 目录下的资源文件

文件发现的俩种方式

  1. / 真实目录 resource/controller/xxxController.en-US.resx 2 . 通过文件名 resource/controller.xxxController.en-US.resx

  2. 共享资源在根目录下定义一个空类,如果此空类位于文件夹下 localize

     namespace WebApplication2.localize
     {
       //伪类
       public class ShareResource
       {
    
    
       }
     }
    

    在 resource 下定义文件夹 localize,文件夹内添加同名的资源文件 ShareResource.en-us.resx

    如果觉得把共享伪类和资源文件分开比较麻烦也可以合并,在资源文件的根目录内新建伪类 ShareResource,并把资源文件合并到伪类里去

     <ItemGroup>
       <EmbeddedResource
        Include="resource/shareResource.en-US.resx" DependentUpon="ShareResource" />
       <EmbeddedResource
       Include="Resource/shareResource.zh-CN.resx" DependentUpon="ShareResource" />
     </ItemGroup>
    

Controller 里使用资源

 //在 xxxController 构造函数里通过 IStringLocalizer 注入
 public xxxController(IStringLocalizer<xxxController>
  localizer,IHttpContextAccessor httpContextAccessor) {
     var localize=localizer
 }

  //在 xxxController 构造函数里通过 IStringLocalizerFactory 注入
  public xxxController(IStringLocalizerFactory factory) {
    _factory = factory;
     var type = typeof(Test2Controller);
     var localize = _factory.Create(type);
  }

   //共享资源也可以通过以上俩种方法进行注入
   public xxxController(IStringLocalizerFactory factory) {
    _factory = factory;
     var type = typeof(ShareResource);
     var localize = _factory.Create(type);
  }

在客户端设置 headers 里的 Accept-Language ,为指定的区域语言字符串

资源传递参数

在 resx 文件里设置 key 为 hello , value 为 hello{0}is{1} 占位符 {0} ,{1},是在使用的时 候被替换的

 var i=0;
 var j=1;
 //传递参数
 var msg = _localizer[$"hello",i,j];

通过 IStringLocalizerFactory 发现资源

共享伪类本质也是如下代码的原理,只是方便编写路径

[HttpPost("index1")]
public ActionResult Get([FromServices]
IStringLocalizerFactory stringLocalizerFactory) {
  /*直接通过程序集定位的方式,第二参数指定程序集名称,要求是程序集名称和根命名空间要一致
    根命名空间是在写一个类时,不指定任何命名空间,默认使用的就是根命名空间
    第一个参数指定使用哪个资源文件,根路径配置的不用写,余下的要写出来,只能通过 . 不能通过 /
    localize.shareResource 指的是配置的根路径下的 localize
    文件夹下的 shareResource 开头的资源文件
    shareResource.en-US.resx 等资源文件
  */

    //查找定位器
    IStringLocalizer stringLocalizer =
    stringLocalizerFactory.Create("localize.shareResource", "WebApplication1");
    //获取定位到的字符串  2,5 是传递的参数
    LocalizedString i = tmp.GetString("hello",2,5);
    return Ok(i);
}

model 里验证错误消息的多语言

指定全局的错误验证多语言提供类

builder.Services.AddControllers()
  .AddDataAnnotationsLocalization(options => {
    //指定全局的,如果不指定,资源文件名要和 模型名保持一致,这样要建很多资源文件
    options.DataAnnotationLocalizerProvider = (type, factory) =>
      factory.Create(typeof(ModuleVerification));
  });

在项目根目录下创建文件夹 dto,生成空的错误验证类

public class ModuleVerification {

}

相应的在 resource 目录下创建 dto 目录并在此目录下创建如下资源文件

moduleVerification.en-US.resxmoduleVerification.zh-CN.resx

文件内的写法如下 占位符的含义 {0} 是字段名称,{1} ,{2} 是 验证特性里填入的参数 key1 {0} must gt {1} less {2} key1 {0} 大于 {1} 小于 {2}

public class ModuleA {
  [Required]
  // 6 和 10  会作为占位参数传递给资源提供文件的占位符 {0} ,{1}
  [Range(6,10, ErrorMessage = "key1")]
  public int Id { get; set; }
}

手动处理模型验证错误,全局禁用自动验证模型

builder.Services.Configure<ApiBehaviorOptions>(options => {
  options.SuppressModelStateInvalidFilter = true;
});

//在controller 里 使用 ModuleA 模型验证的多语言错误消息

[HttpPost("index")]
public  ActionResult Get([FromBody]ModuleA moduleA ) {

    var result= TryValidateModel(moduleA);
    if (!result) {
      var errs = new List<ErrorModel>();
      foreach (var modelStateKey in ModelState.Keys) {
        var ii = ModelState[modelStateKey].Errors;
        foreach (var modelError in ii) {
          errs.Add(new ErrorModel(modelStateKey, modelError.ErrorMessage) );
        }
      }




      return Ok(errs);
    }
    return Ok(moduleA);
  }

区域性回退

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

  1. HomeController.zh-CN.resx
  2. HomeController.zh.resx
  3. HomeController.resx 如果都没找到,则会返回资源 Key 本身

配置 CultureProvider

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" 的值
//可以在这里添加自定义的 IRequestCultureProvider,所有的 IRequestCultureProvider 都是给  RequestLocalizationMiddleware 中间件使用
builder.Services.AddRequestLocalization(p => {

});

asp.net core 对多语言的实现

IStringLocalizer 字符串定位器 及 LocalizedString 定位到的字符串

定位到的字符串

namespace Microsoft.Extensions.Localization
{
  public class LocalizedString
  {
    public LocalizedString(string name, string value)
      : this(name, value, false)
    {
    }
    public LocalizedString(string name, string value, bool resourceNotFound)
      : this(name, value, resourceNotFound, (string) null)
    {
    }
    public LocalizedString(
      string name,
      string value,
      bool resourceNotFound,
      string? searchedLocation)
    {
      if (name == null)
        throw new ArgumentNullException(nameof (name));
      if (value == null)
        throw new ArgumentNullException(nameof (value));
      this.Name = name;
      this.Value = value;
      this.ResourceNotFound = resourceNotFound;
      this.SearchedLocation = searchedLocation;
    }
    public static implicit operator string?(LocalizedString localizedString)
    => localizedString?.Value;
    public string Name { get; }
    public string Value { get; }
    public bool ResourceNotFound { get; }
    //当资源找不到的时候可以看下此路径,自己配置的资源路径是否和它一致
    public string? SearchedLocation { get; }
    public override string ToString() => this.Value;
    public override string ToString() => this.Value;
  }
}

字符串定位器

public interface IStringLocalizer
{
    LocalizedString this[string name] { get; }
    LocalizedString this[string name, params object[] arguments] { get; }
    IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
}

// IStringLocalizer 字符串定位器实现,在资源文件里找
public class ResourceManagerStringLocalizer : IStringLocalizer
{
    private readonly ConcurrentDictionary<string, object> _missingManifestCache =
    new ConcurrentDictionary<string, object>();
    private readonly IResourceNamesCache _resourceNamesCache;
    private readonly ResourceManager _resourceManager;
    private readonly IResourceStringProvider _resourceStringProvider;
    private readonly string _resourceBaseName;
    private readonly ILogger _logger;

    internal ResourceManagerStringLocalizer(
      ResourceManager resourceManager,
      IResourceStringProvider resourceStringProvider,
      string baseName,
      IResourceNamesCache resourceNamesCache,
      ILogger logger)
     {
      if (resourceManager == null)
        throw new ArgumentNullException(nameof (resourceManager));
      if (resourceStringProvider == null)
        throw new ArgumentNullException(nameof (resourceStringProvider));
      if (baseName == null)
        throw new ArgumentNullException(nameof (baseName));
      if (resourceNamesCache == null)
        throw new ArgumentNullException(nameof (resourceNamesCache));
      if (logger == null)
        throw new ArgumentNullException(nameof (logger));
      this._resourceStringProvider = resourceStringProvider;
      this._resourceManager = resourceManager;
      this._resourceBaseName = baseName;
      this._resourceNamesCache = resourceNamesCache;
      this._logger = logger;
    }
}

public interface IStringLocalizer<out T> : IStringLocalizer
{

}

//实现类
public class StringLocalizer<TResourceSource> : IStringLocalizer<TResourceSource>
{
    //
    private readonly IStringLocalizer _localizer;

    //通过工厂类生成 IStringLocalizer
    public StringLocalizer(IStringLocalizerFactory factory)
    {
        if (factory == null)
        {
            throw new ArgumentNullException(nameof(factory));
        }

        // 工厂生成的是 ResourceManagerStringLocalizer
        _localizer = factory.Create(typeof(TResourceSource));
    }

    public virtual LocalizedString this[string name]
    {
        get
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            return _localizer[name];
        }
    }


    public virtual LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            return _localizer[name, arguments];
        }
    }

    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>
        _localizer.GetAllStrings(includeParentCultures);
}

IStringLocalizerFactory 具体语言资源来自哪里是在这个类决定的

如果想要实现自己的多语言内容提供,必须要实现此类,实现 create 方法

namespace Microsoft.Extensions.Localization

public interface IStringLocalizerFactory
{
  IStringLocalizer Create(Type resourceSource);
  IStringLocalizer Create(string baseName, string location);
}

public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly IResourceNamesCache _resourceNamesCache = (IResourceNamesCache)
    new ResourceNamesCache();
    private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer>
    _localizerCache = new ConcurrentDictionary<string, ResourceManagerStringLocalizer>();
    private readonly string _resourcesRelativePath;
    private readonly ILoggerFactory _loggerFactory;


    public ResourceManagerStringLocalizerFactory(
      IOptions<LocalizationOptions> localizationOptions,
      ILoggerFactory loggerFactory)
    {
      if (localizationOptions == null)
        throw new ArgumentNullException(nameof (localizationOptions));
      if (loggerFactory == null)
        throw new ArgumentNullException(nameof (loggerFactory));
      this._resourcesRelativePath = localizationOptions.Value.ResourcesPath ??
       string.Empty;
      this._loggerFactory = loggerFactory;
      if (string.IsNullOrEmpty(this._resourcesRelativePath))
        return;

      // 将目录分隔符“/”和“\”全部替换为“.”
      this._resourcesRelativePath = this._resourcesRelativePath.
      Replace(Path.AltDirectorySeparatorChar, '.').
      Replace(Path.DirectorySeparatorChar, '.') + ".";
    }


    protected virtual string GetResourcePrefix(TypeInfo typeInfo) =>
     !((Type) typeInfo == (Type) null) ? this.GetResourcePrefix(typeInfo,
     this.GetRootNamespace(typeInfo.Assembly), this.GetResourcePath(typeInfo.Assembly)) :
      throw new ArgumentNullException(nameof (typeInfo));


    protected virtual string GetResourcePrefix(
      TypeInfo typeInfo,
      string? baseNamespace,
      string? resourcesRelativePath)
    {
      if ((Type) typeInfo == (Type) null)
        throw new ArgumentNullException(nameof (typeInfo));
      if (string.IsNullOrEmpty(baseNamespace))
        throw new ArgumentNullException(nameof (baseNamespace));
      if (string.IsNullOrEmpty(typeInfo.FullName))
        throw new ArgumentException(Microsoft.Extensions.Localization.Resources.
        FormatLocalization_TypeMustHaveTypeName((object) typeInfo));
      return string.IsNullOrEmpty(resourcesRelativePath) ? typeInfo.FullName :
      baseNamespace + "." + resourcesRelativePath + ResourceManagerStringLocalizerFactory.
      TrimPrefix(typeInfo.FullName, baseNamespace + ".");
    }


    protected virtual string GetResourcePrefix(string baseResourceName, string baseNamespace)
    {
      if (string.IsNullOrEmpty(baseResourceName))
        throw new ArgumentNullException(nameof (baseResourceName));
      Assembly assembly = !string.IsNullOrEmpty(baseNamespace) ? Assembly.Load(
        new AssemblyName(baseNamespace)) : throw new ArgumentNullException(nameof (baseNamespace));
      baseResourceName = this.GetRootNamespace(assembly) + "." + this.
      GetResourcePath(assembly) + ResourceManagerStringLocalizerFactory.
      TrimPrefix(baseResourceName, baseNamespace + ".");
      return baseResourceName;
    }


    public IStringLocalizer Create(Type resourceSource)
    {
      if (resourceSource == (Type) null)
        throw new ArgumentNullException(nameof (resourceSource));
      ResourceManagerStringLocalizer managerStringLocalizer;
      if (!this._localizerCache.TryGetValue(resourceSource.AssemblyQualifiedName,
      out managerStringLocalizer))
      {
        TypeInfo typeInfo = resourceSource.GetTypeInfo();
        string resourcePrefix = this.GetResourcePrefix(typeInfo);
        managerStringLocalizer = this.CreateResourceManagerStringLocalizer(
          typeInfo.Assembly, resourcePrefix);
        this._localizerCache[resourceSource.AssemblyQualifiedName] =
        managerStringLocalizer;
      }
      return (IStringLocalizer) managerStringLocalizer;
    }


    public IStringLocalizer Create(string baseName, string location)
    {
      if (baseName == null)
        throw new ArgumentNullException(nameof (baseName));
      if (location == null)
        throw new ArgumentNullException(nameof (location));
      return (IStringLocalizer) this._localizerCache.GetOrAdd("B=" +
       baseName + ",L=" + location, (Func<string, ResourceManagerStringLocalizer>) (_ =>
      {
        Assembly assembly = Assembly.Load(new AssemblyName(location));
        baseName = this.GetResourcePrefix(baseName, location);
        return this.CreateResourceManagerStringLocalizer(assembly, baseName);
      }));
    }

    protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(
      Assembly assembly,
      string baseName)
    {
      return new ResourceManagerStringLocalizer(new ResourceManager(
        baseName, assembly), assembly, baseName, this._resourceNamesCache,
        (ILogger) this._loggerFactory.CreateLogger<ResourceManagerStringLocalizer>());
    }

    protected virtual string GetResourcePrefix(
      string location,
      string baseName,
      string resourceLocation)
    {
      return location + "." + resourceLocation + ResourceManagerStringLocalizerFactory.
      TrimPrefix(baseName, location + ".");
    }

    protected virtual ResourceLocationAttribute? GetResourceLocationAttribute(
      Assembly assembly) => assembly.GetCustomAttribute<ResourceLocationAttribute>();

    protected virtual RootNamespaceAttribute? GetRootNamespaceAttribute(
      Assembly assembly) => assembly.GetCustomAttribute<RootNamespaceAttribute>();

    private string GetRootNamespace(Assembly assembly)
    {
      RootNamespaceAttribute namespaceAttribute = this.GetRootNamespaceAttribute(assembly);
      return namespaceAttribute != null ? namespaceAttribute.RootNamespace :
      assembly.GetName().Name;
    }

    private string GetResourcePath(Assembly assembly)
    {
      ResourceLocationAttribute locationAttribute = this.GetResourceLocationAttribute(assembly);
      return (locationAttribute == null ? this._resourcesRelativePath :
       locationAttribute.ResourceLocation + ".").Replace(Path.DirectorySeparatorChar,
        '.').Replace(Path.AltDirectorySeparatorChar, '.');
    }

    private static string TrimPrefix(string name, string prefix) =>
    name.StartsWith(prefix, StringComparison.Ordinal) ? name.Substring(prefix.Length) : name;
  }
}

在 asp.net core 里注入以上的服务

namespace Microsoft.Extensions.DependencyInjection
{

  public static class LocalizationServiceCollectionExtensions
  {

    public static IServiceCollection AddLocalization(this IServiceCollection services)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      services.AddOptions();
      LocalizationServiceCollectionExtensions.AddLocalizationServices(services);
      return services;
    }

    public static IServiceCollection AddLocalization(
      this IServiceCollection services,
      Action<LocalizationOptions> setupAction)
    {
      if (services == null)
        throw new ArgumentNullException(nameof (services));
      if (setupAction == null)
        throw new ArgumentNullException(nameof (setupAction));
      LocalizationServiceCollectionExtensions.AddLocalizationServices(services, setupAction);
      return services;
    }

    internal static void AddLocalizationServices(IServiceCollection services)
    {
      //注入核心服务
      services.TryAddSingleton<IStringLocalizerFactory, ResourceManagerStringLocalizerFactory>();

      services.TryAddTransient(typeof (IStringLocalizer<>), typeof (StringLocalizer<>));
    }

    internal static void AddLocalizationServices(
      IServiceCollection services,
      Action<LocalizationOptions> setupAction)
    {
      LocalizationServiceCollectionExtensions.AddLocalizationServices(services);
      //配置 Localization  ResourcesPath
      services.Configure<LocalizationOptions>(setupAction);
    }
  }
}

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
        {
            return _defaultRequestCulture;
        }
        set
        {
            if (value == null)
            {
                throw new ArgumentNullException(nameof(value));
            }

            _defaultRequestCulture = value;
        }
    }

    public bool FallBackToParentCultures { get; set; } = true;


    public bool FallBackToParentUICultures { get; set; } = true;


    public bool ApplyCurrentCultureToResponseHeaders { get; set; }


    public IList<CultureInfo>? SupportedCultures { get; set; } = new
     List<CultureInfo> { CultureInfo.CurrentCulture };

    public IList<CultureInfo>? SupportedUICultures { get; set; } =
     new List<CultureInfo> { CultureInfo.CurrentUICulture };


    public IList<IRequestCultureProvider> RequestCultureProviders { get; set; }


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

    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 interface IRequestCultureProvider
{
  //在请求里获取所有的 Cultures ,UICultures 的字符串判断
  Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext);
}
public static IApplicationBuilder UseRequestLocalization(
        this IApplicationBuilder app,
        RequestLocalizationOptions options)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        return app.UseMiddleware<RequestLocalizationMiddleware>(Options.Create(options));
}

namespace Microsoft.AspNetCore.Localization
{
  public class RequestLocalizationMiddleware
  {
    //回退深度 en-US  如果找不到 en-US,就回退找 en
    //在请求里找到的区域字符串 如果 不在支持的列表里,
    就对请求的的 区域字符串 回退,回退规则是 -分割的字符串,往回找父级别
    //最多回退5次
    private const int MaxCultureFallbackDepth = 5;

    #nullable disable
    private readonly RequestDelegate _next;
    private readonly RequestLocalizationOptions _options;
    private readonly ILogger _logger;

    public RequestLocalizationMiddleware(
      RequestDelegate next,
      //本地化请求的配置项
      IOptions<RequestLocalizationOptions> options,
      ILoggerFactory loggerFactory)
    {
      if (options == null)
        throw new ArgumentNullException(nameof (options));
      this._next = next ?? throw new ArgumentNullException(nameof (next));
      this._logger = (ILogger) ((loggerFactory != null ?
       loggerFactory.CreateLogger<RequestLocalizationMiddleware>()
       : (ILogger<RequestLocalizationMiddleware>) null) ??
        throw new ArgumentNullException(nameof (loggerFactory)));
      this._options = options.Value;
    }

    public async Task Invoke(HttpContext context)
    {
      if (context == null)
        throw new ArgumentNullException(nameof (context));
      RequestCulture requestCulture = this._options.DefaultRequestCulture;
      IRequestCultureProvider winningProvider = (IRequestCultureProvider) null;
      if (this._options.RequestCultureProviders != null)
      {
        foreach (IRequestCultureProvider provider in (
          IEnumerable<IRequestCultureProvider>) this._options.RequestCultureProviders)
        {
          //通过 provider 获取 cultures ,cultures
          ProviderCultureResult providerCultureResult =
           await provider.DetermineProviderCultureResult(context);
          if (providerCultureResult != null)
          {
            IList<StringSegment> cultures = providerCultureResult.Cultures;
            IList<StringSegment> cultures = providerCultureResult.UICultures;
            CultureInfo culture = (CultureInfo) null;
            CultureInfo uiCulture = (CultureInfo) null;
            if (this._options.SupportedCultures != null)
            {
              culture = RequestLocalizationMiddleware.
              GetCultureInfo(cultures, this._options.SupportedCultures,
              this._options.FallBackToParentCultures);
              if (culture == null)
                this._logger.UnsupportedCultures(provider.GetType().Name, cultures);
            }
            if (this._options.SupportedUICultures != null)
            {
              uiCulture = RequestLocalizationMiddleware.
              GetCultureInfo(uiCultures, this._options.SupportedUICultures,
              this._options.FallBackToParentUICultures);
              if (uiCulture == null)
                this._logger.UnsupportedUICultures(provider.GetType().Name, uiCultures);
            }
            if (culture != null || uiCulture != null)
            {
              if (culture == null)
                culture = this._options.DefaultRequestCulture.Culture;
              if (uiCulture == null)
                uiCulture = this._options.DefaultRequestCulture.UICulture;
              //第一个能找出的 provider 就是设置默认的 requestCulture,后续的 provider 就不找了
              requestCulture = new RequestCulture(culture, uiCulture);
              winningProvider = provider;
              break;
            }
          }
        }
      }
      context.Features.Set<IRequestCultureFeature>((IRequestCultureFeature)
      new RequestCultureFeature(requestCulture, winningProvider));
      RequestLocalizationMiddleware.SetCurrentThreadCulture(requestCulture);
      //设置响应头
      if (this._options.ApplyCurrentCultureToResponseHeaders)
        context.Response.Headers.ContentLanguage = (StringValues)
        requestCulture.UICulture.Name;
      await this._next(context);
      //请求返回的时候区域文化设置为null
      requestCulture = (RequestCulture) null;
      winningProvider = (IRequestCultureProvider) null;
    }

    //设置全局的的语言区域信息
    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 (StringSegment cultureName in (IEnumerable<StringSegment>) cultureNames)
      {
        if (cultureName != (StringSegment) (string) null)
        {
          CultureInfo cultureInfo = RequestLocalizationMiddleware.
          GetCultureInfo(cultureName, supportedCultures, fallbackToParentCultures, 0);
          if (cultureInfo != null)
            return cultureInfo;
        }
      }
      return (CultureInfo) null;
    }

    //在支持的 supportedCultures 存在  StringSegment name,用户要求的多语言才能受支持
    private static CultureInfo GetCultureInfo(
      StringSegment name,
      IList<CultureInfo> supportedCultures)
    {
      if (name == (StringSegment) (string) null || supportedCultures == null)
        return (CultureInfo) null;
      CultureInfo ci = supportedCultures.FirstOrDefault<CultureInfo>(
        (Func<CultureInfo, bool>) (supportedCulture => StringSegment.
        Equals((StringSegment) supportedCulture.Name,
        name, StringComparison.OrdinalIgnoreCase)));

      return ci == null ? (CultureInfo) null : CultureInfo.ReadOnly(ci);
    }

    private static CultureInfo GetCultureInfo(
      StringSegment cultureName,
      IList<CultureInfo> supportedCultures,
      bool fallbackToParentCultures,
      int currentDepth)
    {
      CultureInfo cultureInfo = RequestLocalizationMiddleware.
      GetCultureInfo(cultureName, supportedCultures);
      if (cultureInfo == null & fallbackToParentCultures && currentDepth < 5)
      {
        int length = cultureName.LastIndexOf('-');
        if (length > 0)
          cultureInfo = RequestLocalizationMiddleware.
          GetCultureInfo(cultureName.Subsegment(0, length), supportedCultures,
          fallbackToParentCultures, currentDepth + 1);
      }
      return cultureInfo;
    }
  }
}

abp 实现原理

abp 多语言实现了自己的 IStringLocalizerFactory,自己定义了一个 ILocalizableString,IAsyncLocalizableString 接口获取资源

ILocalizableString,IAsyncLocalizableString 顶层接口

顶层接口表明找到 asp.net core 的 LocalizedString 的方法

namespace Volo.Abp.Localization;

//同步接口
public interface ILocalizableString
{
   LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory);
}
//异步接口
public interface IAsyncLocalizableString
{
    Task<LocalizedString> LocalizeAsync(IStringLocalizerFactory stringLocalizerFactory);
}

获取资源字符串的实现类,具体如何找到资源的此类有委托给 IStringLocalizerFactory

public class LocalizableString : ILocalizableString, IAsyncLocalizableString
{
    //资源名称
    public string? ResourceName { get; }

    //资料类型,伪类的名称
    public Type? ResourceType { get; }

    //需要定位的资源 key
    [NotNull]
    public string Name { get; }

    public LocalizableString(Type? resourceType, [NotNull] string name)
    {
        //必须要有资源 key
        Name = Check.NotNullOrEmpty(name, nameof(name));
        ResourceType = resourceType;

        if (resourceType != null)
        {
            //资源名称,如果有伪类,看伪类上是否有 LocalizationResourceNameAttribute ,有的化通过
            //LocalizationResourceNameAttribute 获取 ResourceName
            ResourceName = LocalizationResourceNameAttribute.GetName(resourceType);
        }
    }

    public LocalizableString([NotNull] string name, string? resourceName = null)
    {
        Name = Check.NotNullOrEmpty(name, nameof(name));
        ResourceName = resourceName;
    }

    public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory)
    {
        var localizer = CreateStringLocalizerOrNull(stringLocalizerFactory);
        if (localizer == null)
        {
            //找不到直接返回 key
            return new LocalizedString(Name, Name, resourceNotFound: true);
        }

        //定位到的资源结果
        var result = localizer[Name];

        if (result.ResourceNotFound && ResourceName != null)
        {
            /* Search in the default resource if not found in the provided resource */
            localizer = stringLocalizerFactory.CreateDefaultOrNull();
            if (localizer != null)
            {
                result = localizer[Name];
            }
        }

        return result;
    }

    public async Task<LocalizedString> LocalizeAsync(IStringLocalizerFactory
    stringLocalizerFactory)
    {
        var localizer = await CreateStringLocalizerOrNullAsync(stringLocalizerFactory);
        if (localizer == null)
        {
            throw new AbpException($"Set {nameof(ResourceName)} or
            configure the default localization resource type (in the AbpLocalizationOptions)!");
        }

        var result = localizer[Name];

        if (result.ResourceNotFound && ResourceName != null)
        {
            /* Search in the default resource if not found in the provided resource */
            localizer = stringLocalizerFactory.CreateDefaultOrNull();
            if (localizer != null)
            {
                result = localizer[Name];
            }
        }

        return result;
    }

    //生成资源定位器
    private IStringLocalizer? CreateStringLocalizerOrNull(
      IStringLocalizerFactory stringLocalizerFactory)
    {
        //用类型定位
        if (ResourceType != null)
        {
            return stringLocalizerFactory.Create(ResourceType);
        }

        //用名称定位
        if (ResourceName != null)
        {
            var localizerByName = stringLocalizerFactory.
            CreateByResourceNameOrNull(ResourceName);
            if (localizerByName != null)
            {
                return localizerByName;
            }
        }

        return stringLocalizerFactory.CreateDefaultOrNull();
    }

    private async Task<IStringLocalizer?> CreateStringLocalizerOrNullAsync(
      IStringLocalizerFactory stringLocalizerFactory)
    {
        if (ResourceType != null)
        {
            return stringLocalizerFactory.Create(ResourceType);
        }

        if (ResourceName != null)
        {
            var localizerByName = await stringLocalizerFactory.
            CreateByResourceNameOrNullAsync(ResourceName);
            if (localizerByName != null)
            {
                return localizerByName;
            }
        }

        return stringLocalizerFactory.CreateDefaultOrNull();
    }

    //方便生成对象
    public static LocalizableString Create<TResource>([NotNull] string name)
    {
        //生成定位到的字符串资源
        return Create(typeof(TResource), name);
    }

    public static LocalizableString Create(Type resourceType,[NotNull] string name)
    {
        return new LocalizableString(resourceType, name);
    }

    public static LocalizableString Create([NotNull] string name,
     string? resourceName = null)
    {
        return new LocalizableString(name, resourceName);
    }
}

ILocalizationResourceContributor 本地化的资源贡献器

public interface ILocalizationResourceContributor
{
    bool IsDynamic { get; }

    void Initialize(LocalizationResourceInitializationContext context);

    LocalizedString? GetOrNull(string cultureName, string name);

    void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary);

    Task FillAsync(string cultureName, Dictionary<string, LocalizedString> dictionary);

    Task<IEnumerable<string>> GetSupportedCulturesAsync();
}

LocalizationResourceContributorList 通过贡献器集合找需要的资源

public class LocalizationResourceContributorList : List<ILocalizationResourceContributor>
{

    //在贡献器集合里,倒序找,只找第一个符合的
    public LocalizedString? GetOrNull(
        string cultureName,
        string name,
        bool includeDynamicContributors = true)
    {
        foreach (var contributor in this.Select(x => x).Reverse())
        {
            if (!includeDynamicContributors && contributor.IsDynamic)
            {
                continue;
            }

            var localString = contributor.GetOrNull(cultureName, name);
            if (localString != null)
            {
                //找到就直接退出
                return localString;
            }
        }

        return null;
    }

    public void Fill(
        string cultureName,
        Dictionary<string, LocalizedString> dictionary,
        bool includeDynamicContributors = true)
    {
        foreach (var contributor in this)
        {
            if (!includeDynamicContributors && contributor.IsDynamic)
            {
                continue;
            }

            contributor.Fill(cultureName, dictionary);
        }
    }

    public async Task FillAsync(
        string cultureName,
        Dictionary<string, LocalizedString> dictionary,
        bool includeDynamicContributors = true)
    {
        foreach (var contributor in this)
        {
            if (!includeDynamicContributors && contributor.IsDynamic)
            {
                continue;
            }

            await contributor.FillAsync(cultureName, dictionary);
        }
    }

    //找出所有贡献器的区域语言
    internal async Task<IEnumerable<string>> GetSupportedCulturesAsync()
    {
        var cultures = new List<string>();

        foreach (var contributor in this)
        {
            cultures.AddRange(await contributor.GetSupportedCulturesAsync());
        }

        return cultures;
    }
}

VirtualFileLocalizationResourceContributorBase 虚拟文件本地化提供器


public abstract class VirtualFileLocalizationResourceContributorBase :
 ILocalizationResourceContributor
{
    //不是动态的
    public bool IsDynamic => false;

    private readonly string _virtualPath;
    private IVirtualFileProvider _virtualFileProvider = default!;
    private Dictionary<string, ILocalizationDictionary>? _dictionaries;
    private bool _subscribedForChanges;
    private readonly object _syncObj = new object();
    //包含资源名及继承的资源名及  CultureName
    private LocalizationResourceBase _resource = default!;

    protected VirtualFileLocalizationResourceContributorBase(string virtualPath)
    {
        _virtualPath = virtualPath;
    }

    public virtual void Initialize(LocalizationResourceInitializationContext context)
    {
        _resource = context.Resource;
        _virtualFileProvider = context.ServiceProvider.GetRequiredService<
        IVirtualFileProvider>();
    }

    public virtual LocalizedString? GetOrNull(string cultureName, string name)
    {
        return GetDictionaries().GetOrDefault(cultureName)?.GetOrNull(name);
    }

    public virtual void Fill(string cultureName, Dictionary<string,
    LocalizedString> dictionary)
    {
        GetDictionaries().GetOrDefault(cultureName)?.Fill(dictionary);
    }

    public Task FillAsync(string cultureName, Dictionary<string,
     LocalizedString> dictionary)
    {
        Fill(cultureName, dictionary);
        return Task.CompletedTask;
    }

    public Task<IEnumerable<string>> GetSupportedCulturesAsync()
    {
        return Task.FromResult((IEnumerable<string>)GetDictionaries().Keys);
    }

    private Dictionary<string, ILocalizationDictionary> GetDictionaries()
    {
        var dictionaries = _dictionaries;
        if (dictionaries != null)
        {
            return dictionaries;
        }

        lock (_syncObj)
        {
            dictionaries = _dictionaries;
            if (dictionaries != null)
            {
                return dictionaries;
            }
            //防止重复订阅
            if (!_subscribedForChanges)
            {
                //检测资源文件是否被改动
                ChangeToken.OnChange(() => _virtualFileProvider.Watch(
                  _virtualPath.EnsureEndsWith('/') + "*.*"),
                    () =>
                    {
                        //改动了,需要重新获取
                        _dictionaries = null;
                    });

                _subscribedForChanges = true;
            }

            //第一次加载,及后续变动重新生成
            dictionaries = _dictionaries = CreateDictionaries();
        }

        return dictionaries;
    }

    //
    private Dictionary<string, ILocalizationDictionary> CreateDictionaries()
    {
        var dictionaries = new Dictionary<string, ILocalizationDictionary>();

        foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath))
        {
            if (file.IsDirectory || !CanParseFile(file))
            {
                continue;
            }

            var dictionary = CreateDictionaryFromFile(file);

            if (dictionary == null)
            {
                continue;
            }

            if (dictionaries.ContainsKey(dictionary.CultureName))
            {
                throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()}
                dictionary has a culture name '{dictionary.CultureName}' which is
                already defined! Localization resource: {_resource.ResourceName}");
            }

            dictionaries[dictionary.CultureName] = dictionary;
        }

        return dictionaries;
    }

    protected abstract bool CanParseFile(IFileInfo file);

    protected virtual ILocalizationDictionary? CreateDictionaryFromFile(IFileInfo file)
    {
        using (var stream = file.CreateReadStream())
        {
            return CreateDictionaryFromFileContent(
              Utf8Helper.ReadStringFromStream(stream));
        }
    }

    protected abstract ILocalizationDictionary?
    CreateDictionaryFromFileContent(string fileContent);
}
JsonVirtualFileLocalizationResourceContributor 通过 JSON 文件贡献

JSON 文件包含的信息

public class JsonLocalizationFile
{
    //语言信息
    public string Culture { get; set; } = default!;
    //键值对保存多语言信息
    public Dictionary<string, string> Texts { get; set; }
    public JsonLocalizationFile()
    {
        Texts = new Dictionary<string, string>();
    }
}
public class JsonVirtualFileLocalizationResourceContributor :
VirtualFileLocalizationResourceContributorBase
{
    public JsonVirtualFileLocalizationResourceContributor(string virtualPath)
        : base(virtualPath)
    {

    }

    protected override bool CanParseFile(IFileInfo file)
    {
        return file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
    }

    protected override ILocalizationDictionary?
     CreateDictionaryFromFileContent(string jsonString)
    {
        return JsonLocalizationDictionaryBuilder.BuildFromJsonString(jsonString);
    }
}
public class JsonVirtualFileLocalizationResourceContributor :
 VirtualFileLocalizationResourceContributorBase
{
    public JsonVirtualFileLocalizationResourceContributor(string virtualPath)
        : base(virtualPath)
    {

    }

    protected override bool CanParseFile(IFileInfo file)
    {
        return file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
    }

    //反序列化JSON 文件的内容 , 要求文件的内容格式要符合 JsonLocalizationFile,才能被反序列化成功
    protected override ILocalizationDictionary?
    CreateDictionaryFromFileContent(string jsonString)
    {
        return JsonLocalizationDictionaryBuilder.BuildFromJsonString(jsonString);
    }
}

LocalizationResource 资源和资源贡献者

资源继承

public interface IInheritedResourceTypesProvider
{
    [NotNull]
    Type[] GetInheritedResourceTypes();
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class InheritResourceAttribute : Attribute, IInheritedResourceTypesProvider
{
    public Type[] ResourceTypes { get; }

    public InheritResourceAttribute(params Type[] resourceTypes)
    {
        ResourceTypes = resourceTypes ?? new Type[0];
    }

    public virtual Type[] GetInheritedResourceTypes()
    {
        return ResourceTypes;
    }
}

资源及找到这些资源的贡献器类

public abstract class LocalizationResourceBase
{
    [NotNull]
    public string ResourceName { get; }

    //继承的资源,可以继承多个
    public List<string> BaseResourceNames { get; }

    public string? DefaultCultureName { get; set; }

    // 此资源关联的贡献器
    [NotNull]
    public LocalizationResourceContributorList Contributors { get; }

    public LocalizationResourceBase(
        [NotNull] string resourceName,
        string? defaultCultureName = null,
        ILocalizationResourceContributor? initialContributor = null)
    {
        ResourceName = Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName));
        DefaultCultureName = defaultCultureName;

        Contributors = new LocalizationResourceContributorList();
        BaseResourceNames = new();

        if (initialContributor != null)
        {
            Contributors.Add(initialContributor);
        }
    }
}

这种资源是通过伪类来关联的,找资源是可以通过名称或类型找

public class LocalizationResource : LocalizationResourceBase
{
    [NotNull]
    public Type ResourceType { get; }

    public LocalizationResource(
        [NotNull] Type resourceType,
        string? defaultCultureName = null,
        ILocalizationResourceContributor? initialContributor = null)
        : base(
            LocalizationResourceNameAttribute.GetName(resourceType),
            defaultCultureName,
            initialContributor)
    {
        ResourceType = Check.NotNull(resourceType, nameof(resourceType));
        //添加继承的资源
        AddBaseResourceTypes();
    }

    protected virtual void AddBaseResourceTypes()
    {
        var descriptors = ResourceType
            .GetCustomAttributes(true)
            .OfType<IInheritedResourceTypesProvider>();

        foreach (var descriptor in descriptors)
        {
            foreach (var baseResourceType in descriptor.GetInheritedResourceTypes())
            {
                BaseResourceNames.AddIfNotContains(
                  LocalizationResourceNameAttribute.GetName(baseResourceType));
            }
        }
    }
}

这种资源是通过资源名称来关联的,找资源只能通过名称来找

public class NonTypedLocalizationResource : LocalizationResourceBase
{
    public NonTypedLocalizationResource(
        [NotNull] string resourceName,
        string? defaultCultureName = null,
        ILocalizationResourceContributor? initialContributor = null
    ) : base(
        resourceName,
        defaultCultureName,
        initialContributor)
    {
    }
}

LocalizationResourceDictionary

LocalizationResource 通过伪类或名称关联 Dictionary<Type, LocalizationResourceBase> 的字 典

public class LocalizationResourceDictionary : Dictionary<string, LocalizationResourceBase>
{
    private readonly Dictionary<Type, LocalizationResourceBase> _resourcesByTypes = new();

    public LocalizationResource Add<TResouce>(string? defaultCultureName = null)
    {
        return Add(typeof(TResouce), defaultCultureName);
    }

    public LocalizationResource Add(Type resourceType, string? defaultCultureName = null)
    {
        var resourceName = LocalizationResourceNameAttribute.GetName(resourceType);
        if (ContainsKey(resourceName))
        {
            throw new AbpException("This resource is already added before: " +
            resourceType.AssemblyQualifiedName);
        }

        var resource = new LocalizationResource(resourceType, defaultCultureName);

        this[resourceName] = resource;
        _resourcesByTypes[resourceType] = resource;

        return resource;
    }

    public NonTypedLocalizationResource Add([NotNull] string resourceName,
    string? defaultCultureName = null)
    {
        Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName));

        if (ContainsKey(resourceName))
        {
            throw new AbpException("This resource is already added before: " + resourceName);
        }

        var resource = new NonTypedLocalizationResource(resourceName, defaultCultureName);

        this[resourceName] = resource;

        return resource;
    }

    public LocalizationResourceBase Get<TResource>()
    {
        var resourceType = typeof(TResource);

        var resource = _resourcesByTypes.GetOrDefault(resourceType);
        if (resource == null)
        {
            throw new AbpException("Can not find a resource with given type: "
             + resourceType.AssemblyQualifiedName);
        }

        return resource;
    }

    public LocalizationResourceBase Get(string resourceName)
    {
        var resource = this.GetOrDefault(resourceName);
        if (resource == null)
        {
            throw new AbpException("Can not find a resource with given name: "
             + resourceName);
        }

        return resource;
    }

    public LocalizationResourceBase Get(Type resourceType)
    {
        var resource = GetOrNull(resourceType);
        if (resource == null)
        {
            throw new AbpException("Can not find a resource with given type: "
             + resourceType);
        }

        return resource;
    }

    public LocalizationResourceBase? GetOrNull(Type resourceType)
    {
        return _resourcesByTypes.GetOrDefault(resourceType);
    }

    public bool ContainsResource(Type resourceType)
    {
        return _resourcesByTypes.ContainsKey(resourceType);
    }
}

IStringLocalizer 资源定位器 abp 实现者 IAbpStringLocalizer

资源定位器通过资源贡献器找到资源,具体到一个名为 key1 的资源,先在自己的资源里找,找不到到 继承的资源里找

public interface IAbpStringLocalizer : IStringLocalizer
{
    IEnumerable<LocalizedString> GetAllStrings(
        bool includeParentCultures,
        bool includeBaseLocalizers,
        bool includeDynamicContributors
    );

    Task<IEnumerable<LocalizedString>> GetAllStringsAsync(
        bool includeParentCultures
    );

    Task<IEnumerable<LocalizedString>> GetAllStringsAsync(
        bool includeParentCultures,
        bool includeBaseLocalizers,
        bool includeDynamicContributors
    );

    //找到的资源支持的区域文化
    Task<IEnumerable<string>> GetSupportedCulturesAsync();
}
public class AbpDictionaryBasedStringLocalizer : IAbpStringLocalizer
{
    //资源名及资源贡献器
    public LocalizationResourceBase Resource { get; }

    public List<IStringLocalizer> BaseLocalizers { get; }

    public AbpLocalizationOptions AbpLocalizationOptions { get; }

    //通过索引匹配
    public virtual LocalizedString this[string name] => GetLocalizedString(name);

    public virtual LocalizedString this[string name, params object[] arguments] =>
     GetLocalizedStringFormatted(name, arguments);

    public AbpDictionaryBasedStringLocalizer(
        LocalizationResourceBase resource,
        List<IStringLocalizer> baseLocalizers,
        AbpLocalizationOptions abpLocalizationOptions)
    {
        Resource = resource;
        BaseLocalizers = baseLocalizers;
        AbpLocalizationOptions = abpLocalizationOptions;
    }

    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        return GetAllStrings(
            CultureInfo.CurrentUICulture.Name,
            includeParentCultures
        );
    }

    public async Task<IEnumerable<LocalizedString>> GetAllStringsAsync(
      bool includeParentCultures)
    {
        return await GetAllStringsAsync(
            CultureInfo.CurrentUICulture.Name,
            includeParentCultures
        );
    }

    public IEnumerable<LocalizedString> GetAllStrings(
        bool includeParentCultures,
        bool includeBaseLocalizers,
        bool includeDynamicContributors)
    {
        return GetAllStrings(
            CultureInfo.CurrentUICulture.Name,
            includeParentCultures,
            includeBaseLocalizers,
            includeDynamicContributors
        );
    }

    public async Task<IEnumerable<LocalizedString>> GetAllStringsAsync(
        bool includeParentCultures,
        bool includeBaseLocalizers,
        bool includeDynamicContributors)
    {
        return await GetAllStringsAsync(
            CultureInfo.CurrentUICulture.Name,
            includeParentCultures,
            includeBaseLocalizers,
            includeDynamicContributors
        );
    }

    public Task<IEnumerable<string>> GetSupportedCulturesAsync()
    {
        //所有资源贡献器支持的区域语言
        return Resource.Contributors.GetSupportedCulturesAsync();
    }

    protected virtual LocalizedString GetLocalizedStringFormatted(
      string name, params object[] arguments)
    {
        return GetLocalizedStringFormatted(
          name, CultureInfo.CurrentUICulture.Name, arguments);
    }

    protected virtual LocalizedString GetLocalizedStringFormatted(
      string name, string cultureName, params object[] arguments)
    {
        var localizedString = GetLocalizedString(name, cultureName);
        return new LocalizedString(name, string.Format(
          localizedString.Value, arguments),
          localizedString.ResourceNotFound, localizedString.SearchedLocation);
    }

    protected virtual LocalizedString GetLocalizedString(string name)
    {
        return GetLocalizedString(name, CultureInfo.CurrentUICulture.Name);
    }

    protected virtual LocalizedString GetLocalizedString(string name, string cultureName)
    {
        //在资源关联的贡献器里找
        var value = GetLocalizedStringOrNull(name, cultureName);
        //找不到,在  BaseLocalizers 里找
        if (value == null)
        {
            //
            foreach (var baseLocalizer in BaseLocalizers)
            {
                //临时更改当前线程的区域和文化为当前查询的中指定的区域文化
                using (CultureHelper.Use(CultureInfo.GetCultureInfo(cultureName)))
                {
                    var baseLocalizedString = baseLocalizer[name];
                    if (baseLocalizedString != null &&
                    !baseLocalizedString.ResourceNotFound)
                    {
                        //找到直接退出
                        return baseLocalizedString;
                    }
                }
            }
             //没有找到
            return new LocalizedString(name, name, resourceNotFound: true);
        }

        return value;
    }

    //指定查询的 key(name) ,及  cultureName ,在所有的资源贡献器里找
    protected virtual LocalizedString? GetLocalizedStringOrNull(
        string name,
        string cultureName,
        bool tryDefaults = true)
    {
        //在资源贡献器里找指定的语言字符串
        var strOriginal = Resource.Contributors.GetOrNull(cultureName, name);
        if (strOriginal != null)
        {
            return strOriginal;
        }

        if (!tryDefaults)
        {
            return null;
        }

        //如果上面每找到,进行语言区域回退,在找
        if (AbpLocalizationOptions.TryToGetFromBaseCulture)
        {

            if (cultureName.Contains("-"))
            {
                //tr
                var strLang = Resource.Contributors.GetOrNull(
                  CultureHelper.GetBaseCultureName(cultureName), name);
                if (strLang != null)
                {
                    return strLang;
                }
            }
        }

        //语言回退还没有找到,用 default 区域语言找
        if (AbpLocalizationOptions.TryToGetFromDefaultCulture)
        {
            //Try to get from default language
            if (!Resource.DefaultCultureName.IsNullOrEmpty())
            {
                var strDefault = Resource.Contributors.GetOrNull(
                  Resource.DefaultCultureName!, name);
                if (strDefault != null)
                {
                    return strDefault;
                }
            }
        }

        //Not found
        return null;
    }

    protected virtual IReadOnlyList<LocalizedString> GetAllStrings(
        string cultureName,
        bool includeParentCultures = true,
        bool includeBaseLocalizers = true,
        bool includeDynamicContributors = true)
    {
        //TODO: Can be optimized (example: if it's already default
        //dictionary, skip overriding)

        var allStrings = new Dictionary<string, LocalizedString>();

        if (includeBaseLocalizers)
        {
            foreach (var baseLocalizer in BaseLocalizers.Select(l => l))
            {
                using (CultureHelper.Use(CultureInfo.GetCultureInfo(cultureName)))
                {
                    //TODO: Try/catch is a workaround here!
                    try
                    {
                        var baseLocalizedString = baseLocalizer.GetAllStrings(
                            includeParentCultures,
                            includeBaseLocalizers, // Always true, I know!
                            includeDynamicContributors
                        );

                        foreach (var localizedString in baseLocalizedString)
                        {
                            allStrings[localizedString.Name] = localizedString;
                        }
                    }
                    catch (MissingManifestResourceException)
                    {

                    }
                }
            }
        }

        if (includeParentCultures)
        {
            //Fill all strings from default culture
            if (!Resource.DefaultCultureName.IsNullOrEmpty())
            {
                Resource.Contributors.Fill(Resource.DefaultCultureName!,
                allStrings, includeDynamicContributors);
            }

            //Overwrite all strings from the language based on country culture
            if (cultureName.Contains("-"))
            {
                Resource.Contributors.Fill(CultureHelper.GetBaseCultureName(cultureName),
                 allStrings, includeDynamicContributors);
            }
        }

        //Overwrite all strings from the original culture
        Resource.Contributors.Fill(cultureName, allStrings, includeDynamicContributors);

        return allStrings.Values.ToImmutableList();
    }

    protected virtual async Task<IReadOnlyList<LocalizedString>> GetAllStringsAsync(
        string cultureName,
        bool includeParentCultures = true,
        bool includeBaseLocalizers = true,
        bool includeDynamicContributors = true)
    {
        //TODO: Can be optimized (example: if it's already default dictionary,
        // skip overriding)

        var allStrings = new Dictionary<string, LocalizedString>();

        if (includeBaseLocalizers)
        {
            foreach (var baseLocalizer in BaseLocalizers.Select(l => l))
            {
                using (CultureHelper.Use(CultureInfo.GetCultureInfo(cultureName)))
                {
                    //TODO: Try/catch is a workaround here!
                    try
                    {
                        var baseLocalizedString = await baseLocalizer.GetAllStringsAsync(
                            includeParentCultures,
                            includeBaseLocalizers, // Always true, I know!
                            includeDynamicContributors
                        );

                        foreach (var localizedString in baseLocalizedString)
                        {
                            allStrings[localizedString.Name] = localizedString;
                        }
                    }
                    catch (MissingManifestResourceException)
                    {

                    }
                }
            }
        }

        if (includeParentCultures)
        {
            //Fill all strings from default culture
            if (!Resource.DefaultCultureName.IsNullOrEmpty())
            {
                await Resource.Contributors.FillAsync(
                    Resource.DefaultCultureName!,
                    allStrings,
                    includeDynamicContributors
                );
            }

            //Overwrite all strings from the language based on country culture
            if (cultureName.Contains("-"))
            {
                await Resource.Contributors.FillAsync(
                    CultureHelper.GetBaseCultureName(cultureName),
                    allStrings,
                    includeDynamicContributors
                );
            }
        }

        //Overwrite all strings from the original culture
        await Resource.Contributors.FillAsync(
            cultureName,
            allStrings,
            includeDynamicContributors
        );

        return allStrings.Values.ToImmutableList();
    }
}

IAbpStringLocalizerFactory

有了 IStringLocalizer 资源定位器 abp 实现者 IAbpStringLocalizer,IAbpStringLocalizerFactory 就可以生成资源字符串了

public interface IAbpStringLocalizerFactory
{
    IStringLocalizer? CreateDefaultOrNull();

    IStringLocalizer? CreateByResourceNameOrNull([NotNull] string resourceName);

    Task<IStringLocalizer?> CreateByResourceNameOrNullAsync([NotNull] string resourceName);
}
public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLocalizerFactory
{
    protected internal AbpLocalizationOptions AbpLocalizationOptions { get; }
    protected ResourceManagerStringLocalizerFactory InnerFactory { get; }
    protected IServiceProvider ServiceProvider { get; }
    protected IExternalLocalizationStore ExternalLocalizationStore { get; }

    //资源名及资源定位器字典
    protected ConcurrentDictionary<string, StringLocalizerCacheItem> LocalizerCache { get; }

    //控制进行并发量,控制LocalizerCache 只能同时有一个线程操作
    protected SemaphoreSlim LocalizerCacheSemaphore { get; } = new(1, 1);

    public AbpStringLocalizerFactory(
        //asp.net core 多语言,嵌入式资源文件资源生成工厂
        ResourceManagerStringLocalizerFactory innerFactory,

        IOptions<AbpLocalizationOptions> abpLocalizationOptions,
        IServiceProvider serviceProvider,
        //外部额资源提供
        IExternalLocalizationStore externalLocalizationStore)
    {
        InnerFactory = innerFactory;
        ServiceProvider = serviceProvider;
        ExternalLocalizationStore = externalLocalizationStore;
        AbpLocalizationOptions = abpLocalizationOptions.Value;

        LocalizerCache = new ConcurrentDictionary<string, StringLocalizerCacheItem>();
    }

    public virtual IStringLocalizer Create(Type resourceType)
    {
        return Create(resourceType, lockCache: true);
    }

    //
    private IStringLocalizer Create(Type resourceType, bool lockCache)
    {
        //通过伪类获取 LocalizationResourceBase
        var resource = AbpLocalizationOptions.Resources.GetOrNull(resourceType);
        if (resource == null)
        {
            //获取不到,去 asp.net core 的嵌入式资源里找
            return InnerFactory.Create(resourceType);
        }

        //生成一个资源定位器
        return CreateInternal(resource.ResourceName, resource, lockCache);
    }

    public IStringLocalizer? CreateByResourceNameOrNull(string resourceName)
    {
        return CreateByResourceNameOrNullInternal(resourceName, lockCache: true);
    }

    private IStringLocalizer? CreateByResourceNameOrNullInternal(
        string resourceName,
        bool lockCache)
    {
        var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceName);
        if (resource == null)
        {
            resource = ExternalLocalizationStore.GetResourceOrNull(resourceName);
            if (resource == null)
            {
                return null;
            }
        }

        return CreateInternal(resourceName, resource, lockCache);
    }

    public Task<IStringLocalizer?> CreateByResourceNameOrNullAsync(string resourceName)
    {
        return CreateByResourceNameOrNullInternalAsync(resourceName, lockCache: true);
    }

    private async Task<IStringLocalizer?> CreateByResourceNameOrNullInternalAsync(
        string resourceName,
        bool lockCache)
    {
        var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceName);
        if (resource == null)
        {
            resource = await ExternalLocalizationStore.GetResourceOrNullAsync(resourceName);
            if (resource == null)
            {
                return null;
            }
        }

        return await CreateInternalAsync(resourceName, resource, lockCache);
    }

    //生成资源定位器
    private IStringLocalizer CreateInternal(
        string resourceName,
        LocalizationResourceBase resource,
        bool lockCache)
    {
        //先在缓存里找
        if (LocalizerCache.TryGetValue(resourceName, out var cacheItem))
        {
            return cacheItem.Localizer;
        }

        IStringLocalizer GetOrCreateLocalizer()
        {
            //在上锁的情况下,二次检查,缓存里有没有
            if (LocalizerCache.TryGetValue(resourceName, out var cacheItem2))
            {
                return cacheItem2.Localizer;
            }

            return LocalizerCache.GetOrAdd(
                resourceName,
                _ => CreateStringLocalizerCacheItem(resource)
            ).Localizer;
        }

        if (lockCache)
        {
            using (LocalizerCacheSemaphore.Lock())
            {
                return GetOrCreateLocalizer();
            }
        }
        else
        {
            return GetOrCreateLocalizer();
        }
    }

    private async Task<IStringLocalizer> CreateInternalAsync(
        string resourceName,
        LocalizationResourceBase resource,
        bool lockCache)
    {
        if (LocalizerCache.TryGetValue(resourceName, out var cacheItem))
        {
            return cacheItem.Localizer;
        }

        async Task<IStringLocalizer> GetOrCreateLocalizerAsync()
        {
            // Double check
            if (LocalizerCache.TryGetValue(resourceName, out var cacheItem2))
            {
                return cacheItem2.Localizer;
            }

            var newCacheItem = await CreateStringLocalizerCacheItemAsync(resource);
            LocalizerCache[resourceName] = newCacheItem;
            return newCacheItem.Localizer;
        }

        if (lockCache)
        {
            using (await LocalizerCacheSemaphore.LockAsync())
            {
                return await GetOrCreateLocalizerAsync();
            }
        }
        else
        {
            return await GetOrCreateLocalizerAsync();
        }
    }

    private StringLocalizerCacheItem CreateStringLocalizerCacheItem(
      LocalizationResourceBase resource)
    {
        //加入全局的资源贡献器
        foreach (var globalContributorType in AbpLocalizationOptions.GlobalContributors)
        {
            resource.Contributors.Add(
                Activator
                    .CreateInstance(globalContributorType)!
                    .As<ILocalizationResourceContributor>()
            );
        }

        var context = new LocalizationResourceInitializationContext(resource,
         ServiceProvider);

        //初始化所有的贡献器
        foreach (var contributor in resource.Contributors)
        {
            contributor.Initialize(context);
        }

        // 生成一个资源定位器
        return new StringLocalizerCacheItem(
            new AbpDictionaryBasedStringLocalizer(
                resource,
                resource
                    .BaseResourceNames
                    .Select(x => CreateByResourceNameOrNullInternal(x, lockCache: false))
                    .Where(x => x != null)
                    .ToList()!,
                AbpLocalizationOptions
            )
        );
    }

    private async Task<StringLocalizerCacheItem> CreateStringLocalizerCacheItemAsync(
      LocalizationResourceBase resource)
    {
        foreach (var globalContributorType in AbpLocalizationOptions.GlobalContributors)
        {
            resource.Contributors.Add(
                Activator
                    .CreateInstance(globalContributorType)!
                    .As<ILocalizationResourceContributor>()
            );
        }

        var context = new LocalizationResourceInitializationContext(
          resource, ServiceProvider);

        foreach (var contributor in resource.Contributors)
        {
            contributor.Initialize(context);
        }

        var baseLocalizers = new List<IStringLocalizer>();

        foreach (var baseResourceName in resource.BaseResourceNames)
        {
            var baseLocalizer = await CreateByResourceNameOrNullInternalAsync(
              baseResourceName, lockCache: false);
            if (baseLocalizer != null)
            {
                baseLocalizers.Add(baseLocalizer);
            }
        }

        return new StringLocalizerCacheItem(
            new AbpDictionaryBasedStringLocalizer(
                resource,
                baseLocalizers,
                AbpLocalizationOptions
            )
        );
    }

    public virtual IStringLocalizer Create(string baseName, string location)
    {
        return InnerFactory.Create(baseName, location);
    }

    internal static void Replace(IServiceCollection services)
    {
        services.Replace(ServiceDescriptor.Singleton<
        IStringLocalizerFactory, AbpStringLocalizerFactory>());
        services.AddSingleton<ResourceManagerStringLocalizerFactory>();
    }

    protected class StringLocalizerCacheItem
    {
        public AbpDictionaryBasedStringLocalizer Localizer { get; }

        public StringLocalizerCacheItem(AbpDictionaryBasedStringLocalizer localizer)
        {
            Localizer = localizer;
        }
    }

    public IStringLocalizer? CreateDefaultOrNull()
    {
        if (AbpLocalizationOptions.DefaultResourceType == null)
        {
            return null;
        }

        return Create(AbpLocalizationOptions.DefaultResourceType);
    }
}

ILocalizationDictionary 本地化字符串缓存字典

public interface ILocalizationDictionary
{
    string CultureName { get; }

    LocalizedString? GetOrNull(string name);

    void Fill(Dictionary<string, LocalizedString> dictionary);
}
public class StaticLocalizationDictionary : ILocalizationDictionary
{

    public string CultureName { get; }

    protected Dictionary<string, LocalizedString> Dictionary { get; }

    public StaticLocalizationDictionary(string cultureName, Dictionary<string,
     LocalizedString> dictionary)
    {
        CultureName = cultureName;
        Dictionary = dictionary;
    }

    public virtual LocalizedString? GetOrNull(string name)
    {
        return Dictionary.GetOrDefault(name);
    }

    public void Fill(Dictionary<string, LocalizedString> dictionary)
    {
        foreach (var item in Dictionary)
        {
            dictionary[item.Key] = item.Value;
        }
    }
}

AbpLocalizationOptions 配置类

public class AbpLocalizationOptions
{
    // LocalizationResourceBase 和资源名和伪类的字典
    public LocalizationResourceDictionary Resources { get; }

    /// <summary>
    /// Used as the default resource when resource was not specified on a
    ///localization operation.
    /// </summary>
    public Type? DefaultResourceType { get; set; }

    //用于本地化资源的全局贡献者列表。
    public ITypeList<ILocalizationResourceContributor> GlobalContributors { get; }
    //支持的语言列表。
    public List<LanguageInfo> Languages { get; }

    public Dictionary<string, List<NameValue>> LanguagesMap { get; }

    public Dictionary<string, List<NameValue>> LanguageFilesMap { get; }

    //是否指出区域语言回退
    public bool TryToGetFromBaseCulture { get; set; }
    //
    public bool TryToGetFromDefaultCulture { get; set; }
    // 指示是否尝试从默认文化获取本地化。
    public AbpLocalizationOptions()
    {
        Resources = new LocalizationResourceDictionary();
        GlobalContributors = new TypeList<ILocalizationResourceContributor>();
        Languages = new List<LanguageInfo>();
        LanguagesMap = new Dictionary<string, List<NameValue>>();
        LanguageFilesMap = new Dictionary<string, List<NameValue>>();
        TryToGetFromBaseCulture = true;
        TryToGetFromDefaultCulture = true;
    }
}

AbpLocalizationModule

在此模块里没有看到注入以下的必须的服务,abp 注入这些服务是在 AbpApplicationBase 里直接作为 核心服务注入了

// asp.net core 在 AddLocalization  方法里调用如下方法注入
internal static void AddLocalizationServices(IServiceCollection services)
{
  services.TryAddSingleton<IStringLocalizerFactory,
  ResourceManagerStringLocalizerFactory>();
  services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
}

AbpApplicationBase 直接注入核心服务

services.AddCoreServices();

internal static void AddCoreServices(this IServiceCollection services)
{
  services.AddOptions();
  services.AddLogging();
  services.AddLocalization();
}
namespace Volo.Abp.Localization;

[DependsOn(
    typeof(AbpVirtualFileSystemModule),
    typeof(AbpSettingsModule),
    typeof(AbpLocalizationAbstractionsModule),
    typeof(AbpThreadingModule)
    )]
public class AbpLocalizationModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        /**
         services.AddSingleton<ResourceManagerStringLocalizerFactory>();
         services.Replace(ServiceDescriptor.Singleton<IStringLocalizerFactory,
          AbpStringLocalizerFactory>());

         用自己的工厂替换 asp.net core 的工厂
         ResourceManagerStringLocalizerFactory 单独注入
         */
        AbpStringLocalizerFactory.Replace(context.Services);

        Configure<AbpVirtualFileSystemOptions>(options =>
        {
            options.FileSets.AddEmbedded<AbpLocalizationModule>("Volo.Abp", "Volo/Abp");
        });

        Configure<AbpLocalizationOptions>(options =>
        {
            options
                .Resources
                .Add<DefaultResource>("en");

            options
                .Resources
                .Add<AbpLocalizationResource>("en")
                .AddVirtualJson("/Localization/Resources/AbpLocalization");
        });
    }
}

通过 demo 验证源码

  1. 生成虚拟文件,在项目目录下创建 EmbedResources, EmbedResources1 目录,目录内添加 en.json 文件
     {
     "culture": "en",
     "texts": {
       "key1": "Hello World!"
       }
     }
    
  2. 生成物理文件,在项目目录下创建 PhyResource 目录,在此目录内添加 zh.json 文件
     {
       "culture": "zh",
       "texts": {
         "key1": "你好!"
       }
     }
    
//通过请求里获取区域语言文化
app.UseRequestLocalization("en", "zh");

配置虚拟文件

var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PhyResource");
Configure<AbpVirtualFileSystemOptions>(options =>
{
  //嵌入式文件1
  options.FileSets.AddEmbedded<StartModule>
    //此命名空间下的所有 /EmbedResources 目录内及子目录内的所有文件都作为嵌入资源文件加入虚拟文件
    //第一个参数是根命名空间,嵌入样文件时的命名空间时 root namespace
    ("WebApplicationMainStart","/EmbedResources");

  //嵌入式文件2 /EmbedResources1 目录内及子目录内的所有文件都作为嵌入资源文件加入虚拟文件
  options.FileSets.AddEmbedded<StartModule>
    //此命名空间下的所有
    ("WebApplicationMainStart","/EmbedResources1");

  //本地文件 path 目录下的及子目录下的所有文件,加入虚拟文件
  options.FileSets.AddPhysical(path);
});
//可以在任意目录内新建伪类文件,作为和资源文件的绑定,本身没有意义
[LocalizationResourceName("myResource1")]
public class MyResource {

}

//配置资源
Configure<AbpLocalizationOptions>(options =>
{
  options.DefaultResourceType = typeof(MyResource);

  //通过伪类和资源文件进行绑定
  options.Resources.Add<MyResource>("en").AddVirtualJson("/EmbedResources").
  AddVirtualJson("/");

  //通过资源名称和资源文件绑定
  options.Resources.Add("resourceNoType","en").AddVirtualJson("/EmbedResources1");

});
//获取同一个资源通过类型或名称
[HttpGet("getBar")]
public async Task<IActionResult> GetBar([FromServices] IStringLocalizer<MyResource>
localizer,[FromServices] IStringLocalizerFactory factory) {
  //通过伪类获取
  var tmp= localizer.GetString("key1");

  //通过伪类上的资源名称获取
  var localizer2= factory.CreateByResourceName("myResource1");

  // localizer2["key1"]; 通过索引方式也可以
  var tmp2= localizer2.GetString("key1");

  return Ok(new {tmp,tmp2});
}

[HttpGet("getFoo")]
public async Task<IActionResult> GetFoo([FromServices] IStringLocalizerFactory factory) {
  //通过资源名称获取
  var localizer= factory.CreateByResourceName("resourceNoType");
  var tmp= localizer.GetString("key2");

  return Ok(tmp);
}

和 asp.net core 一样可以带参数化

如下带有俩个参数的文本,可以传递参数 localizer.GetString("key2",10,20) abp string.Format(localizedString.Value, arguments) 进行的参数化

{
  "culture": "en",
  "texts": {
    "key2": "Hello by name {0} , {1}"
  }
}

有时候不清楚虚拟文件的路径可以打印虚拟文件路径获知 IVirtualFileProvider,总结一下,通过 配置类 AbpLocalizationOptions 作为单例,资源及资源配置的贡献器及其他参数,此类因为是单例的 就能做到已经是匹配过的就可以放入字典缓存

👍🎉🎊