One of the web applications we develop has support for multiple languages, and we got the request to have also the route names translated. As such, a user visiting the site in english and navigating to the stores page should be redirected to example.com/stores, whereas a spanish customer should see example.com/comercios in the URL bar.
The main challenge we faced was how to update every ActionLink invocation to reflect the current user's language. This would require a huge amount of refactoring throughout the application. Luckily, the ActionLink method (as well as Url.Action and any other route-related method) make use of not only the RouteCollection defined in the MvcApplication, but also of their defined constraints.
For those of you not familiar with them, constraints are additional conditions evaluated when the engine determines the current route based on the URL. They can be plain strings, representing regex patterns:
routes.MapRoute( "Product", "Product/{productId}", new {controller="Product", action="Details"}, new {productId = @"d+" } );
Or even custom classes implementing IRouteConstraint. This allows you to specify any condition to match a specific route, such as the current language:
public class LanguageRouteConstraint : IRouteConstraint { public string Language { get; set; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return CurrentLanguage == this.Language; } }
(you may use whatever method you please to check the current language, such as checking the thread's CurrentUICulture; you may also want to implement a slightly better way to compare different cultures, such as one supporting fallbacks to neutral cultures.)
And now we can use this class to constraint the routes for different languages when building the route collection in the RegisterRoutes method:
routes.MapRoute("stores-en", "stores", new { controller = "Stores", action = "Index" }, new { lang = new LanguageRouteConstraint { Language = "en" }}); routes.MapRoute("stores-es", "comercios", new { controller = "Stores", action = "Index" }, new { lang = new LanguageRouteConstraint { Language = "es" }});
Why is this useful? So far, what we have only achieved seems to be forbidding a spanish user from accessing the /stores route.
But as we said, all MVC methods that resolve the URL from the action/controller pair, such as Url.Action, make use of these constraints as well. Therefore, when rendering the page for a spanish user, an invocation to:
Url.Action("Index", "Stores")
Will yield /comercios as we wanted, without needing to modify all of our aspx code to adapt to the new requirement.
Note that if you want to keep english (neutral) names accessible for all languages, you simply have to add them at the bottom without any constraint:
routes.MapRoute("stores-es", "comercios", new { controller = "Stores", action = "Index" }, new { lang = new LanguageRouteConstraint { Language = "es" }}); routes.MapRoute("stores", "stores", new { controller = "Stores", action = "Index" });
Since the route lookup is done in order, returning the first one that matches, a spanish user will first match the first route, correctly returning /comercios, whereas any user will match the last one. This will allow a spanish user navigating to /stores to actually view the page.
After a while, with some helper methods for more declarative code, we can end up writing our routing configuration like this:
routes.MapInternationalizedRoute("stores", new { controller = "Stores", action = "Index" }) .MapLanguage("comercios", "es") .MapLanguage("magasines", "fr") .Default("stores")