Simple ASP.Net MVC Globalization with Graceful Fallback

We recently began work on a website project using ASP.Net MVC 3.0 where one of the requirements was that the site will be globalized into different languages. However, one of the design considerations is that the content is going to be originally produced in English and then translated and published as available into the other supported languages. What the site needs to do is gracefully degrade to English when the desired globalized content is not available.

Other issues are that some languages are wordier or  more compact than English which can affect the graphic design. For example “Structured Investment Vehicles” (30 characters) translates in French to “Véhicules d’Investissement Structurés” (37 characters). The French version requires 23% characters which is—roughly speaking (because of proportional fonts)—about 20% more space than the English version. It’s pretty typical for French phrases to contain 20-30% more characters than the English equivalent which means that resource file strings and just swapping out text can ruin the visual design layout. What is needed is re-layout of parts of views to accommodate the space consumed by different languages.

The idea is that we have English as an invariant which has every view the site uses in the normal place. We then have a /Views/Globalization directory which contains a subdirectory with the ISO 639-1 two-letter macro-language code ( ar = Arabic, fr = French, es = Spanish, etc.) which contain whatever translated content is available. If the browser requests a particular non-English language translation, we want to views to come from the appropriate Globalization directory  but if the content doesn’t exist, we fall back to English.

Our solution is to use views, partial views and master pages as the unit of globalization. In order to do this we created a new IViewEngine which extends whatever view engine we would otherwise be using. Our team is comfortable with WebForms so we extended WebFormViewEngine.

Example View Layout

/Views
    /Globalization
        /ar
            /Home
                /Index.aspx
            /Shared
                /Site.master
                /Navigation.aspx
        /es
            /Home
                /Index.aspx
            /Shared
                /Navigation.aspx
        /fr
            /Home
                /Index.aspx
            /Shared
    /Home
        /Index.aspx
    /Shared
        /Error.aspx
        /Footer.aspx
        /Navigation.aspx
        /Site.master

In the sample above, French is not fully globalized so when French views are served, the Navigation.ascx partial view should be served from /Views/Shared/Navigation.ascx, the Arabic views declare themselves as using the /Views/Globalization/ar/Shared/Site.master rather than /Views/Shared/Site.master and the default English Error.aspx view is always served.

jQuery Script to Switch Languages

/// <reference path="jquery-1.4.4.js" />
/// <reference path="jquery.cookie.js" />

//declare namespace to avoid collisions with extension js in the browser
var mySiteNamespace = {};

mySiteNamespace.switchLanguage = function (lang) {
    $.cookie('language', lang);
    window.location.reload();
};

$(document).ready(function () {
    // attach mySiteNamespace.switchLanguage to click events based on css classes
    $('.lang-english').click(function () { mySiteNamespace.switchLanguage('en'); });
    $('.lang-french').click(function () { mySiteNamespace.switchLanguage('fr'); });
    $('.lang-arabic').click(function () { mySiteNamespace.switchLanguage('ar'); });
    $('.lang-spanish').click(function () { mySiteNamespace.switchLanguage('es'); });
});

This little JavaScript attaches a switchLanguage function to elements (such as <a/>) which declare the specified css classes. It sets a language cookie which our custom IViewEngine is looking for.

Extending WebFormViewEngine

The key methods to override when extending WebFormViewEngine are CreatePartialView and CreateView. What we need to do is detect what language the browser is requesting and then choose the correct globalized view if it exists and fall back to the default behavior if it doesn’t. For simplicity, I’m showing code that uses a cookie to determine what language the end user wants but this detection can be as sophisticated as you like.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Text.RegularExpressions;
using System.IO;

namespace System.Web.Mvc
{
    public sealed class WebFormGlobalizationViewEngine : WebFormViewEngine
    {
        protected override IView CreatePartialView( ControllerContext controllerContext, string partialPath )
        {
            partialPath = GlobalizeViewPath( controllerContext, partialPath );
            return new WebFormView( controllerContext, partialPath, null, ViewPageActivator );
        }

        protected override IView CreateView( ControllerContext controllerContext, string viewPath, string masterPath )
        {
            viewPath = GlobalizeViewPath( controllerContext, viewPath );
            return base.CreateView( controllerContext, viewPath, masterPath );
        }

        private static string GlobalizeViewPath( ControllerContext controllerContext, string viewPath )
        {
            var request = controllerContext.HttpContext.Request;
            var lang = request.Cookies["language"];
            if( lang != null &&
                !string.IsNullOrEmpty(lang.Value) &&
                !string.Equals( lang.Value, "en", StringComparison.InvariantCultureIgnoreCase ) )
            {
                string localizedViewPath = Regex.Replace(
                    viewPath,
                    "^~/Views/",
                    string.Format( "~/Views/Globalization/{0}/",
                    lang.Value
                    ) );
                if( File.Exists( request.MapPath( localizedViewPath ) ) )
                { viewPath = localizedViewPath; }
            }
            return viewPath;
        }
    }
}

Registration in Global.asax.cs

The final step is to register WebFormsGlobalizationViewEngine when the Application.Start event fires.

protected void Application_Start()
{

    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add( new WebFormGlobalizationViewEngine() );

    // rest of application start stuff
}

