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

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

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

不啰嗦了,開(kāi)始正題了!

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

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

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

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

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

WorkingLanguage

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

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

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

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

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

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

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

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

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

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

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

下面是這個(gè)屬性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)檫@里目前不涉及對(duì)這個(gè)屬性的set操作,只有在切換語(yǔ)言的時(shí)候會(huì)涉及,所以set的內(nèi)容會(huì)放到切換語(yǔ)言的小節(jié)說(shuō)明。并且在大部分情況下,用到的都是get操作。

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

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

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

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

在視圖中,可以看到很多這樣的寫(xiě)法,幾乎每個(gè)cshtml文件都會(huì)有!

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

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

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

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

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

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

上圖中也有部分ResourcesValue用到了這個(gè)占位符的寫(xiě)法。

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

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

此時(shí)可能大家會(huì)有個(gè)疑問(wèn),這里返回的是一個(gè)LocalizedString對(duì)象,并不是一個(gè)字符串,那么,它是怎么輸出到頁(yè)面并呈現(xiàn)到我們面前的呢??

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

public class LocalizedString : MarshalByRefObject, IHtmlString{}

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

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

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

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

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

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

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

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

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

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

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

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

Nop只在Nop.Admin這個(gè)項(xiàng)目中用到這個(gè)擴(kuò)展的,在Nop.Web是沒(méi)有用到的。

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

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

View中的用法

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

Model的定義

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

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

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

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

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

先來(lái)看一下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"; }
    }
}

重寫(xiě)了DisplayNameAttribute的DisplayName ,這樣在界面中展示的時(shí)候就會(huì)顯示這個(gè)值 , 實(shí)現(xiàn)了IModelAttribute的Name。

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

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

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

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

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

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

最后的效果如下:

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

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

切換語(yǔ)言

Nop中的切換語(yǔ)言是通過(guò)在一個(gè)下拉框中選中后通過(guò)js跳轉(zhuǎn)來(lái)完成。

window.location.href=/Common/SetLanguage/{langid}?returnUrl=xxx

可以看到,它是由CommonController下面的SetLanguage這個(gè)Action來(lái)處理的。

在setlanguage處理的時(shí)候,主要有4大步(第三步是Nop.Web這個(gè)項(xiàng)目用的),大致的流程如下:

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

其中還給當(dāng)前上下文(workcontext)的WorkingLanguage屬性為找到的那個(gè)Language實(shí)體。

同時(shí)會(huì)向GenericAttribute這個(gè)表中添加或者更新記錄,這個(gè)表就像是一個(gè)配置表那樣,存著許多的配置信息。這里添加或更新的依據(jù)是KeyGroup為Customer,Key為L(zhǎng)anguageId。

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

具體設(shè)置的片段代碼如下:

var languageId = value != null ? value.Id : 0;
_genericAttributeService.SaveAttribute(this.CurrentCustomer,
    SystemCustomerAttributeNames.LanguageId,
    languageId, _storeContext.CurrentStore.Id);//reset cache_cachedLanguage = null;

總結(jié)

多語(yǔ)言的解決方案有很多,但是不乎下面這幾種情況居多:

  • 資源文件、XML文件等外部文件

  • 基于數(shù)據(jù)庫(kù)(字段級(jí)別、表級(jí)別等)

  • 為每種語(yǔ)言單獨(dú)生成一個(gè)頁(yè)面

  • 為每種語(yǔ)言單獨(dú)做一個(gè)站點(diǎn)

  • 第三方的翻譯API

Nop的多語(yǔ)言是基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)的,我個(gè)人也是比較偏向于這種實(shí)現(xiàn)!

最后用一張思維導(dǎo)圖來(lái)概括本文的內(nèi)容

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)