Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years experience in .NET. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Aliyun) as well as WCF and ASP.NET MVC. Recently he's falling in love with JavaScript and Node.js.

Currently Shaun is working at IGT Technology Development (Beijing) Co., Ltd. as the architect responsible for product framework design and development.

MVP

My Stats

  • Posts - 96
  • Comments - 344
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories



This is the upgraded version of one of my previous post named “Localization in ASP.NET MVC – 3 Days Investigation, 1 Day Job”. I updated my solution to the latest ASP.NET MVC 4, Visual Studio 2012 with some bug fixes.

I also tried to provide some solutions which I mentioned in the original post but didn’t implement.

 

Thanks and History

Since I posted the original post two years ago the view count is over 30 hundred and there are about 55 comments till now. I would like to take this opportunity to say thank you for all the people who read and commented to that post. Your feedback gave me the passion to continue blog posting. And it’s my great honor.

I wrote that post when I was working at Ethos Technologies on a web application project. At that moment I need to add localization feature to an ASP.NET MVC 2 web application.

Since I changed my career to IGT I’m more focusing on the cloud computing, backend service development and architecture especially in Windows Azure and WCF. Even though I’m keeping an eye on ASP.NET platform I didn’t pay much time on using them. This is the reason that I didn’t provide enough answers to the questions in that post.

This week my team had released a new version of our production successfully I would like to back to the ASP.NET world and using a separate post to get my localization solution upgraded. So, before read this post I’d like to suggest you take several minutes to quick go through the original one which should provide some background knowledge.

In this post I ‘m going to focus on the areas below:

- Migrate the whole solution to the latest ASP.NET MVC4 and Visual Studio 2012.

- Moved the localization codes in a separated project so that it can be used more easily.

- Provided a solution to abstract the localization resource provider, so that we can put the localization items in any data source such as database, configuration file, and of course the resource file.

- Updated the language selector helper class so that we can use texts and images, scripts, etc..

 

Principle of Localization

The basic principle of localization is to retrieve the the localized value from the center localization framework by specifying the localization key. Never ever put any final localized string in the application. For example, when we need to print “Hello World”, we should use the localization framework to retrieve the localized “Hello World” to us by providing the culture and the key.

image

In ASP.NET MVC, all strings in the application, not only in view pages but those in the controllers, models and backend services, should specify the localization key, and then use that key to get the final value from the localization framework.

The localization framework takes the responsible for:

- Helps the ASP.NET MVC application to get the culture based on the URL and set to the UI culture.

- Helps to figure out the current culture from the URL and render the proper URLs based on the currently culture.

- Html helper to render the language select section in views.

- Retrieve the localized value based on the localization key.

 

Upgraded to ASP.NET MVC 4 and Separated Project

We still need to use the route feature of ASP.NET to help us implement the localization feature. When the application was started we need to register new routes with the language information. So that we know which culture the user set currently.

In ASP.NET MVC 4, all configuration procedures are split into the App_Start folder. So we will add a new static class in our localization project and this should be invoked in the Global.asax, Application_Start function.

In this post there will be a project contains all codes for localization. And then an ASP.NET MVC 4 web application will be using this project.

   1: public static void RegisterRoutes(RouteCollection routes)
   2: {
   3:     routes.MapRoute(
   4:         "Account", // Route name
   5:         "Account/{action}", // URL with parameters
   6:         new { controller = "Account", action = "Index" } // Parameter defaults
   7:     );
   8:  
   9:     routes.MapRoute(
  10:         Constants.ROUTE_NAME, // Route name
  11:         string.Format("{{{0}}}/{{controller}}/{{action}}/{{id}}", Constants.ROUTE_PARAMNAME_LANG), // URL with parameters
  12:         new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  13:     );
  14: }

An other class which contains all constants used by the localization framework.

   1: internal static class Constants
   2: {
   3:     internal static string ROUTE_NAME = "Localization";
   4:     internal static string ROUTE_PARAMNAME_LANG = "lang";
   5: }

Then we can add the route in Global.asax. One thing need to be noticed that our localization routes must be in front of the default route. So we must invoke the LocalizationConfig.RegisterRoutes(RouteTable.Routes) before the default route register RouteConfig.RegisterRoutes(RouteTable.Routes).

I really don’t know why Microsoft doesn’t provide the insert or reorder functions in its RouteCollection class. It utilized a Collection<RouteBase> to store all routes but utilizes a private dictionary to store the relationship between the route name and route object. And there’s no way for us to touch the private dictionary and set the order of routes.

   1: protected void Application_Start()
   2: {
   3:     AreaRegistration.RegisterAllAreas();
   4:  
   5:     WebApiConfig.Register(GlobalConfiguration.Configuration);
   6:     FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
   7:  
   8:     // register the localization routes
   9:     // note: this must be invoked before the RouteConfig.RegisterRoutes
  10:     LocalizationConfig.RegisterRoutes(RouteTable.Routes);
  11:     
  12:     RouteConfig.RegisterRoutes(RouteTable.Routes);
  13:     BundleConfig.RegisterBundles(BundleTable.Bundles);
  14:     AuthConfig.RegisterAuth();
  15: }

The next step is to implement the localization procedure in all controllers. In the previous post I created a base controller which override the ExecuteCore method, then put the localization related logic there. Then all controllers in my project should be inherited from this base controller to have the localization feature enabled.

But there are some problems in this solution. The first one is, in ASP.NET MVC 4, the ExecuteCore method will NOT be invoked by default. So I need to move the localization logic to the BeginExecuteCore.

In order to support the synchronized controller in ASP.NET MVC 4 the ExecuteCore will not be invoked unless you set the DisableAsyncSupport to FALSE. For more information about this breaking change please refer here and here.

The second one is related with architecture. Putting the localization logic in a base controller is OK. But this means you must have all your controllers inherited from this base controller. This could be a problem if you have an existing base controller which cannot be modified. For example, assuming we are going to create an ASP.NET MVC website and all controllers must be inherited from the Contoso.BaseController which contains some common business logic of my company, and this base controller defined in an assembly that could not be able to change. Now if I need the localization, all controllers must be inherited from the localization base controller, as well as the company base controller. How can we do that?

image

Hence, I decided to move my localization logic into a standalone class. Then we can create a base controller class from the Contoso.BaseController and invoke our localization logic in its BeginExecuteCore, and have all my controllers from this base one so that all of them have the common business logic and the localization feature.

image

