简体   繁体   English

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

[英]MVC5 and setting Culture/CultureUI with DropDownList, Cookie, User Profile Setting

I have partially implemented Globalization/Localization in my project. 我在我的项目中部分实施了全球化/本地化。 The project requires a database to be used for resource strings and I found an excellent NuGet package called WestWind.Globalization that does exactly what I needed. 该项目需要一个用于资源字符串的数据库,我找到了一个出色的NuGet包,名为WestWind.Globalization,它完全可以满足我的需求。

This NuGet package allows you to display resource strings using several different methods. 此NuGet包使您可以使用几种不同的方法显示资源字符串。 It provides an option to generate a strongly typed class that contains all of your resource strings so you can use it like: 它提供了一个生成包含所有资源字符串的强类型类的选项,因此您可以像这样使用它:

@Html.Encode( Resources.lblResourceName )

or 要么

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

or 要么

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

and even: 乃至:

dbRes.T(resourceName, resourceSet, culture)

I didn't want to specify the culture manually, so I opted for this method: 我不想手动指定区域性,因此选择了此方法:

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

For me, Westwind.Globalization is magical. 对我来说,西风。全球化是神奇的。 It resolved a huge issue for me, but I ran into a snag that I wasn't sure how to overcome. 它为我解决了一个巨大的问题,但是我遇到了一个不确定的问题,我不确定该如何克服。 That was, how to set the Culture/CultureUI so that the package would automatically use a specified language resource. 也就是说,如何设置Culture / CultureUI,以便程序包将自动使用指定的语言资源。

I created a PartialView that contains a dropdown list of languages. 我创建了一个PartialView,其中包含语言的下拉列表。 It is contained in the ~/Views/Shared/ folder and gets included in _Layout.cshtml. 它包含在〜/ Views / Shared /文件夹中,并包含在_Layout.cshtml中。 I coded the GET and POST Controller Actions which work as intended, except that I was unable to persist the Culture/CultureUI settings. 我对GET和POST Controller Actions进行了编码,它们可以按预期工作,但是我无法保存Culture / CultureUI设置。 I suspect that it was due to a redirect immediately following language selection (explained below) 我怀疑这是由于在选择语言后立即进行了重定向(如下所述)

So, I found an SO question that had an answer that seemed viable. 因此,我发现一个SO问题的答案似乎可行。 I integrated that answer into my project. 我将该答案整合到我的项目中。 The relevant code is: 相关代码为:

RouteConfig.cs: RouteConfig.cs:

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

~/Helpers/InternationalizationAttribute.cs 〜/助手/ 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
{
    ...
}

So far so good. 到现在为止还挺好。 This works in that I am able to go to a URL of http://example.com/en-mx/Account/Login/ and see the page being localized by Westwind.Globalization and the resource strings I've created. 之所以可以这样做,是因为我能够转到http://example.com/en-mx/Account/Login/的URL,并看到Westwind.Globalization正在本地化的页面以及我创建的资源字符串。

The problems I have with this are: 我的问题是:

  1. If the user is anonymous their language preference should be controlled by cookie (if it exists) otherwise default to en-US. 如果用户是匿名用户,则其语言首选项应由Cookie(如果存在)控制,否则默认为en-US。

  2. If the user is authenticated their language preference should be controlled by the Language field in their profile settings. 如果用户通过了身份验证,则其语言首选项应由其配置文件设置中的“语言”字段控制。 (Simple Membership using ASP.NET Identity 2.0). (使用ASP.NET Identity 2.0的简单成员资格)。

  3. There is a language selection dropdown in a global header. 全局标题中有一个语言选择下拉列表。 The user should be able to choose their language preference from the dropdown and if they do, the setting gets written to cookie (for both anonymous and authenticated users) and if the user is authenticated their Language setting in the user profile gets updated. 用户应该能够从下拉菜单中选择他们的语言首选项,并且如果这样做,设置将被写入cookie(对于匿名用户和已认证用户),并且如果用户通过了身份验证,则其用户配置文件中的Language设置也会被更新。

  4. Not the end of the world, but it would be highly preferable that the language not be included in the URL. 并非世界末日,但最好不要在URL中包含该语言。 Some might ask, well why did I install @jao's solution? 有人可能会问,为什么我要安装@jao的解决方案? Let me explain that. 让我解释一下。

All of the code was in place for the dropdown to allow a user to make a language selection. 下拉菜单中的所有代码均已就绪,以允许用户进行语言选择。 The logic for #1, #2, and #3 above were working correctly, but wouldn't take effect and trigger Westwind.Globalization's DbResourceProvider to pass the selected language resource strings. 上面#1,#2和#3的逻辑正常工作,但是不会生效并触发Westwind.Globalization的DbResourceProvider传递所选语言资源字符串。

What I discovered through debugging was that my settings were not persisting in: 通过调试发现,我的设置没有持久化:

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

Through responses provided by my question here on SO I learned that those settings would not persist/take effect if a redirect was made prior to the original View rendering. 通过我在此处关于SO的问题提供的答复,我了解到,如果在原始View呈现之前进行重定向,则这些设置将不会持久/不会生效。 Yet a redirect back to the original View seemed sensible since the language was being changed and needed to be rendered again. 但是,由于更改了语言并需要重新渲染,因此重定向回原始视图似乎是明智的。 I think @jao's solution overcomes the redirect problem, but it forces Globalization/Localization to be specified by the URL? 我认为@jao的解决方案克服了重定向问题,但是它强制通过URL指定全球化/本地化吗? Somewhat of a catch-22... 有点抓22 ...

I have asked @jao to review this question and provide any hints on this. 我已要求@jao审查此问题并提供任何提示。 I think my question is best summed up as this: 我认为我的问题最好总结如下:

How can I use the user's cookie/profile settings to set the Culture/CultureUI once and for all so that Westwind.Globalization can read Globalization/Localization instead of relying on the Culture being passed in the URL? 我如何使用用户的cookie /配置文件设置一劳永逸地设置Culture / CultureUI,以便Westwind.Globalization可以读取Globalization / Localization而不是依赖于URL中传递的Culture?

I am posting this answer as an alternate, custom way of doing localization with ASP.NET MVC5 with asynchronous controller. 我将此答案发布为使用带有异步控制器的ASP.NET MVC5进行本地化的另一种自定义方式。 Perhaps you may find some gotchas in my solution especially when it comes to routing and setting cookies. 也许您可能会在我的解决方案中发现一些陷阱,尤其是在路由和设置Cookie方面。

This is sort of a short tutorial I scribbled down for my heterogeneous/custom approach. 这是我为异类/自定义方法编写的简短教程。 So I preferred SO over WordPress. 所以我更喜欢WordPress。 :) :)

Sorry for not giving the precise and discrete answer to your problem. 很抱歉没有为您的问题提供准确和离散的答案。 Hopefully it will help you in some other way, and other folks as well; 希望它会以其他方式帮助您,以及其他人; who are looking to do the same sort of setup. 谁想要做同样的设置。


In his blog post , Nadeem Afana described a strategy of creating a separate project Resource in the solution to implement internationalization using static resource files. Nadeem Afana在他的博客文章中描述了一种在解决方案中创建单独项目Resource的策略,以使用静态资源文件实现国际化。 In the blog sequel , he detailed on extending the same project to handle resources via Databases and XML-driven approaches. 博客续集中 ,他详细介绍了如何扩展同一项目以通过数据库和XML驱动的方法来处理资源。 For the former one, he used ADO.NET, decoupled from Entity Framework. 对于前一个,他使用了与实体框架分离的ADO.NET。

We needed to implement both static and dynamic resources within the MVC project, respecting the concepts of MVC conventions. 我们需要在MVC项目中实现静态和动态资源,同时要尊重MVC约定的概念。

First lets add a Resources folder in project root, with the desired language variants: ~/Resources/Resources.resx (the default resource file corresponds to en-US culture), ~/Resources/Resources.fi.resx and ~/Resources/Resources.nl.resx . 首先让我们在项目根目录中添加一个Resources文件夹,其中包含所需的语言变体: ~/Resources/Resources.resx (默认资源文件对应于美国文化), ~/Resources/Resources.fi.resx~/Resources/Resources.nl.resx Mark the resources as public, so to make them available in Views. 将资源标记为公共资源,以便使其在视图中可用。

In ~/Views/Web.config , add the resources namespace under <namespace> element: <add namespace="YourMainNamespace.Reousrces" /> . ~/Views/Web.config ,在<namespace>元素下添加资源名称<namespace><add namespace="YourMainNamespace.Reousrces" /> Under controllers, create a base controller class: 在控制器下,创建基本控制器类:

Here comes the cookies 饼干来了

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

Next, register a global filter to in ~/Global.asax.cs to ensure that every action should use the correct culture before executing: 接下来,在~/Global.asax.cs注册一个全局过滤器,以确保每个操作在执行之前都应使用正确的区域性:

Here comes the cookies again! 饼干又来了!

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

And add GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute()); 并添加GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute()); in MyApplication.Application_Start() method. MyApplication.Application_Start()方法中。

In ~/App_Start/RoutesConfig.cs , change the default route to: ~/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 }
);

At this point, we would be able to use resources in view. 在这一点上,我们将能够使用资源。 For instance; 例如; @Resources.Headline . @Resources.Headline

Next, we will create a custom attribute called Translatable for model properties. 接下来,我们将为模型属性创建一个名为Translatable的自定义属性。

class TranslatableAttribute : Attribute
{ }

This is enough. 这就够了。 But if you want to be able to specify scope, you can use this class to implement it. 但是,如果您希望能够指定范围,则可以使用此类来实现它。

Now add a model called Resource with three properties and a helper method: 现在添加一个名为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
}

This helper method will help you retrieve the translated content. 此帮助方法将帮助您检索翻译的内容。 For instance in view, you can say: 例如,您可以说:

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

Note that, at any point, the data in the translatable field column is unreliable; 请注意,在任何时候,可翻译字段列中的数据都是不可靠的; it will always hold the last updated value. 它将始终保存最后更新的值。 On creating the record, we will mirror all the translatable properties' values in Resource model for all supported cultures. 创建记录时,我们将在资源模型中反映所有受支持区域性的所有可翻译属性的值。

We are using asynchronous controllers, so for insertion, modification and deletion we will be overriding SaveChangesAsync() in our DbContext class: 我们正在使用异步控制器,因此对于插入,修改和删除,我们将在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;
}

This will take care of update and delete. 这将负责更新和删除。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM