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
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