MVC5并使用DropDownList,Cookie,用户配置文件设置设置Culture / CultureUI

时间:2014-06-25 21:07:08

标签: asp.net asp.net-mvc asp.net-mvc-4 localization

我在项目中部分实现了全球化/本地化。该项目需要一个数据库用于资源字符串,我找到了一个名为WestWind.Globalization的优秀NuGet包,它完全符合我的需要。

此NuGet包允许您使用多种不同的方法显示资源字符串。它提供了一个生成强类型类的选项,该类包含所有资源字符串,因此您可以像以下一样使用它:

@Html.Encode( Resources.lblResourceName )

object Value = this.GetLocalResourceObject("ResourceName");

object GlobalValue = this.GetGlobalResourceObject("Resources","ResourceKey");

甚至:

dbRes.T(resourceName, resourceSet, culture)

我不想手动指定文化,所以我选择了这种方法:

<p class="pageprompt">@AccountRequestAccount.pagePrompt</p>

对我而言,Westwind.Globalization是神奇的。它为我解决了一个巨大的问题,但我遇到了一个障碍,我不确定如何克服。也就是说,如何设置Culture / CultureUI以使包自动使用指定的语言资源。

我创建了一个包含语言下拉列表的PartialView。它包含在〜/ Views / Shared /文件夹中,并包含在_Layout.cshtml中。我编写了GET和POST控制器动作,它们按预期工作,除了我无法持久保存Culture / CultureUI设置。我怀疑这是由于语言选择后立即重定向(如下所述)

所以,我发现SO question有一个似乎可行的答案。我将这个答案整合到我的项目中。相关代码是:

RouteConfig.cs:

 routes.MapRoute("DefaultLocalized",
 "{language}-{culture}/{controller}/{action}/{id}",
 new
 {
     controller = "Home",
     action = "Index",
     id = "",
     language = "en",
     culture = "US"
 });

〜/助手/ InternationalizationAttribute.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;

namespace GPS_Web_App.Helpers
{
    public class InternationalizationAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            string language = 
                (string)filterContext.RouteData.Values["language"] ?? "en";

            string culture = 
                (string)filterContext.RouteData.Values["culture"] ?? "US";

            Thread.CurrentThread.CurrentCulture =
                CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
                language, culture));

            Thread.CurrentThread.CurrentUICulture = 
                CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
                language, culture));
        }
    }
}

在我的控制器中:

[Authorize]
[Internationalization]
public class AccountController : Controller
{
    ...
}

到目前为止一切顺利。这样做的原因是我可以转到http://example.com/en-mx/Account/Login/的网址,并看到由Westwind.Globalization本地化的页面以及我创建的资源字符串。

我遇到的问题是:

  1. 如果用户是匿名用户,他们的语言偏好应由Cookie控制(如果存在),否则默认为en-US。

  2. 如果用户已通过身份验证,则其语言首选项应由其配置文件设置中的“语言”字段控制。 (使用ASP.NET Identity 2.0的简单成员资格)。

  3. 全局标题中有一个语言选择下拉列表。用户应该能够从下拉列表中选择他们的语言首选项,如果他们这样做,则设置将写入cookie(对于匿名用户和经过身份验证的用户),如果用户通过身份验证,则用户配置文件中的语言设置会更新。

  4. 不是世界末日,但最好不要在URL中包含该语言。有些人可能会问,为什么我要安装@ jao的解决方案呢?让我解释一下。

  5. 所有代码都适用于下拉列表,以允许用户进行语言选择。上面#1,#2和#3的逻辑工作正常,但不会生效并触发Westwind.Globalization的DbResourceProvider传递选定的语言资源字符串。

    我通过调试发现的是我的设置没有坚持:

    System.Threading.Thread.CurrentThread.CurrentCulture = 
        System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
    System.Threading.Thread.CurrentThread.CurrentUICulture = 
        System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
    

    通过我的问题在这里提供的回复,我了解到如果在原始View渲染之前进行重定向,那些设置将不会持续/生效。然而,重新定向回原始视图似乎是明智的,因为语言正在被更改并需要再次呈现。我认为@jao的解决方案克服了重定向问题,但它强制通过URL指定全球化/本地化?有点抓 - 22 ......

    我已经要求@jao审核这个问题并提供任何有关此问题的提示。我认为我的问题总结如下:

    如何使用用户的Cookie /个人资料设置一劳永逸地设置Culture / CultureUI,以便Westwind.Globalization可以阅读全球化/本地化,而不是依赖于传入的文化URL吗

