ABP中的本地化处理(上)
今天这篇文章主要来总结一下ABP中的多语言是怎么实现的,在后面我们将结合ABP中的源码和相关的实例来一步步进行说明,在介绍这个之前我们先来看看ABP的官方文档,通过这个文档我们就知道怎样在我们的系统中使用ABP自带的本地化处理方式了,当前文章将分为三个部分,1 怎样在应用层和领域层添加本地化支持。2 ABP本地化中的源码分析。3 怎么实现Dto中本地化。下面就每一个部分来进行深入的分析。
一 怎样在应用层和领域层添加本地化支持
按照ABP官方文档中介绍,存储本地化资源推荐使用XML File 或者是Json File 这里主要是介绍怎么使用Json File来存储需要实现本地化的一些重要信息。
1 在PreInitialize方法中添加支持的语言。
在我们的项目中我们将这个方法的实现放在了每一个Domain层的Module的 PreInitialize()方法里面,这里我们将当前的方法写成了一个静态方法,然后再在PreInitialize()里面调用,这里我们来看看具体的代码实现。
public static class DcsLocalizationConfigurer { public static void Configure(ILocalizationConfiguration localizationConfiguration) { localizationConfiguration.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flags england")); localizationConfiguration.Languages.Add(new LanguageInfo("zh-Hans", "简体中文", "famfamfam-flags cn", isDefault: true)); localizationConfiguration.Sources.Add( new DictionaryBasedLocalizationSource(DcsConsts.LocalizationSourceName, new JsonEmbeddedFileLocalizationDictionaryProvider( typeof(DcsLocalizationConfigurer).GetAssembly(), "XXX.Localization.SourceFiles" ) ) ); } }
注意这里的XXX就是当前Domain对应的命名空间了,Localization、SourceFiles都是我们放置本地化信息文件具体路径,具体目录结构如下图所示,这里我们的源文件中实现了中文和英文两种本地化配置文件,通过文件名称中间的en或者zh-Hans我们就能够加以区分。
图一 本地化文件路径
这里我们在看看具体Json文件该怎么进行定义?
{ "culture": "zh-Hans", "texts": { "HelloWorld": "欢迎", "RegionNotFound": "省市区信息未找到", "DutyUnitNotFound": "责任单位信息未找到", "ExistsDutyUnitCode": "已存在责任单位编码为", "FaultModelNotFound": "故障模式未找到", "CompanyNotFound": "当前用户所在企业信息未找到", "PartNotFound": "配件信息未找到", "ProductNotFound": "产品信息未找到", "BranchNotFound": "营销分公司信息未找到" } }
这里介绍了中文一些本地化信息的配置,我们可以看到定义是按照Key、Value的形式进行存储的,culture代表当前的语言类型。通过这一步我们就能够将定义的本地化文件添加到ABP中了。
2 在应用层或者是领域层中使用本地化
由于在ABP中应用层和领域层中都是继承自抽象类AbpServiceBase,所以我们便可以直接在代码中进行使用L方法,具体使用如下面的代码所示。
private DutyUnit GetDutyUnitById(Guid id) { var dutyUnit = _dutyUnitRepository.GetAll().FirstOrDefault(p => p.Id == id && p.Status == DutyUnitStatus.生效 && p.Type == DutyUnitType.配件供应商); if (dutyUnit == null) { throw new PreconditionFailedException(L("DutyUnitNotFound")); } return dutyUnit; }
上面的代码是一个定义在领域层的方法,该方法就是当数据库中到不到当前Id对应的DutyUnit的时候,就会抛出一个异常,这个异常信息就用L("DutyUnitNotFound"),这个L方法就会根据当前配置的Culture到对应的Json文件中找到匹配项,就像当前代码我们配置当前的Culture为中文的时候,那么最终抛出的异常信息将会是:“责任单位信息未找到”,通过这种方式我们就会发现通过配置不同语言的Json文件我们就能够实现多语言的报错提示信息了。
谈到这里我们就应该明白了到底如何配置和使用L方法了,但是我们还有一个疑问就是到底该如何去配置当前的Culture呢?因为最终到底会到哪个本地化文件中查询多语言的信息是取决于当前项目中的Culture,这里我们需要在我们的项目中配置好我们所需要的本地化SourceName,当然在实际的项目中这个可以根据前端按钮的选择从而向项目中传递不同的Culture,最终实现界面、抛错提示信息等等的动态切换,当然我们这里举例是在代码中默认选定一种LocalizationSourceName,而不是进行动态切换,实际项目需要根据实际情况进行考虑。
public abstract class DcsDomainServiceBase : DomainService { protected DcsDomainServiceBase() { LocalizationSourceName = DcsConsts.LocalizationSourceName; } }
这里我们在所有的Domain服务的基类中指定LocalizationSourceName = DcsConsts.LocalizationSourceName(注: DcsConsts.LocalizationSourceName值为"zh-Hans"),这样就为当前项目中指定Cluture啦,这样我们领域层所有引用L方法的地方均能正确找到本地化信息了。
二 ABP本地化中的源码分析
上面的代码只是告诉了我们如何在自己的项目中集成ABP自带的本地化方式,那么这些背后实现的原理到底是怎么样的呢?通过分析源码相信我们能够一步步找到问题的答案。
通过第一部分的介绍我们知道本地化的核心实现在于AbpServiceBase类中的L方法,那么我们首先来看看ABP中的这个方法吧。
using System.Globalization; using Abp.Configuration; using Abp.Domain.Uow; using Abp.Localization; using Abp.Localization.Sources; using Abp.ObjectMapping; using Castle.Core.Logging; namespace Abp { /// <summary> /// This class can be used as a base class for services. /// It has some useful objects property-injected and has some basic methods /// most of services may need to. /// </summary> public abstract class AbpServiceBase { /// <summary> /// Reference to the setting manager. /// </summary> public ISettingManager SettingManager { get; set; } /// <summary> /// Reference to <see cref="IUnitOfWorkManager"/>. /// </summary> public IUnitOfWorkManager UnitOfWorkManager { get { if (_unitOfWorkManager == null) { throw new AbpException("Must set UnitOfWorkManager before use it."); } return _unitOfWorkManager; } set { _unitOfWorkManager = value; } } private IUnitOfWorkManager _unitOfWorkManager; /// <summary> /// Gets current unit of work. /// </summary> protected IActiveUnitOfWork CurrentUnitOfWork { get { return UnitOfWorkManager.Current; } } /// <summary> /// Reference to the localization manager. /// </summary> public ILocalizationManager LocalizationManager { get; set; } /// <summary> /// Gets/sets name of the localization source that is used in this application service. /// It must be set in order to use <see cref="L(string)"/> and <see cref="L(string,CultureInfo)"/> methods. /// </summary> protected string LocalizationSourceName { get; set; } /// <summary> /// Gets localization source. /// It's valid if <see cref="LocalizationSourceName"/> is set. /// </summary> protected ILocalizationSource LocalizationSource { get { if (LocalizationSourceName == null) { throw new AbpException("Must set LocalizationSourceName before, in order to get LocalizationSource"); } if (_localizationSource == null || _localizationSource.Name != LocalizationSourceName) { _localizationSource = LocalizationManager.GetSource(LocalizationSourceName); } return _localizationSource; } } private ILocalizationSource _localizationSource; /// <summary> /// Reference to the logger to write logs. /// </summary> public ILogger Logger { protected get; set; } /// <summary> /// Reference to the object to object mapper. /// </summary> public IObjectMapper ObjectMapper { get; set; } /// <summary> /// Constructor. /// </summary> protected AbpServiceBase() { Logger = NullLogger.Instance; ObjectMapper = NullObjectMapper.Instance; LocalizationManager = NullLocalizationManager.Instance; } /// <summary> /// Gets localized string for given key name and current language. /// </summary> /// <param name="name">Key name</param> /// <returns>Localized string</returns> protected virtual string L(string name) { return LocalizationSource.GetString(name); } /// <summary> /// Gets localized string for given key name and current language with formatting strings. /// </summary> /// <param name="name">Key name</param> /// <param name="args">Format arguments</param> /// <returns>Localized string</returns> protected string L(string name, params object[] args) { return LocalizationSource.GetString(name, args); } /// <summary> /// Gets localized string for given key name and specified culture information. /// </summary> /// <param name="name">Key name</param> /// <param name="culture">culture information</param> /// <returns>Localized string</returns> protected virtual string L(string name, CultureInfo culture) { return LocalizationSource.GetString(name, culture); } /// <summary> /// Gets localized string for given key name and current language with formatting strings. /// </summary> /// <param name="name">Key name</param> /// <param name="culture">culture information</param> /// <param name="args">Format arguments</param> /// <returns>Localized string</returns> protected string L(string name, CultureInfo culture, params object[] args) { return LocalizationSource.GetString(name, culture, args); } } }
我们可以看到这其中的L方法有四种实现,通过添加不同的参数我们能够实现不同功能。这里我们就先从最简单的带一个name参数的方法说起吧。这里面又调用了一个ILocalizationSource接口中的GetString方法,这里我们先来看看LocalizationSource这个定义的属性来开始说吧。
/// <summary> /// Gets localization source. /// It's valid if <see cref="LocalizationSourceName"/> is set. /// </summary> protected ILocalizationSource LocalizationSource { get { if (LocalizationSourceName == null) { throw new AbpException("Must set LocalizationSourceName before, in order to get LocalizationSource"); } if (_localizationSource == null || _localizationSource.Name != LocalizationSourceName) { _localizationSource = LocalizationManager.GetSource(LocalizationSourceName); } return _localizationSource; } }
在这个方法中第一次进入的时候会创建私有的_localizationSource对象,在这个创建的时候调用LocalizationManager.GetSource(LocalizationSourceName)这个方法,这其中LocalizationManager是通过属性注入的对象,LocalizationSourceName这个参数通过上面的分析你应该知道为什么需要在DcsDomainServiceBase 中首先定义这个属性了吧。到了这里我们来看看LocalizationManager中定义的GetSource方法。
using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Abp.Configuration.Startup; using Abp.Dependency; using Abp.Localization.Dictionaries; using Abp.Localization.Sources; using Castle.Core.Logging; namespace Abp.Localization { internal class LocalizationManager : ILocalizationManager { public ILogger Logger { get; set; } private readonly ILanguageManager _languageManager; private readonly ILocalizationConfiguration _configuration; private readonly IIocResolver _iocResolver; private readonly IDictionary<string, ILocalizationSource> _sources; /// <summary> /// Constructor. /// </summary> public LocalizationManager( ILanguageManager languageManager, ILocalizationConfiguration configuration, IIocResolver iocResolver) { Logger = NullLogger.Instance; _languageManager = languageManager; _configuration = configuration; _iocResolver = iocResolver; _sources = new Dictionary<string, ILocalizationSource>(); } public void Initialize() { InitializeSources(); } private void InitializeSources() { if (!_configuration.IsEnabled) { Logger.Debug("Localization disabled."); return; } Logger.Debug(string.Format("Initializing {0} localization sources.", _configuration.Sources.Count)); foreach (var source in _configuration.Sources) { if (_sources.ContainsKey(source.Name)) { throw new AbpException("There are more than one source with name: " + source.Name + "! Source name must be unique!"); } _sources[source.Name] = source; source.Initialize(_configuration, _iocResolver); //Extending dictionaries if (source is IDictionaryBasedLocalizationSource) { var dictionaryBasedSource = source as IDictionaryBasedLocalizationSource; var extensions = _configuration.Sources.Extensions.Where(e => e.SourceName == source.Name).ToList(); foreach (var extension in extensions) { extension.DictionaryProvider.Initialize(source.Name); foreach (var extensionDictionary in extension.DictionaryProvider.Dictionaries.Values) { dictionaryBasedSource.Extend(extensionDictionary); } } } Logger.Debug("Initialized localization source: " + source.Name); } } /// <summary> /// Gets a localization source with name. /// </summary> /// <param name="name">Unique name of the localization source</param> /// <returns>The localization source</returns> public ILocalizationSource GetSource(string name) { if (!_configuration.IsEnabled) { return NullLocalizationSource.Instance; } if (name == null) { throw new ArgumentNullException("name"); } ILocalizationSource source; if (!_sources.TryGetValue(name, out source)) { throw new AbpException("Can not find a source with name: " + name); } return source; } /// <summary> /// Gets all registered localization sources. /// </summary> /// <returns>List of sources</returns> public IReadOnlyList<ILocalizationSource> GetAllSources() { return _sources.Values.ToImmutableList(); } } }
在这个GetSource方法中,会到类型为IDictionary<string, ILocalizationSource>的私有变量_sources中找到当前名称为LocalizationSourceName对应的ILocalizationSource,那么我们在DomainModule中通过PreInitialize()方法配置ILocalizationConfiguration中的Source集合,这个集合中我们默认添加了一个Key为DcsConsts.LocalizationSourceName,value为JsonEmbeddedFileLocalizationDictionaryProvider的对象,那么这个ILocalizationConfiguration中定义的Source和我们在LocalizationManager中定义的_sources是怎么关联到一起的呢?答案是通过LocalizationManager中的Initialize()来实现的,通过代码分析我们发现LocalizationManager中的Initialize()是在AbpKernelModule中的PostInitialize()方法中的调用的。
public override void PostInitialize() { RegisterMissingComponents(); IocManager.Resolve<SettingDefinitionManager>().Initialize(); IocManager.Resolve<FeatureManager>().Initialize(); IocManager.Resolve<PermissionManager>().Initialize(); IocManager.Resolve<LocalizationManager>().Initialize(); IocManager.Resolve<NotificationDefinitionManager>().Initialize(); IocManager.Resolve<NavigationManager>().Initialize(); if (Configuration.BackgroundJobs.IsJobExecutionEnabled) { var workerManager = IocManager.Resolve<IBackgroundWorkerManager>(); workerManager.Start(); workerManager.Add(IocManager.Resolve<IBackgroundJobManager>()); } }
如果熟悉ABP模块加载顺序的应该知道,如果对ABP中的模块化还不太熟悉,请点击上篇、下篇了解相关内容,AbpKernelModule是整个ABP Module集合中永远处在第一个位置的模块,并且所有的模块会按照拓扑排序进行存放的,并且在初始化整个ABP系统的过程中会依次调用每一个模块PreInitialize()、Initialize()、PostInitialize()方法来进行模块的初始化,当我们的业务DomainModule通过PreInitialize()方法添加到ILocalizationConfiguration中的Source中的对象会被后面的AbpKernelModule执行PostInitialize()(在这其中调用LocalizationManager的Initialize())的时候将我们在具体业务Module中配置的Source添加到LocalizationManager私有的_sources对象只能够,后面我们再执行LocalizationManager中的GetSource方法的时候就能够从私有的_sources集合中获取到最终的ILocalizationSource对象了,这个就是整个实现过程,这里也贴出LocalizationManager中的Initialize方法中关键的代码。
private void InitializeSources() { if (!_configuration.IsEnabled) { Logger.Debug("Localization disabled."); return; } Logger.Debug(string.Format("Initializing {0} localization sources.", _configuration.Sources.Count)); foreach (var source in _configuration.Sources) { if (_sources.ContainsKey(source.Name)) { throw new AbpException("There are more than one source with name: " + source.Name + "! Source name must be unique!"); } _sources[source.Name] = source; source.Initialize(_configuration, _iocResolver); //Extending dictionaries if (source is IDictionaryBasedLocalizationSource) { var dictionaryBasedSource = source as IDictionaryBasedLocalizationSource; var extensions = _configuration.Sources.Extensions.Where(e => e.SourceName == source.Name).ToList(); foreach (var extension in extensions) { extension.DictionaryProvider.Initialize(source.Name); foreach (var extensionDictionary in extension.DictionaryProvider.Dictionaries.Values) { dictionaryBasedSource.Extend(extensionDictionary); } } } Logger.Debug("Initialized localization source: " + source.Name); } }
到了这里结合具体的代码你应该明白整个过程了,这一部分具体过程请参考下篇。
三 怎么实现Dto中本地化
最后在这里说一下,Dto中怎么去为验证信息添加本地化支持,我们知道在ABP中会默认提供一个ICustomValidate的接口,这个里面提供了一个AddValidationErrors的方法,用于对Dto进行各种验证操作,例如下面的代码。
public class AddOrUpdateWarrantyPolicyBase : ICustomValidate { public AddOrUpdateWarrantyPolicyBase() { Items = Array.Empty<AddOrUpdatePolicyItemInput>(); } //名称 [Required] public string Name { get; set; } //启用日期 public DateTime StartTime { get; set; } //备注 public string Remark { get; set; } public AddOrUpdatePolicyItemInput[] Items { get; set; } public void AddValidationErrors(CustomValidationContext context) { if (StartTime <= DateTime.Now) { context.Results.Add(new ValidationResult("启用日期必须大于服务器当前日期")); } if (Items.Length == 0) { context.Results.Add(new ValidationResult("整车保修政策清单列表不允许为空")); } } }
由于在Dto中无法使用L方法,那么像这样的类型该进行怎样的处理呢?也许我们可以获取到LocalizationManager对象并调用其中的GetSource方法来实现,对,我们发现AddValidationErrors(CustomValidationContext context)这个方法的参数context中有公共的IocResolver,我们也可以先看看这个CustomValidationContext 的定义。
public class CustomValidationContext { /// <summary> /// List of validation results (errors). Add validation errors to this list. /// </summary> public List<ValidationResult> Results { get; } /// <summary> /// Can be used to resolve dependencies on validation. /// </summary> public IIocResolver IocResolver { get; } public CustomValidationContext(List<ValidationResult> results, IIocResolver iocResolver) { Results = results; IocResolver = iocResolver; } }
有了这个就好办了,我们可以获取ABP中的各种实例了,当然也包括ILocalizationManager对象了,这里我们来看看我们通过一个拓展方法的实现。
/// <summary> /// 主要是用作对Dto的验证信息提供本地化支持 /// </summary> public static class CustomValidationContextExtension { /// <summary> /// Gets localized string for given key name and current language. /// </summary> /// <param name="context">CustomValidation Context</param> /// <param name="keyName">key name</param> /// <param name="localizationSourceName">the default source name is chinese</param> /// <returns></returns> public static string LocalizeString(this CustomValidationContext context, string keyName, string localizationSourceName = DcsConsts.LocalizationSourceName) { var localizationManager = context.IocResolver.Resolve<ILocalizationManager>(); if (null == localizationManager) { throw new AbpValidationException("Can not resolve instance of ILocalizationManager"); } var localizationSource = localizationManager.GetSource(localizationSourceName); return localizationSource.GetString(keyName); } /// <summary> /// Gets localized string for given key name and current language. /// </summary> /// <param name="context">CustomValidation Context</param> /// <param name="keyName">key name</param> /// <param name="culture">culture information</param> /// <param name="localizationSourceName">the default source name is chinese</param> /// <returns></returns> public static string LocalizeString(this CustomValidationContext context, string keyName, CultureInfo culture, string localizationSourceName = DcsConsts.LocalizationSourceName) { var localizationManager = context.IocResolver.Resolve<ILocalizationManager>(); if (null == localizationManager) { throw new AbpValidationException("Can not resolve instance of ILocalizationManager"); } var localizationSource = localizationManager.GetSource(localizationSourceName); return localizationSource.GetString(keyName, culture); } /// <summary> /// Gets localized string for given key name and current language. /// </summary> /// <param name="context">CustomValidation Context</param> /// <param name="keyName">key name</param> /// <param name="culture">culture information</param> /// <param name="localizationSourceName">the default source name is chinese</param> /// <param name="args">Format arguments</param> /// <returns></returns> public static string LocalizeString(this CustomValidationContext context, string keyName, CultureInfo culture, string localizationSourceName = DcsConsts.LocalizationSourceName, params object[] args) { var localizationManager = context.IocResolver.Resolve<ILocalizationManager>(); if (null == localizationManager) { throw new AbpValidationException("Can not resolve instance of ILocalizationManager"); } var localizationSource = localizationManager.GetSource(localizationSourceName); return localizationSource.GetString(keyName, culture, args); } }
通过这个我们就直接通过LocalizeString等相关的重载方法来完成我们所需要的各种本地化操作了,本篇文章就介绍到这里。
最后,点击这里返回整个ABP系列的主目录。