今天這篇文章主要來總結一下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系列的主目錄。