In this article we are going to explore all aspects of localization web application based on ASP.NET MVC framework. The version I'll be using for that purpose will be 2 RC 2 which is last available at the time of writing.

NOTE 01.09.2010: In this article Session is used for storing current culture, please also as an addition consider reading my next post about localization where routing mechanism used for that purpose(better SEO and simpler implementation). Also you will find there link to the source code.

Before we start I would like to thank the MVC framework team, great job guys, I really like it :) I really enjoy writing web application with the framework.  I was searching for such kind a framework after small experience with Ruby on Rails

OK, lets see what issues we'll cover
  1. Views validation
  2. Simple culture switching mechanism
  3. Model Validation messages localization
  4. DisplayName attribute localization
  5. OutputCache and Localization
For this guide you'll need Visual Studio 2008 Express and ASP.NET MVC 2 RC2 installed. To follow instructions of the guide please create new MVC 2 web project.

Views localization


For localized strings, of course, we'll be using resource files, but we'll not use asp.net standard folders for storing them.

Here is folder structure I suggest

Resources folder structure

Views - resource files for views aspx pages. Models - resource files for view models localization.

Views folder contains sub folders for each controller and each folder will contain resource files(as much as many languages we'll support)

Models contains sub folders for each group of view models. For generated Account models (LogOn, Register, ChangePassword) we have Account folder and resource files for each language-culture.


Resource files


Some word about resource files naming convention. Resource files have following format
[RESOURCE-NAME].[CULTURE].resx

RESOURCE-NAME - the name of file. Can be anything you like. It's used for grouping, so when there are several resource files with same resource-name they construct one resource with different cultures described by CULTURE

CULTURE - word indicating culture of resource file. Cultures are two type: neutral(also called invariant) and concrete. Neutral culture consists from language code only(examples: en, ru, de etc), Concrete culture consists from language code and region code(examples: en-US, en-UK etc).

Also there is special meaning for resource files that haven't any culture specified, they are called default or fall-back. As you can guess from it's name they are used as default resource files if string was not found in specified culture's resource file or even when there is no resource file for specified culture. I strongly encourage you to use default resource file if user somehow can change to unsupported culture.

Some example of resource files:

MyStrings.en-US.resx -English US

MyStrings.en-UK.resx - English UK

MyStrings.en.resx - English neutral (this is also fall back for English)

MyStrings.ru.resx - Russian neutral

MyStrings.resx - Fall back resource file

OK, now we are ready to localize something and look how it works. I'll show you small example how to localize title of the created web application. Throughout the tutorial I'll use two languages: English(as default) and Russian neutral, but you are free to use any languages you wish.

First of all create folder structure I described above for resource files, and particularly we'll need resource files for Site.Master master page. I create folder Shared under Resources\Views and create two resource files

SharedStrings.resx - Default resource file with English values

SharedStrings.ru.resx - resource file with Russian values

Add property "Title" in both files and fill values.


Title property in resource file

Important! Make sure to change access modifier of each resource file you create to public. Also check that resource files Custom Tool property value is "PublicResXFileCodeGenerator". Otherwise resource files won't be compiled and won't be accessible.





Custom tool property

Some words about resource files namespace. Created this way resource file will have namespace:

[PROJECT-NAME].Resources.Views.Shared

To make it more readable and convenient I changed Custom Tool Namespace properties of resource files to ViewRes (for views resource files)

Now it's time to make modifications in Site.Master page.

Locate following HTML code snippet


<div id="title">
<h1>My MVC Application</h1>
</div>
and replace with
<div id="title">
<h1><%=ViewRes.SharedStrings.Title%></h1>
</div>


Run application and make sure everything is still working, and you can see title in its place(now it should be read from resource file). If everything is OK, it's time to somehow change culture and check whether that will work.

To change culture we need to change CurrentCulture and CurrentUICulture properties of CurrentThread for every request!!! To do this we need to place culture changing code to Global.asax Application_AcquireRequestState method(this method is event handler and is called for every request).

Add following code to Global.asax.cs file




   1:  protected void Application_AcquireRequestState(object sender, EventArgs e)
   2:  {
   3:    //Create culture info object 
   4:    CultureInfo ci = new CultureInfo("en");
   5:  
   6:    System.Threading.Thread.CurrentThread.CurrentUICulture = ci;
   7:    System.Threading.Thread.CurrentThread.CurrentCulture = 
CultureInfo.CreateSpecificCulture(ci.Name);
   8:  }

Run application to check that everything is working. Than change string parameter of CulturInfo constructor (in my case this will be "ru") and run again. You should get following results for both cases

Master page localized Title


Master page localized title


That's all. We have localized Site.Master's title and you can do the same with any string you need.


Simple culture switching mechanism



In the previous chapter we successfully localized title of the application, but there wasn't any chance to change the culture at the runtime. Now we are going to create some mechanism which can help us to control culture setting at the runtime.

As a place for storing users selected culture we'll use session object. And for changing culture we'll place links for each language on the master page. Clicking links will call some action in Account controller which will change session's value corresponding to culture.

Add following code to AccountController class

   1:  public ActionResult ChangeCulture(string lang, string returnUrl)
   2:  {
   3:       Session["Culture"] = new CultureInfo(lang);
   4:       return Redirect(returnUrl);
   5:  }

We have here action method with two parameters, first one is for culture code, second is for redirecting back user to original page. There is no much to do in this action, it's just setting new culture to session dictionary, but remember to add some user input validation here to prevent setting unsupported culture code.

Now we'll create simple user control with supported culture hyper links. Add new partial view to Views\Shared folder CultureChooserUserControl.ascx and paste following

<%= Html.ActionLink("English", "ChangeCulture", "Account",  
     new { lang = "en", returnUrl = this.Request.RawUrl }, null)%>
<%= Html.ActionLink("Русский", "ChangeCulture", "Account",  
     new { lang = "ru", returnUrl = this.Request.RawUrl }, null)%>

We just now created two hyper links, first one for English and second one for Russian languages. And now its time to place this culture chooser user control to Site.Master master page. I'll add this to <div> corresponding to login functionality just as an example.

Find and replace <div id="logindisplay"> with following

<div id="logindisplay">
<% Html.RenderPartial("LogOnUserControl"); %>
<% Html.RenderPartial("CultureChooserUserControl"); %>
</div>

What else? Right, the most important is left, we put culture info object to session, but we never use it. It's time to make some changes in Global.asax.cs, and again this is Application_AcquireRequestState method.

   1:  protected void Application_AcquireRequestState(object sender, EventArgs e)
   2:  {
   3:       //It's important to check whether session object is ready
   4:       if (HttpContext.Current.Session != null)
   5:       {
   6:           CultureInfo ci = (CultureInfo)this.Session["Culture"];
   7:           //Checking first if there is no value in session 
   8:           //and set default language 
   9:           //this can happen for first user's request
  10:           if (ci == null)
  11:           {
  12:               //Sets default culture to english invariant
  13:               string langName = "en";
  14:  
  15:               //Try to get values from Accept lang HTTP header
  16:               if (HttpContext.Current.Request.UserLanguages != null && 
HttpContext.Current.Request.UserLanguages.Length != 0)
  17:               {
  18:                   //Gets accepted list 
  19:                   langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
  20:               }
  21:               ci = new CultureInfo(langName);
  22:               this.Session["Culture"] = ci;
  23:           }
  24:           //Finally setting culture for each request
  25:           Thread.CurrentThread.CurrentUICulture = ci;
  26:           Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
  27:       }
  28:  }


Running this will result with following page, and clicking language links will reload page with selected culture

Culture Chooser



Model Validation Messages Localization



There is already good solution I found recently available on the net posted by Phil Haack, but as this should be full guide, I can't leave that aspect untouched and because there are some misunderstandings which I want to clarify. But before you begin I strongly recommend you to read Phil Haack's post.

I'm going to explain how to localize Account models validation messages, and particularly for RegistrationModel. Also I want to describe how to localize Membership validation messages which are hard coded in AccountController.

OK, lets create ValidationStrings.resx and ValidationStrings.ru.resx in Resources\Models\Account folder(make sure you set access modifier to public). As you can guess we'll be storing all validation messages in that files.

I created following properties in both resource files(English example)

Validation messages

We need to modify our models in following way(example of RegisterModel)


   1:  [PropertiesMustMatch("Password", "ConfirmPassword"
ErrorMessageResourceName = "PasswordsMustMatch"
ErrorMessageResourceType = typeof(ValidationStrings))]
   2:      public class RegisterModel
   3:      {
   4:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
   5:          [DisplayName("Username")]
   6:          public string UserName { get; set; }
   7:  
   8:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
   9:          [DataType(DataType.EmailAddress)]
  10:          [DisplayName("Email")]
  11:          public string Email { get; set; }
  12:  
  13:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
  14:          [ValidatePasswordLength(ErrorMessageResourceName = "PasswordMinLength"
ErrorMessageResourceType = typeof(ValidationStrings))]
  15:          [DataType(DataType.Password)]
  16:          [DisplayName("Password")]
  17:          public string Password { get; set; }
  18:  
  19:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
  20:          [DataType(DataType.Password)]
  21:          [DisplayName("Confirm password")]
  22:          public string ConfirmPassword { get; set; }
  23:      }

We add ErrorMessageResourceName and ErrorMessageResourceType properties to Required, PropertiesMustMatch and ValidatePasswordLength attributes, where ErrorMessageResourceType is type of resource class where messages are stored and ErrorMessageResourceName is property name. Unfortunately there is no way to provide strongly typed mechanism for reading that values, so be sure that these magic string have right values.

We are almost done, just one little thing. There are two custom validation attributes PropertiesMustMatchAttribute and ValidatePasswordLenghtAttribute in which we should change CultureInfo.CurrentUICulture in FormatErrorMessage method to CultureInfo.CurrentCulture otherwise this will not work for our configuration.

OK, now run application, go to registration page, click language link to change culture and you should get something like this when try to submit empty form

Registration page validation


Oops, as you can notice we forgot to localize names of properties in view models and now it's mixed language. To do so we need to localize DisplayName attribute value, but it's not so simple as it seems to be. I'm going to cover this issue in the next chapter, and now we have some little thing left. It's Membership API validation messages localization.

Open AccountController and scroll down to the end, there should be method ErrorCodeToString which create error messages in case when user registration was failed. All messages are hard coded. All we need to do is create appropriate properties for each one in already created ValidationStrings resource files and put them instead of strings in ErrorCodeToString method.

That's all with model validation. Now it's time for DisplayName!



DisplayName Attribute localization


As we see in the previous chapter DisplayName value participates in validation messages which are using parameters for formatting. Also one more reason to think about DisplayName attribute is labels of fields in HTML form, these are created using value of DisplayName.

The real problem is that DisplayName doesn't support localization, there is no way to provide resource file from which it can read its value.

This both mean that we need to extend DisplayNameAttribute and override DisplayName property which will always return localized name. I created such derived class and named it LocalizedDisplayName


   1:  public class LocalizedDisplayNameAttribute : DisplayNameAttribute
   2:  {
   3:     private PropertyInfo _nameProperty;
   4:     private Type _resourceType;
   5:  
   6:     public LocalizedDisplayNameAttribute(string displayNameKey)
   7:         : base(displayNameKey)
   8:     {
   9:  
  10:     }
  11:  
  12:     public Type NameResourceType
  13:     {
  14:         get
  15:         {
  16:             return _resourceType;
  17:         }
  18:         set
  19:         {
  20:             _resourceType = value;
  21:             //initialize nameProperty when type property is provided by setter
  22:             _nameProperty = _resourceType.GetProperty(base.DisplayName, 
BindingFlags.Static | BindingFlags.Public);
  23:         }
  24:     }
  25:  
  26:     public override string DisplayName
  27:     {
  28:        get
  29:        {
  30:             //check if nameProperty is null and return original display name value
  31:             if (_nameProperty == null)
  32:             {
  33:                 return base.DisplayName;
  34:             }
  35:  
  36:             return (string)_nameProperty.GetValue(_nameProperty.DeclaringType, null);
  37:         }
  38:      }
  39:  
  40:  }


Important thing here to understand is that we need to read property value every time it's called, that's why GetValue method called in the getter of DisplayName property, and not in the constructor.

For storing display names I created Names.resx and Names.ru.resx resource files under Resources\Models\Account folder and create following properties

Display names localized

Now we need to change DisplayName attribute to LocalizedDisplayName and provide resource class type. The modified RegisterModel code will look like this


   1:  [PropertiesMustMatch("Password", "ConfirmPassword"
ErrorMessageResourceName = "PasswordsMustMatch"
ErrorMessageResourceType = typeof(ValidationStrings))]
   2:      public class RegisterModel
   3:      {
   4:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
   5:          [LocalizedDisplayName("RegUsername", NameResourceType = typeof(Names))]
   6:          public string UserName { get; set; }
   7:  
   8:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
   9:          [DataType(DataType.EmailAddress)]
  10:          [LocalizedDisplayName("RegEmail", NameResourceType = typeof(Names))]
  11:          public string Email { get; set; }
  12:  
  13:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
  14:          [ValidatePasswordLength(ErrorMessageResourceName = "PasswordMinLength"
ErrorMessageResourceType = typeof(ValidationStrings))]
  15:          [DataType(DataType.Password)]
  16:          [LocalizedDisplayName("RegPassword", NameResourceType = typeof(Names))]
  17:          public string Password { get; set; }
  18:  
  19:          [Required(ErrorMessageResourceName = "Required"
ErrorMessageResourceType = typeof(ValidationStrings))]
  20:          [DataType(DataType.Password)]
  21:          [LocalizedDisplayName("RegConfirmPassword", NameResourceType = typeof(Names))]
  22:          public string ConfirmPassword { get; set; }
  23:      }


Run application to make sure that everything work as expected, for me this will be like

DisplayNames Fixed Registration



OutputCache and Localization



What a strange chapter? You think how can caching and localization be connected? OK, lets try following scenario: open HomeController and add OutputCache attribute to Index action method, so that action code will look like following:


   1:  [OutputCache(Duration=3600, VaryByParam="none")]
   2:  public ActionResult Index()
   3:  {
   4:      ViewData["Message"] = "Welcome to ASP.NET MVC!";
   5:  
   6:      return View();
   7:  }


Now build application and try to change language in Index page to check that localized Title is still localized.

Oh no? You already think that these two things can't be used together? Don't hurry, there is a solution :)

What do you know about OutputCache and particularly what do you know about VaryByCustom property? Now it's time to use it.

What's happening here? When we request Index page for first time OutputCache caches the page. For the second request (when we click language link)  OutputCache thinks that nothing was changed and returns result from cache, so the page is not created again. That's why language chooser doesn't work. To solve the problem we need somehow say to OutputCache that page version was changed(like as it's working in case if action has some param and we put that param to VaryByParam property).

VaryByCustom is ideal candidate for that purpose and there is special method in System.Web.HttpApplication which derived class placed in Global.asax.cs file. We'll override the default implementation of that method.


   1:  public override string GetVaryByCustomString(HttpContext context, string value)
   2:  {
   3:       if (value.Equals("lang"))
   4:       {
   5:           return Thread.CurrentThread.CurrentUICulture.Name;
   6:       }
   7:       return base.GetVaryByCustomString(context,value);
   8:  }


First the method checks if value param equals to "lang" (no special meaning, just string which will be used as value for VaryByCustom) and in case if they are equal returns name of current culture. Otherwise we return value of default implementation.

Now add VaryByCustom property with value "lang" to each OutputCache attribute you want to use together with localization and that's all. The updated Index action method will look like this


   1:  [OutputCache(Duration=3600,VaryByParam="none", VaryByCustom="lang")]
   2:  public ActionResult Index()
   3:  {
   4:       ViewData["Message"] = "Welcome to ASP.NET MVC!";
   5:       return View();
   6:  }

Try to run again and ensure that culture chooser is working again.

We've finished the last chapter and I hope I didn't miss anything. If you don't think so please let me know.

Thanks for reading, feel free to contact me if you have any question and make comments :)

kick it on DotNetKicks.com

Shout it
108

View comments


  1. In my previous post I described the way of localization using session, but in real-world applications it's definitely not the best way of localization. Now I'll describe very simple and very powerful way of storing it in URL using routing mechanism.

    Also this way of localization will not require OutputCache tricks described in previous post

    The goal of this post is to show how to get URL  like this /{culture}/{Controller}/{Action}... in your application like /ru/Home/About.


    Custom Route Handlers

    First of all we'll need to extend standard MvcRouteHandler class. One class MultiCultureMvcRouteHandler for routes that will use culture in params and SingleCultureMvcRouteHandler class (will be used as a marker, no implementation changes)

    public class MultiCultureMvcRouteHandler : MvcRouteHandler
    {
        protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            var culture = requestContext.RouteData.Values["culture"].ToString();
            var ci = new CultureInfo(culture);
            Thread.CurrentThread.CurrentUICulture = ci;
            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
            return base.GetHttpHandler(requestContext);
        }
    }

    In the overridden GetHttpHandler before calling it's base implementation we just get "culture" param from RouteData collection, create CultureInfo object and set it to current thread current culture. So here is a place where we set culture and will not use Application_AcquireRequestState method in Global.asax

    public class SingleCultureMvcRouteHandler : MvcRouteHandler {}

    As I mention this class will be used only for marking some routes for case if you'll need some routes to be culture independent.


    Registering routes

    Now lets go to Global.asax file where we have route registering method RegisterRoutes(). Right after last route mapping add foreach statement code snippet like in the following example.

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
        routes.MapRoute(
             "Default", // Route name
             "{controller}/{action}/{id}", // URL with parameters
             new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );
    
        foreach (Route r in routes)
        {
            if (!(r.RouteHandler is SingleCultureMvcRouteHandler))
            {
                r.RouteHandler = new MultiCultureMvcRouteHandler();
                r.Url = "{culture}/" + r.Url;
               //Adding default culture 
               if (r.Defaults == null)
               {
                   r.Defaults = new RouteValueDictionary();
               }
               r.Defaults.Add("culture", Culture.ru.ToString());
    
               //Adding constraint for culture param
               if (r.Constraints == null)
               {
                   r.Constraints = new RouteValueDictionary();
               }
               r.Constraints.Add("culture", new CultureConstraint(Culture.en.ToString(), 
    Culture.ru.ToString()));
            }
       }
    
    }
    
    

    OK, lets go through this code... So for each route we first of all check whether its handler type is SingleCultureMvcRouteHandler or not... So if not we change RouteHandler property of the current route to MultiCulture one, add prefix to Url, add default culture and finally add constraint for culture param checking.

    public class CultureConstraint : IRouteConstraint
    {
        private string[] _values;
        public CultureConstraint(params string[] values)
        {
            this._values = values;
        }
    
        public bool Match(HttpContextBase httpContext,Route route,string parameterName,
                            RouteValueDictionary values, RouteDirection routeDirection)
        {
    
            // Get the value called "parameterName" from the 
            // RouteValueDictionary called "value"
            string value = values[parameterName].ToString();
            // Return true is the list of allowed values contains 
            // this value.
            return _values.Contains(value);
    
        }
    
    }

    And enum of cultures
        public enum Culture
        {
            ru = 1,
            en = 2
        }


    Simple culture switching mechanism

    For changing culture we'll need following simple action which I placed in AccountController

    public ActionResult ChangeCulture(Culture lang, string returnUrl)
    {
         if (returnUrl.Length >= 3)
         {
             returnUrl = returnUrl.Substring(3);
         }
         return Redirect("/" + lang.ToString() + returnUrl);
    }

    and partial view with languages links - CultureSwitchControl.ascx


    <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
    
    <%= Html.ActionLink("eng", "ChangeCulture", "Account",
        new { lang = (int)MvcLocalization.Helpers.Culture.en, returnUrl =  
        this.Request.RawUrl }, new { @class = "culture-link" })%>
    
    <%= Html.ActionLink("рус", "ChangeCulture", "Account",
        new { lang = (int)MvcLocalization.Helpers.Culture.ru, returnUrl = 
        this.Request.RawUrl }, new { @class = "culture-link" })%>



    Single culture case

    Finally, if we need some single culture route all we need to do is to set RouteHandler property to SingleCultureMvcRouteHandler  like this

    routes.MapRoute(
              "AboutRoute",
              "About",
              new { controller = "Home", action = "About"}
       ).RouteHandler = new SingleCultureMvcRouteHandler();


    So, that's it :) Localization without using Session, without problems with OutputCache(will be explained in my next post) and with use of routing.

    Here is the link of  source code(project created in VS2010)



    kick it on DotNetKicks.com
    42

    View comments


  2. In this article we are going to explore all aspects of localization web application based on ASP.NET MVC framework. The version I'll be using for that purpose will be 2 RC 2 which is last available at the time of writing.

    NOTE 01.09.2010: In this article Session is used for storing current culture, please also as an addition consider reading my next post about localization where routing mechanism used for that purpose(better SEO and simpler implementation). Also you will find there link to the source code.

    Before we start I would like to thank the MVC framework team, great job guys, I really like it :) I really enjoy writing web application with the framework.  I was searching for such kind a framework after small experience with Ruby on Rails

    OK, lets see what issues we'll cover
    1. Views validation
    2. Simple culture switching mechanism
    3. Model Validation messages localization
    4. DisplayName attribute localization
    5. OutputCache and Localization
    For this guide you'll need Visual Studio 2008 Express and ASP.NET MVC 2 RC2 installed. To follow instructions of the guide please create new MVC 2 web project.

    Views localization


    For localized strings, of course, we'll be using resource files, but we'll not use asp.net standard folders for storing them.

    Here is folder structure I suggest

    Resources folder structure

    Views - resource files for views aspx pages. Models - resource files for view models localization.

    Views folder contains sub folders for each controller and each folder will contain resource files(as much as many languages we'll support)

    Models contains sub folders for each group of view models. For generated Account models (LogOn, Register, ChangePassword) we have Account folder and resource files for each language-culture.


    Resource files


    Some word about resource files naming convention. Resource files have following format
    [RESOURCE-NAME].[CULTURE].resx

    RESOURCE-NAME - the name of file. Can be anything you like. It's used for grouping, so when there are several resource files with same resource-name they construct one resource with different cultures described by CULTURE

    CULTURE - word indicating culture of resource file. Cultures are two type: neutral(also called invariant) and concrete. Neutral culture consists from language code only(examples: en, ru, de etc), Concrete culture consists from language code and region code(examples: en-US, en-UK etc).

    Also there is special meaning for resource files that haven't any culture specified, they are called default or fall-back. As you can guess from it's name they are used as default resource files if string was not found in specified culture's resource file or even when there is no resource file for specified culture. I strongly encourage you to use default resource file if user somehow can change to unsupported culture.

    Some example of resource files:

    MyStrings.en-US.resx -English US

    MyStrings.en-UK.resx - English UK

    MyStrings.en.resx - English neutral (this is also fall back for English)

    MyStrings.ru.resx - Russian neutral

    MyStrings.resx - Fall back resource file

    OK, now we are ready to localize something and look how it works. I'll show you small example how to localize title of the created web application. Throughout the tutorial I'll use two languages: English(as default) and Russian neutral, but you are free to use any languages you wish.

    First of all create folder structure I described above for resource files, and particularly we'll need resource files for Site.Master master page. I create folder Shared under Resources\Views and create two resource files

    SharedStrings.resx - Default resource file with English values

    SharedStrings.ru.resx - resource file with Russian values

    Add property "Title" in both files and fill values.


    Title property in resource file

    Important! Make sure to change access modifier of each resource file you create to public. Also check that resource files Custom Tool property value is "PublicResXFileCodeGenerator". Otherwise resource files won't be compiled and won't be accessible.





    Custom tool property

    Some words about resource files namespace. Created this way resource file will have namespace:

    [PROJECT-NAME].Resources.Views.Shared

    To make it more readable and convenient I changed Custom Tool Namespace properties of resource files to ViewRes (for views resource files)

    Now it's time to make modifications in Site.Master page.

    Locate following HTML code snippet


    <div id="title">
    <h1>My MVC Application</h1>
    </div>
    and replace with
    <div id="title">
    <h1><%=ViewRes.SharedStrings.Title%></h1>
    </div>


    Run application and make sure everything is still working, and you can see title in its place(now it should be read from resource file). If everything is OK, it's time to somehow change culture and check whether that will work.

    To change culture we need to change CurrentCulture and CurrentUICulture properties of CurrentThread for every request!!! To do this we need to place culture changing code to Global.asax Application_AcquireRequestState method(this method is event handler and is called for every request).

    Add following code to Global.asax.cs file




       1:  protected void Application_AcquireRequestState(object sender, EventArgs e)
       2:  {
       3:    //Create culture info object 
       4:    CultureInfo ci = new CultureInfo("en");
       5:  
       6:    System.Threading.Thread.CurrentThread.CurrentUICulture = ci;
       7:    System.Threading.Thread.CurrentThread.CurrentCulture = 
    CultureInfo.CreateSpecificCulture(ci.Name);
       8:  }

    Run application to check that everything is working. Than change string parameter of CulturInfo constructor (in my case this will be "ru") and run again. You should get following results for both cases

    Master page localized Title


    Master page localized title


    That's all. We have localized Site.Master's title and you can do the same with any string you need.


    Simple culture switching mechanism



    In the previous chapter we successfully localized title of the application, but there wasn't any chance to change the culture at the runtime. Now we are going to create some mechanism which can help us to control culture setting at the runtime.

    As a place for storing users selected culture we'll use session object. And for changing culture we'll place links for each language on the master page. Clicking links will call some action in Account controller which will change session's value corresponding to culture.

    Add following code to AccountController class

       1:  public ActionResult ChangeCulture(string lang, string returnUrl)
       2:  {
       3:       Session["Culture"] = new CultureInfo(lang);
       4:       return Redirect(returnUrl);
       5:  }

    We have here action method with two parameters, first one is for culture code, second is for redirecting back user to original page. There is no much to do in this action, it's just setting new culture to session dictionary, but remember to add some user input validation here to prevent setting unsupported culture code.

    Now we'll create simple user control with supported culture hyper links. Add new partial view to Views\Shared folder CultureChooserUserControl.ascx and paste following

    <%= Html.ActionLink("English", "ChangeCulture", "Account",  
         new { lang = "en", returnUrl = this.Request.RawUrl }, null)%>
    <%= Html.ActionLink("Русский", "ChangeCulture", "Account",  
         new { lang = "ru", returnUrl = this.Request.RawUrl }, null)%>

    We just now created two hyper links, first one for English and second one for Russian languages. And now its time to place this culture chooser user control to Site.Master master page. I'll add this to <div> corresponding to login functionality just as an example.

    Find and replace <div id="logindisplay"> with following

    <div id="logindisplay">
    <% Html.RenderPartial("LogOnUserControl"); %>
    <% Html.RenderPartial("CultureChooserUserControl"); %>
    </div>

    What else? Right, the most important is left, we put culture info object to session, but we never use it. It's time to make some changes in Global.asax.cs, and again this is Application_AcquireRequestState method.

       1:  protected void Application_AcquireRequestState(object sender, EventArgs e)
       2:  {
       3:       //It's important to check whether session object is ready
       4:       if (HttpContext.Current.Session != null)
       5:       {
       6:           CultureInfo ci = (CultureInfo)this.Session["Culture"];
       7:           //Checking first if there is no value in session 
       8:           //and set default language 
       9:           //this can happen for first user's request
      10:           if (ci == null)
      11:           {
      12:               //Sets default culture to english invariant
      13:               string langName = "en";
      14:  
      15:               //Try to get values from Accept lang HTTP header
      16:               if (HttpContext.Current.Request.UserLanguages != null && 
    HttpContext.Current.Request.UserLanguages.Length != 0)
      17:               {
      18:                   //Gets accepted list 
      19:                   langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
      20:               }
      21:               ci = new CultureInfo(langName);
      22:               this.Session["Culture"] = ci;
      23:           }
      24:           //Finally setting culture for each request
      25:           Thread.CurrentThread.CurrentUICulture = ci;
      26:           Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
      27:       }
      28:  }


    Running this will result with following page, and clicking language links will reload page with selected culture

    Culture Chooser



    Model Validation Messages Localization



    There is already good solution I found recently available on the net posted by Phil Haack, but as this should be full guide, I can't leave that aspect untouched and because there are some misunderstandings which I want to clarify. But before you begin I strongly recommend you to read Phil Haack's post.

    I'm going to explain how to localize Account models validation messages, and particularly for RegistrationModel. Also I want to describe how to localize Membership validation messages which are hard coded in AccountController.

    OK, lets create ValidationStrings.resx and ValidationStrings.ru.resx in Resources\Models\Account folder(make sure you set access modifier to public). As you can guess we'll be storing all validation messages in that files.

    I created following properties in both resource files(English example)

    Validation messages

    We need to modify our models in following way(example of RegisterModel)


       1:  [PropertiesMustMatch("Password", "ConfirmPassword"
    ErrorMessageResourceName = "PasswordsMustMatch"
    ErrorMessageResourceType = typeof(ValidationStrings))]
       2:      public class RegisterModel
       3:      {
       4:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
       5:          [DisplayName("Username")]
       6:          public string UserName { get; set; }
       7:  
       8:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
       9:          [DataType(DataType.EmailAddress)]
      10:          [DisplayName("Email")]
      11:          public string Email { get; set; }
      12:  
      13:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
      14:          [ValidatePasswordLength(ErrorMessageResourceName = "PasswordMinLength"
    ErrorMessageResourceType = typeof(ValidationStrings))]
      15:          [DataType(DataType.Password)]
      16:          [DisplayName("Password")]
      17:          public string Password { get; set; }
      18:  
      19:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
      20:          [DataType(DataType.Password)]
      21:          [DisplayName("Confirm password")]
      22:          public string ConfirmPassword { get; set; }
      23:      }

    We add ErrorMessageResourceName and ErrorMessageResourceType properties to Required, PropertiesMustMatch and ValidatePasswordLength attributes, where ErrorMessageResourceType is type of resource class where messages are stored and ErrorMessageResourceName is property name. Unfortunately there is no way to provide strongly typed mechanism for reading that values, so be sure that these magic string have right values.

    We are almost done, just one little thing. There are two custom validation attributes PropertiesMustMatchAttribute and ValidatePasswordLenghtAttribute in which we should change CultureInfo.CurrentUICulture in FormatErrorMessage method to CultureInfo.CurrentCulture otherwise this will not work for our configuration.

    OK, now run application, go to registration page, click language link to change culture and you should get something like this when try to submit empty form

    Registration page validation


    Oops, as you can notice we forgot to localize names of properties in view models and now it's mixed language. To do so we need to localize DisplayName attribute value, but it's not so simple as it seems to be. I'm going to cover this issue in the next chapter, and now we have some little thing left. It's Membership API validation messages localization.

    Open AccountController and scroll down to the end, there should be method ErrorCodeToString which create error messages in case when user registration was failed. All messages are hard coded. All we need to do is create appropriate properties for each one in already created ValidationStrings resource files and put them instead of strings in ErrorCodeToString method.

    That's all with model validation. Now it's time for DisplayName!



    DisplayName Attribute localization


    As we see in the previous chapter DisplayName value participates in validation messages which are using parameters for formatting. Also one more reason to think about DisplayName attribute is labels of fields in HTML form, these are created using value of DisplayName.

    The real problem is that DisplayName doesn't support localization, there is no way to provide resource file from which it can read its value.

    This both mean that we need to extend DisplayNameAttribute and override DisplayName property which will always return localized name. I created such derived class and named it LocalizedDisplayName


       1:  public class LocalizedDisplayNameAttribute : DisplayNameAttribute
       2:  {
       3:     private PropertyInfo _nameProperty;
       4:     private Type _resourceType;
       5:  
       6:     public LocalizedDisplayNameAttribute(string displayNameKey)
       7:         : base(displayNameKey)
       8:     {
       9:  
      10:     }
      11:  
      12:     public Type NameResourceType
      13:     {
      14:         get
      15:         {
      16:             return _resourceType;
      17:         }
      18:         set
      19:         {
      20:             _resourceType = value;
      21:             //initialize nameProperty when type property is provided by setter
      22:             _nameProperty = _resourceType.GetProperty(base.DisplayName, 
    BindingFlags.Static | BindingFlags.Public);
      23:         }
      24:     }
      25:  
      26:     public override string DisplayName
      27:     {
      28:        get
      29:        {
      30:             //check if nameProperty is null and return original display name value
      31:             if (_nameProperty == null)
      32:             {
      33:                 return base.DisplayName;
      34:             }
      35:  
      36:             return (string)_nameProperty.GetValue(_nameProperty.DeclaringType, null);
      37:         }
      38:      }
      39:  
      40:  }


    Important thing here to understand is that we need to read property value every time it's called, that's why GetValue method called in the getter of DisplayName property, and not in the constructor.

    For storing display names I created Names.resx and Names.ru.resx resource files under Resources\Models\Account folder and create following properties

    Display names localized

    Now we need to change DisplayName attribute to LocalizedDisplayName and provide resource class type. The modified RegisterModel code will look like this


       1:  [PropertiesMustMatch("Password", "ConfirmPassword"
    ErrorMessageResourceName = "PasswordsMustMatch"
    ErrorMessageResourceType = typeof(ValidationStrings))]
       2:      public class RegisterModel
       3:      {
       4:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
       5:          [LocalizedDisplayName("RegUsername", NameResourceType = typeof(Names))]
       6:          public string UserName { get; set; }
       7:  
       8:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
       9:          [DataType(DataType.EmailAddress)]
      10:          [LocalizedDisplayName("RegEmail", NameResourceType = typeof(Names))]
      11:          public string Email { get; set; }
      12:  
      13:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
      14:          [ValidatePasswordLength(ErrorMessageResourceName = "PasswordMinLength"
    ErrorMessageResourceType = typeof(ValidationStrings))]
      15:          [DataType(DataType.Password)]
      16:          [LocalizedDisplayName("RegPassword", NameResourceType = typeof(Names))]
      17:          public string Password { get; set; }
      18:  
      19:          [Required(ErrorMessageResourceName = "Required"
    ErrorMessageResourceType = typeof(ValidationStrings))]
      20:          [DataType(DataType.Password)]
      21:          [LocalizedDisplayName("RegConfirmPassword", NameResourceType = typeof(Names))]
      22:          public string ConfirmPassword { get; set; }
      23:      }


    Run application to make sure that everything work as expected, for me this will be like

    DisplayNames Fixed Registration



    OutputCache and Localization



    What a strange chapter? You think how can caching and localization be connected? OK, lets try following scenario: open HomeController and add OutputCache attribute to Index action method, so that action code will look like following:


       1:  [OutputCache(Duration=3600, VaryByParam="none")]
       2:  public ActionResult Index()
       3:  {
       4:      ViewData["Message"] = "Welcome to ASP.NET MVC!";
       5:  
       6:      return View();
       7:  }


    Now build application and try to change language in Index page to check that localized Title is still localized.

    Oh no? You already think that these two things can't be used together? Don't hurry, there is a solution :)

    What do you know about OutputCache and particularly what do you know about VaryByCustom property? Now it's time to use it.

    What's happening here? When we request Index page for first time OutputCache caches the page. For the second request (when we click language link)  OutputCache thinks that nothing was changed and returns result from cache, so the page is not created again. That's why language chooser doesn't work. To solve the problem we need somehow say to OutputCache that page version was changed(like as it's working in case if action has some param and we put that param to VaryByParam property).

    VaryByCustom is ideal candidate for that purpose and there is special method in System.Web.HttpApplication which derived class placed in Global.asax.cs file. We'll override the default implementation of that method.


       1:  public override string GetVaryByCustomString(HttpContext context, string value)
       2:  {
       3:       if (value.Equals("lang"))
       4:       {
       5:           return Thread.CurrentThread.CurrentUICulture.Name;
       6:       }
       7:       return base.GetVaryByCustomString(context,value);
       8:  }


    First the method checks if value param equals to "lang" (no special meaning, just string which will be used as value for VaryByCustom) and in case if they are equal returns name of current culture. Otherwise we return value of default implementation.

    Now add VaryByCustom property with value "lang" to each OutputCache attribute you want to use together with localization and that's all. The updated Index action method will look like this


       1:  [OutputCache(Duration=3600,VaryByParam="none", VaryByCustom="lang")]
       2:  public ActionResult Index()
       3:  {
       4:       ViewData["Message"] = "Welcome to ASP.NET MVC!";
       5:       return View();
       6:  }

    Try to run again and ensure that culture chooser is working again.

    We've finished the last chapter and I hope I didn't miss anything. If you don't think so please let me know.

    Thanks for reading, feel free to contact me if you have any question and make comments :)

    kick it on DotNetKicks.com

    Shout it
    108

    View comments

    1. This post is really good. I need it for my project. Thank Alex for this!

      ReplyDelete
    2. Check out Asp.Net Mvc Extensibility provider. I think it's a good thing to reference resources in strongly typed fashion when applying metadata too.

      Apart from that - nice article. I wish it was posted 1/2 year ago. ^^

      ReplyDelete
    3. I'd say that some of the most important aspects of localization is still missing...

      A generic way to do "URL" localization and argument parsing/output localization...

      ReplyDelete
    4. Poul, good point! You're right, it's important to have localized URLs and generic routing mechanism for that. I'll try to find out some elegant solution. Thanks

      ReplyDelete
    5. Спасибо, за пост. Как раз встала проблема локализации приложения на ASP.NET MVC.

      ReplyDelete
    6. Hello,

      why did you not use the App_GlobalResources folder? How to access localized messages from a controller?

      ReplyDelete
    7. Hi Alex,
      I did some Views localization (the way you show here) in my MVC 2 app and it works fine when I run my app with the Visual Studio development web server. However, the localization in my app is not working on my production server (Windows 2003 with IIS 6). Have you had this problem before?
      Regards,
      Thanh

      ReplyDelete
    8. Hi THD, I think I know the solution for your problem, set "Copy to Output Directory" property value for each resource file to "Copy always" or "Copy if newer". It then will force to create dll files for each language you supported, so don't forget to get them in place on your production server. These files will be in bin\{culture-code}\ folder.
      Thanks for noticing, I'll add this to post soon.
      Please tell me if it's not working for your case.
      Regards

      ReplyDelete
    9. Hi Dominic, you can simply access resources anywhere in your code just as you do it for other classes. There is no difference, resource files described in this article get compiled to ordinary classes with static properties.
      Regards

      ReplyDelete
    10. Hi Alex,
      Yes - my problem was forgetting to deploy all DLLs to the server. I didn't know each culture generated a DLL. Now I know. Thanks for writing this great tutorial!
      Regards,
      Thanh

      ReplyDelete
    11. Excellent overview and introduction.

      A little notice: never store the language within a session variable when building a public website. The language has to be a part of the url, so search robots and so on can handle the multiple versions correctly (otherwise it's not possible to differentiate russian and english content).

      ReplyDelete
    12. HI ALex,

      great article...question

      how can i customize the viewData["message"] and the errorCodeToSting messages, you mentioned something is your blog but i did not quite get it.

      ReplyDelete
    13. Very Good Yaar But My Luck is not working

      ReplyDelete
    14. Why you don't give the sample code
      ???

      ReplyDelete
    15. Great article, working here, thanks

      ReplyDelete
    16. Thanks for taking the time to share this, I feel strongly about it and love reading more on this topic. If possible, as you gain knowledge, would you mind updating your blog with more information? It is extremely useful for me.
      Internet Marketing

      ReplyDelete
    17. Hi, The above articles is very impressive, and I really enjoyed reading your blog and points that you expressed. I love to come back on a regular basis, pl. post more on the subject. Thanks.
      Fat Burning Furnace

      ReplyDelete
    18. Hi Alex
      tanx for your good article, but i have a problem,when i create a class for RegisterModel
      in Resources/Models/Account/RegisterModel.cs
      and whrie the code
      [PropertiesMustMatch("Password", "ConfirmPassword", ErrorMessageResourceName = "PasswordsMustMatch", ErrorMessageResourceType = typeof(ValidationStrings))]
      public class RegisterModel
      {
      [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings ......
      it cant know propertiesMustMtch and Requierd and ...
      what shoud i do???

      ReplyDelete
    19. Hi Alex,
      It works excellent with polish language too ;-) Thank you :)

      Krzysztof

      ReplyDelete
    20. This comment has been removed by the author.

      ReplyDelete
    21. Hi
      Its great article, share some other places as well. its mean you are interested, please contact me.

      ReplyDelete
    22. Does this work for Resource Satellite Assemblies? What if we had a de-DE directory within Views/Account?

      ReplyDelete
    23. Hi
      Its great article.. I given reference in my blog @ www.nitrix-reloaded.com. Hope you don't mind..

      ReplyDelete
    24. Hi guys,
      Sorry for not replying to your questions and comments, there was really hard-work times for me. Just completed some project using ASP.NET MVC 2 (http://www.3dtuning.ru - currently it's in Russian, but you can look at English beta version at http://www.3dtuning.ru/en). Now I think I'll have more time to post several articles including updates to localization issue(like storing culture code in url),
      Thanks for reading
      Alex

      ReplyDelete
    25. Being absolutely new to localization, my main questions would be:
      1) where do the translations come from?
      2) I guess this removes the possibility of the site being a real-time CMS, correct? Since all content would need to be translated first, you can't have members adding new content in one language without having all languages translated.

      Please let me know if I'm missing something here.

      Otherwise, great post.

      Kahanu

      ReplyDelete
    26. BTW, nice site, 3D Tuning. Re: localization - I can see that when I go to the English beta path that not all content has been translated.

      Is there any automated way of translating in real-time?

      thanks,
      Kahanu

      ReplyDelete
    27. you are give very clear post about .NET keep it up more post .NET ASP.Net developers India

      ReplyDelete
    28. Does the localized DisplayName attribute work with global resource files? For some reason, when I specify App_GlobalResource.MyResource it can't find it! Thanks for your help.

      ReplyDelete
    29. This is a nice solution! Only thing indeed that is missing is the URL translation/routing.

      A nice article to archieve something like that is:
      http://blog.maartenballiauw.be/post/2010/01/26/Translating-routes-%28ASPNET-MVC-and-Webforms%29.aspx

      But when I want to combine these two together, is not completely working out. Basically, if the route has a URL that change the language you are using, it should set the session to match this language, otherwise the language will be get from the session, and u will you back to your old language again.
      But setting this session from the RouteHandler is the problem...

      Anyone has any suggestions?

      ReplyDelete
    30. unfortunately there is still a problem with the LocalizedDisplayName, because it is bad to maintain/refactoring. A strongly typed solution would be awesome =)

      ReplyDelete
    31. I really like blog and points that you expressed. I love to come back on a regular basis, Could you please give us more on topics like this? Thanks.
      Athlean X

      ReplyDelete
    32. Great stuff, especially about the LocalizedDisplayName, but as other commenters pointed out you are missing the most important part. To get the correct languageversion to be displayed with its own URL such as /en/ for english and /ru/ for russian.

      I've done this for our site http://arrivalguides.com by adding a nice trick to the default routes... but I don't have access to the code right now (on vacation) so I can't share just yet, but I'll post a solution soon on my blog at http://anders.tyckr.com.

      ReplyDelete
    33. Great post,

      Here what i need, thanks Alex

      My new blog is http://programmingdiscussions.blogspot.com/ . Visit to dicuss about Programming.

      ReplyDelete
    34. Hi.. this is cool. I blog at http://w3mentor.com

      ReplyDelete
    35. hi! indeed great post, but I can't get the LocalizedDisplayName part to work. I am using getText's dll approach, so it's not exactly the same I guess. I use a class DisplayNamesResources where I have my translations as

      public static string NewPassword
      {
      get
      {
      return _gettextValidationMessagesResourceManager.GetString("New Password");
      }
      }

      the same approach works for the validation messages.

      my problem here is that while I have set three properties in this way ("NewPassword", "CurrentPassword", "ConfirmNewPassword"), I only get a localization for the first one on my model. The other two return null at

      _nameProperty = _resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);

      if I call
      _resourceType.GetProperties( BindingFlags.Static | BindingFlags.Public)
      I see all three of them.

      Any ideas?

      ReplyDelete
    36. something interesting(?):

      when I changed my Property names to "A", "B", and "C", everything worked.

      Is there a restriction to the length or naming of properties for .GetProperties to work??

      ReplyDelete
    37. Nice tutorial - it works nice when you have pure MVC project.

      What I have is a mixed solution: WebForms with added MVC. MVC is working fine, but localization doesn't - always default version is picked, even when I set culture to de-DE or de and so on.

      It only works when I hit F5 in Visual Studio (the virtual server built-in), it doesn't work on IIS 7.5 (Win7).

      Any tips?

      ReplyDelete
    38. I like this tutorial,but I don't find source code to download.Thanks

      ReplyDelete
    39. This fantastic. They tried to cover some of this at Code Camp in Toronto but it wasnt nearly as complete or straight forward as your article. good job.

      ReplyDelete
    40. 303 - please make sure you have all necessary dll files in place on IIS from bin\Resources folder

      ReplyDelete
    41. I've posted source code for updated version using routing(the link is in next post)

      ReplyDelete
    42. very nice post. very informative one and can be used for my
      profit instruments project work.

      thanks a lot.

      ReplyDelete
    43. I made an extension to MvcContrib based on your post. More can be found here: http://fknet.wordpress.com/2010/08/12/mvccontrib-extension-localized-labels-description/

      ReplyDelete
    44. Thanks for the good info. This will help out on my Athlean X project. Keep up the good work!

      ReplyDelete
    45. This comment has been removed by the author.

      ReplyDelete
    46. This comment has been removed by the author.

      ReplyDelete
    47. I'm curious as to why you are setting 'different' cultures on the CurrentCulture vs the CurrentUICulture? It appears your CurrentUICulture is always set to the 'less specific' version of the culture? en vs en-US?

      Similarily, you made the comment "There are two custom validation attributes PropertiesMustMatchAttribute and ValidatePasswordLenghtAttribute in which we should change CultureInfo.CurrentUICulture in FormatErrorMessage method to CultureInfo.CurrentCulture otherwise this will not work for our configuration."

      It probably relates to my first question as well. Googling more to understand differences between the two culture properties, but any information is appreciated.

      ReplyDelete
About Me
About Me
My Photo
We are a team of enthusiastic developers with strong believes that

"source code can be beautiful"

"software can be 100% bug free"

"perfectness is not a feature, it's mandatory requirement"

"things are correct only if they change the world to make it better"

"nothing is impossible"

"be successful is as easy as thinking you are"

e: info(at)simplytech.co
fb: www.facebook.com/alexander.adamyan
t: +(37455)520005
a: Proshyan 2/1, Yerevan, Armenia, 0010
Blog Archive
Labels
Loading