[Abp vNext 源码分析] – 21. 界面与文字的本地化

  • A+
所属分类:.NET技术
摘要

ABP vNext 提供了全套的本地化字符串支持,具体用法可以参考官方使用文档。vNext 本身是对 Microsoft 提供的本地化组件进行了实现,通过 JSON 文件提供本地化源,这一点与老 ABP 不太一样,老 ABP 框架是全套自己手撸。vNext 针对服务端和客户端都提供了文字本地化的工具类,这样开发人员可以很快速地开发支持多语言的网站程序。


一、简介

ABP vNext 提供了全套的本地化字符串支持,具体用法可以参考官方使用文档。vNext 本身是对 Microsoft 提供的本地化组件进行了实现,通过 JSON 文件提供本地化源,这一点与老 ABP 不太一样,老 ABP 框架是全套自己手撸。vNext 针对服务端和客户端都提供了文字本地化的工具类,这样开发人员可以很快速地开发支持多语言的网站程序。

二、源码分析

本地化涉及的主要模块有 Volo.Abp.Localization.AbstractionsVolo.Abp.Localization,可以看到 Volo 针对包的结构也逐渐向 Microsoft 的构建方式靠拢。有直接依赖的模块是 Volo.Abp.VirtualFileSystem,之所以会引用到这个模块,是因为默认的本地化数据源是通过内嵌 JSON 文件实现的,所以会用到虚拟文件系统读取数据。

2.1 本地化的抽象接口

首先打开 Volo.Abp.Localization.Abstractions 项目,它的基本结构如下图所示,需要注意的核心类型就是 ILocalizableString 接口和它的两个具体实现 FixedLocalizableStringLocalizableString

这里的 IAbpStringLocalizerFactoryWithDefaultResourceSupport 接口是为 AbpStringLocalizerFactoryExtensions 服务的,后面会详细解释,主要作用是根据默认资源类型快速创建一个 IStringLocalizer 实例。

[Abp vNext 源码分析] - 21. 界面与文字的本地化

2.1.1 本地化字符串对象的封装

可以看到在该项目内部定义了一个 ILocalizableString 的接口,在 ABP vNext 内部需要用到多语言表示的字符串属性,都是定义的 ILocalizableString 类型。本质上它是针对 Microsoft 提供的 LocalizedString 进行了一层包装,这个接口只提供了一个方法 Localize(),具体的签名见下面的代码。

public interface ILocalizableString {   LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory); } 

在 ABP vNext 框架当中,拥有两个实现,分别是 LocalizableStringFixedLocalizableString,后者用于创建固定字串的显示。例如 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>>();     } } 

从上述代码我们可以知道,要让本地化系统正常工作,我们会接触到下面这几个类型 LocalizationResourceDictionaryLocalizationResourceILocalizationResourceContributorLanguageInfo

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 的默认实现是 AbpDictionaryBasedStringLocalizerIStringLocalizer 主要包含两个索引器和一个 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() 方法内部,可以看到获取本地化字符串的具体逻辑。

  1. 首先会从本地化资源定义的 Contributors 中获取本地化字符串。
  2. 如果没有找到则尝试从类似的区域文化信息字典中获取,例如 zh-Hans(简体中文) 源没有拿到则考虑从 zh-Hant(繁体中文)获取。
  3. 还是没有取得,最后会使用默认的区域文化信息匹配对应的本地化字符串,一般来说该值建议设置为 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; } 

三、总结