5/26/2008

LocalizationFilterAttribute

The MVC Framework is highly extensible, which makes it really fun to tinker around with ;) . I just got done integrating jQuery based AJAX and JSON request/response patterns, using different variations of the work laid out by Aaron Lerch in Unifying Web "Sites" and Web Services with the ASP.NET MVC Framework and Nikhil Kothari in Ajax with the ASP.NET MVC Framework . I was surprised to find so many different extension points built into the framework, it really gives you the power to do just about anything.

I though it might be great to leverage this extensibility and add localization into my new WebSite/WebService monster.

When it comes to localization on the human web, we have a few choices.

  1. The Accept-Language HTTP Header Field is supported by most browsers and a lot of sites.
  2. Some sites, like MSDN Library, allow users to put the desired language-culture specifier in the request URL. (en-US, es-ES, fr-FR, etc).
When I was working for Microsoft in Japan, I loved the fact that KB articles and the MSDN library supported language-culture specifier in the request URL. This allowed me to do my research in English, change the URL from 'en-US/whatever' to 'ja-JP/whatever', and send the resulting URL to my native Japanese customers. So option #2 is what I chose.

I'm using the SimplyRestfulRouteHandler to establish my routing, so this is what my route registration looked like:

SimplyRestfulRouteHandler.BuildRoutes(RouteTable.Routes, "{lang}");

You could also do it the old-fashioned way:

RouteTable.Routes.Add(new Route(
"{lang}/{controller}",
new {Action = "Index", Controller = controllerName},
new RouteValueDictionary(new { httpMethod = "GET" }),
new MvcRouteHandler()));

Now the Controller.RouteData.Values["lang"] key will store the specified language-culture for a GET request like 'http://msdn.microsoft.com/en-us/library', where library is the controller.

Next, I want to snag that value and use it to set my thread's CurrentCulture/CurrentUICulture properties. I also want to make sure that I revert the thread's settings when my action is done processing because these properties won't be reverted if our thread is executing in a ThreadPool.

Fortunately, the ActionFilterAttribute class exposes an OnActionExecuting() method which is triggered before an action executes, and a OnActionExecuted() method which is triggered right after. So, I created the following LocalizationFilterAttribute class to do the dirty work for me.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class LocalizationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Controller c = filterContext.Controller as Controller;
if(c == null)
return;

string lang = c.RouteData.Values["lang"] as string;
if(lang == null)
return;

try
{
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(lang);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang);
}
catch
{
throw new NotFoundException("The specified language " + lang +
" was not found.");
}
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InstalledUICulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture;
}
}

It's a good idea to reset the culture information in the Application_Error (Global.asax.cs) method as well, in case an unexpected exception causes the action to terminate.

protected void Application_Error(object sender, EventArgs e)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InstalledUICulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture;
}

That's all there is to it. You can throw this filter attribute on any controller class you want, and you're ready to localize.

2 comments:

aaron said...

Very cool!

Kevin Ortman said...

Thanks, aaron. Let me know how this works out for you ;)