Automatically Including Current Language In Generated URLs With ASP.NET MVC

4 commentsWritten on April 6th, 2011 by
Categories: ASP.NET MVC

When you're pushing out localized content to your users, you don't want to mess up any possible output caching you've got set up (or would want to set up later on). One common approach to deal with this is to always include the relevant language code as a route parameter in your URLs. It works great with output caching because each localized version of the content will be accessible through its own URL, and as an extra benefit, your content is indexable by search engines in every language you support as well.

The only downside to that approach is that you absolutely have to make sure that the correct language code is always included in each URL you put on your pages. That's tedious work at best, error-prone at worst. Ideally, each URL that you generate on your pages automatically has the current language code included in it. And obviously, you want to be able to provide it explicitly as well (for language selection links for example). It took me a while to figure out how this can be done with ASP.NET MVC but i did manage to find a pretty nice solution.

I was browsing the MVC source code (see how useful this whole open source thing is?) to look for some kind of hook i could use to influence how URLs are generated when you use Url.Action or Html.ActionLink in your views. And it turns out that there is one, though it's not really an obvious one. Whenever you use Url.Action or Html.ActionLink, ASP.NET MVC calls the GetVirtualPath method for each defined Route object and the first returned VirtualPathData instance is the one that will provide the final URL that is rendered in your links. So we first need to come up with our own custom Route class:

    public class AutoLocalizingRoute : Route
    {
        public AutoLocalizingRoute(string url, object defaults, object constraints)
            : base(url, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new MvcRouteHandler()) { }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            // only set the culture if it's not present in the values dictionary yet
            // this check ensures that we can link to a specific language when we need to (fe: when picking your language)
            if (!values.ContainsKey("language"))
            {
                values["language"] = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
            }

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

Now we have to make sure that we define a route of this type before our normal routes are defined:

            var localizingRoute = new AutoLocalizingRoute("{language}/{controller}/{action}/{id}",
                new { id = UrlParameter.Optional }, new { language = "^[a-z]{2}$" });
            RouteTable.Routes.Add("LocalizingRoute", localizingRoute);

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

This ensures that our AutoLocalizingRoute instance will get a chance to provide a VirtualPathData instance whenever an action-URL is needed, before the standard Route instance is called to create one.

Now, all we need is something that sets the current thread's Culture and UICulture property based on the language code in the URL of each request. I did this with a HttpModule, of which only this part is relevant here:

        private void OnBeginRequest(object sender, EventArgs e)
        {
            var currentContext = new HttpContextWrapper(HttpContext.Current);
            var routeData = RouteTable.Routes.GetRouteData(currentContext);
            if (routeData == null || routeData.Values.Count == 0) return;

            if (routeData.Values["language"] == null)
            {
                RedirectToUrlWithAppropriateLanguage(currentContext, routeData);
            }

            var languageCode = (string)routeData.Values["language"];
            Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(languageCode);
            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(languageCode);
        }

And that's it. Every time we generate an URL to a Controller Action, the current language code will be included automagically, so there's no chance of us forgetting it somewhere.

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #829

  • efdee

    I could be wrong, but doesn’t MVC handle this automatically already (or at least to some extent) ?

    If the route for the current request is {language}/foo and you’re linking to a route {language}/bar, the current value for language is used if you don’t specify a new value when creating the link.

    • http://davybrion.com Davy Brion

      huh, now this is weird… i just changed the localization route to use a regular Route instance instead of an AutoLocalizingRoute instance and you’re right: it just works

      the weird part is that it didn’t work like that when i first tried it, which led me to look for this solution in the first place. I guess i must’ve done something stupid when i first tried it :s

  • Anonymous

    This programming is great working with achievement caching because all localized adaptation of the agreeable will be attainable through its own URL.