Routing requests to actions within controllers is the backbone of any ASP.NET MVC web application. Being able to define different routes by using static words, such as “Products” or “Search”, enables search engine optimized and user friendly URLs. However, recently I was asked to localize those static words to maintain consistency in the way URLs are presented in different languages (meaning that you wouldn’t have terms from different languages forced into the URL). I started by using a Code Project article to approach how the localization would occur. The requirements for my implementation specified that multiple routes needed to have localized terms. Therefore, I created an interface that allowed me to implement specific ways to localize individual terms and include them all in URL route localization.
public interface IRouteTranslation { string RouteTerm { get; } //Used by GetRouteData to convert virtual path to match the route in the routeconfig.cs file string DeLocalizePath(string currentPath); //Used by GetVirtualPath to localize specific term in Url VirtualPathData LocalizePath(VirtualPathData data, string locale); }
The implementation of the interface contains all of the information needed to make the substitution of localized terms.
public class ProductTranslation : IRouteTranslation { private string _rteTerm; public string RouteTerm { get { return this._rteTerm; } } public ProductTranslation() { this._rteTerm = "product"; } public string DeLocalizePath(string currentPath) { try { //Get Locale string locale = currentPath.Substring(2, currentPath.IndexOf('/', 2) - 2); return currentPath.Replace(GetLocalizedTerm(locale), this._rteTerm); } catch { return currentPath; } } public VirtualPathData LocalizePath(VirtualPathData data, string locale) { try { data.VirtualPath = data.VirtualPath.Replace(this._rteTerm, GetLocalizedTerm(locale)); } catch { } return data; } //Get localized term that will replace the specified route term private string GetLocalizedTerm(string locale) { return Data.ProductLocalizationDictionary[locale.ToLower()]; } }
For purposes of this blog post I created a simple way to store and get the localized terms in a simple dictionary.
public static class Data { public static Dictionary<string, string> ProductLocalizationDictionary { get { Dictionary<string, string> dict= new Dictionary<string, string>(); dict.Add("en-us", "product"); dict.Add("es-es", "producto"); dict.Add("fr-fr", "produit"); dict.Add("de-de", "produkt"); return dict; } } }
The interface is used within a TranslatedRoute class that extends the Route class. The TranslatedRoute class contains a List of IRouteTranslation so that one could specify multiple route terms to translate within a single route.
public class TranslatedRoute : Route { public IList<IRouteTranslation> Translations { get; private set; } public TranslatedRoute(string url, RouteValueDictionary values, IList<IRouteTranslation> translations, IRouteHandler handler) : base(url, values, handler) { this.Translations = translations; } public TranslatedRoute(string url, RouteValueDictionary values, IList<IRouteTranslation> translations, RouteValueDictionary constraints, IRouteHandler handler) : base(url, values, constraints, handler) { this.Translations = translations; } //Maps Route to Controller and Action public override RouteData GetRouteData(HttpContextBase httpContext) { string currentPath = httpContext.Request.AppRelativeCurrentExecutionFilePath; foreach (IRouteTranslation translation in this.Translations) currentPath = translation.DeLocalizePath(currentPath); httpContext.RewritePath(currentPath); return base.GetRouteData(httpContext); } //Builds Url for Route public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { VirtualPathData data = base.GetVirtualPath(requestContext, values); if (data != null) { foreach(IRouteTranslation translation in this.Translations) data = translation.LocalizePath(data, values["locale"].ToString()); } return data; } }
The TranslatedRoutes are registered by creating an extension for the RouteCollection that allows for a TranslatedRoute to be added with the specified Translation.
public static class TranslatedRouteCollectionExtensions { public static TranslatedRoute MapTranslatedRoute(this RouteCollection routes, string name, string url, object defaults, IList<IRouteTranslation> translations) { TranslatedRoute route = new TranslatedRoute( url, new RouteValueDictionary(defaults), translations, new MvcRouteHandler()); routes.Add(name, route); return route; } public static TranslatedRoute MapTranslatedRoute(this RouteCollection routes, string name, string url, object defaults, IList<IRouteTranslation> translations, object constraints) { TranslatedRoute route = new TranslatedRoute( url, new RouteValueDictionary(defaults), translations, new RouteValueDictionary(constraints), new MvcRouteHandler()); routes.Add(name, route); return route; } }
From there, you just need to add a route to the global routing in the Global.asax file either directly within the Global.asax file or the RouteConfig.cs file.
routes.MapTranslatedRoute( name: "LocalizedRoute", url: "{locale}/product", defaults: new { controller = "Home", action = "Index" }, translations: new List<IRouteTranslation> { new ProductTranslation() } );
After the routes have been registered, the route will localize the word that is specified in the translations section of the MapTranslatedRoute function.