Simple ASP.Net MVC Globalization with Graceful Fallback
March 23, 2011 23 Comments
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.
Pingback: Scott Hanselman
Pingback: Web and Cloud
Pingback: ASP.NET and Silverlight
Pingback: Microsoft MVC bloggers
Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 - Bilim-Teknoloji | Positive Pozitive.NeT
Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 « Chandara
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 ?
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
Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 - Technology John
Pingback: Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery – Part 1 | 哈哈808,开心呵呵网
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 ?
I found the problem : you must have a web.config in LocalizedViews folder, the same than in the Veiws folder.
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
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
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.
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
Pingback: Scott Hanselman的中文博客
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
Pingback: Internacionalización y Globalización de aplicaciones web | Pensando bajo la lluvia
Pingback: Localising MVC Views using Display Modes | greatrexpectations
Pingback: Martijn Muurman's Developers Blog | Globalizing MVC views using cshtml per locale
Just wanted to say thank you. Very nice solution.
Pingback: http://vb.uiraqi.com