- A+
一、简介
ABP vNext 提供了全套的本地化字符串支持,具体用法可以参考官方使用文档。vNext 本身是对 Microsoft 提供的本地化组件进行了实现,通过 JSON 文件提供本地化源,这一点与老 ABP 不太一样,老 ABP 框架是全套自己手撸。vNext 针对服务端和客户端都提供了文字本地化的工具类,这样开发人员可以很快速地开发支持多语言的网站程序。
二、源码分析
本地化涉及的主要模块有 Volo.Abp.Localization.Abstractions 和 Volo.Abp.Localization,可以看到 Volo 针对包的结构也逐渐向 Microsoft 的构建方式靠拢。有直接依赖的模块是 Volo.Abp.VirtualFileSystem,之所以会引用到这个模块,是因为默认的本地化数据源是通过内嵌 JSON 文件实现的,所以会用到虚拟文件系统读取数据。
2.1 本地化的抽象接口
首先打开 Volo.Abp.Localization.Abstractions 项目,它的基本结构如下图所示,需要注意的核心类型就是 ILocalizableString
接口和它的两个具体实现 FixedLocalizableString
与 LocalizableString
。
这里的 IAbpStringLocalizerFactoryWithDefaultResourceSupport
接口是为 AbpStringLocalizerFactoryExtensions
服务的,后面会详细解释,主要作用是根据默认资源类型快速创建一个 IStringLocalizer
实例。
2.1.1 本地化字符串对象的封装
可以看到在该项目内部定义了一个 ILocalizableString
的接口,在 ABP vNext 内部需要用到多语言表示的字符串属性,都是定义的 ILocalizableString
类型。本质上它是针对 Microsoft 提供的 LocalizedString
进行了一层包装,这个接口只提供了一个方法 Localize()
,具体的签名见下面的代码。
public interface ILocalizableString { LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory); }
在 ABP vNext 框架当中,拥有两个实现,分别是 LocalizableString
和 FixedLocalizableString
,后者用于创建固定字串的显示。例如 ABP vNext 自带的权限系统,针对权限名称和描述必须传递 ILocalizableString
类型的值,但是开发人员暂时没有提供对应的本地化翻译,这个时候就可以使用 FixedLocalizableString
传递固定字符串。
实现也是很简单,在调用了 Localize()
方法之后,会根据构造函数的 Value 创建一个新的 LocalizedString
对象。
public class FixedLocalizableString : ILocalizableString { public string Value { get; } public FixedLocalizableString(string value) { Value = value; } public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory) { return new LocalizedString(Value, Value); } }
用法举例:
public class DataDictionaryDefinitionPermissionProvider : PermissionDefinitionProvider { public override void Define(IPermissionDefinitionContext context) { var dataDictionaryGroup = context.AddGroup(DataDictionaryPermissions.GroupName, L("Permission:DataDictionary")); var dataDictionary = dataDictionaryGroup.AddPermission(DataDictionaryPermissions.DataDictionary.Default, L("Permission:DataDictionary")); dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Create, L("Permission:Create")); dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Update, L("Permission:Edit")); dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Delete, L("Permission:Delete")); // 这里使用了 FixedLocalizableString 提供本地化字符串。 dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Management, new FixedLocalizableString("字典管理")); } private static LocalizableString L(string name) { return LocalizableString.Create<DataDictionaryResource>(name); } }
另一个 LocalizableString
就是正常通过 IStringLocalizerFactory
获取的本地化字符串对象。
public class LocalizableString : ILocalizableString { [CanBeNull] public Type ResourceType { get; } [NotNull] public string Name { get; } public LocalizableString(Type resourceType, [NotNull] string name) { Name = Check.NotNullOrEmpty(name, nameof(name)); ResourceType = resourceType; } public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory) { return stringLocalizerFactory.Create(ResourceType)[Name]; } public static LocalizableString Create<TResource>([NotNull] string name) { return new LocalizableString(typeof(TResource), name); } }
在类型里面定义了一个静态方法,用于目标类型与 KEY 的 LocalizableString
对象,常见于权限定义的地方,在上面的示例代码中有用到过。
2.1.2 本地化资源类型的别名
TODO
2.2 本地化的基础设施
下文指代的基础设施是指 ABP vNext 为了优雅地实现 Microsoft 本地化接口所构建的一系列组件,ABP vNext 的大部分组件都是基于 Microsoft 提供的抽象体系,也是为了更好地兼容。
2.2.1 模块的启动
具体的实现模块逻辑也不复杂,首先替换了默认的本地化资源容器工厂。接着往 ABP 的虚拟文件系统添加了当前模块,以便后续访问对应的 JSON 文件,最后往本地化的相关配置项添加了两个本地化资源。
public class AbpLocalizationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { 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"); }); } }
2.2.2 本地化的配置
AbpLocalizationOptions
内部定义了本地化系统的相关参数,主要由资源集合(Resources
)、默认资源(DefaultResourceType
)、全局的本地化数据源提供者(GlobalContributors
)、支持的语言(Languages
)。
注意:
当进行本地化操作时,没有指定资源类型的时候会使用默认资源类型。
public class AbpLocalizationOptions { public LocalizationResourceDictionary Resources { get; } 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 AbpLocalizationOptions() { Resources = new LocalizationResourceDictionary(); GlobalContributors = new TypeList<ILocalizationResourceContributor>(); Languages = new List<LanguageInfo>(); LanguagesMap = new Dictionary<string, List<NameValue>>(); LanguageFilesMap = new Dictionary<string, List<NameValue>>(); } }
从上述代码我们可以知道,要让本地化系统正常工作,我们会接触到下面这几个类型 LocalizationResourceDictionary
、LocalizationResource
、ILocalizationResourceContributor
、LanguageInfo
。
2.2.3 本地化资源的定义
在使用本地化系统的时候,ABP vNext 文档首先会让我们定义一个类型,并在模块的 ConfigureService()
周期,通过配置项添加到本地化系统当中,就像这样。
Configure<AbpLocalizationOptions>(options => { options.Resources .Add<DataDictionaryResource>("en") .AddVirtualJson("/Localization/Resources"); });
这里可以看到,ABP vNext 实现了一套流畅方法(Fluent Method),通过这一系列的操作,我们会生成一个 LocalizationResource
实例,添加到配置系统当中,以便后续进行使用。
这里的 Add()
方法是由 LocalizationResourceDictionary
提供的,它本质上就是一个字典,只不过由 ABP vNext 封装了一些自定义的方法,方便添加字典项数据。可以看到它的实现也很简单,首先判断字典是否存在对应的 Key,如果不存在就使用资源类型和区域文化信息构造一个新的 LocalizationResource
对象,并将其添加到字典当中。
public class LocalizationResourceDictionary : Dictionary<Type, LocalizationResource> { public LocalizationResource Add<TResouce>([CanBeNull] string defaultCultureName = null) { return Add(typeof(TResouce), defaultCultureName); } public LocalizationResource Add(Type resourceType, [CanBeNull] string defaultCultureName = null) { if (ContainsKey(resourceType)) { throw new AbpException("This resource is already added before: " + resourceType.AssemblyQualifiedName); } return this[resourceType] = new LocalizationResource(resourceType, defaultCultureName); } // ... 其他代码。 }
转到 LocalizationResouce
的定义,内部存储了具体的资源类型、资源名称、当前资源默认的区域文化信息、本地化数据源提供者(与全局的不同,这里仅作用于某个具体本地化资源)、继承的基类资源类型集合。
资源类型和资源名称用于区分不同的本地化资源。默认区域文化信息代表当前资源,当获取指定语言的本地化字符串失败时,会读取默认的区域文化信息对应的本地化字符串。
这里的 BaseResouceTypes
是为了复用其他资源的本地化字符串,例如你定义了一个 AppleResouceType
,但是你想要获取 FruitResouceType
对应的字符串,那么就需要往这个集合添加需要服用的资源类型。ABP vNext 为 LocalizationResource
提供了一个扩展方法 AddBaseTypes()
便于在模块配置时添加需要复用的类型。除此之外 ABP vNext 也提供了特性支持,跟模块定义一样,在类定义上面添加 InheritResourceAttribute
特性,传入需要复用的类型定义即可。
[InheritResource( typeof(LocalizationTestValidationResource), typeof(LocalizationTestCountryNamesResource) )] public sealed class LocalizationTestResource { }
可以看到在下述代码当中,ABP vNext 会扫描当前 ResouceType
的特性,并将其定义的复用类型添加到基类集合当中。
public class LocalizationResource { [NotNull] public Type ResourceType { get; } [NotNull] public string ResourceName => LocalizationResourceNameAttribute.GetName(ResourceType); [CanBeNull] public string DefaultCultureName { get; set; } [NotNull] public LocalizationResourceContributorList Contributors { get; } [NotNull] public List<Type> BaseResourceTypes { get; } public LocalizationResource( [NotNull] Type resourceType, [CanBeNull] string defaultCultureName = null, [CanBeNull] ILocalizationResourceContributor initialContributor = null) { ResourceType = Check.NotNull(resourceType, nameof(resourceType)); DefaultCultureName = defaultCultureName; BaseResourceTypes = new List<Type>(); Contributors = new LocalizationResourceContributorList(); if (initialContributor != null) { Contributors.Add(initialContributor); } AddBaseResourceTypes(); } protected virtual void AddBaseResourceTypes() { var descriptors = ResourceType .GetCustomAttributes(true) .OfType<IInheritedResourceTypesProvider>(); foreach (var descriptor in descriptors) { foreach (var baseResourceType in descriptor.GetInheritedResourceTypes()) { BaseResourceTypes.AddIfNotContains(baseResourceType); } } } }
当资源类型(Resource Type) 定义好之后,通过上面的一番操作,就能够得到一个 LocalizationResource
实例,并将其添加到了 AbpLocalizationOptions
内的 LocalizationResourceDictionary
对象。开发人员定义了多少个本地化资源类型,就会往这个字典添加多少个 LocaliztaionResource
实例。
2.2.4 本地化的数据源
不论是配置项还是某个本地化资源定义类,都会存储一组 Contributor
。转到对应的接口定义,这个接口定义了三个公开方法,分别用于初始化(Initialize()
)、获取某个具体的本地化字符串(LocalizedString()
)、为指定的字典填充本地化资源数据(Fill()
)。
public interface ILocalizationResourceContributor { void Initialize(LocalizationResourceInitializationContext context); LocalizedString GetOrNull(string cultureName, string name); void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary); }
所有的数据源都是由各个 Contributor
提供的,这里以 VirtualFileLocalizationResourceContributorBase
为例,在内部通过虚拟文件系统获取到了文件数据,通过文件数据构造一系列的字典。这里的 字典有两层,第一层的 Key 是区域文化信息,Value 是对应区域文化信息的本地化字符串字典。第二层的 Key 是本地化字符串的标识,Value 就是具体的 LocalizedString
实例。
public abstract class VirtualFileLocalizationResourceContributorBase : ILocalizationResourceContributor { // ... 其他代码 public void Initialize(LocalizationResourceInitializationContext context) { _virtualFileProvider = context.ServiceProvider.GetRequiredService<IVirtualFileProvider>(); } public LocalizedString GetOrNull(string cultureName, string name) { return GetDictionaries().GetOrDefault(cultureName)?.GetOrNull(name); } public void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary) { GetDictionaries().GetOrDefault(cultureName)?.Fill(dictionary); } private Dictionary<string, ILocalizationDictionary> GetDictionaries() { // ... 获取本地化资源的字典,这里的字典按区域文化进行分组。 } private Dictionary<string, ILocalizationDictionary> CreateDictionaries() { var dictionaries = new Dictionary<string, ILocalizationDictionary>(); foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath)) { // ... 其他代码。 // 根据文件创建某个区域文化的具体的数据源字典。 var dictionary = CreateDictionaryFromFile(file); if (dictionaries.ContainsKey(dictionary.CultureName)) { throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()} dictionary has a culture name '{dictionary.CultureName}' which is already defined!"); } 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); }
2.2.5 本地化资源字典
具体存储本地化字符串标识和展示文本的对象是 ILocalizationDictionary
,它的具体实现是 StaticLocalizationDictionary
,在其内部有一个 Dictionary<string, LocalizedString>
字典,这个字典就对应的 JSON 文件当中的本地化数据了。
{ "culture": "zh-Hans", "texts": { "DisplayName:Abp.Localization.DefaultLanguage": "默认语言", "Description:Abp.Localization.DefaultLanguage": "应用程序的默认语言." } }
对应的字典形式就是内部字典的 KEY:Value
。
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; } } }
当最外层的 Contributor 获取文本的时候,实际是结合区域文化信息(Culture Name) 定义到对应的本地化资源字典,再通过 Name 获取资源字典内部对应的 LocalizedString
对象。
2.3 同 Microsoft 本地化集成
2.2 节讲完了 ABP vNext 实现的基础设施,结合某个资源类型附带的 Contributor 组就能够获取到具体的本地化字符串数据,在本节主要讲解 ABP vNext 同 Microsoft 的集成。
2.3.1 IStringLocalizer 工厂
在 AbpLocalizationModule
模块中,第一句就是替换了默认的 String Localizer 工厂,并注入了 ResourceManagerStringLocalizerFactory
类型,这个类型主要用于后续的默认行为。
internal static void Replace(IServiceCollection services) { services.Replace(ServiceDescriptor.Singleton<IStringLocalizerFactory, AbpStringLocalizerFactory>()); services.AddSingleton<ResourceManagerStringLocalizerFactory>(); }
在 Microsoft 提供的 IStringLocalizerFactory
接口中,只定义了两个创建 IStringLocalizer
的方法。
public interface IStringLocalizerFactory { IStringLocalizer Create(Type resourceSource); IStringLocalizer Create(string baseName, string location); }
第二个方法 ABP 是直接调用的默认工厂(ResouceManagerStringLocalizerFactory
) 提供的方法,而且还加了个 TODO 注明不知道什么时候会被调用。
public virtual IStringLocalizer Create(string baseName, string location) { //TODO: Investigate when this is called? return InnerFactory.Create(baseName, location); }
这里我们着重关注第一个方法,ABP 主要实现的也是第一个方法,它会根据传入的 resourceSource
参数从缓存当中获取(不存在则构造)对应的 IStringLocalizer
。如果在 ABP 提供的资源集合当中,没有查找到对应的 Type,则直接调用默认工厂返回 IStringLocalizer
。如果存在则会以 Type 作为 Key,StringLocalizerCacheItem
(就是 LocalizationResource
的马甲) 作为 Value,从缓存拿,没拿到就构建一个新的并加入到缓存中。
public virtual IStringLocalizer Create(Type resourceType) { var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceType); if (resource == null) { return InnerFactory.Create(resourceType); } if (LocalizerCache.TryGetValue(resourceType, out var cacheItem)) { return cacheItem.Localizer; } lock (LocalizerCache) { return LocalizerCache.GetOrAdd( resourceType, _ => CreateStringLocalizerCacheItem(resource) ).Localizer; } } private StringLocalizerCacheItem CreateStringLocalizerCacheItem(LocalizationResource resource) { // 构造时会将全局配置的 Contributor 添加到对应的组。 foreach (var globalContributor in AbpLocalizationOptions.GlobalContributors) { resource.Contributors.Add((ILocalizationResourceContributor) Activator.CreateInstance(globalContributor)); } using (var scope = ServiceProvider.CreateScope()) { var context = new LocalizationResourceInitializationContext(resource, scope.ServiceProvider); // 调用各个 Contributor 的初始化方法,进行初始化操作。 foreach (var contributor in resource.Contributors) { contributor.Initialize(context); } } return new StringLocalizerCacheItem( new AbpDictionaryBasedStringLocalizer( resource, resource.BaseResourceTypes.Select(Create).ToList() ) ); }
2.3.2 IStringLocalizer
ABP 针对 IStringLocalizer
的默认实现是 AbpDictionaryBasedStringLocalizer
,IStringLocalizer
主要包含两个索引器和一个 GetAllStrings()
方法。
索引器本身直接就调用的 GetLocalizedString()
与 GetLocalizedStringFormatted()
方法。后者用于处理格式化的参数,内部就是利用的 string.Format()
方法替换占位符的内容。
public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance { // ... 其他代码 public virtual LocalizedString this[string name] => GetLocalizedString(name); public virtual LocalizedString this[string name, params object[] arguments] => GetLocalizedStringFormatted(name, arguments); // ... 其他代码 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); // 如果没有从当前容器取得对应的本地化字符串,就从复用的基类中获取。 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; } }
转到 GetLocalizedStringOrNull()
方法内部,可以看到获取本地化字符串的具体逻辑。
- 首先会从本地化资源定义的 Contributors 中获取本地化字符串。
- 如果没有找到则尝试从类似的区域文化信息字典中获取,例如 zh-Hans(简体中文) 源没有拿到则考虑从 zh-Hant(繁体中文)获取。
- 还是没有取得,最后会使用默认的区域文化信息匹配对应的本地化字符串,一般来说该值建议设置为
en
。
protected virtual LocalizedString GetLocalizedStringOrNull(string name, string cultureName, bool tryDefaults = true) { //Try to get from original dictionary (with country code) var strOriginal = Resource.Contributors.GetOrNull(cultureName, name); if (strOriginal != null) { return strOriginal; } if (!tryDefaults) { return null; } //Try to get from same language dictionary (without country code) if (cultureName.Contains("-")) //Example: "tr-TR" (length=5) { var strLang = Resource.Contributors.GetOrNull(CultureHelper.GetBaseCultureName(cultureName), name); if (strLang != null) { return strLang; } } //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; }