I moved the code from the base controller’s ExecuteCore to this new helper class.

   1: public class LocalizationControllerHelper
   2: {
   3:     public static void OnBeginExecuteCore(Controller controller)
   4:     {
   5:         if (controller.RouteData.Values[Constants.ROUTE_PARAMNAME_LANG] != null &&
   6:             !string.IsNullOrWhiteSpace(controller.RouteData.Values[Constants.ROUTE_PARAMNAME_LANG].ToString()))
   7:         {
   8:             // set the culture from the route data (url)
   9:             var lang = controller.RouteData.Values[Constants.ROUTE_PARAMNAME_LANG].ToString();
  10:             Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
  11:         }
  12:         else
  13:         {
  14:             // load the culture info from the cookie
  15:             var cookie = controller.HttpContext.Request.Cookies[Constants.COOKIE_NAME];
  16:             var langHeader = string.Empty;
  17:             if (cookie != null)
  18:             {
  19:                 // set the culture by the cookie content
  20:                 langHeader = cookie.Value;
  21:                 Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  22:             }
  23:             else
  24:             {
  25:                 // set the culture by the location if not speicified
  26:                 langHeader = controller.HttpContext.Request.UserLanguages[0];
  27:                 Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  28:             }
  29:             // set the lang value into route data
  30:             controller.RouteData.Values[Constants.ROUTE_PARAMNAME_LANG] = langHeader;
  31:         }
  32:  
  33:         // save the location into cookie
  34:         HttpCookie _cookie = new HttpCookie(Constants.COOKIE_NAME, Thread.CurrentThread.CurrentUICulture.Name);
  35:         _cookie.Expires = DateTime.Now.AddYears(1);
  36:         controller.HttpContext.Response.SetCookie(_cookie);
  37:     }
  38: }

Last, let’s move the language selector bar to the new project without any code changes.

   1: public static class LanguageBarHelper
   2: {
   3:     private class Language
   4:     {
   5:         public string Url { get; set; }
   6:         public string ActionName { get; set; }
   7:         public string ControllerName { get; set; }
   8:         public RouteValueDictionary RouteValues { get; set; }
   9:         public bool IsSelected { get; set; }
  10:  
  11:         public MvcHtmlString HtmlSafeUrl
  12:         {
  13:             get
  14:             {
  15:                 return MvcHtmlString.Create(Url);
  16:             }
  17:         }
  18:     }
  19:  
  20:     private static Language LanguageUrl(this HtmlHelper helper, string cultureName, bool strictSelected = false)
  21:     {
  22:         // set the input language to lower
  23:         cultureName = cultureName.ToLower();
  24:         // retrieve the route values from the view context
  25:         var routeValues = new RouteValueDictionary(helper.ViewContext.RouteData.Values);
  26:         // copy the query strings into the route values to generate the link
  27:         var queryString = helper.ViewContext.HttpContext.Request.QueryString;
  28:         foreach (string key in queryString)
  29:         {
  30:             if (queryString[key] != null && !string.IsNullOrWhiteSpace(key))
  31:             {
  32:                 if (routeValues.ContainsKey(key))
  33:                 {
  34:                     routeValues[key] = queryString[key];
  35:                 }
  36:                 else
  37:                 {
  38:                     routeValues.Add(key, queryString[key]);
  39:                 }
  40:             }
  41:         }
  42:         var actionName = routeValues["action"].ToString();
  43:         var controllerName = routeValues["controller"].ToString();
  44:         // set the language into route values
  45:         routeValues[Constants.ROUTE_PARAMNAME_LANG] = cultureName;
  46:         // generate the language specify url
  47:         var urlHelper = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);
  48:         var url = urlHelper.RouteUrl(Constants.ROUTE_NAME, routeValues);
  49:         // check whether the current thread ui culture is this language
  50:         var current_lang_name = Thread.CurrentThread.CurrentUICulture.Name.ToLower();
  51:         var isSelected = strictSelected ?
  52:             current_lang_name == cultureName :
  53:             current_lang_name.StartsWith(cultureName);
  54:         return new Language()
  55:         {
  56:             Url = url,
  57:             ActionName = actionName,
  58:             ControllerName = controllerName,
  59:             RouteValues = routeValues,
  60:             IsSelected = isSelected
  61:         };
  62:     }
  63:  
  64:     public static MvcHtmlString LanguageSelectorLink(this HtmlHelper helper,
  65:         string cultureName, string selectedText, string unselectedText,
  66:         IDictionary<string, object> htmlAttributes, bool strictSelected = false)
  67:     {
  68:         var language = LanguageUrl(helper, cultureName, strictSelected);
  69:         var link = helper.RouteLink(language.IsSelected ? selectedText : unselectedText,
  70:             Constants.ROUTE_NAME, language.RouteValues, htmlAttributes);
  71:         return link;
  72:     }
  73: }

 

Oops! Bugs in System.ComponentModel.DataAnnotations

All migration job are done. Let’s have a try. First of all we need two resources files one for English and the other for Chinese. Then put some values there.

image

In order to make this sample simple I just demonstrate the localization feature in the three major places in ASP.NET MVC: view pages, controllers and model attributes. Now let’s have a very quick test. In the home index view I specified the title through the resource.

   1: @{
   2:     ViewBag.Title = Resources.Global.View_HomeIndex_Title;
   3: }
   4: @section featured {
   5:     <section class="featured">
   6:         <div class="content-wrapper">
   7:             <hgroup class="title">
   8:                 <h1>@ViewBag.Title.</h1>
   9:                 <h2>@ViewBag.Message</h2>
  10:             </hgroup>
  11:             <p>
  12:                 ... ...
  13:             </p>
  14:         </div>
  15:     </section>
  16: }
  17: ... ...
  18: ... ...

In the home controller I set the message through the resource.

   1: public class HomeController : DefaultController
   2: {
   3:     public ActionResult Index()
   4:     {
   5:         ViewBag.Message = Resources.Global.Controller_HomeIndex_Message;
   6:  
   7:         return View();
   8:     }
   9: }

And in the account login model we specified the user name field’s display and validation message through the resource on the attributes.

   1: public class LoginModel
   2: {
   3:     [Required(ErrorMessageResourceName = "Model_Account_UserName_Required",
   4:               ErrorMessageResourceType = typeof(Resources.Global))]
   5:     [Display(Name = "Model_Account_UserName_Display",
   6:              ResourceType = typeof(Resources.Global))]
   7:     public string UserName { get; set; }
   8:  
   9:     [Required]
  10:     [DataType(DataType.Password)]
  11:     [Display(Name = "Password")]
  12:     public string Password { get; set; }
  13:  
  14:     [Display(Name = "Remember me?")]
  15:     public bool RememberMe { get; set; }
  16: }

