Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Azure, PHP, OpenXML, VSTS, ...

About the author

Maarten Balliauw is currently employed as .NET Technical Consultant at RealDolmen. His interests are mainly web applications developed in ASP.NET (C#) or PHP and the Windows Azure cloud platform.
More about me More about me
Send mail E-mail me


ASP.NET MVC Quickly Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
View Maarten Balliauw's MVP profile

Search

Latest Twitter

    Follow me on Twitter...

    My projects

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

    © Copyright Maarten Balliauw 2010

    Translating routes (ASP.NET MVC and Webforms)

    Localized route in ASP.NET MVC - Translated route in ASP.NET MVC For one of the first blog posts of the new year, I thought about doing something cool. And being someone working with ASP.NET MVC, I thought about a cool thing related to that: let’s do something with routes! Since System.Web.Routing is not limited to ASP.NET MVC, this post will also play nice with ASP.NET Webforms. But what’s the cool thing? How about… translating route values?

    Allow me to explain… I’m tired of seeing URLs like http://www.example.com/en/products and http://www.example.com/nl/products. Or something similar, with query parameters like “?culture=en-US”. Or even worse stuff. Wouldn’t it be nice to have http://www.example.com/products mapping to the English version of the site and http://www.exaple.com/producten mapping to the Dutch version? Better to remember when giving away a link to someone, better for SEO as well.

    Of course, we do want both URLs above to map to the ProductsController in our ASP.NET MVC application. We do not want to duplicate logic because of a language change, right? And what’s more: it’s not fun if this would mean having to switch from <%=Html.ActionLink(…)%> to something else because of this.

    Let’s see if we can leverage the routing engine in System.Web.Routing for this…

    Want the sample code? Check LocalizedRouteExample.zip (23.23 kb).

    Mapping a translated route

    First things first: here’s how I see a translated route being mapped in Global.asax.cs:

    routes.MapTranslatedRoute(
        "TranslatedRoute",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" },
        new { controller = translationProvider, action = translationProvider },
        true
    );

    Looks pretty much the same as you would normally map a route, right? There’s only one difference: the new { controller = translationProvider, action = translationProvider } line of code. This line of code basically tells the routing engine to use the object translationProvider as a provider which allows to translate a route value. In this case, the same translation provider will handle translating controller names and action names.

    Translation providers

    The translation provider being used can actually be anything, as long as it conforms to the following contract:

    public interface IRouteValueTranslationProvider
    {
        RouteValueTranslation TranslateToRouteValue(string translatedValue, CultureInfo culture);
        RouteValueTranslation TranslateToTranslatedValue(string routeValue, CultureInfo culture);
    }

    This contract provides 2 method definitions: one for mapping a translated value to a route value (like: mapping the Dutch “Thuis” to “Home”). The other method will do the opposite.

    TranslatedRoute

    The “core” of this solution is the TranslatedRoute class. It’s basically an overridden implementation of the System.Web.Routing.Route class, using the IRouteValueTranslationProvider for translating a route. As a bonus, it also tries to set the current thread culture to the CultureInfo detected based on the route being called. Note that this is just a reasonable guess, not the very truth. It will not detect nl-NL versus nl-BE, for example. Here’s the code:

    public class TranslatedRoute : Route
    {
        // ...

        public RouteValueDictionary RouteValueTranslationProviders { get; private set; }

        // ...

        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            RouteData routeData = base.GetRouteData(httpContext);

            // Translate route values
            foreach (KeyValuePair<string, object> pair in this.RouteValueTranslationProviders)
            {
                IRouteValueTranslationProvider translationProvider = pair.Value as IRouteValueTranslationProvider;
                if (translationProvider != null
                    && routeData.Values.ContainsKey(pair.Key))
                {
                    RouteValueTranslation translation = translationProvider.TranslateToRouteValue(
                        routeData.Values[pair.Key].ToString(),
                        CultureInfo.CurrentCulture);

                    routeData.Values[pair.Key] = translation.RouteValue;

                    // Store detected culture
                    if (routeData.DataTokens[DetectedCultureKey] == null)
                    {
                        routeData.DataTokens.Add(DetectedCultureKey, translation.Culture);
                    }

                    // Set detected culture
                    if (this.SetDetectedCulture)
                    {
                        System.Threading.Thread.CurrentThread.CurrentCulture = translation.Culture;
                        System.Threading.Thread.CurrentThread.CurrentUICulture = translation.Culture;
                    }
                }
            }

            return routeData;
        }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            RouteValueDictionary translatedValues = values;

            // Translate route values
            foreach (KeyValuePair<string, object> pair in this.RouteValueTranslationProviders)
            {
                IRouteValueTranslationProvider translationProvider = pair.Value as IRouteValueTranslationProvider;
                if (translationProvider != null
                    && translatedValues.ContainsKey(pair.Key))
                {
                    RouteValueTranslation translation =
                        translationProvider.TranslateToTranslatedValue(
                            translatedValues[pair.Key].ToString(), CultureInfo.CurrentCulture);

                    translatedValues[pair.Key] = translation.TranslatedValue;
                }
            }

            return base.GetVirtualPath(requestContext, translatedValues);
        }
    }

    The GetRouteData finds a corresponding route translation if I entered “/Thuis/Over” in the URL. The GetVirtualPath method does the opposite, and will be used for mapping a call to <%=Html.ActionLink(“About”, “About”, “Home”)%> to a route like “/Thuis/Over” if the current thread culture is nl-NL. This is not rocket science, it simply tries to translate every token in the requested path and update the route data with it so the ASP.NET MVC subsystem will know that “Thuis” maps to HomeController.

    Tying everything together

    We already tied the route definition in Global.asax.cs earlier in this blog post, but let’s do it again with a sample DictionaryRouteValueTranslationProvider that will be used for translating routes. This one goes in Global.asax.cs:

    public static void RegisterRoutes(RouteCollection routes)
    {
        CultureInfo cultureEN = CultureInfo.GetCultureInfo("en-US");
        CultureInfo cultureNL = CultureInfo.GetCultureInfo("nl-NL");
        CultureInfo cultureFR = CultureInfo.GetCultureInfo("fr-FR");

        DictionaryRouteValueTranslationProvider translationProvider = new DictionaryRouteValueTranslationProvider(
            new List<RouteValueTranslation> {
                new RouteValueTranslation(cultureEN, "Home", "Home"),
                new RouteValueTranslation(cultureEN, "About", "About"),
                new RouteValueTranslation(cultureNL, "Home", "Thuis"),
                new RouteValueTranslation(cultureNL, "About", "Over"),
                new RouteValueTranslation(cultureFR, "Home", "Demarrer"),
                new RouteValueTranslation(cultureFR, "About", "Infos")
            }
        );

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

        routes.MapTranslatedRoute(
            "TranslatedRoute",
            "{controller}/{action}/{id}",
            new { controller = "Home", action = "Index", id = "" },
            new { controller = translationProvider, action = translationProvider },
            true
        );

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

    }

    This is basically it! What I can now do is set the current thread’s culture to, let’s say fr-FR, and all action links generated by ASP.NET MVC will be using French. Easy? Yes! Cool? Yes!

    Localizing ASP.NET MVC routing

    Want the sample code? Check LocalizedRouteExample.zip (23.23 kb).

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | General | MVC | Projects

    Comments

    DotNetKicks.com | Reply

    Tuesday, January 26, 2010 7:59 AM

    trackback

    Translating routes (ASP.NET MVC and Webforms)

    You've been kicked (a good thing) - Trackback from DotNetKicks.com

    Tom Deleu Belgium | Reply

    Tuesday, January 26, 2010 9:28 AM

    Tom Deleu

    Aha, now THAT is a useful article. Been looking for such a functionality some weeks ago Smile

    maartenba Belgium | Reply

    Tuesday, January 26, 2010 9:50 AM

    maartenba

    Not sure, URL's that have values in language A will be recognised by search engines as language A, and language B will be recognised as language B. Do you have any resources on how this would impact SEO?

    Martin H. Normark Denmark | Reply

    Tuesday, January 26, 2010 4:58 PM

    Martin H. Normark

    I know that you can use Google Webmaster Tools, to control what language en subfolder will target. Also, be sure to set the meta tag: <meta http-equiv="Content-Language" content="nl" />...

    You could also have a subdomain per language - this will make you able to have a local server for each language. Which is known to give you better results in search engines.

    There's a few resources here: www.google.com/search

    I got most of my knowledge on the topic from a well respected SEO consultant in Denmark, Thomas Rosenstand.

    Tom Janssens Belgium | Reply

    Tuesday, January 26, 2010 10:02 AM

    Tom Janssens

    Oops, I have been looking at the source code, and apparently you are not using an intermediate controller but you add new route handlers.. However, kudos for the inspiration, and it still is a great implementation...

    Arnis L. Latvia | Reply

    Tuesday, January 26, 2010 11:49 AM

    Arnis L.

    Read this right after reading post by K. Scott (odetocode.com/.../...our-asp-net-mvc-routes.aspx).

    Anyway - this is inspiring and cool. But still - wondering if there are any more caveats (e.g. - SEO related).

    Augi Czech Republic | Reply

    Tuesday, January 26, 2010 1:37 PM

    Augi

    Hehe, I blogged about url localization in ASP.NET MVC the last weekend (proof of concept only): www.augi.cz/.../

    Your code is very nice and usable. I have only two notes:
    1) I would use "Convert.ToString(routeData.Values[pair.Key])" instead of "routeData.Values[pair.Key].ToString()".
    2) You are setting current culture by changing culture of current thread. It's noticable that this will not work with asynchronous controller (because different thread could be used for the second phase - it would require thread culture re-setting).

    maartenba Belgium | Reply

    Tuesday, January 26, 2010 2:10 PM

    maartenba

    Good tips, thanks!

    Tony Testa United States | Reply

    Thursday, January 28, 2010 7:09 PM

    Tony Testa

    How does this work in a hosted environment where you don't have access to the underlying IIS?  Do you still have issues with routes and not having the .aspx situation?

    maartenba Belgium | Reply

    Thursday, January 28, 2010 7:21 PM

    maartenba

    It's just like any other route, so if normal routing works you should be fine with this solution.

    Add comment




      Country flag

    biuquote
    • Comment
    • Preview
    Loading