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 文件
- controller 单独的资源
资源按照 controller 进行区分 ,每个单独的 controller 都有对应的资源文件
文件发现方式有来中 . 和 / ,
options.ResourcesPath = "resource";
项目的根目录下找 resource 目录下的资源文件
文件发现的俩种方式
-
/ 真实目录 resource/controller/xxxController.en-US.resx 2 . 通过文件名 resource/controller.xxxController.en-US.resx
-
共享资源在根目录下定义一个空类,如果此空类位于文件夹下 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.resx
, moduleVerification.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 资源文件查找顺序如下:
- HomeController.zh-CN.resx
- HomeController.zh.resx
- HomeController.resx 如果都没找到,则会返回资源 Key 本身
配置 CultureProvider
ASP.NET Core 框架默认添加了 3 种 Provider,分别为:
- 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" 的值
//可以在这里添加自定义的 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 验证源码
-
生成虚拟文件,在项目目录下创建 EmbedResources, EmbedResources1 目录,目录内添加
en.json 文件
{ "culture": "en", "texts": { "key1": "Hello World!" } }
- 生成物理文件,在项目目录下创建 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 作为单例,资源及资源配置的贡献器及其他参数,此类因为是单例的
就能做到已经是匹配过的就可以放入字典缓存