Let’s start our MVC application and have a look. First, the proper string was shown in the home page which one comes from the view and the other come from the controller.

image

And it worked well once we clicked on the language select bar.

image

Then click login and verify the localization in System.ComponentModel.DataAnnotations attributes. Oops!

image

This error message means that there’s no public and static string property in the class generated from our resource file, so that the ASP.NET MVC cannot retrieve the localized string from the DisplayAttribute.

If we dig into the source code of the DisplayAttribute in System.ComponentModel.DataAnnotations we will find that it retrieves the localized value through an internal class named LocalizableString. And the LocaliableString class tried to find a visible property in the type defined in DisplayAttribute.ReourceType with the name of DisplayAttribute.Name.

image

But if we opened the Resource.Global class we will find that the class and all its properties were generated as “internal”. This means it cannot be retrieved by the DisplayAttribute since its IsVisible is FALSE. Someone believe this is a bug of DataAnnotations assembly since another attribute, ValidationAttribute retrieves the resource from the properties only if it’s static.

image

I’m not sure if this was implemented “by design” or a mistake in .NET BCL. I hope it could be fixed in the “.NET 5.0” but now we need to have some workaround. One solution is to move the resources in another project so that we can specify its access mode from “internal” to “public”.

image

Alternatively we can force Visual Studio to generate the resource class as “public”. Select the resource files and open its property windows, change the Build Action to Embedded Resource and change the Custom Tool to PublicResXFileCodeGenerator.

image

We also need to modify the codes those are using the resource class since this generator will have the output class under the App_GlobalResouces namespace. Now let’s retry our web application and we will see the localization strings in view page, controller and model attributes are working well.

image

 

Beyond the Resources: Localization Source Provider

In my previous post there are some comments that wondered to know how to use database to store the localization strings instead of the resource files. At that moment I replied that we can implement our own resource manager. But this might not be a simple task.

In ASP.NET MVC there are three places we may need the localization: views, controllers and models. It should not be a big problem that retrieving the localized string through database in views and controllers, since we have to full control of coding. For example, assuming that we created an interface named ILocalizationSrouce which contains one method that returns the localized strings. Then in views and controllers we can simply implement like the pseudo-code below.

- <p>@ILocalizationSrouce.GetString(cultureName, “Home_Index_Title_Key”)</p>

- ViewBag.Message = ILocalizationSrouce.GetString(cultureName, “Home_Index_Message_Key”);

But this will be difficult when implemented in the model attributes.

As I described in the previous post, when received a HTTP request ASP.NET MVC will create the related controller, use the action invoker and build the model binder. By default, the model binder will validate the input model. In ASP.NET MVC 4 almost all parts can be extended. So when the default model binder tried to validate a property of the model it will retrieve the appropriate ModelValidator and call its Validate method. The validation result will be turned back and will be added into the BindingContext.ModelState.

image

ASP.NET MVC 4 utilizes DataAnnotationsModelMetadataProvider to retrieve the metadata for each models, and utilizes DataAnnotationsModelValidatorProvider for each model validation. And both of these providers leverage the DisplayAttribute and ValidationAttribute in DataAnnotations to retrieve the localized display name and error message. For example, in ValidationAttribute it invoked the a private method named SetResourceAccessorByPropertyLookup to get the delegation on how to retrieve the localized string. And then invoke this delegation (_errorMessageResourceAccessor) and add the display name. Then return the error message alone with the ModelValidationResult.

image

The problem is that, Microsoft doesn’t provide any extension points in DataAnnotations assembly. When it needs the localized validation message it finds the static string property defined in the ResourceType. If yes then it will use reflection to invoke this property and retrieve the localized string. Otherwise, it will throw an exception. All logic defined in the DataAnnotations assembly.

In order to use any kinds of localization source there are two solutions. The first one is very straightforward. Since the default behavior of DataAnnotations is to find the static string property for each localization items, we can create our own class which have the properties for each items.

For example, I created a table in SQL Server which contains all localization strings. The structure of this table would be like this. The primary key consists of Key and Culture.

image

Then copy the resource values into this table.

image

Now let’s create a class which contains the static properties for each localization items. It will connect to the database and use the current UI culture and the key to find the proper localized string.

   1: public class DbResources
   2: {
   3:     private static string CST_CONNSTRING = "Data Source=.;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False";
   4:  
   5:     private static string GetValue(string key)
   6:     {
   7:         var culture = Thread.CurrentThread.CurrentUICulture.Name;
   8:         using(var conn = new SqlConnection(CST_CONNSTRING))
   9:         {
  10:             using(var cmd = conn.CreateCommand())
  11:             {
  12:                 cmd.CommandText = "SELECT [Value] FROM [Caspar].[dbo].[Resource] WHERE [Key] = @key AND [Culture] = @culture";
  13:                 cmd.Parameters.AddWithValue("key", key);
  14:                 cmd.Parameters.AddWithValue("culture", culture);
  15:                 conn.Open();
  16:                 var value = cmd.ExecuteScalar();
  17:                 return (string)value;
  18:             }
  19:         }
  20:     }
  21:  
  22:     public static string Controller_HomeIndex_Message
  23:     {
  24:         get
  25:         {
  26:             return GetValue("Controller_HomeIndex_Message");
  27:         }
  28:     }
  29:  
  30:     public static string Model_Account_UserName_Display
  31:     {
  32:         get
  33:         {
  34:             return GetValue("Model_Account_UserName_Display");
  35:         }
  36:     }
  37:  
  38:     public static string Model_Account_UserName_Required
  39:     {
  40:         get
  41:         {
  42:             return GetValue("Model_Account_UserName_Required");
  43:         }
  44:     }
  45:  
  46:     public static string View_HomeIndex_Title
  47:     {
  48:         get
  49:         {
  50:             return GetValue("View_HomeIndex_Title");
  51:         }
  52:     }
  53: }

Then we need to change the code in views, controllers and models to use this class instead of the resource generate class. The model attribute code would be like this.

   1: public class LoginModel
   2: {
   3:     [Required(ErrorMessageResourceName = "Model_Account_UserName_Required",
   4:               ErrorMessageResourceType = typeof(DbResources))]
   5:     [Display(Name = "Model_Account_UserName_Display",
   6:              ResourceType = typeof(DbResources))]
   7:     public string UserName { get; set; }
   8:  
   9:     [Required]
  10:     [DataType(DataType.Password)]
  11:     [Display(Name = "Password")]
  12:     public string Password { get; set; }
  13:  
  14:     [Display(Name = "Remember me?")]
  15:     public bool RememberMe { get; set; }
  16: }

