getting mvc3 to speak your langauge

Blog

July 12, 2012

By Michael Johnson in Programming

Handling multiple languages in an MVC3 application

We build lots of ASP.NET MVC3 applications, but recently we had a requirement for one to be available in both French and English. I didn’t find a solution from the interwebs that I liked, so here’s how I did it.

Before I get to the solution though, let’s talk about some of the constraints and rules we were working with. First, both language versions needed to be detectable and indexed by search engines. Second, I wanted to auto-detect the users language from their browser settings or let them choose - not using a flag. Third, I wanted my views and controllers to stay clean and readable.

My solution consists of first implementing a custom IRouteHandler, and extending the Controller class. Now you could probably put the following code in a different spot, but I’m using a the custom IRouteHandler anyway. Check it out:

public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
    var url = requestContext.RouteData.Values["url"] as string;

    url = url ?? "";

    var lang = requestContext.HttpContext.Request.UserLanguages[0];

    if (url.StartsWith("en/"))
        lang = "en-us";
    else if (url.StartsWith("fr/"))
        lang = "fr-ca";

    Thread.CurrentThread.CurrentUICulture = new CultureInfo(lang);
    ...

Now you’ll notice that I’m first checking the language coming from the user’s browser, which gets sent in the request scope, but overwriting it if they have the language specified in the URL. I then take that language, parse it into a CultureInfo and set it for the current thread. If I was using localized resource files, this would ensure that the correct one was selected. I’m not, but the CurrentUICulture is a safe place to keep it for use later. In the controller, I added a couple of extra view methods that use that culture information to choose a view. This method is a sibling to the ones you already use all the time that go at the end of the actions in your controller and look like: return View(); or return View(model);

public ViewResult CulturedView(object model)
{
    var lang = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
    var action = ControllerContext.RouteData.Values["action"] as string;

    if (!lang.Equals("en", StringComparison.OrdinalIgnoreCase))
    {
        var view = ViewEngines.Engines.FindView(ControllerContext, action + "-" + lang, null);

        if (view.View != null)
            return base.View(action + "-" + lang, model);
    }

    return base.View(action, model);
}

I’m assuming English is my base language and the ViewEngine will choose the regular view. But if the culture is set to something else, I have the ViewEngine check for a view specific to that language, and if it exists, use that instead of the default English one. Then, in my views directory, I can create a view for the Contact Us page named Contact.cshtml and a French version named Contact-fr.cshtml.

This is all great, so now if a user requests http://host/Contact or http://host/en/Contact, they get the same page, but if they request http://host/fr/Contact or http://host/Contact with their browser using a French language setting, they get the Contact-fr.cshtml view instead. All I did in my Controller was to add that one method (or a few similar ones depending on whether you're passing a model) and then change my action to return a CulturedView(model) rather than a View(model).

Now, only one more problem exists, and that is how do we handle internal links to keep the user on the same language version of the page? Well I solved that by adding an extension method the HtmlHelper, so instead of calling @Html.ActionLink(“Contact Us”, “Contact”), I use @Html.CulturedActionLink(“Contact Us”, “Contact”) which automatically puts the language into the URL in the appropriate spot, and sounds more sophisticated.

public static MvcHtmlString CulturedActionLink(this HtmlHelper helper, string linkText, string actionName)
{
    var link = helper.ActionLink(linkText, actionName).ToHtmlString();
    var lang = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;

    if (!lang.Equals("en", StringComparison.OrdinalIgnoreCase))
        link = link.Replace("href=\"", "href=\"/" + lang);

    return new MvcHtmlString(link);
}

So, hopefully this helps when creating your next localized MVC3 application. Let me know if you have any ideas on how to make this better.

** UPDATE **

The CulturedActionLink method I put above is all well and great, but I ran into the case where I wanted the link text to change along with the URL itself (obviously). So, I decide to add an additional parameter and pass an anonymous type that holds the link text for each available language. Now I call the method like this @Html.ActionLink("English Home Page", "Home", new { fr= "French Home Page", es= "Spanish Home Page" }). You could also choose to pull it from a localized resource file as well, rather than passing it in an anonymous type. Just use the linkText as the resource key and pull it from a resource file rather than using the linkText directly.

public static MvcHtmlString ActionLink(this HtmlHelper helper, string linkText, string actionName, object langText)
{
    var culturedText = linkText;

    var lang = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
    var prop = langText.GetType().GetProperty(lang);

    if (prop != null)
    {
        var newLink = prop.GetValue(langText, null) as string;

        if (!string.IsNullOrEmpty(newLink))
            culturedText = newLink;
    }

    var link = helper.ActionLink(culturedText, actionName).ToHtmlString();

    if (!lang.Equals("en", StringComparison.OrdinalIgnoreCase))
        link = link.Replace("href=\"", "href=\"/" + lang);

    return new MvcHtmlString(link);
}
In addition to our blog, we also send out an email newsletter. Subscribe to the newsletter and get notified whenever we have something wise to share, which is totally, like, all the time.

Comments

No comments available.