前言

這段時間在研究多語言的實(shí)現(xiàn),就找了NopCommerce這個開源項目來研究了一下,并把自己對這個項目的粗淺認(rèn)識與大家分享一下。

挺碰巧的是昨天收到了NopCommerce 3.90 發(fā)布測試版的郵件:

電腦培訓(xùn),計算機(jī)培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

不啰嗦了,開始正題了!

其實(shí)對于Nop的多語言,最主要的元素有下面兩個:

  • WebWorkContext(IWorkContext的實(shí)現(xiàn)類)

  • LocalizationService(ILocalizationService的實(shí)現(xiàn)類)

其他相關(guān)的元素可以說都是在這兩個的基礎(chǔ)上體現(xiàn)價值的。

下面先來介紹一下WebWorkContext的WorkingLanguage屬性,這個是貫穿整個應(yīng)用的,所以必須要先從這個講起。

WorkingLanguage

WebWorkContext中對多語言來說最為重要的一個屬性就是WorkingLanguage,它決定了我們當(dāng)前瀏覽頁面所采用的是那種語言。

每次打開一個頁面,包括切換語言時,都是讀取這個WorkingLanguage的值。當(dāng)然在讀的時候,也做了不少操作:

  1. 從當(dāng)前上下文中的_cachedLanguage變量是否有值,有就直接讀取了這個值。

  2. GenericAttribute表中查詢當(dāng)前用戶的語言ID,這張表中的字段Key對應(yīng)的值是LanguageId時,就表明是某個用戶當(dāng)前正在使用的語言ID。

  3. Language表中查詢出語言信息(當(dāng)前店鋪->當(dāng)前店鋪默認(rèn)->當(dāng)前店鋪的第一個->所有語言的第一個)

查詢語言表時,首先查出店鋪支持的所有語言,然后找到當(dāng)前用戶正在使用的語言ID,根據(jù)這兩個條件組合得到的Language實(shí)體就是當(dāng)前的WorkingLanguage。

如果說這兩個條件的組合拿不到相應(yīng)的語言實(shí)體,就會根據(jù)當(dāng)前Store的默認(rèn)語言ID(如下圖所示)去找。

電腦培訓(xùn),計算機(jī)培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

如果根據(jù)Store的默認(rèn)語言還是不能找到,就會取這個Store語言列表的第一個。

如果還是沒有查找到相應(yīng)的語言,那就不會根據(jù)Store去找語言,而是直接取所有發(fā)布語言中的第一個,這就要確保在數(shù)據(jù)庫中必須存在一個初始化的語言。

初始化對任何一個系統(tǒng)都是必不可少的??!

下面是這個屬性get具體的實(shí)現(xiàn)片段:

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 languagevar 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;

因?yàn)檫@里目前不涉及對這個屬性的set操作,只有在切換語言的時候會涉及,所以set的內(nèi)容會放到切換語言的小節(jié)說明。并且在大部分情況下,用到的都是get操作。

視圖中常規(guī)的用法

來看看Nop中比較常規(guī)的用法:

我拿了BlogMonths.cshtml中的一小段代碼做演示:

電腦培訓(xùn),計算機(jī)培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

在視圖中,可以看到很多這樣的寫法,幾乎每個cshtml文件都會有!

這里的T其實(shí)是一個delegate。這個delegate有2個輸入?yún)?shù),并最終返回一個LocalizedString對象。

比較經(jīng)常的都是只用到了第一個參數(shù)。第一個參數(shù)就是對應(yīng) LocaleStringResource表中的ResourceName字段

可以把這個對應(yīng)關(guān)系理解為一個key-value,就像用網(wǎng)上不少資料用資源文件處理多語言那樣。

下圖是在LocaleStringResource表中用Blog做模糊查詢的示例結(jié)果:

電腦培訓(xùn),計算機(jī)培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

至于第二個參數(shù)怎么用,想想我們string.Format的用法就知道個所以然了。只要在ResourcesValue中存儲一個帶有占位符的字符串即可!

上圖中也有部分ResourcesValue用到了這個占位符的寫法。

其實(shí)我們看了它的實(shí)現(xiàn)會更加清晰的理解:

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對象,并不是一個字符串,那么,它是怎么輸出到頁面并呈現(xiàn)到我們面前的呢??

最開始的時候我也遲疑了一下,因?yàn)樵创a在手,所以查看了一下類的定義:

public class LocalizedString : MarshalByRefObject, IHtmlString{}

看到這個類繼承了IHtmlString接口,應(yīng)該就知道個七七八八了!這個接口的ToHtmlString方法就是問題的本質(zhì)所在!

當(dāng)斷點(diǎn)在LocalizedString實(shí)現(xiàn)的ToHtmlString方法時會發(fā)現(xiàn),大部分都是走的這個方法,返回的內(nèi)容也就是所謂鍵值對中的值。

其中還有部分是顯式調(diào)用Text等其他屬性的。

有興趣深入了解這個接口的內(nèi)容,可以去看看msdn上面相關(guān)的內(nèi)容。

視圖中強(qiáng)類型的使用

說起強(qiáng)類型,大家應(yīng)該也不會陌生,畢竟大部分的MVC教程都會涉及。

在System.Web.Mvc.Html這個命名空間下,有不少靜態(tài)類(如InputExtensions,SelectExtensions等)和靜態(tài)方法(如TextBoxFor,PasswordFor等)。

其中這些靜態(tài)方法中,以For結(jié)尾的都是歸屬于強(qiáng)類型。

看看它們的方法簽名就知道了為什么叫強(qiáng)類型了。

public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression);

下面就來看看,Nop在多語言這一塊是怎么個強(qiáng)類型法。

Nop在強(qiáng)類型這一塊的就一個擴(kuò)展:NopLabelFor

Nop只在Nop.Admin這個項目中用到這個擴(kuò)展的,在Nop.Web是沒有用到的。

在我個人看來,這一塊的實(shí)現(xiàn)可以說是挺妙的!下面來看看它是怎么個妙法:

先來看看它的用法,既然是強(qiáng)類型的,就必然有兩個方面,一個是View,一個是Model

View中的用法

@Html.NopLabelFor(model => model.Name)

Model的定義

[NopResourceDisplayName("Admin.Configuration.Languages.Fields.Name")]
[AllowHtml]public string Name { get; set; }

在View中的用法和其他強(qiáng)類型的寫法并沒有什么太大的區(qū)別!只是在Model定義的時候要加上一個Attribute做為標(biāo)識

下面來看看它的實(shí)現(xiàn),其實(shí)這個的實(shí)現(xiàn)主要涉及的相關(guān)類就只有兩個:

  • 一個是視圖的擴(kuò)展-HtmlExtensions

  • 一個是模型相關(guān)的Attribute-NopResourceDisplayName

先來看一下NopResourceDisplayName的實(shí)現(xiàn)

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 ,這樣在界面中展示的時候就會顯示這個值 , 實(shí)現(xiàn)了IModelAttribute的Name。

其中DisplayName中是根據(jù)ResourcesKey去數(shù)據(jù)庫中找到要顯示的文字。Name是在HtmlExtensions中用于拿到對應(yīng)的NopResourceDisplayName對象。

然后是擴(kuò)展的具體寫法:

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());
}

這個擴(kuò)展做的事其實(shí)也很簡單,根據(jù)模型的NopResourceDisplayName這個Attribute去顯示對應(yīng)的信息。

不過要注意的是在這里還做了一個額外的操作:在文字的前面添加了一個小圖標(biāo)!

可以看到這句代碼helper.Hint(hintResource).ToHtmlString(),它調(diào)用了另一個Html的擴(kuò)展,這個擴(kuò)展就只是創(chuàng)建了一個img標(biāo)簽。

最后的效果如下:

電腦培訓(xùn),計算機(jī)培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

這里還有一個關(guān)于驗(yàn)證相關(guān)的實(shí)現(xiàn),這里的多語言實(shí)現(xiàn)與強(qiáng)類型的實(shí)現(xiàn)相類似,就不重復(fù)了,它的實(shí)現(xiàn)依賴于FluentValidation

模型Property的用法

上面提到的基本都是在頁面上的操作的多語言,Nop中還有不少是直接在controller等地方將多語言的結(jié)果查出來賦值給對應(yīng)的視圖模型再呈現(xiàn)到界面上的!這一點(diǎn)十分感謝 Spraus 前輩的評論提醒!

下面以首頁的Featured products為例補(bǔ)充說明一下這種用法。

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}

通過上面的代碼片段,可以看出,它也是用了一個泛型的擴(kuò)展方法來實(shí)現(xiàn)的。這個擴(kuò)展方法就是GetLocalized。

大家應(yīng)該已經(jīng)發(fā)現(xiàn)這里的寫法與我們前面提到的強(qiáng)類型寫法有那么一點(diǎn)類似~~都是我們熟悉的lambda表達(dá)式。

有那么一點(diǎn)不同的是,這里的實(shí)現(xiàn)是借助了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);
    }


作者:Catcher ( 黃文清 )

來源:http://catcher1994.cnblogs.com/

聲明: 本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。如果您發(fā)現(xiàn)博客中出現(xiàn)了錯誤,或者有更好的建議、想法,請及時與我聯(lián)系?。∪绻胝椅宜较陆涣?,可以私信或者加我QQ。

http://www.cnblogs.com/catcher1994/p/6445871.html