Skip navigation

Seamlessly Localizing ASP.NET MVC Views

A few weeks ago, I addressed the topic of ASP.NET MVC localization. I also recently addressed ways to customize ASP.NET MVC views. In this article, I'll showcase how to combine tricks from both previous articles into a technique that allows for seamlessly localizing ASP.NET MVC views.

Localizing Largely Static ASP.NET MVC Views

In my article on ASP.NET MVC Localization, I provided a step-by-step overview of using local resources and satellite assemblies with ASP.NET MVC websites. I've been using that approach in production on a project I'm currently working on, and I've enjoyed considerable success with it. However, when it comes to static, or mostly-static, pages (such as about or policy pages), then the approach of using .resx files with name-value pairs to localize big blocks of text just doesn't work. In fact, in contemplating the localization of a site handful of static pages, the idea of trying to cram static, html-formatted, text into .resx files just seemed like it would get ugly really fast. That, and thoughts about maintaining such a solution over time just fell flat.

Consequently, the idea occurred to me to couple some core ASP.NET localization features with my approach for customizing ASP.NET MVC views –as outlined previously. Now, as I pointed out in my article on customizing view results, the goal here isn't to implement a new View Engine. Rather, the goal is to encapsulate some localization logic into some additional control over how ASP.NET MVC view results are selected and rendered by the MVC pipeline.

So, for example, with largely static pages (or views), assume that we have a Home Controller, along with an About() action - which we could then create a handful of views for, like so:

About.aspx
About.en-GB.aspx
About.fr-FR.aspx
About.fr.aspx

With this approach, the idea would be to create a new LocalizedResult class that would return a localized version of a View based upon the language and culture settings defined by the user requesting the page.

Building A LocalizedResult

In my article on customizing ASP.NET MVC View Results, I created a CustomizedViewResult class that acted as a sort of “base” customizable result class. This base class (which derives from ActionResult) consists of two methods that provide a seam between customization needs and the glue that MVC applications use to find and render views.

In this article, I'm just going to inherit from that base class, and implement an overridden ExecuteResult() method that will use a new helper method to try to find a localized version of the View that has been requested.

public override void ExecuteResult(ControllerContext context)

\\{

    base.ViewName = this.GetLocalizedViewName(context);

    base.ExecuteResult(context);

\\}

As you can see from the following code, the helper method in this case is going to check against the default language, then check to see if localized versions of the requested view exist for the requesting user's current language and culture, or language:

private string GetLocalizedViewName(ControllerContext context)

\\{

    string culture =
      context.HttpContext.Request.UserLanguages\\[0\\] ?? "en-US";

 

    string output = base.ViewName;

 

    if(culture

"en-US")

        return output;

 

    // check for other languages using lang-CULTURE, then just lang:

    string localizedView =

      string.Format("\\{0\\}.\\{1\\}", base.ViewName, culture);

 

    ViewEngineResult result =

      ViewEngines.Engines.FindView(context, localizedView, base.MasterName);

    if (result.View

null)

    \\{

        localizedView =

            string.Format("\\{0\\}.\\{1\\}", base.ViewName, culture.Substring(0, 2));

 

        result =

            ViewEngines.Engines.FindView(context, localizedView,                              base.MasterName);

        if (result.View != null)

            return localizedView;

    \\}

    else

        // i.e. found a 'view.ln-CL.aspx' view...

        return localizedView;

 

    // no matching, LOCALIZED, view found. return the default.

    return base.ViewName;

\\}

Of course, for this code to work, you'll need to modify your web.config to enable auto-detection of end-user culture/language as outlined here. Otherwise, consuming a new LocalizedResult is pretty simple, as you just need to return one from your Action method - just as you would any other customized ActionResult type. For example, here's how you would return one in an 'About' Action Method - matching the Views listed previously:

public ActionResult About()

\\{

    return new LocalizedResult("About");

\\}

By returning a LocalizedResult, the application will check to see if the user needs a localized version of the requested view. Then, if they do, it will attempt to pull one back for them, if it exists. If a localized version doesn't exist (or if the user doesn't need one), then the default (English in my case) view is returned.

Taking it to the next step

As cool as it is to be able to return LocalizedResult objects, I wanted my own projects to be able to use cleaner semantics, like so:

public ActionResult About()

\\{

    // add some view data:

    ViewData\\["Something"\\] = "....";

 

    return LocalizedView();

\\}

Enabling this sort of behavior is both easy and very educational if you've never really considered what is going on when you tell your Action methods to do something like "return View();" within your ASP.NET MVC applications.

If you crack open the ASP.NET MVC source code, you'll see that the View() method within Controller.cs is actually just a handful of overloads that return a ViewResult. Consequently, any time you tell your controller actions to return any of these View() overloads, you're just handing off control to a number of helper methods implemented by the ASP.NET team to make it easier to bind ViewResults.

Borrowing from that approach, it's easy to implement a set of LocalizedView() helpers that do roughly the same thing:

// controller super-type

public class SiteController : Controller

\\{

    // sample implementation:

    public LocalizedResult LocalizedView()

    \\{

        return this.LocalizedView(null, null, null);

    \\}

 

    public LocalizedResult LocalizedView(object model)

    \\{

        return this.LocalizedView(null, null, model);

    \\}

   

    public LocalizedResult

        LocalizedView(string viewName)

    \\{

        return this.LocalizedView(viewName, null, null);

    \\}

 

    public LocalizedResult LocalizedView

        (string viewName, string masterName)

    \\{

        return this.LocalizedView(viewName, masterName, null);

    \\}

 

    public LocalizedResult LocalizedView

        (string viewName, object model)

    \\{

        return this.LocalizedView(viewName, null, model);

    \\}

 

    public LocalizedResult LocalizedView

        (string viewName, string masterName, object model)

    \\{

        if (model != null)

            ViewData.Model = model;

 

        if (viewName == null)

            viewName =

                this.ValueProvider\\["action"\\].RawValue.ToString();

 

        LocalizedResult output = new LocalizedResult(viewName);

 

        if (masterName != null)

            output.MasterName = masterName;

 

        return output;

    \\}

\\}

Note that in the implementation method for all of these overloads, I've added some code that assigns a default ViewName if one hasn't been passed in. This, of course, was added to achieve the same kind of functionality offered by ASP.NET MVC's own View() helpers, and is implemented by pulling in the name of the current action.

Likewise, I've also taken the approach of applying dependency injection with my LocalizationResult class to make it obvious that this class requires a ViewName when being created. My LocalizedView() helper-method hides those details from consumers - but I may change that in my own projects if it becomes too cumbersome.

Otherwise, when developing with a LocalizationResult and the additional helper methods outlined above, it's easy to develop just as you normally would only you can hand off execution of your views to a quick bit of localization code that will pull back a localized page (or view) for you when available. I've designed this primarily for what I call static or mostly-static pages - but I believe it would work just fine with heavily dynamic pages, and so on. (In my current project though, those pages are going to be easier to just implement with a few snippets of text pulled from .resx files here and there, along with other variables and messages pulled from satellite assemblies).

My Next Steps

I'll be using these changes in an actual, real-life, project over the next few weeks. And on my static or mostly-static pages, this approach is going to save me a ton of time up front—to say nothing of maintenance concerns down the road. I may end up finding problems or better angles of attack on some of these approaches outlined today, and if warranted, I'll be sure to share.

The great thing though, is that if I do find better ways to address some of my localization concerns down the road, it will be easy for me to address them - as ASP.NET MVC applications make it so easy to quickly and easily extend sites to meet customization needs.

And with that in mind, ping me at [email protected] if you'd like to take a look at (or poke holes in) my code in a sample application that I've created.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish