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


  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

    1. Hi Alex,

      Thank you for the second article for this localization series.

      This approach is much better than the first one. Because now the Search Engines can index the pages by language. And there is no need to do tricky things for output cache.

      Thanks again

      ReplyDelete
    2. Hi Alex, Great work!

      How do you solve the problem with loginUrl in web.config?

      ASP MVC uses LoginUrl:
      1) When a User is not autenticated
      2) when you use AuthorizeAttribute in Controllers and user has enough user rights.

      I writed a SingleCulture Action /Account/LogOnNeutral that redirect to the Culture Dependent url {culture}/Account/LogOn, but I needed the last valid culture that I stored in session.





      Thanks
      Marco

      ReplyDelete
    3. Hi Marco,
      Good question. I didn't find any solution for setting automatically culture in loginUrl attribute.
      But I can suggest you to take current culture from param returnUrl in your LogOnNeutral action and set it to RouteData.Values["culture"] and redirect again to the culture dependent action RedirectToAction("LogOn"),

      This works for me, hope it'll work for you

      ReplyDelete
      Replies
      1. Hi Alex, I have the same problem, can you explain step by step that "solution"? I don't understand how to do that.
        Thanks

        Delete
    4. Alex,
      A great pair of posts here. I have implemented things just as you laid out but I am having a problem. Noting to do with your code. It is working great. I am trying to localize for Tagalog (tl), which is not a built in culture in windows. So have created it using the CultureAndRegionInfoBuilder class. I have created .resx and .de.resx and .tl.resx files. When I go to http://localhost:1907/de I get german resources. When I go to http//localhost/tl I get the default (english) resources.

      Any thoughts? I have a more complete explanation on StackOverflow (http://stackoverflow.com/questions/3743801), but its not getting any bites.

      Any ideas?

      --Ken

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

      ReplyDelete
    6. Hi Alex, thank for the answer.
      Just another question. What about culture switching after a Post that have failed validation?

      The problem was this.Request.RawUrl in CultureSwitchControl.ascx.
      If you come from a post that have failed validation, typically in your Controller you return the View. In the view Request.RawUrl correctly has only the parameter ?Length=XX, while the last GET Url can have several parameters.
      For example:
      GET Url: /en/Account/Edit/999
      After POST Validation Failed Url: /en/Account/Edit?Length=YY.
      You cannot use Request.UrlReferrer (if mehod is POST) because it is correct only the first time you fails validation.

      Marco

      ReplyDelete
    7. Looks familiar... http://blog.jeroenverhulst.be/post/2010/08/14/Extending-MvcRouteHandler-to-enable-localization-of-an-ASPNET-MVC2-application.aspx

      ReplyDelete
    8. Hi Alex,
      Very helpful post. I have one question. If I want to rewrite an Url, for example www.host.com/section/id to call the "section" action in the "home" controller how would I write the MapRoute? Normally it would be something like this:
      routes.MapRoute("Name","Section/{id}", new {controller= "Home", action = "Section", id=UrlParameter.Optional});. But this only works for SingleCulture routes. Is there a solution for MultiCulture? For ex: www.host.com/en/section/id to call the "section" action in the "home" controller.
      Thanks,
      Dinu

      ReplyDelete
    9. Hi Alex,
      Than you for the post. Could you validate link to the source code? It looks like not functional anymore.
      Thank you.

      Sean

      ReplyDelete
    10. Great post.

      Yep, looks like link to source code is no longer working.

      ReplyDelete
    11. Thnx , very helpful article.
      The download link is doesnt work may you reupload please.
      Namik SHehu

      ReplyDelete
    12. Thnx , very helpful article.
      The download link is doesnt work may you reupload please.
      Antonio

      ReplyDelete
    13. i would like to download the code too but the link is broken. Can it be fixed please?

      thank you!

      ReplyDelete
    14. it's works great tutorial thanks for share!

      ReplyDelete
    15. hey dude this is really interesing blog and its described routes information and how its works and please continue posted this type of another blog.great tutorial and also improving my knowledge and thanks for sharing information

      ReplyDelete
    16. great! but the link following to download sourcecode don't work...please fix it so i can download the sourcecode

      ReplyDelete
    17. Thanks, great article.
      But I found that this approach doesn't work properly with ignored pathes, i.e.

      routes.IgnoreRoute("{file}.html");
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      IgnoreRoute doesn't return mapped route. Need a workaround here.

      ReplyDelete
    18. Thanks¡ This solutions looks great, can you update the link please to download the project? It seems broken.

      ReplyDelete
    19. Thanks¡ This solutions looks great, can you update the link please to download the project? It seems broken.

      ReplyDelete
    20. Hi, as say Alessandro, can you reupdate the broken link?

      Thanks Alex.

      ReplyDelete
    21. Sorry guys, I was quite busy for a long time... But there are good news :) I'm currently working on new blog post covering localization of ASP.NET MVC 4 and the source code will be hosted on github.com.

      Thank you to everyone for following and reading my posts.

      ReplyDelete
      Replies
      1. Any update on this statement? Can we find an example out on github yet?

        Delete
    22. Hi,

      This article was of great use. Thanx a lot Alex.
      Today i tried it with mvc 4 razor and it worked like a charm.

      So I wanted to share it to you guys : http://sdrv.ms/UxN9lD
      Hope this skydrive link works.

      ReplyDelete
      Replies
      1. Hi Frank,

        Yes your skydrive link works.
        Thanks for your code.

        roger

        Delete
      2. This comment has been removed by the author.

        Delete
      3. Hi Frank, thanks.
        Thanks Alex very much.

        Delete
    23. Change the route to "{culture}/{controller}/{action}/{id}"
      and you don't need all those code to dynamically change the route.Only following code will be required
      public static void RegisterRoutes(RouteCollection routes)
      {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapRoute(
      "Default", // Route name
      "{culture}/{controller}/{action}/{id}", // URL with parameters
      new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
      ).RouteHandler=new MyRouteHandler();

      }

      and

      public class MyRouteHandler:MvcRouteHandler
      {
      protected override IHttpHandler GetHttpHandler(System.Web.Routing.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);
      }
      }

      You may have constraint though

      ReplyDelete
    24. Thanks for the great article and solution,
      I have tried the previous approach with session and it work fine, but when I have applied the routing approach every page is not found

      I have errors when running the app, it says that The resource cannot be found.

      can you help please

      ReplyDelete
    25. I have raised a question on stack overflow
      http://stackoverflow.com/questions/18674859/routing-to-webapi-with-localization

      I am trying to route to a WebApi controller and the culture code is being added before the hyperlink. I have tried using the SingleCultureRouteHandeler and it is not removing the culture code. I would great appreciate any help.

      Other than that thanks for the great posts on Globalization/Localization your solution works great
      -Tim

      ReplyDelete
    26. hello,
      looks like source code is not available at any place.

      ReplyDelete
    27. Hi Alex, thanks for the great article.
      I have a problem here.
      if I go to address mydomain.com/en/home/index or mydomain.com/fa/home/index there is no problem. but if i go to mydomain.com/en/ or mydomain.com/fa/ it cannot find the page.
      please help me

      ReplyDelete
    28. Project Link dosent work you can update link

      ReplyDelete
    29. Hi, Alex! If you're interested in a good localization tool to help you manage software localization projects, I recommend you this software translation app: https://poeditor.com/

      It's especially useful for collaborative work and crowdsourcing translations.

      ReplyDelete
    30. Beginner C#, jQuery, SQL Server, ASP.NET MVC, LINQ and much more..

      Beginner Programming Tutorials

      ReplyDelete
    31. Alex, I would also like to suggest a website where people interested in translation and localization can read the latest news in these areas, thought it might be helpful. http://l10nhub.com/ Thanks!

      ReplyDelete
    32. Thank you for the solution, I am trying to adapt it to my needs but I have a hard time. Can you help me please with http://stackoverflow.com/questions/33468136/why-am-i-receiving-the-exception-a-public-action-method-was-not-found-on-contro? Thanks.

      ReplyDelete
    33. Link is broken for source code(project created in VS2010)

      ReplyDelete

  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

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