前言
其實對於Nop的多語言,最主要的元素有下面兩個:javascript
-
WebWorkContext(IWorkContext的實現類)php
-
LocalizationService(ILocalizationService的實現類)html
其餘相關的元素能夠說都是在這兩個的基礎上體現價值的。java
下面先來介紹一下WebWorkContext的WorkingLanguage屬性,這個是貫穿整個應用的,因此必需要先從這個講起。python
WorkingLanguage
WebWorkContext中對多語言來講最爲重要的一個屬性就是WorkingLanguage,它決定了咱們當前瀏覽頁面所採用的是那種語言。typescript
每次打開一個頁面,包括切換語言時,都是讀取這個WorkingLanguage的值。固然在讀的時候,也作了很多操做:數據庫
-
從當前上下文中的_cachedLanguage變量是否有值,有就直接讀取了這個值。express
-
從GenericAttribute表中查詢當前用戶的語言ID,這張表中的字段Key對應的值是LanguageId時,就代表是某個用戶當前正在使用的語言ID。swift
-
從Language表中查詢出語言信息(當前店鋪->當前店鋪默認->當前店鋪的第一個->全部語言的第一個)markdown
查詢語言表時,首先查出店鋪支持的全部語言,而後找到當前用戶正在使用的語言ID,根據這兩個條件組合獲得的Language實體就是當前的WorkingLanguage。
若是說這兩個條件的組合拿不到相應的語言實體,就會根據當前Store的默認語言ID(以下圖所示)去找。
若是根據Store的默認語言仍是不能找到,就會取這個Store語言列表的第一個。
若是仍是沒有查找到相應的語言,那就不會根據Store去找語言,而是直接取全部發布語言中的第一個,這就要確保在數據庫中必須存在一個初始化的語言。
初始化對任何一個系統都是必不可少的!!
下面是這個屬性get具體的實現片斷:
if (_cachedLanguage != null) return _cachedLanguage; Language detectedLanguage = null; if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled) { //get language from URL detectedLanguage = GetLanguageFromUrl(); } if (detectedLanguage == null && _localizationSettings.AutomaticallyDetectLanguage) { //get language from browser settings //but we do it only once if (!this.CurrentCustomer.GetAttribute<bool>(SystemCustomerAttributeNames.LanguageAutomaticallyDetected, _genericAttributeService, _storeContext.CurrentStore.Id)) { detectedLanguage = GetLanguageFromBrowserSettings(); if (detectedLanguage != null) { _genericAttributeService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageAutomaticallyDetected, true, _storeContext.CurrentStore.Id); } } } if (detectedLanguage != null) { //the language is detected. now we need to save it if (this.CurrentCustomer.GetAttribute<int>(SystemCustomerAttributeNames.LanguageId, _genericAttributeService, _storeContext.CurrentStore.Id) != detectedLanguage.Id) { _genericAttributeService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageId, detectedLanguage.Id, _storeContext.CurrentStore.Id); } } var allLanguages = _languageService.GetAllLanguages(storeId: _storeContext.CurrentStore.Id); //find current customer language var languageId = this.CurrentCustomer.GetAttribute<int>(SystemCustomerAttributeNames.LanguageId, _genericAttributeService, _storeContext.CurrentStore.Id); var language = allLanguages.FirstOrDefault(x => x.Id == languageId); if (language == null) { //it not found, then let's load the default currency for the current language (if specified) languageId = _storeContext.CurrentStore.DefaultLanguageId; language = allLanguages.FirstOrDefault(x => x.Id == languageId); } if (language == null) { //it not specified, then return the first (filtered by current store) found one language = allLanguages.FirstOrDefault(); } if (language == null) { //it not specified, then return the first found one language = _languageService.GetAllLanguages().FirstOrDefault(); } //cache _cachedLanguage = language; return _cachedLanguage;
由於這裏目前不涉及對這個屬性的set操做,只有在切換語言的時候會涉及,因此set的內容會放到切換語言的小節說明。而且在大部分狀況下,用到的都是get操做。
視圖中常規的用法
來看看Nop中比較常規的用法:
我拿了BlogMonths.cshtml中的一小段代碼作演示:
在視圖中,能夠看到不少這樣的寫法,幾乎每一個cshtml文件都會有!
這裏的T實際上是一個delegate。這個delegate有2個輸入參數,並最終返回一個LocalizedString對象。
比較常常的都是隻用到了第一個參數。第一個參數就是對應 LocaleStringResource表中的ResourceName字段
能夠把這個對應關係理解爲一個key-value,就像用網上很多資料用資源文件處理多語言那樣。
下圖是在LocaleStringResource表中用Blog作模糊查詢的示例結果:
至於第二個參數怎麼用,想一想咱們string.Format的用法就知道個因此然了。只要在ResourcesValue中存儲一個帶有佔位符的字符串便可!
上圖中也有部分ResourcesValue用到了這個佔位符的寫法。
其實咱們看了它的實現會更加清晰的理解:
public Localizer T
{
get
{
if (_localizer == null) { //null localizer //_localizer = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); //default localizer _localizer = (format, args) => { var resFormat = _localizationService.GetResource(format); if (string.IsNullOrEmpty(resFormat)) { return new LocalizedString(format); } return new LocalizedString((args == null || args.Length == 0) ? resFormat : string.Format(resFormat, args)); }; } return _localizer; } }
此時可能你們會有個疑問,這裏返回的是一個LocalizedString對象,並非一個字符串,那麼,它是怎麼輸出到頁面並呈現到咱們面前的呢??
最開始的時候我也遲疑了一下,由於源碼在手,因此查看了一下類的定義:
public class LocalizedString : MarshalByRefObject, IHtmlString {}
看到這個類繼承了IHtmlString接口,應該就知道個七七八八了!這個接口的ToHtmlString方法就是問題的本質所在!
當斷點在LocalizedString實現的ToHtmlString方法時會發現,大部分都是走的這個方法,返回的內容也就是所謂鍵值對中的值。
其中還有部分是顯式調用Text等其餘屬性的。
有興趣深刻了解這個接口的內容,能夠去看看msdn上面相關的內容。
視圖中強類型的使用
提及強類型,你們應該也不會陌生,畢竟大部分的MVC教程都會涉及。
在System.Web.Mvc.Html這個命名空間下,有很多靜態類(如InputExtensions,SelectExtensions等)和靜態方法(如TextBoxFor,PasswordFor等)。
其中這些靜態方法中,以For結尾的都是歸屬於強類型。
看看它們的方法簽名就知道了爲何叫強類型了。
public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression);
下面就來看看,Nop在多語言這一塊是怎麼個強類型法。
Nop在強類型這一塊的就一個擴展:NopLabelFor
Nop只在Nop.Admin這個項目中用到這個擴展的,在Nop.Web是沒有用到的。
在我我的看來,這一塊的實現能夠說是挺妙的!下面來看看它是怎麼個妙法:
先來看看它的用法,既然是強類型的,就必然有兩個方面,一個是View,一個是Model
View中的用法
@Html.NopLabelFor(model => model.Name)
Model的定義
[NopResourceDisplayName("Admin.Configuration.Languages.Fields.Name")] public string Name { get; set; }
在View中的用法和其餘強類型的寫法並無什麼太大的區別!只是在Model定義的時候要加上一個Attribute作爲標識
下面來看看它的實現,其實這個的實現主要涉及的相關類就只有兩個:
-
一個是視圖的擴展-HtmlExtensions
-
一個是模型相關的Attribute-NopResourceDisplayName
先來看一下NopResourceDisplayName的實現
public class NopResourceDisplayName : System.ComponentModel.DisplayNameAttribute, IModelAttribute { private string _resourceValue = string.Empty; //private bool _resourceValueRetrived; public NopResourceDisplayName(string resourceKey) : base(resourceKey) { ResourceKey = resourceKey; } public string ResourceKey { get; set; } public override string DisplayName { get { //do not cache resources because it causes issues when you have multiple languages //if (!_resourceValueRetrived) //{ var langId = EngineContext.Current.Resolve<IWorkContext>().WorkingLanguage.Id; _resourceValue = EngineContext.Current .Resolve<ILocalizationService>() .GetResource(ResourceKey, langId, true, ResourceKey); // _resourceValueRetrived = true; //} return _resourceValue; } } public string Name { get { return "NopResourceDisplayName"; } } }
重寫了DisplayNameAttribute的DisplayName ,這樣在界面中展現的時候就會顯示這個值 , 實現了IModelAttribute的Name。
其中DisplayName中是根據ResourcesKey去數據庫中找到要顯示的文字。Name是在HtmlExtensions中用於拿到對應的NopResourceDisplayName對象。
而後是擴展的具體寫法:
public static MvcHtmlString NopLabelFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, bool displayHint = true) { var result = new StringBuilder(); var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData); var hintResource = string.Empty; object value; if (metadata.AdditionalValues.TryGetValue("NopResourceDisplayName", out value)) { var resourceDisplayName = value as NopResourceDisplayName; if (resourceDisplayName != null && displayHint) { var langId = EngineContext.Current.Resolve<IWorkContext>().WorkingLanguage.Id; hintResource = EngineContext.Current.Resolve<ILocalizationService>() .GetResource(resourceDisplayName.ResourceKey + ".Hint", langId); result.Append(helper.Hint(hintResource).ToHtmlString()); } } result.Append(helper.LabelFor(expression, new { title = hintResource })); return MvcHtmlString.Create(result.ToString()); }
這個擴展作的事其實也很簡單,根據模型的NopResourceDisplayName這個Attribute去顯示對應的信息。
不過要注意的是在這裏還作了一個額外的操做:在文字的前面添加了一個小圖標!
能夠看到這句代碼helper.Hint(hintResource).ToHtmlString()
,它調用了另外一個Html的擴展,這個擴展就只是建立了一個img標籤。
最後的效果以下:
這裏還有一個關於驗證相關的實現,這裏的多語言實現與強類型的實現相相似,就不重複了,它的實現依賴於FluentValidation。
模型Property的用法
上面提到的基本都是在頁面上的操做的多語言,Nop中還有很多是直接在controller等地方將多語言的結果查出來賦值給對應的視圖模型再呈現到界面上的!這一點十分感謝 Spraus 前輩的評論提醒!
下面以首頁的Featured products爲例補充說明一下這種用法。
foreach (var product in products) { var model = new ProductOverviewModel { Id = product.Id, Name = product.GetLocalized(x => x.Name), ShortDescription = product.GetLocalized(x => x.ShortDescription), FullDescription = product.GetLocalized(x => x.FullDescription), //... }; //other code }
經過上面的代碼片斷,能夠看出,它也是用了一個泛型的擴展方法來實現的。這個擴展方法就是GetLocalized。
你們應該已經發現這裏的寫法與咱們前面提到的強類型寫法有那麼一點相似~~都是咱們熟悉的lambda表達式。
有那麼一點不一樣的是,這裏的實現是藉助了Linq的Expression。
var member = keySelector.Body as MemberExpression; var propInfo = member.Member as PropertyInfo; TPropType result = default(TPropType); string resultStr = string.Empty; string localeKeyGroup = typeof(T).Name; string localeKey = propInfo.Name; if (languageId > 0) { //localized value if (loadLocalizedValue) { var leService = EngineContext.Current.Resolve<ILocalizedEntityService>(); resultStr = leService.GetLocalizedValue(languageId, entity.Id, localeKeyGroup, localeKey); if (!String.IsNullOrEmpty(resultStr)) result = CommonHelper.To<TPropType>(resultStr); } } //set default value if required if (String.IsNullOrEmpty(resultStr) && returnDefaultValue) { var localizer = keySelector.Compile(); result = localizer(entity); } return result;
上面是這種方式的核心代碼片斷。這裏還涉及到了另外的一張數據表LocalizedProperty
對商品這一塊來講,這樣作的意義就是維護多套不一樣語言的商品資料。有專人來維護這一塊能夠作到更好的分工!
- EntityId -> 實體id(例:商品的id)
- LanguageId -> 語言id
- LocaleKeyGroup -> 所在分組(例:商品組,這裏以類名或表名做爲定義)
- LocaleKey -> 鍵(例:商品名稱,這裏是類的屬性名或表的字段名)
- LocalValue ->值(例:Lumia 950XL,這裏是類的屬性值或表的字段值)
固然這樣子的作法會致使這個表的數據量飆升!尤爲是商品基數太大的時候。這個時候就能夠採用分庫分爲表的方式來處理這個問題。
切換語言
Nop中的切換語言是經過在一個下拉框中選中後經過js跳轉來完成。
window.location.href=/Common/SetLanguage/{langid}?returnUrl=xxx
能夠看到,它是由CommonController下面的SetLanguage這個Action來處理的。
在setlanguage處理的時候,主要有4大步(第三步是Nop.Web這個項目用的),大體的流程以下:
其中還給當前上下文(workcontext)的WorkingLanguage屬性爲找到的那個Language實體。
同時會向GenericAttribute這個表中添加或者更新記錄,這個表就像是一個配置表那樣,存着許多的配置信息。這裏添加或更新的依據是KeyGroup爲Customer,Key爲LanguageId。
具體設置的片斷代碼以下:
var languageId = value != null ? value.Id : 0; _genericAttributeService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageId, languageId, _storeContext.CurrentStore.Id); //reset cache _cachedLanguage = null;
總結
多語言的解決方案有不少,可是不乎下面這幾種狀況居多:
- 資源文件、XML文件等外部文件
- 基於數據庫(字段級別、表級別等)
- 爲每種語言單獨生成一個頁面
- 爲每種語言單獨作一個站點
- 第三方的翻譯API
Nop的多語言是基於數據庫實現的,我我的也是比較偏向於這種實現!
最後用一張思惟導圖來歸納本文的內容