Then when we start our website and navigate to the home page and login page we will see the localized strings come from the database.

image

But, is this the best solution? Absolutely not. This solution tightly leverage the inner logic of DataAnnotations. We must have a class with many static properties. And any time If we need to add or remove some localization items, we also have to update this class. Although it’s possible to build a tool the help us generate this class from database, this is not flexible enough. So the solution two comes to us.

Since the default DataAnnotations and its validator doesn’t provide enough extension points how about create our own validator. But in order to have the capability of the DataAnnotations attributes we need the new validator inherited from the DataAnnotations ones.

So the solution two is that,  we create a validator inherited from DataAnnotationsModelMetadataProvider and a new validator provider inherited from DataAnnotationsModelValidatorProvider, leverage its default behavior to generate the ModelMetadata and validate the model. But we skip the procedure of using the resource type and replace that part of logic by performing our own localization logic.

In the following code I created a class from the DataAnnotationsModelMetadataProvider and invoked its base method CreateMetadata. Since the base method will try to retrieve the localized value if it met any DisplayAttribute I skip them before invoked it. And then for the DisplayAttribute I get the localized string from my own resource provider named LocalizationResourceProvider, which will be implemented later.

image

   1: public class LocalizableDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
   2: {
   3:     public LocalizableDataAnnotationsModelMetadataProvider()
   4:         : base()
   5:     {
   6:     }
   7:  
   8:     protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
   9:     {
  10:         // invoke the base method but skip the DisplayAttribute since it will use resource internally
  11:         var attributesWithoutDisplay = attributes.Where(a => a.GetType() != typeof(DisplayAttribute));
  12:         var metadata = base.CreateMetadata(attributesWithoutDisplay, containerType, modelAccessor, modelType, propertyName);
  13:         // retrieve the DisplayAttribute
  14:         var display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
  15:         if (display != null)
  16:         {
  17:             var source = LocalizationResourceProvider.Current;
  18:             metadata.DisplayName = source.GetString(display.Name);
  19:             metadata.Description = source.GetString(display.Description);
  20:             metadata.ShortDisplayName = source.GetString(display.ShortName);
  21:             metadata.Watermark = source.GetString(display.Prompt);
  22:             metadata.Order = display.GetOrder() ?? ModelMetadata.DefaultOrder;
  23:         }
  24:         return metadata;
  25:     }
  26: }

Similarly I created a class from the ModelValidator. As the original validator had been passed through its constructor I can override the validate method, perform the original validation, and then use its error message as the localization key to find the localized value through my provider.

image

   1: public class LocalizableDataAnnotationsModelValidator : ModelValidator
   2: {
   3:     private ModelValidator _innerValidator;
   4:     private ModelMetadata _metadata;
   5:  
   6:     public LocalizableDataAnnotationsModelValidator(ModelValidator innerValidator, ModelMetadata metadata, ControllerContext controllerContext)
   7:         : base(metadata, controllerContext)
   8:     {
   9:         _innerValidator = innerValidator;
  10:         _metadata = metadata;
  11:     }
  12:  
  13:     public override IEnumerable<ModelValidationResult> Validate(object container)
  14:     {
  15:         // execute the inner validation which doesn't have localization
  16:         var results = _innerValidator.Validate(container);
  17:         // convert the error message (which should be the localization resource key) to the localized value through the ILocalizationResourceProvider
  18:         var source = LocalizationResourceProvider.Current;
  19:         return results.Select((result) =>
  20:         {
  21:             var key = result.Message;
  22:             var message = source.GetString(key);
  23:             return new ModelValidationResult() { Message = string.Format(message, _metadata.DisplayName) };
  24:         });
  25:     }
  26: }

LocalizableDataAnnotationsModelValidator utilizes ErrorMessage property of the ValidationAttribute as the localization key. Hence when using this validator in the validation attributes on each model properties we need to specify the localization item key in the ErrorMessage property, and we must leave the ErrorMessageResourceType as null.

Then we need to create LocalizableDataAnnotationsModelValidatorProvider as well, which from the base class DataAnnotationsModelValidatorProvider. It will return the LocalizableDataAnnotationsModelValidator we had just finished.

   1: public class LocalizableDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
   2: {
   3:     public LocalizableDataAnnotationsModelValidatorProvider()
   4:         : base()
   5:     {
   6:     }
   7:  
   8:     protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
   9:     {
  10:         var validators = base.GetValidators(metadata, context, attributes);
  11:         var result = new List<LocalizableDataAnnotationsModelValidator>();
  12:         foreach (var validator in validators)
  13:         {
  14:             result.Add(new LocalizableDataAnnotationsModelValidator(validator, metadata, context));
  15:         }
  16:         return result;
  17:     }
  18: }

The final step is to create our own localization resource provider. In order to make it flexible I will create an interface that is able to return the proper localized string.

   1: public interface ILocalizationResourceProvider
   2: {
   3:     string GetString(string cultureName, string key);
   4:  
   5:     string GetString(string key);
   6: }

And then a base class that covers some basic logic. Which means when the localization key is null we will not invoke the underlying resource provider to find the value. And it will return the key directly if there’s no related localized string from the provider. These logic is more for backend capacity if we put the error message or display value directly in the attribute and not want to use localization.

   1: public abstract class LocalizationResourceProviderBase : ILocalizationResourceProvider
   2: {
   3:     protected LocalizationResourceProviderBase()
   4:     {
   5:     }
   6:  
   7:     public string GetString(string cultureName, string key)
   8:     {
   9:         return OnGetString(cultureName, key);
  10:     }
  11:  
  12:     public string GetString(string key)
  13:     {
  14:         // find the localized result only if the key is not null
  15:         var result = string.IsNullOrWhiteSpace(key) ? null : GetString(LocalizationResourceProvider.CultureName, key);
  16:         // return the original key if didn't find the localized result
  17:         return string.IsNullOrWhiteSpace(result) ? key : result;
  18:     }
  19:  
  20:     protected abstract string OnGetString(string cultureName, string key);
  21: }

Then we can migrate our database localization logic into this architecture. Just inherit from the base provider and implement the OnGetString method.

   1: public class LocalizationDbResourceProvider : LocalizationResourceProviderBase
   2: {
   3:     private string _connectionString;
   4:  
   5:     public LocalizationDbResourceProvider()
   6:         : this(Constants.CONNSTRING_DEFAULT_NAME)
   7:     {
   8:     }
   9:  
  10:     public LocalizationDbResourceProvider(string connectionStringName)
  11:         : base()
  12:     {
  13:         _connectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
  14:     }
  15:  
  16:     protected override string OnGetString(string cultureName, string key)
  17:     {
  18:         using(var conn = new SqlConnection(_connectionString))
  19:         {
  20:             using(var cmd = conn.CreateCommand())
  21:             {
  22:                 cmd.CommandText = "SELECT [Value] FROM [Caspar].[dbo].[Resource] WHERE [Key] = @key AND [Culture] = @culture";
  23:                 cmd.Parameters.AddWithValue("key", key);
  24:                 cmd.Parameters.AddWithValue("culture", cultureName);
  25:                 conn.Open();
  26:                 var value = cmd.ExecuteScalar();
  27:                 return (string)value;
  28:             }
  29:         }
  30:     }
  31: }

Please be aware that this database resource provider is just for test and POC (prove of concept). Do not use it in a production environment. When implementing your own resource provider I strong recommend to add some cache feature to maximize the performance.

A singleton class should be useful of getting the current localization provider from any where in the project.

   1: public class LocalizationResourceProvider
   2: {
   3:     private static ILocalizationResourceProvider _instance;
   4:     private static Func<ILocalizationResourceProvider> _initializer;
   5:     private static Func<string> _cultureNameResolver;
   6:     private static object _aLock;
   7:  
   8:     public static ILocalizationResourceProvider Current
   9:     {
  10:         get
  11:         {
  12:             if (_instance == null)
  13:             {
  14:                 lock (_aLock)
  15:                 {
  16:                     if (_instance == null)
  17:                     {
  18:                         if (_initializer == null)
  19:                         {
  20:                             throw new ArgumentNullException("_initializer");
  21:                         }
  22:                         _instance = _initializer.Invoke();
  23:                     }
  24:                 }
  25:             }
  26:             return _instance;
  27:         }
  28:     }
  29:  
  30:     public static string CultureName
  31:     {
  32:         get
  33:         {
  34:             return _cultureNameResolver.Invoke();
  35:         }
  36:     }
  37:  
  38:     static LocalizationResourceProvider()
  39:     {
  40:         _aLock = new object();
  41:     }
  42:  
  43:     private LocalizationResourceProvider()
  44:     {
  45:     }
  46:  
  47:     internal static void RegisterCultureNameResolver(Func<string> cultureNameResolver)
  48:     {
  49:         _cultureNameResolver = cultureNameResolver;
  50:     }
  51:  
  52:     internal static void RegisterResourceProvider(Func<ILocalizationResourceProvider> initializer)
  53:     {
  54:         _initializer = initializer;
  55:     }
  56: }

Next, extend our configuration class so that the user can configure the providers in the Global.asax.

   1: public static class LocalizationConfig
   2: {
   3:     public static void RegisterRoutes(RouteCollection routes)
   4:     {
   5:         ... ...
   6:     }
   7:  
   8:     public static void RegisterResourceProvider(Func<ILocalizationResourceProvider> initializer)
   9:     {
  10:         LocalizationResourceProvider.RegisterResourceProvider(initializer);
  11:         LocalizationResourceProvider.RegisterCultureNameResolver(() => Thread.CurrentThread.CurrentUICulture.Name);
  12:     }
  13:  
  14:     public static void RegisterResourceProvider(Func<ILocalizationResourceProvider> initializer, Func<string> cultureNameResolver)
  15:     {
  16:         LocalizationResourceProvider.RegisterResourceProvider(initializer);
  17:         LocalizationResourceProvider.RegisterCultureNameResolver(cultureNameResolver);
  18:     }
  19:  
  20:     public static void RegisterModelProviders()
  21:     {
  22:         // register the model metadata provider
  23:         ModelMetadataProviders.Current = new LocalizableDataAnnotationsModelMetadataProvider();
  24:  
  25:         // register the model validation provider
  26:         var provider = ModelValidatorProviders.Providers.Where(p => p.GetType() == typeof(DataAnnotationsModelValidatorProvider)).FirstOrDefault();
  27:         if (provider != null)
  28:         {
  29:             ModelValidatorProviders.Providers.Remove(provider);
  30:         }
  31:         provider = new LocalizableDataAnnotationsModelValidatorProvider();
  32:         ModelValidatorProviders.Providers.Add(provider);
  33:     }
  34: }

Now let’s have a try. Open the Global.asax file and configure the model providers and resource provider.

   1: protected void Application_Start()
   2: {
   3:     // register the localization routes
   4:     // note: this must be invoked before the RouteConfig.RegisterRoutes
   5:     LocalizationConfig.RegisterRoutes(RouteTable.Routes);
   6:     // specify the localiztion resource provider (and culture name resolver)
   7:     LocalizationConfig.RegisterResourceProvider(() => new LocalizationDbResourceProvider());
   8:     // register the localizable model providers
   9:     LocalizationConfig.RegisterModelProviders();
  10:  
  11:     AreaRegistration.RegisterAllAreas();
  12:     WebApiConfig.Register(GlobalConfiguration.Configuration);
  13:     FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  14:     RouteConfig.RegisterRoutes(RouteTable.Routes);
  15:     BundleConfig.RegisterBundles(BundleTable.Bundles);
  16:     AuthConfig.RegisterAuth();
  17: }

Then specify the connection string in the Web.config.

   1: <connectionStrings>
   2:   <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-Caspar.Sample-20120831144634;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-Caspar.Sample-20120831144634.mdf" providerName="System.Data.SqlClient" />
   3:   <add name="Caspar" connectionString="Data Source=.;Initial Catalog=Caspar;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False"/>
   4: </connectionStrings>

The usage in view page.

   1: @{
   2:     ViewBag.Title = Caspar.ResourceProviders.LocalizationResourceProvider.Current.GetString("View_HomeIndex_Title");
   3: }

The usage in the controller.

   1: public ActionResult Index()
   2: {
   3:     ViewBag.Message = LocalizationResourceProvider.Current.GetString("Controller_HomeIndex_Message");
   4:  
   5:     return View();
   6: }
And the usage in the model attributes. Note that I didn’t specify the ResourceType and ErrorMessageType, and use the Name and ErrorMessage as the localization keys.
   1: public class LoginModel
   2: {
   3:     [Required(ErrorMessage = "Model_Account_UserName_Required")]
   4:     [Display(Name = "Model_Account_UserName_Display")]
   5:     public string UserName { get; set; }
   6:  
   7:     [Required]
   8:     [DataType(DataType.Password)]
   9:     [Display(Name = "Password")]
  10:     public string Password { get; set; }
  11:  
  12:     [Display(Name = "Remember me?")]
  13:     public bool RememberMe { get; set; }
  14: }

And here is the result.

image

And if we want to store the localization items in a XML file we can simply create another resource provider class which inherited from the LocalizationResourceProviderBase, and implement the value retrieving logic in its OnGetString method. We don’t need to change any code in our web application. And if we need to add or remove some items, no need to adjust the provider code.

We still have a problem in this solution. In the DataAnnotations attributes each validation attribute can override the base ValidationAttribute’s FormatErrorMessage method to implement its own error message. For example in StringLengthAttribute it will format the error message with the property name, minimum length and maximum length. But since it utilizes the inner procedure to retrieve the localized value we cannot directly invoke. Hence in my implementation I format the error message with the display name hard-coded. This means we cannot only put one format placeholder in the localized string in my solution.

For example, for the password string length error message we have to say something like “{0} has invalid length.”, but not “The length of {0} must be greater than {2}.”.

 

Customize Language Selector

Another question in the previous post was about the language selector. I mentioned that we can use images as the selector rather than the text in that post, but I didn’t provide any samples. A reader, Gwen tried a bit but failed. Both of us though that this is because I used HtmlHelper.RouteLink to generate the language linkage and it will encode the image section if we just simply put the image HTML string in its selectedText and unselectedText parameter. Hence I’m going to update my language helper then you can put anything as the content.

Here I would like to leverage the ASP.NET MVC partial view functionality. As you know, we can put anything into a partial view and MVC will help us render it to HTML. So the solution is, user can specify the partial view name of selected and unselected style of a language section. Underlying I firstly render the linkage by using the HtmlHelper.RouteLink but the use a placeholder string as the link text. Then I use the HtmlHelper.Partial to render the selected or unselected content. Finally replace the placeholder by the rendered partial view HTML string. In this way we can put anything in the partial view. For example, images, texts, JavaScript, and even a piece of Flash.

   1: public static class LanguageBarHelper
   2: {
   3:     private const string CST_PARTIAL_PLACEHOLDER = "__partial_placeholder__";
   4:  
   5:     private class Language
   6:     {
   7:         ... ...
   8:     }
   9:  
  10:     private static Language LanguageUrl(this HtmlHelper helper, string cultureName, bool strictSelected = false)
  11:     {
  12:         ... ...
  13:     }
  14:  
  15:     public static MvcHtmlString LanguageSelectorLink(this HtmlHelper helper,
  16:         string cultureName, string selectedText, string unselectedText,
  17:         IDictionary<string, object> htmlAttributes, bool strictSelected = false)
  18:     {
  19:         ... ...
  20:     }
  21:  
  22:     public static MvcHtmlString LanguageSelectorPartial(this HtmlHelper helper,
  23:         string cultureName, string selectedPartialViewName, string unselectedPartialViewName, object model,
  24:         IDictionary<string, object> htmlAttributes, bool strictSelected = false)
  25:     {
  26:         var language = LanguageUrl(helper, cultureName, strictSelected);
  27:         var partial = helper.Partial(language.IsSelected ? selectedPartialViewName : unselectedPartialViewName, model, helper.ViewData).ToHtmlString();
  28:         var link = helper.RouteLink(CST_PARTIAL_PLACEHOLDER, Constants.ROUTE_NAME, language.RouteValues, htmlAttributes).ToHtmlString();
  29:         return MvcHtmlString.Create(link.Replace(CST_PARTIAL_PLACEHOLDER, partial));
  30:     }
  31: }

Then we can create four partial views, the selected and unselected style for English and Chinese.

   1: <img style="border: none; width: 32px; height: 32px;" id="lang-selector-en-us" src="~/Content/en-us.selected.png" alt="English" />
   1: <img style="border: none; width: 32px; height: 32px;" id="lang-selector-en-us" src="~/Content/en-us.unselected.png" alt="English" />
   2:  
   3: <script type="text/javascript">
   1:  
   2:     $("#lang-selector-en-us").hover(
   3:         function (evt) {
   4:             $(this).attr("src", "/Content/en-us.hover.png");
   5:         },
   6:         function (evt) {
   7:             $(this).attr("src", "/Content/en-us.unselected.png");
   8:         });
</script>

I used the image as the linkage and I also added some scripts in the unselected partial view so that the image will be highlighted when mouse hover.

Similar partial views of Chinese.

   1: <img style="border: none; width: 32px; height: 32px;" id="lang-selector-zh-cn" src="~/Content/zh-cn.selected.png" alt="English" />
   1: <img style="border: none; width: 32px; height: 32px;" id="lang-selector-zh-cn" src="~/Content/zh-cn.unselected.png" alt="English" />
   2:  
   3: <script type="text/javascript">
   1:  
   2:     $("#lang-selector-zh-cn").hover(
   3:         function (evt) {
   4:             $(this).attr("src", "/Content/zh-cn.hover.png");
   5:         },
   6:         function (evt) {
   7:             $(this).attr("src", "/Content/zh-cn.unselected.png");
   8:         });
</script>

We also need to tweak the _Layout.cshtml. Since I used jQuery in my partial view I need to move the jQuery script section in the beginning of the _Layout.cshtml so that it can recognize my scripts when rendered.

   1: @using Caspar;
   2:  
   3: <!DOCTYPE html>
   4: <html lang="en">
   5:     <head>
   6:         <meta charset="utf-8" />
   7:         <title>@ViewBag.Title - My ASP.NET MVC Application</title>
   8:         <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
   9:         <meta name="viewport" content="width=device-width" />
  10:         @Styles.Render("~/Content/css")
  11:         @Scripts.Render("~/bundles/modernizr")
  12:         @Scripts.Render("~/bundles/jquery")
  13:     </head>
  14:     <body>
  15:         ... ...
  16:     </body>
  17: </html>

OK. Let’s have a look on the image language selector bar with some animation, but please ignore my poor CSS skill.

image

 

Summary

In this post I continued my ASP.NET MVC localization solution. The original solution was based on ASP.NET MVC 2. So the first mission was to move it to the latest ASP.NET MVC 4.

I also explained and demonstrated some features I mentioned in the original post. After investigated the implementation of ASP.NET MVC model providers and DataAnnotations, I decided to replace the default providers and give more flexibility to the solution. So that we can put the localization items in database, files or whatever we want by just implement our own provider.