1 个答案:

答案 0 :(得分:1)

我将此答案作为使用异步控制器进行ASP.NET MVC5本地化的替代自定义方式发布。也许你可能会在我的解决方案中找到一些问题,特别是在路由和设置cookie时。

这是我为异构/自定义方法潦草的简短教程。所以我更喜欢WordPress。 :)

很抱歉没有为您的问题提供准确而独立的答案。希望它能以其他方式帮助你,以及其他人;谁想要做同样的设置。


在他的blog post中,Nadeem Afana描述了在解决方案中创建单独的项目资源以使用静态资源文件实现国际化的策略。在blog sequel中,他详细介绍了扩展同一个项目以通过数据库和XML驱动的方法处理资源。对于前一个,他使用了与实体框架分离的ADO.NET。

我们需要在MVC项目中实现静态和动态资源,尊重MVC约定的概念。

首先,让我们在项目根目录中添加一个Resources文件夹,其中包含所需的语言变体:~/Resources/Resources.resx(默认资源文件对应于en-US文化),~/Resources/Resources.fi.resx~/Resources/Resources.nl.resx。将资源标记为公共资源,以便在视图中使用它们。

~/Views/Web.config中,在<namespace>元素下添加资源名称空间:<add namespace="YourMainNamespace.Reousrces" />。在控制器下,创建一个基本控制器类:

以下是Cookie

namespace YourNamespace.Controllers
{
    // Don't forget to inherit other controllers with this
    public class BaseController : Controller
    {
        protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
        {
            string cultureName = null;

            // Attempt to read the culture cookie from Request
            HttpCookie cultureCookie = Request.Cookies["_culture"];
            if (cultureCookie != null)
                cultureName = cultureCookie.Value;
            else
                cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
                        Request.UserLanguages[0] :  // obtain it from HTTP header AcceptLanguages
                        null;
            // Validate culture name
            cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe

            // Modify current thread's cultures            
            Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
            Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

            return base.BeginExecuteCore(callback, state);
        }
    }
}

接下来,在~/Global.asax.cs中注册一个全局过滤器,以确保每个操作在执行之前都应使用正确的文化:

这里再次出现了cookies!

public class SetCultureActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
       base.OnActionExecuting(filterContext);

        var response = filterContext.RequestContext.HttpContext.Response;
        var culture = filterContext.RouteData.Values["culture"].ToString();

        // Validate input
        culture = CultureHelper.GetImplementedCulture(culture);

        Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);

        // Save culture in a cookie
        HttpCookie cookie = filterContext.RequestContext.HttpContext.Request.Cookies["_culture"];
        if (cookie != null)
            cookie.Value = culture;   // update cookie value
        else
        {
            cookie = new HttpCookie("_culture");
            cookie.Value = culture;
            cookie.Expires = DateTime.Now.AddYears(1);
        }
        response.Cookies.Add(cookie);
    }
}

GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute());方法中添加MyApplication.Application_Start()

~/App_Start/RoutesConfig.cs中,将默认路线更改为:

routes.MapRoute(
    name: "Default",
    url: "{culture}/{controller}/{action}/{id}",
    defaults: new { culture = "en-US", controller = "Home", action = "Index", id = UrlParameter.Optional }
);

此时,我们可以在视图中使用资源。例如; @Resources.Headline

接下来,我们将为模型属性创建一个名为Translatable的自定义属性。

class TranslatableAttribute : Attribute
{ }

这就够了。但是,如果您希望能够指定范围,则可以使用此类来实现它。

现在添加一个名为Resource的模型,其中包含三个属性和一个辅助方法:

public class Resource
{
    [Key, Column(Order = 0)]
    public string Culture { get; set; }

    [Key, Column(Order = 1)]
    public string Name { get; set; }

    public string Value { get; set; }

    #region Helpers
    // Probably using reflection not the best approach.
    public static string GetPropertyValue<T>(string id, string propertyName) where T : class
    {
        return GetPropertyValue<T>(id, propertyName, Thread.CurrentThread.CurrentUICulture.Name);
    }
    public static string GetPropertyValue<T>(string id, string propertyName, string culture) where T : class
    {
        Type entityType = typeof(T);
        string[] segments = propertyName.Split('.');

        if (segments.Length > 1)
        {
            entityType = Type.GetType("YourNameSpace.Models." + segments[0]);
            propertyName = segments[1];
        }

        if (entityType == null)
            return "?<invalid type>";

        var propertyInfo = entityType.GetProperty(propertyName);
        var translateableAttribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true)
                                    .FirstOrDefault();
        /*var requiredAttribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), true)
                                .FirstOrDefault();*/

        if (translateableAttribute == null)
            return "?<this field has no translatable attribute>";

        var dbCtx = new YourNamespaceDbContext();
        var className = entityType.Name;
        Resource resource = dbCtx.Resources.Where(r =>
                            (r.Culture == culture) &&
                            r.Name == className + id + propertyName).FirstOrDefault();

        if (resource != null)
            return resource.Value;

        //return requiredAttribute == null ? string.Empty : "?<translation not found>";
        return string.Empty;
    }
    #endregion
}

此辅助方法将帮助您检索已翻译的内容。例如,您可以说:

var name = Resource.GetPropertyValue<Product>(item.Id.ToString(), "Name");

请注意,在任何时候,可翻译字段列中的数据都是不可靠的;它将始终保持最后更新的值。在创建记录时,我们将镜像所有可翻译的属性&#39;资源模型中所有受支持文化的值。

我们正在使用异步控制器,因此对于插入,修改和删除,我们将在SaveChangesAsync()类中覆盖DbContext

public override Task<int> SaveChangesAsync()
{
    ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;

    List<ObjectStateEntry> objectDeletedStateEntryList =
        ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
        .ToList();

    List<ObjectStateEntry> objectCreateOrModifiedStateEntryList =
        ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
                                                    | EntityState.Modified)
        .ToList();

    // First handle the delition case,
    // before making changes to entry state
    bool changed = UpdateResources(objectDeletedStateEntryList);

    // Now save the changes
    int result = base.SaveChangesAsync().Result;

    // Finally handle the remaining cases
    changed |= UpdateResources(objectCreateOrModifiedStateEntryList);

    if (changed)
        return base.SaveChangesAsync();

    return Task.FromResult<int>(result);
}

private bool UpdateResources(List<ObjectStateEntry> objectStateEntryList)
{
    bool changed = false;

    foreach (ObjectStateEntry entry in objectStateEntryList)
    {
        var typeName = entry.EntitySet.ElementType.Name;

        if (entry.IsRelationship || typeName == "Resource")
            return false;

        var type = Type.GetType("YourNamespace.Models." + typeName);

        if (type == null) // When seeds run (db created for the first-time), sometimes types might not be create
            return false;

        if (entry.State == EntityState.Deleted)
        {
            changed |= DeleteResources(type, typeName, entry);
            continue;
        }

        foreach (var propertyInfo in type.GetProperties())
        {
            var attribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true).FirstOrDefault();

            if (attribute == null)
                continue;

            CurrentValueRecord current = entry.CurrentValues;
            object idField = current.GetValue(current.GetOrdinal("Id"));

            if (idField == null)
                continue;

            var id = idField.ToString();
            var propertyName = propertyInfo.Name;
            string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();
            var name = typeName + id + propertyName;

            Resource existingResource = this.Resources.Find(Thread.CurrentThread.CurrentUICulture.Name, name);

            if (existingResource == null)
            {
                foreach (var culture in CultureHelper.Cultures)
                {
                    this.Resources.Add(new Resource
                    {
                        Culture = culture,
                        Name = name,
                        Value = newValue
                    });

                    changed |= true;
                }
            }
            else
            {
                existingResource.Value = newValue;
                changed |= true;
            }
        }
    }

    return changed;
}

private bool DeleteResources(Type type, string typeName, ObjectStateEntry entry)
{
    bool changed = false;
    var firstKey = entry.EntityKey.EntityKeyValues.Where(k => k.Key.Equals("Id", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();

    if (firstKey == null)
        return false;

    var id = firstKey.Value.ToString();

    foreach (var propertyInfo in type.GetProperties())
    {
        var name = typeName + id + propertyInfo.Name;

        foreach (var culture in CultureHelper.Cultures)
        {
            Resource existingResource = this.Resources.Find(culture, name);

            if (existingResource == null)
                continue;

            this.Resources.Remove(existingResource);
            changed |= true;
        }
    }

    return changed;
}

这将负责更新和删除。