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)
Hi Alex,
ReplyDeleteThank 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
Hi Alex, Great work!
ReplyDeleteHow 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
Hi Marco,
ReplyDeleteGood 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
Hi Alex, I have the same problem, can you explain step by step that "solution"? I don't understand how to do that.
DeleteThanks
Alex,
ReplyDeleteA 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
This comment has been removed by the author.
ReplyDeleteHi Alex, thank for the answer.
ReplyDeleteJust 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
Looks familiar... http://blog.jeroenverhulst.be/post/2010/08/14/Extending-MvcRouteHandler-to-enable-localization-of-an-ASPNET-MVC2-application.aspx
ReplyDeleteHi Alex,
ReplyDeleteVery 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
Hi Alex,
ReplyDeleteThan you for the post. Could you validate link to the source code? It looks like not functional anymore.
Thank you.
Sean
Great post.
ReplyDeleteYep, looks like link to source code is no longer working.
Thnx , very helpful article.
ReplyDeleteThe download link is doesnt work may you reupload please.
Namik SHehu
Thnx , very helpful article.
ReplyDeleteThe download link is doesnt work may you reupload please.
Antonio
i would like to download the code too but the link is broken. Can it be fixed please?
ReplyDeletethank you!
it's works great tutorial thanks for share!
ReplyDeletehey 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
ReplyDeletegreat! but the link following to download sourcecode don't work...please fix it so i can download the sourcecode
ReplyDeleteThanks, great article.
ReplyDeleteBut 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.
Thanks¡ This solutions looks great, can you update the link please to download the project? It seems broken.
ReplyDeleteThanks¡ This solutions looks great, can you update the link please to download the project? It seems broken.
ReplyDeleteHi, as say Alessandro, can you reupdate the broken link?
ReplyDeleteThanks Alex.
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.
ReplyDeleteThank you to everyone for following and reading my posts.
Any update on this statement? Can we find an example out on github yet?
Deletesource code plz...
ReplyDeleteHi,
ReplyDeleteThis 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.
Hi Frank,
DeleteYes your skydrive link works.
Thanks for your code.
roger
This comment has been removed by the author.
DeleteHi Frank, thanks.
DeleteThanks Alex very much.
thanx for the code
DeleteChange the route to "{culture}/{controller}/{action}/{id}"
ReplyDeleteand 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
Thanks for the great article and solution,
ReplyDeleteI 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
I have raised a question on stack overflow
ReplyDeletehttp://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
hello,
ReplyDeletelooks like source code is not available at any place.
Hi Alex, thanks for the great article.
ReplyDeleteI 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
Project Link dosent work you can update link
ReplyDeleteHi, 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/
ReplyDeleteIt's especially useful for collaborative work and crowdsourcing translations.
Beginner C#, jQuery, SQL Server, ASP.NET MVC, LINQ and much more..
ReplyDeleteBeginner Programming Tutorials
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!
ReplyDeleteAlex,
ReplyDeleteThank very much. Good work
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.
ReplyDeleteLink is broken for source code(project created in VS2010)
ReplyDeleteGreat Article
ReplyDeleteASP.NET MVC Training
MVC Online Training
Online MVC Training India
Dot Net Training in Chennai