But to be honest, if Microsoft provides more flexibility in its DataAnnotations assembly we don’t need to build a bunch of classes to shortcut its ugly default localization implementation.

 

You can download the full code here, or get the latest source code here.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

Comments

Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Yakir Sabag on 9/11/2012 11:14 PM
Hey, thanks for your detailed article!
One thing that didn't work for me, was the model client side validation... using the DBResourceProvider - it always took the en-US as the culture, eventhough the cookies was on he-IL...

Do you have any idea why?
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Shaun on 9/12/2012 9:40 AM

@Yakir Sabag
This solution doesn't cover the client side validation. This mainly because I didn't have the time to investigate. I will have a try when I have the time.
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Peter on 9/12/2012 11:09 AM
Thanks for great code! The code works well most part, however the client validation is broken. Could you look into it?
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Peter on 9/12/2012 11:43 AM
Since the client validation is broken, can we put the en-US or zh-cn at the end instead of front? like /home/contact/en-US
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Shaun on 9/12/2012 1:42 PM

@Peter
As I mentioned above I didn't cover the client validation. I'm not sure if it can solve the client validation that putting the language part to the end of the URL. But I don't think this will break the solution. Just need to amend the language route table, moving the {lang} to the end of the URL.
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Peter on 9/12/2012 4:03 PM
Shaun, I tried put the {lang} at the end, it rather not work at all, both client validation and localization. There must be more need to be changed, I think
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Peter on 9/12/2012 4:22 PM
Shaun, how about to eliminate the routing part all together, but rather just use cookie, would that be better?
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by peter on 9/13/2012 7:34 PM
any one figured out how to fix the client side validation problem? it would be great pity for such a wonderful article without that part working.
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Sola Oderinde on 10/24/2012 7:24 PM
To solve the client-side validation problem, you will need to extend the DisplayName Attribute. The DisplaName Attribute in System.ComponentModel does not support localisation.
Check out how to do it on this page http://holyhoehle.wordpress.com/2010/02/15/localizing-displaynameattribute-asp-net-mvc/
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Robbert Draaisma on 10/28/2012 9:09 PM
Thanks for this article,
As to the clientside validation, I think I have a viable solution. The problem is due to the fact that the LocalizableDataAnnotationsModelValidator does not implement IClientValidatable which is used to populate the html validation attribute tags. However after some thought I figured it's probably better to keep using the regular DataAnnotationsModelValidator and instead replace the (range/required/etc)AttributeAdapter's, you see you can register your own factory for supplying these with DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(type, factory expression). The Adapters are ideal to handle the localization. You can implement the logic to generate clientside validation messages, and you can override the Validate(container) to handle serverside message generation.
Here's my implementation for my own RequiredAttributeAdapter:

public class RequiredAttributeAdapter : DataAnnotationsModelValidator<RequiredAttribute>
{
public RequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
var source = LocalizationResourceProvider.Current;
var baseResult = base.Validate(container);
return baseResult.Select(t => new ModelValidationResult { MemberName = t.MemberName,Message = source.GetString(t.Message) });
}

public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var source = LocalizationResourceProvider.Current;
return new[] { new ModelClientValidationRequiredRule(source.GetString(ErrorMessage)) };
}
}

You can register this with
DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(typeof(RequiredAttribute), (metadata, context, attribute) => new MyOwn.RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute));


Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by sharon on 11/3/2012 12:54 AM
Hi,
Great work!!
can u plz!! fix the method below it is not supporting string format with more than 1 parameter ... only taking the display name:


public override IEnumerable<ModelValidationResult> Validate(object container)
{
// execute the inner validation which doesn't have localization
var results = _innerValidator.Validate(container);
// convert the error message (which should be the localization resource key) to the localized value through the ILocalizationResourceProvider
var source = LocalizationResourceProvider.Current;
return results.Select((result) =>
{
var key = result.Message;
var message = source.GetString(key);
return new ModelValidationResult() { Message = string.Format(message, _metadata.DisplayName) };
});
}
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Shaun Xu on 11/3/2012 6:53 PM
@sharon, the main problem is where to specify if replace parameters more than one. Do you mind to put all parameters in DisplayName divided by comma? Or put the second parameter in Description? This is the reason I had to support one parameter. But I think you can inherit my attribute a put parameters as you want. Hope this helps.
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by jaml on 11/16/2012 12:32 AM
var queryString =helper.ViewContext.HttpContext.Request.QueryString;
foreach (string key in queryString)
{
if (queryString[key] != null && !string.IsNullOrWhiteSpace(key))
{
if (routeValues.ContainsKey(key))
{
routeValues[key] = queryString[key];
}
else
{
routeValues.Add(key, queryString[key]);
}
}
}

the above code seems not working any idea i have the folowing error :

CurrentNotification = 'helper.ViewContext.HttpContext.CurrentNotification' threw an exception of type 'System.PlatformNotSupportedException'
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Rabih on 11/27/2012 2:27 AM
Hello Shaun, very nice article, i've been trying to use the code, but stucked with Areas, by returning the error message below. I am newly introduced to areas in mvc, and i seek your help
The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Logging/Index.cshtml
~/Views/Logging/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml

Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by sharon on 12/13/2012 9:46 PM
Hi.
for some reason the Ajax validation tags are not being generated by microsoft framwork.(when im creating a fresh mvc project it does).
Can u tell me please if you fix it, or if the problem is known and what do i have to do in order to fix it?

Thanks a lot !!
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by sharon on 12/13/2012 9:50 PM
Hi,
I have another quesistion please.
If i dont want to use cookies or support multi-users accessing in same IE with different Tabs, what do i have to change?

Thank You Very Much
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Shaun Xu on 12/14/2012 10:10 AM
sharon,

1. I didn't investigated the client validation in my solution. Maybe this is something I missed but there should be some comment above regarding how they tried to enable the client validation.
2. I'm not pretty sure what you mean and what you want. I guess you want to save the language preference per user, is that correct? I think you should save the language setting per user in your database and when a user logged in, you can pass his/her language into the route data.
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by AR on 1/3/2013 9:09 AM
Shaun, brilliant work, many thanks for sharing.

Here's how I implemented a possible but still quite limited workaround for the "single String.Format() replacement only" issue.

First, to make things easier, create a simple attribute for holding the possible replacement values, e.g.

---
public class AdditionalValidationDataAttribute : Attribute
{
public String[] Values { get; set; }
}
---

Example usage on the model:

---
[Required(ErrorMessage = "Model_AccountModels_LoginModel_Password_Required")]
[StringLength(12, ErrorMessage = "Model_AccountModels_RegisterModel_Password_StringLength", MinimumLength = 2)]
[DataType(DataType.Password)]
[Display(Name = "Model_AccountModels_LoginModel_Password_Display")]
[AdditionalValidationData(Values = new[] { "2", "12" })]
public string Password { get; set; }
---

You get the idea - it basically acts as storage for values that will be used in constructing the validation message. In this case, it duplicates the range values for the StringLength attribute. Yes, I know...

So now, in LocalizableDataAnnotationsModelMetadataProvider, we grab *both* the "display" and the new "additional values" attributes (and also exclude them earlier on).

---
var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
var additionalMessageDataAttribute = attributes.OfType<AdditionalValidationDataAttribute>().FirstOrDefault();
---

And a little further, the solution to "transporting" the message values over to LocalizableDataAnnotationsModelValidator - store it in the "AdditionalValues" property of ModelMetadata:

---
var resourceProvider = LocalizationResourceProvider.Current;
var i = 0;

if (additionalMessageDataAttribute != null)
{
foreach (var property in additionalMessageDataAttribute.Values)
{
metadata.AdditionalValues.Add(i.ToString(), property);
i++;
}
}
---

Then, at validation time (in LocalizableDataAnnotationsModelValidator) we can reconstruct as follows:

---
return results.Select((validationResult) =>
{
var resourceKey = validationResult.Message;
var message = resourceProvider.GetString(resourceKey);
var messageData = new List<String>();

messageData.Add(_metadata.DisplayName); // Add the display name, always

if (_metadata.AdditionalValues != null) // Add any additional properties, if any
{
foreach (var additionalValue in _metadata.AdditionalValues)
{
messageData.Add(additionalValue.Value.ToString());
}
}

return new ModelValidationResult { Message = String.Format(message, messageData.ToArray()) };
});
---

Note that this is only useful (or at least usable) when the values being passed are already localization-independent, e.g. things like numbers, otherwise there will be excessive lookups (and the code needs to be modified further to perform these lookups anyway).

But with this, validation messages on the RegisterModel used above can now be displayed (and localized) as:

"The Password must be at least 2 but less than 12 characters in length."

Not 100% sure yet if storing things in the AdditionalValues collection will affect anything else, but as this is supposed to be general-purpose store (Dictionary<string, object>), we should be okay... I think. (Except maybe when some other check expects the collection to be empty at particular points, but I can't think of what at the moment.)
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Shaun on 1/3/2013 2:35 PM
Nice work AR!
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by sharon on 2/5/2013 1:59 PM
Hi All,
I think I have a solution for the client side validation and the "single string format".
I took Robbert advice and implemented my own RequiredAttributeAdapter, StringLengthAttributeAdapter, RegexAttributeAdapter etc... and register it in Global.asax . This solved the Ajax/Client validation side.
Very simple solution with simple and small coding, (that way you can easely add new adapters for your own DataAnotation) Thanks Robert for the Tip!!

As for the "Single string format" i have changed the ILocalizationResourceProvider Interface to:
public interface ILocalizationResourceProvider
{
string GetString(string cultureName, string key, string[] args = null);

string GetString(string key, string[] args = null);

IHtmlString GetHtmlString(string cultureName, string key);

IHtmlString GetHtmlString(string key);
}

and support the string[] args all over the way.
simple solution but doing the work :-)

Hope it helped to people with the same problem that i had.

Have a nice day!
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by salmon on 3/2/2013 7:50 PM
I was Written your sample bu it doesnt work for DataAnnotation when it was read message from database
can u help me?!!
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by local on 3/14/2013 4:48 AM
Great Article man....kudos !!

I am trying to implement using Resource files in MVC4.
1) I keep on getting "favicon.ico" in "lang" Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang); I got rid of it by routes.IgnoreRoute("favicon.ico");but what if i really need favicon.ico later on.
2)I have 3 routes
routes.MapRoute(
"Account", // Route name
"Account/{action}", // URL with parameters
new { controller = "Account", action = "CreateAccount" } // Parameter defaults
);

routes.MapRoute(
Constants.ROUTE_NAME, // Route name
string.Format("{{{0}}}/{{controller}}/{{action}}/{{id}}", Constants.ROUTE_PARAMNAME_LANG), // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
in Localization.config followed by

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

in RouteConfig.cs. with this route settings i get "Home" in "lang"
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);

key[0] = "lang" value[0] = "Home"
key[1] = "controller" value[1] ="Home"
key[2] = "action" value=[2]="Index"

PLease help ..i am stuck here as i am pretty new to mvc

Thanks in Advance


Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Trebor on 3/27/2013 9:02 PM
For client validation to work same way as it does in original DataAnnotationsModelValidatorProvider add following three lines to LocalizableDataAnnotationsModelValidator class.

public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
return _innerValidator.GetClientValidationRules();
}
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by MNE on 4/13/2013 1:15 AM
Thx for the job
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Muhammad Adnan on 4/18/2013 5:28 PM
Thanks Trebor,
In order apply localization on the fields use the following code.

public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var results = _innerValidator.GetClientValidationRules();

var source = LocalizationResourceProvider.Current;
var type = source.GetType();

for (int i = 0; i < results.Count(); i++)
{
var key = results.ElementAt(i).ErrorMessage;
var resourceSet = _metadata.ContainerType.Name;
if (key.IndexOf(":") != -1)
{
string[] array = key.Split(':');
resourceSet = array[0];
key = array[1];
}
string display = _metadata.DisplayName;
string message = source.GetString(key, resourceSet);
if (display != null)
message = String.Format(message, display);

results.ElementAt(i).ErrorMessage = message;
}


return results;
}

Thanks
Adnan
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Anees on 5/8/2013 5:19 PM
This solution is not based on SEO Friendly. If English is default language en-us should never come in urls.

I selected zh-cn language, then i removed zh-cn from the urls then it was supposed to be default language. But even then it is showing zh-cn lang.

asp.net mvc is bad in this scenario....
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Ess on 8/6/2013 11:02 PM
I suggest you review this localization tool: https://poeditor.com. To me it's the best thing I have used until now.
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by Mehul rathod on 8/24/2013 10:26 PM
clean n neat code for working with G11n.
thanks a lot...
Gravatar # re: Localization in ASP.NET MVC – Upgraded
Posted by New MVC Developer on 7/8/2014 5:27 PM
Hi,

It would be great if you can look into the client validation issue. All the options provided above are not working. Thanks for the great code. Keep up the good work...
Post A Comment
Title:
Name:
Email:
Comment:
Verification: