MVC5和设置文化/文化与下拉列表,Cookie,用户配置文件设置



我已经在我的项目中部分实现了全球化/本地化。这个项目需要一个数据库用于资源字符串,我发现了一个很好的NuGet包,叫做WestWind。全球化正是我需要的。

这个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>

给我,西风。全球化是神奇的。它为我解决了一个巨大的问题,但我遇到了一个我不确定如何克服的障碍。也就是说,如何设置Culture/culturei,以便包将自动使用指定的语言资源。

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

所以,我找到了一个So问题,它的答案似乎可行。我把这个答案融入到我的项目中。相关代码为:

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

In my Controllers:

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

到目前为止一切顺利。这工作,我能够去到http://example.com/en-mx/Account/Login/的URL,看到被西风本地化的页面。全球化和我创建的资源字符串。

我遇到的问题是:

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

  2. 如果用户通过身份验证,他们的语言首选项应该由配置文件设置中的语言字段控制。(使用ASP的简单会员。. NET Identity 2.0).

  3. 在全局标题中有一个语言选择下拉菜单。用户应该能够从下拉菜单中选择他们的语言偏好,如果他们这样做,设置被写入cookie(匿名和认证用户),如果用户被认证,他们在用户配置文件中的语言设置得到更新。

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

所有的代码都准备好了,下拉菜单允许用户进行语言选择。上面第1、2和3条的逻辑是正确的,但不会生效并触发西风。全球化的DbResourceProvider传递选择的语言资源字符串。

我通过调试发现我的设置没有在:

中持久化。
System.Threading.Thread.CurrentThread.CurrentCulture = 
    System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
System.Threading.Thread.CurrentThread.CurrentUICulture = 
    System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);

通过我在SO上的问题提供的回答,我了解到如果在原始视图呈现之前进行重定向,这些设置将不会持久/生效。然而,重定向回原始视图似乎是明智的,因为语言正在改变,需要再次呈现。我认为@jao的解决方案克服了重定向问题,但它迫使全球化/本地化由URL指定?有点进退两难…

我已经要求@jao复习这个问题并提供任何提示。我想我的问题可以总结如下:

如何使用用户的cookie/profile设置来一劳永逸地设置Culture/CultureUI,以便西风。全球化可以阅读全球化/本地化,而不是依赖于在URL中传递的文化?

我张贴这个答案作为一个替代的,自定义的方式做本地化与ASP。. NET MVC5与异步控制器。也许你会在我的解决方案中发现一些漏洞,特别是在路由和设置cookie时。

这是我为我的异构/自定义方法写的一个简短的教程。所以比起WordPress,我更喜欢So。:)

很抱歉没有给你的问题一个精确而离散的答案。希望它能在其他方面对你有所帮助,对其他人也有帮助;他们也想做同样的设置。


在他的博客文章中,Nadeem Afana描述了在解决方案中创建一个单独的项目Resource的策略,以使用静态资源文件实现国际化。在博客的后续文章中,他详细介绍了如何扩展同一个项目,通过数据库和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" />。在controllers下,创建一个基本的控制器类:

饼干来了

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中注册一个全局过滤器,以确保每个操作在执行之前都应该使用正确的区域性:

饼干又来了!

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

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

~/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");

请注意,在任何时候,可翻译字段列中的数据都是不可靠的;它将始终保存最后更新的值。在创建记录时,我们将为所有支持的区域性镜像Resource模型中所有可翻译属性的值。

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

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

这将负责更新和删除

最新更新