Summing Up

There are a lot of ways to globalize content. This solution has the advantage of being straightforward and flexible. The main disadvantage is that we have to maintain translations of all the views and partial views.

About these ads

22 Responses to Simple ASP.Net MVC Globalization with Graceful Fallback

  1. Pingback: Scott Hanselman

  2. Pingback: Web and Cloud

  3. Pingback: ASP.NET and Silverlight

  4. Pingback: Microsoft MVC bloggers

  5. Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 - Bilim-Teknoloji | Positive Pozitive.NeT

  6. Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 « Chandara

  7. Ahmed Galal says:

    Great article, i like the way of doing globalization this way, however, CreateView and CreatePartialView in the implemented ViewEngine doesnt get fired, FindView is getting fired and after am getting an exception as it couldnt find the view.

    Did i miss anything ?

    • Ahmed Galal says:

      my mistake, since am using the RazorViewEngine, so i had to implement its engine and not WebFormViewEngine, it would be nice if you calrified that on your article,

      great article tho

  8. Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 - Technology John

  9. Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 | 哈哈808,开心呵呵网

  10. Arnaud says:

    It’s exactly what i want to do to globalize my application. The only difference is i use Razor view engine. So i transposed your code to Razor but at runtime i have the following error :

    “The name ‘ViewBag’ does not exist in the current context ” if i’m in a globalized view. It does not append if i’m in the normal view.

    My structure is :
    /Views
    /Home
    /index
    /LocalizedViews
    /uk
    /Home
    /Index
    /en
    /Home …

    and my code :
    public class LocalizedRazorViewEngine : RazorViewEngine
    {

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
    partialPath = LocalizeViewPath(controllerContext, partialPath);
    return new RazorView(controllerContext, partialPath,
    layoutPath: null, runViewStartPages: false, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator);
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
    viewPath = LocalizeViewPath(controllerContext, viewPath);
    return base.CreateView(controllerContext, viewPath, masterPath);
    }

    private static string LocalizeViewPath(ControllerContext controllerContext, string viewPath)
    {
    var request = controllerContext.HttpContext.Request;
    string localizedViewPath = Regex.Replace(viewPath, “^~/Views/”, string.Format(“~/LocalizedViews/{0}/”, CultureUtility.CurrentTwoLetter));

    if (File.Exists(request.MapPath(localizedViewPath)))
    return localizedViewPath;
    else
    return viewPath;
    }
    }

    CultureUtility.CurrentTwoLetter is a static class that embed a call to system.threading…currentuiculture…

    Do you see why i can’t access Viewbag in my localized view ?

  11. Arnaud says:

    I found the problem : you must have a web.config in LocalizedViews folder, the same than in the Veiws folder.

  12. George says:

    Brian,
    What is your scheme for the content folders?
    I have made this work for Razor, and Areas, and would like to see what your code would look like when dealing with areas.

    Thanks

  13. JR says:

    Hi,

    I’m new to MVC so forgive what is probably a stupid question, but I don’t know where\how to use the code to extend WebFormViewEngine?

    Also wondering how best to incorporate device detection to this archtitecture, ie, have a seperate set of views for each language for different devices.

    Thanks

    JR

    • George says:

      http://stackoverflow.com/q/4246783

      I used this guys version for my Razor one. I don’t have my exact version right now but if you like I can send you the entire file Monday. If you pull it off, I killed off all the webform stuff and went 100% to razor.
      Another note, these view engines make it easy to adapt to your organizational style. In this article things are organized in folders. Others use a naming convention like. View.en.cshtml. I am currently using a naming convention that also accounts for devices. i.e. View.Mobile.En.cshtml, and falls back to View.En.Cshtml if you are mobile and an exact view doesn’t exist.

  14. JR says:

    Update to say that I’ve figured out how extend WebFormViewEngine. (ie just create a new class)

    Also to say that I found the language was reverting back to english because the cookie was specifically being set for each controller. To fix this I changed the javascript as follows;

    from
    $.cookie(‘language’, lang);

    to
    $.cookie(‘language’, lang, {path: ‘/’});

    … now, what suggestions do we have for ensuring google et al can find & index the translated content?

    ..also any thoughts on adding specific views for specific devices (eg iPad, mobiles etc)?

    Thanks

    JR

  15. Pingback: Scott Hanselman的中文博客

  16. Ankit Singh says:

    Very informative post. It’s really helpful for me and helped me lot to complete my task.

    Thanks for sharing with us. I had found another nice post over the internet which was

    also explained very well about ASP.Net Globalization, for

    more details of this post check out this link…

    http://mindstick.com/Articles/d4f03931-09c1-4546-9bb4-b2fa7945a2ab/?ASP.Net%20Globalization

    Thanks

  17. Pingback: Internacionalización y Globalización de aplicaciones web | Pensando bajo la lluvia

  18. Pingback: Localising MVC Views using Display Modes | greatrexpectations

  19. Pingback: Martijn Muurman's Developers Blog | Globalizing MVC views using cshtml per locale

  20. Erhan Atesoglu says:

    Just wanted to say thank you. Very nice solution.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 120 other followers

%d bloggers like this: