0.簡介
如果你所開發的需要走向世界的話,那麼肯定需要針對每一個使用者進行不同的本地化處理,有可能你的客戶在日本,需要使用日語作為顯示文本,也有可能你的客戶在美國,需要使用英語作為顯示文本。如果你還是一樣的寫死錯誤資訊,或者描述資訊,那麼就無法做到多語言適配。
Abp 架構本身提供了一套多語言機制來協助我們實現本地化,基本思路是 Abp 本身維護一個索引值對集合。只需要將展示給客戶的文字資訊處都使用一個語言 Key 來進行填充,當使用者登入系統之後,會取得目前使用者的地區文化資訊進行文本渲染。
0.1 如何使用
我們首先來看一下如何定義一個多語言資源並使用。首先 Abp 自身支援三種類型的本地化資源來源,第一種是 XML 檔案,第二種則是 JSON 檔案,第三種則是內嵌資源檔,如果這三種都不能滿足你的需求,你可以自行實現 ILocalizationSource 介面來返回多語言資源。
小提示:
Abp Zero 模組就提供了資料庫持久化儲存多語言資源的功能。
0.1.1 定義應用程式支援的語言
如果你需要為你的應用程式添加不同語言的支援,就必須在你任意模組的預先載入方法當中添加語言來進行配置:
Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true));Configuration.Localization.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flag-tr"));
例如以上代碼,就能夠讓我們的程式擁有針對英語與土耳其語的多語言處理能力。
這裡的 famfamfam-flag-england 與 famfamfam-flag-tr 是一個 CSS 類型,是 Abp 為前端展示所封裝的小國旗表徵圖。
0.1.2 建立多語言資源檔
有了語言之後,Abp 還需要你提供標準的多語言資源檔,這裡我們以 內建的 XML 資源檔為例,其檔案名稱為 Abp-zh-Hans.xml ,路徑為 Abp\Localization\Sources\AbpXmlSource。
<?xml version="1.0" encoding="utf-8" ?><localizationDictionary culture="zh-Hans"> <texts> <text name="SmtpHost">SMTP主機</text> <text name="SmtpPort">SMTP連接埠</text> <text name="Username">使用者名稱</text> <text name="Password">密碼</text> <text name="DomainName">網域名稱</text> <text name="UseSSL">使用SSL</text> <text name="UseDefaultCredentials">使用預設驗證</text> <text name="DefaultFromSenderEmailAddress">預設寄件者郵箱地址</text> <text name="DefaultFromSenderDisplayName">預設寄件者名字</text> <text name="DefaultLanguage">預設語言</text> <text name="ReceiveNotifications">接收通知</text> <text name="CurrentUserDidNotLoginToTheApplication">目前使用者沒有登入到系統!</text> <text name="TimeZone">時區</text> <text name="AllOfThesePermissionsMustBeGranted">您沒有許可權進行此操作,您需要以下許可權: {0}</text> <text name="AtLeastOneOfThesePermissionsMustBeGranted">您沒有許可權進行此操作,您至少需要下列許可權的其中一項: {0}</text> <text name="MainMenu">主菜單</text> </texts></localizationDictionary>
每個檔案內部,會有一個 <localizationDictionary culture="zh-Hans"> 節點用於說明當前檔案是針對於哪個地區適用的,而在其 <texts> 內部則就是結合索引值對的形式,name 裡面的內容就是多語言文本項的鍵,在標籤內部的就是其真正的值。
開啟一個針對俄語國家的 XML 資源檔,檔案名稱叫做 Abp-ru.xml。
<?xml version="1.0" encoding="utf-8" ?><localizationDictionary culture="ru"> <texts> <text name="SmtpHost">SMTP сервер</text> <text name="SmtpPort">SMTP порт</text> <text name="Username">Имя пользователя</text> <text name="Password">Пароль</text> <text name="DomainName">Домен</text> <text name="UseSSL">Использовать SSL</text> <text name="UseDefaultCredentials">Использовать учетные данные по умолчанию</text> <text name="DefaultFromSenderEmailAddress">Электронный адрес отправителя по умолчанию</text> <text name="DefaultFromSenderDisplayName">Имя отправителя по умолчанию</text> <text name="DefaultLanguage">Язык по умолчанию</text> <text name="ReceiveNotifications">Получать уведомления</text> <text name="CurrentUserDidNotLoginToTheApplication">Текущий пользователь не вошёл в приложение!</text> </texts></localizationDictionary>
可以看到 Key 值都是一樣的,只是其 <text> 內部的值根據地區國家的不同值不一樣而已。
其次從檔案名稱我們就可以看到需要使用 XML 資源檔對於檔案的命名格式會有一定要求,還是以 Abp 內建的資源檔為例,可以看一下他們基本上都是由 {SourceName}-{CultureInfo}.xml 這樣構成的。
0.1.3 註冊本地化的 XML 資源
那麼如果我們需要註冊之前的兩個 XML 資源到 Abp 架構當中的話,則需要在預先載入模組處通過如下代碼來執行註冊,並且需要右鍵 XML 檔案,更改其構建操作為 內嵌資源。
Configuration.Localization.Sources.Add( new DictionaryBasedLocalizationSource( // 本地化資源名稱 AbpConsts.LocalizationSourceName, // 資料來源提供者,這裡使用的是 XML ,除了 XML 提供者,還有 JSON 等 new XmlEmbeddedFileLocalizationDictionaryProvider( typeof(AbpKernelModule).GetAssembly(), "Abp.Localization.Sources.AbpXmlSource" )));
0.1.4 擷取多語言文本
如果你需要在某處擷取指定 Key 所對應的具體顯示文本,只需要注入 ILocalizationManager ,通過其 GetString() 方法就可以獲得具體的值。如果你需要擷取本地化資源的地方不能夠使用依賴注入,你可以使用 LocalizationHelper 靜態類來進行操作。
var @string = _localizationManager.GetString("Abp", "MainMenu");
它預設是從 Thread.CurrentThread.CurrentUICulture 擷取到的目前範圍資訊,從而來取得某個 Key 所對應的顯示值,而目前範圍資訊是由 Abp 注入的一系列 RequestCultureProviders 所提供的,他按照以下順序來進行設定。
- QueryStringRequestCultureProvider(ASP .NET Core 預設提供):該預設提供器使用的是 QueryString 的
culture&ui-culture 所提供的地區文化資訊來初始化該值,例如:culture=es-MX&ui-culture=es-MX。
- AbpUserRequestCultureProvider (Abp 提供):該提供器會讀取目前使用者的
IAbpSession 資訊,並且從 ISettingManager 中擷取使用者所配置的 "Abp.Localization.DefaultLanguageName" 屬性,將其作為預設的地區文化資訊。
- AbpLocalizationHeaderRequestCultureProvider (Abp 提供):使用每次要求標頭當中的 .AspNetCore.Culture 值作為當前的地區文化資訊,例如
c=en|uic=en-US。
- CookieRequestCultureProvider (ASP .NET Core 提供):使用每次請求的 Cookie 當中 Key 為 .AspNetCore.Culture 值作為目前範圍文化資訊。
- AbpDefaultRequestCultureProvider (Abp 提供):如果之前這些提供器都沒有為目前範圍文化賦值,則從
ISettingMananger 當中取得 Abp.Localization.DefaultLanguageName 的預設值。
- AcceptLanguageHeaderRequestCultureProvider (ASP .NET Core 預設提供):該提供器最終會使用使用者每次請求時傳遞的 Accept-Language 頭部作為目前範圍文化資訊。
小提示:
這裡 Abp 注入的提供器是有順序的,注入這麼多提供器就是為了最後確定目前使用者的地區文化資訊以便展示相應的語言文本。
1.啟動流程1.1 啟動流程圖
1.2 代碼流程
根據使用方法我們可以得知,要配置 Abp 的多語言,必須得等 IAbpStartupConfiguration 初始化完畢才可以。即在 AbpBootstrapper 的 Initialize() 方法之中:
public virtual void Initialize(){ // ... 其他代碼 // 注入 IAbpStartupConfiguration 配置與本地化資源配置 IocManager.IocContainer.Install(new AbpCoreInstaller()); // ... 其他代碼 // 初始化 AbpStartupConfiguration 類型 IocManager.Resolve<AbpStartupConfiguration>().Initialize(); // ... 其他代碼}
配置類裡麵包含了使用者所配置的所有語言與多語言資源資訊,在被成功注入到 Ioc 容器之後,Abp 就開始使用本地化資源管理員來初始化這些多語言資料了。
public override void PostInitialize(){ // 註冊缺少的組件,防止遺漏註冊組件 RegisterMissingComponents(); IocManager.Resolve<SettingDefinitionManager>().Initialize(); IocManager.Resolve<FeatureManager>().Initialize(); IocManager.Resolve<PermissionManager>().Initialize(); // 重點在這裡,這個 PostInitialize 方法是存放在核心模組當中的,在這裡調用了本地化資源管理員的初始化方法 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>()); }}
具體 LocalizationManager 及其內部的實現我們在下一節程式碼分析中詳細進行講述。
這些動作僅僅是在注入 Abp 架構的時候所需要執行的一些步驟,如果你要啟用多語言,需要在 ASP .NET Core 程式的 Startup 類中的 Configure() 處通過更改 UseAbpRequestLocalization 狀態為 True,才會將地區文化識別中介軟體注入到程式當中。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory){ app.UseAbp(options => { options.UseAbpRequestLocalization = false; //disable automatic adding of request localization }); //...authentication middleware(s) app.UseAbpRequestLocalization(); //manually add request localization //...other middlewares app.UseMvc(routes => { //... });}
其實這裡的 UseAbpRequestLocalization() 就已經將上文說的那些 RequestProvider 按照順序依次注入到 MVC 之中了。
2.程式碼分析
Abp 架構針對本地化處理相關的類型與方法定義都存放在 Abp 庫的 Localization 檔案夾下。關係還是相對複雜的,這裡我們先從其核心的 Abp 庫針對於多語言的處理開始講起。
2.1 多語言模組配置
Abp 需要使用的所有資訊都是由使用者在自己啟動模組的 PreInitialize() 當中,通過 ILocalizationConfiguration 進行注入配置。也就是說在 ILocalizationConfiguration 內部,主要是包含了語言,與多語言資源提供者兩種重點資訊。
public interface ILocalizationConfiguration{ // 當前應用程式可配置的語言列表 IList<LanguageInfo> Languages { get; } // 本地化資源清單 ILocalizationSourceList Sources { get; } // 是否啟用多語言(本地化) 系統 bool IsEnabled { get; set; } // 以下四個布爾類型的參數主要用於確定當沒有找到多語言文本時的處理邏輯,預設都為 True bool ReturnGivenTextIfNotFound { get; set; } bool WrapGivenTextIfNotFound { get; set; } bool HumanizeTextIfNotFound { get; set; } bool LogWarnMessageIfNotFound { get; set; }}
2.2 語言資訊
當前應用程式能夠支援哪一些語言,取決於使用者在預先載入的時候給多語言模組設定物件分配了哪些語言。通過第 0.1.1 節我們看到使用者可以直接通過初始化一個新的 LanguageInfo 對象,將其添加到 Languages 屬性之中。
public class LanguageInfo{ /// <summary> /// 地區文化代碼名稱 /// 應該是一個有效地區文化代碼名稱,更多的可以通過 CultureInfo 靜態類獲得所有文化代碼。 /// 例如: "en-US" 是北美適用的, "tr-TR" 適用於土耳其。 /// </summary> public string Name { get; set; } /// <summary> /// 該語言預設應該展示的語言名稱。 /// 例如: 英語應該展示為 "English", "zh-Hans" 應該展示為 "簡體中文" /// </summary> public string DisplayName { get; set; } /// <summary> /// 用於展示的表徵圖 CSS 類名,選擇性參數 /// </summary> public string Icon { get; set; } /// <summary> /// 是否為預設語言 /// </summary> public bool IsDefault { get; set; } /// <summary> /// 該語言是否被禁用 /// </summary> public bool IsDisabled { get; set; } /// <summary> /// 語言的展示方式是自左向右還是自右向左 /// </summary> public bool IsRightToLeft { get { try { return CultureInfo.GetCultureInfo(Name).TextInfo?.IsRightToLeft ?? false; } catch { return false; } } } public LanguageInfo(string name, string displayName, string icon = null, bool isDefault = false, bool isDisabled = false) { Name = name; DisplayName = displayName; Icon = icon; IsDefault = isDefault; IsDisabled = isDisabled; }}
關於語言的定義還是相當簡單的,主要參數就是語言的 地區文化代碼 與 展示的名稱,其餘的都可以是選擇性參數。
小提示:
關於當前系統所支援的地區文化代碼,可以通過執行 CultureInfo.GetCultures(CultureTypes.AllCultures); 得到。
2.3 語言管理器
Abp 針對語言也提供了一個管理器,介面叫做 ILanguageManager,定義簡單,兩個方法。
public interface ILanguageManager{ // 獲得當前語言 LanguageInfo CurrentLanguage { get; } // 獲得所有語言 IReadOnlyList<LanguageInfo> GetLanguages();}
實現也不複雜,它內部的實現就是從一個 ILanguageProvider 拿取有哪一些語言資料。
private readonly ILanguageProvider _languageProvider;public IReadOnlyList<LanguageInfo> GetLanguages(){ return _languageProvider.GetLanguages();}// 擷取當前語言,其實就是擷取的 CultureInfo.CurrentUICulture.Name 的資訊,然後去查詢語言集合。private LanguageInfo GetCurrentLanguage(){ var languages = _languageProvider.GetLanguages(); // ... 省略了的代碼 var currentCultureName = CultureInfo.CurrentUICulture.Name; var currentLanguage = languages.FirstOrDefault(l => l.Name == currentCultureName); if (currentLanguage != null) { return currentLanguage; } // ... 省略了的代碼 return languages[0];}
預設實現就是直接讀取之前通過 Configuration 的 Languages 裡面的資料。
在 Abp.Zero 模組還有兩外一個實現,叫做 ApplicationLanguageProvider ,這個提供者則是從資料庫表 ApplicationLanguage 擷取的這些語言列表資料,並且這些語言資訊還與租戶有關,不同的租戶他所能夠獲得到的語言資料也不一樣。
public IReadOnlyList<LanguageInfo> GetLanguages(){ // 可以看到這裡傳入的當前登入使用者的租戶 Id,通過這個參數去查詢的語言表資料 var languageInfos = AsyncHelper.RunSync(() => _applicationLanguageManager.GetLanguagesAsync(AbpSession.TenantId)) .OrderBy(l => l.DisplayName) .Select(l => l.ToLanguageInfo()) .ToList(); SetDefaultLanguage(languageInfos); return languageInfos;}
2.4 本地化資源2.4.1 本地化資源清單
在多語言模組配置內部使用的是 ILocalizationSourceList 類型的一個 Sources 屬性,該類型其實就是繼承自 IList<ILocalizationSource> 的一個具體實現而已,一個類型為 ILocalizationSource 的集合,不過其擴充了一個
Extensions 屬性用於存放擴充的多語言資料欄位。
2.4.2 本地化資源
其介面定義為 ILocalizationSource ,Abp 預設為我們實現了四種本地化資源的實現。
第一個是空實現,可以跳過,第二個則是針對資源檔進行讀取的的本地化資源,第三個是基於字典的的本地化資源定義,最後一個是由 Abp Zero 模組所提供的資料庫版本的多語言資源定義。
首先看一下該介面的定義:
public interface ILocalizationSource{ // 本地化資源唯一的名稱 string Name { get; } // 用於初始化本地化資源,在 Abp 架構初始化的時候被調用 void Initialize(ILocalizationConfiguration configuration, IIocResolver iocResolver); // 從當前本地化資源中擷取給定關鍵字的多語言文本項,為使用者當前語言 string GetString(string name); // 從當前本地化資源中擷取給定關鍵字與地區文化的多語言文本項 string GetString(string name, CultureInfo culture); // 作用同上,只不過不存在會返回 NULL string GetStringOrNull(string name, bool tryDefaults = true); // 作用同上,只不過不存在會返回 NULL string GetStringOrNull(string name, CultureInfo culture, bool tryDefaults = true); // 獲得當前語言所有的多語言文本項集合 IReadOnlyList<LocalizedString> GetAllStrings(bool includeDefaults = true); // 獲得給定地區文化的所有多語言文本項集合 IReadOnlyList<LocalizedString> GetAllStrings(CultureInfo culture, bool includeDefaults = true);}
也就可以這麼來看,我們有幾套本地化資源,他們通過 Name 來進行標識,如果你需要在本地化管理器擷取某一套本地化資源,那麼你可以直接通過 Name 來進行定位。而每一套本地化資源,自身都擁有具體的多語言資料,這些多語言資料有可能來自檔案也有可能來自資料庫,這取決於你具體的實現。
2.4.3 基於字典的本地化資源
最開始我們在使用範例當中,通過 DictionaryBasedLocalizationSource 來建立我們的本地化資來源物件。該對象實現了 ILocalizationSource 與 IDictionaryBasedLocalizationSource 介面,內部定義了一個本地化資源字典提供器。
當調用本地化資源的 Initialize() 方法的時候,會使用具體的本地化資源字典提供器來擷取資料,而這個字典提供器可以為 XmlFileLocalizationDictionaryProvider、JsonEmbeddedFileLocalizationDictionaryProvider 等。
這些內部字典提供器在初始化的時候,會將自身的資料按照 語言/多語言項 的形式將多語言資訊存放在一個字典之中,而這個字典又可以分為 XML、JSON 等等等等...
// 內部字典提供器public interface ILocalizationDictionaryProvider{ // 語言/多語言項字典 IDictionary<string, ILocalizationDictionary> Dictionaries { get; } // 本地化資源初始化時被調用 void Initialize(string sourceName);}
而這裡的 ILocalizationDictionary 其實就是一個索引值對,鍵關聯的是多語言項的標識 KEY,例如 "Home",而 Value 就是具體的展示文本資訊了。
而是用字典本地化資來源物件擷取資料的時候,其實也就是從其內部的字典提供器來擷取資料。
例如本地化資源有一個 GetString() 方法,它內部擁有一個字典提供器 DictionaryProvider,我要擷取某個 KEY 為 "Home" 所需要經過的步驟如下。
public ILocalizationDictionaryProvider DictionaryProvider { get; }public string GetString(string name){ // 擷取目前使用者地區文化,標識為 "Home" 的展示文本 return GetString(name, CultureInfo.CurrentUICulture);}public string GetString(string name, CultureInfo culture){ // 擷取值 var value = GetStringOrNull(name, culture); // 判斷值為空白的話,根據配置的要求是否拋出異常 if (value == null) { return ReturnGivenNameOrThrowException(name, culture); } return value;}// 獲得 KEY 關聯的文本public string GetStringOrNull(string name, CultureInfo culture, bool tryDefaults = true){ var cultureName = culture.Name; var dictionaries = DictionaryProvider.Dictionaries; // 在這裡就開始從初始化所載入完成的語言字典裡面,擷取具體的多語言項字典 ILocalizationDictionary originalDictionary; if (dictionaries.TryGetValue(cultureName, out originalDictionary)) { // 多語言項字典拿取具體的多語言文本值 var strOriginal = originalDictionary.GetOrNull(name); if (strOriginal != null) { return strOriginal.Value; } } if (!tryDefaults) { return null; } //Try to get from same language dictionary (without country code) if (cultureName.Contains("-")) //Example: "tr-TR" (length=5) { ILocalizationDictionary langDictionary; if (dictionaries.TryGetValue(GetBaseCultureName(cultureName), out langDictionary)) { var strLang = langDictionary.GetOrNull(name); if (strLang != null) { return strLang.Value; } } } //Try to get from default language var defaultDictionary = DictionaryProvider.DefaultDictionary; if (defaultDictionary == null) { return null; } var strDefault = defaultDictionary.GetOrNull(name); if (strDefault == null) { return null; } return strDefault.Value;}
2.3.4 基於資料庫的本地化資源
如果你有整合 Abp.Zero 模組的話,可以通過在啟動模組的預先載入方法編寫以下代碼啟用 Zero 的多語言機制。
Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization();
Abp.Zero 針對原有的本地化資源進行了擴充,新增的本地化資源類叫做 MultiTenantLocalizationSource,該類同語言管理器一樣,是一個基於多租戶實現的本地化資源,內部字典的值是從資料庫當中擷取的,其大體邏輯與字典本地化資源一樣,都是內部維護有一個字典提供器。
在通過 EnableDbLocalization() 方法的時候就直接替換掉了 ILanguageProvider 的預設實現,並且在配置的 Sources 源裡面也增加了 MultiTenantLocalizationSource 作為一個本地化資源。
2.5 本地化資源管理員
扯了這麼多,讓我們來看一下最為核心的 ILocalizationManager 介面,如果我們需要擷取某個資料來源的某個 Key 所對應的多語言值肯定是要注入這個本地化資源管理員來進行操作的。
public interface ILocalizationManager{ // 根據名稱獲得本地化資料來源 ILocalizationSource GetSource(string name); // 擷取所有的本地化資料來源 IReadOnlyList<ILocalizationSource> GetAllSources();}
這裡的資料來源標識的就是一個命名空間的作用,比如我在 A 模組當中有一個 Key 為 "Home" 的多語言項,在 B 模組也有一個 Key 為 "Home" 的多語言項,這個時候就可以用資料來源標識來區分這兩個 "Home" 。
本地化資源管理員通過在初始化的時候調用其 Initialize() 來初始化所有被注入的本地化資源,最後並將其放在一個字典之中,以便後續使用。
private readonly IDictionary<string, ILocalizationSource> _sources;foreach (var source in _configuration.Sources){ // ... 其他代碼 _sources[source.Name] = source; source.Initialize(_configuration, _iocResolver); // ... 其他代碼}
3.結語
針對 Abp 的多語言處理本篇文章不太適合作為入門瞭解,其中大部分知識需要結合 Abp 源碼進行閱讀才能夠加深理解,此文僅作拋磚引玉之用,如有任何意見或建議歡迎大家在評論當中指出。
4.點此跳轉到總目錄