MVC5 and setting Culture / CultureUI with DropDownList, Cookie, setting user profile

I partially implemented globalization / localization in my project. The project requires the database to be used for resource strings, and I found a great NuGet package called WestWind.Globalization that does exactly what I need.

This NuGet package allows you to display resource strings using several different methods. It provides the ability to generate a strongly typed class that contains all of your resource strings so you can use it like this:

@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 did not want to specify the culture manually, so I chose this method:

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

For me, Westwind.Globalization is magical. This solved a huge problem for me, but I ran into a problem that I was not sure how to win. That is, how to set Culture / CultureUI so that the package automatically uses the specified language resource.

I created a PartialView that contains a drop-down list of languages. It is contained in the ~ / Views / Shared / folder and is included in _Layout.cshtml. I encoded the GET and POST Controller actions that worked as intended, except that I was unable to save the Culture / CultureUI settings. I suspect this was due to a redirect right after choosing a language (explained below)

So, I found a SO question in which there was an answer that seemed viable. I have included this answer in my project. Relevant Code:

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

In my controllers:

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

So far so good. This works in that I can go to the URL http://example.com/en-mx/Account/Login/ and see that the page is localized by Westwind.Globalization and the resource strings I created.

I have the following issues:

  • If the user is anonymous, their language must be controlled by a cookie (if one exists), otherwise en-US is used by default.

  • 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).

  • The global header has a drop-down list of language choices. The user should be able to choose their language preferences from the drop-down list, and if they do, the setting will be written to the cookie (for both anonymous and authenticated users), and if the user is authenticated, their language settings in the user profile will be updated .

  • Not the end of the world, but it would be very desirable that the language is not included in the URL. Some may ask, why did I install the @jao solution? Let me explain this.

All code was in place for a drop-down list allowing the user to select a language. The logic for # 1, # 2, and # 3 above worked correctly, but did not take effect and initiated Westwind.Globalization DbResourceProvider to pass the selected lines of the language resource.

What I discovered using debugging was that my settings were not saved:

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

Thanks to the answers provided by my question here on SO, I found out that these settings will not be saved / take effect if the redirection was done before the original rendering of the View. However, redirecting back to its original form seemed reasonable, as the language changed and needed to be selected again. I think the @jao solution overcomes the redirection problem, but does it force globalization / localization to be indicated by the url? To some extent, trick-22 ...

I asked @jao to consider this issue and give any hints of it. I think my question is best summarized as follows:

How can I use the cookie / user profile settings to set Culture / CultureUI once and for all so that Westwind.Globalization can read globalization / localization instead of relying on Culture passed in the URL?

+1
asp.net-mvc asp.net-mvc-4 localization
Jun 25 '14 at 21:07
source share
1 answer

I am posting this answer as an alternative, customizable way to localize with ASP.NET MVC5 with an asynchronous controller. You may find some problems in my solution, especially when it comes to routing and setting cookies.

This is a kind of short tutorial that I wrote for my heterogeneous / user approach. So I preferred SO over WordPress. :)

Sorry for not giving an accurate and discrete answer to your problem. Hope this helps you in some other way and to other people; who want to do the same setup.




On her blog blog, Nadeem Afana described a strategy for creating a separate Resource project in a solution for implementing internationalization using static resource files. On the blog he described in detail the extension of the same project for processing resources through databases and XML-based approaches. For the first, he used ADO.NET, detachable from the Entity Framework.

We needed to implement both static and dynamic resources in the MVC project, observing the concepts of MVC conventions.

First, add the resources folder to the root of the project with the desired language options: ~/Resources/Resources.resx (the default resource file matches the culture in the USA), ~/Resources/Resources.fi.resx and ~/Resources/Resources.nl.resx . Mark resources as public to make them available in Views.

In ~/Views/Web.config add the resource namespace in the <namespace> element: <add namespace="YourMainNamespace.Reousrces" /> . Under the controllers, create a base controller class:

Here are 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 cultures Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName); Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture; return base.BeginExecuteCore(callback, state); } } } 

Then register the global filter in ~/Global.asax.cs to ensure that every action must use the correct culture before executing:

Here are 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()); in the MyApplication.Application_Start() method.

In ~/App_Start/RoutesConfig.cs change the default route to:

 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 will be able to use the available resources. For example; @Resources.Headline .

Then we will create a custom Translatable attribute for the model properties.

 class TranslatableAttribute : Attribute { } 

It's enough. But if you want to specify a scope, you can use this class to implement it.

Now add a model called Resource with three properties and a helper method:

 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 get translated content. For example, you can say:

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

Note that at any point, the data in the column of the field being translated is unreliable; it will always contain the last updated value. When creating the record, we will reflect all the values โ€‹โ€‹of the translated properties in the resource model for all supported cultures.

We use asynchronous controllers, so for insertion, modification, and deletion, we will override SaveChangesAsync() in our DbContext class:

 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 updating and uninstalling.

+1
Nov 11 '14 at 15:57
source share



All Articles