【问题标题】:MVC5 and setting Culture/CultureUI with DropDownList, Cookie, User Profile SettingMVC5 和使用 DropDownList、Cookie、用户配置文件设置设置 Culture/CultureUI
【发布时间】:2014-06-25 21:07:08
【问题描述】:

我在我的项目中部分实施了全球化/本地化。该项目需要一个用于存储资源字符串的数据库,我发现了一个名为 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"
 });

~/Helpers/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/ 的 URL 并查看由 Westwind.Globalization 本地化的页面以及我创建的资源字符串。

我遇到的问题是:

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

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

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

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

下拉菜单的所有代码都已到位,以允许用户进行语言选择。上面 #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);

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

我已经要求@jao 审查这个问题并提供任何提示。我认为我的问题最好总结为:

如何使用用户的 cookie/配置文件设置一劳永逸地设置 Culture/CultureUI,以便 Westwind.Globalization 可以读取 Globalization/Localization 而不是依赖于 URL 中传递的 Culture?强>

【问题讨论】:

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


    【解决方案1】:

    我将此答案发布为使用带有异步控制器的 ASP.NET MVC5 进行本地化的另一种自定义方式。也许您会在我的解决方案中发现一些问题,尤其是在路由和设置 cookie 方面。

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

    很抱歉没有对您的问题给出准确而离散的答案。希望它会以其他方式帮助您,以及其他人;谁正在寻求进行相同类型的设置。


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

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

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

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

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

    饼干又来了!

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

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

    我们正在使用异步控制器,因此对于插入、修改和删除,我们将在 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;
    }
    

    这将负责更新和删除。

    【讨论】:

      猜你喜欢
      • 2022-01-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多