eWorld.UI - Matt Hawley

Ramblings of Matt

ASP.NET MVC - Localization Helpers

May 16, 2008 15:58 by matthaw

You're localizing your application right? Sure, I bet we ALL are - or at least, we're all storing our strings in resource files so that later we can localize. I know, I don't either :) but that doesn't mean if you're working on a large application that needs to be localized in many different languages, you shouldn't be thinking about it. While localization was possible in 1.0/1.1, ASP.NET 2.0 introduced us to a new expression syntax that made localization much easier, simply writing the following code

   1:  <asp:Label Text="<%$ Resources:Strings, MyGlobalResource %>" runat="server" />
   2:  <asp:Label Text="<%$ Resources:MyLocalResource %>" runat="server" />

Of course, you could always use the verbose way and call out to the HttpContext to get local and global resources, but I really enjoy writing the expression syntax much better as it truly implies that the code knows the context of your view / page. So, you could write both of the above examples like

   1:  <%= HttpContext.Current.GetGlobalResourceString("Strings", "MyGlobalResources",
   2:            CultureInfo.CurrentUICulture) %>
   3:  <%= HttpContext.Current.GetLocalResourceString("~/views/products/create.aspx", 
   4:            "MyLocalResource", CultureInfo.CurrentUICulture) %>

So now, you've started on that next big project and have been given the green light to use ASP.NET MVC, but ... your application needs to be localized in Spanish as well. In the current bits, there's really no way of using localized resources aside from (gasp!) using the Literal server control or the verbose method. But, you're moving to MVC to get away from the web forms model & nomenclature, so those are not an option any longer. Well, taking my earlier example of PRG pattern, I decided to "localize" it in an example of your project. First off, you'll need to create your global and local resources. Add a "App_GlobalResources" folder to the root. Add a Strings.resx file, and start to enter your text. Next, we'll add 2 local resources for our views. Under /Views/Products, create a "App_LocalResources", and 2 .resx files named "Create.aspx.resx" and "Confirm.aspx.resx".

 

Okay, now you're all set. Let's start converting our code to use the resources. You'll see that I'm using a new extension method (code will come later) in both the controller actions and in the view itself.

 

   1:  public class ProductsController : Controller
   2:  {
   3:      public ActionResult Create()
   4:      {
   5:          if (TempData["ErrorMessage"] != null)
   6:          {
   7:              ViewData["ErrorMessage"] = TempData["ErrorMessage"];
   8:              ViewData["Name"] = TempData["Name"];
   9:              ViewData["Price"] = TempData["Price"];
  10:              ViewData["Quantity"] = TempData["Quantity"];
  11:          }
  12:          return RenderView();
  13:      }
  14:   
  15:      public ActionResult Submit()
  16:      {
  17:          string error = null;
  18:          string name = Request.Form["Name"];
  19:          if (string.IsNullOrEmpty(name))
  20:              error = this.Resource("Strings, NameIsEmpty");
  21:   
  22:          decimal price;
  23:          if (!decimal.TryParse(Request.Form["Price"], out price))
  24:              error += this.Resource("Strings, PriceIsEmpty");
  25:   
  26:          int quantity;
  27:          if (!int.TryParse(Request.Form["Quantity"], out quantity))
  28:              error += this.Resource("Strings, QuantityIsEmpty");
  29:   
  30:          if (!string.IsNullOrEmpty(error))
  31:          {
  32:              TempData["ErrorMessage"] = error;
  33:              TempData["Name"] = Request.Form["Name"];
  34:              TempData["Price"] = Request.Form["Price"];
  35:              TempData["Quantity"] = Request.Form["Quantity"];
  36:              return RedirectToAction("Create");
  37:          }
  38:   
  39:          return RedirectToAction("Confirm");
  40:      }
  41:   
  42:      public ActionResult Confirm()
  43:      {
  44:          return RenderView();
  45:      }
  46:  }

Next, convert views over to use the new Resource extension method, below is the Create view:

   1:  <% using (Html.Form<ProductsController>(c => c.Submit())) { %>
   2:      <% if (!string.IsNullOrEmpty((string)ViewData["ErrorMessage"])) { %>
   3:          <div style="color:Red;">
   4:              <%= ViewData["ErrorMessage"] %>
   5:          </div>
   6:      <% } %>
   7:      <%= Html.Resource("Name") %> <%= Html.TextBox("Name", ViewData["Name"]) %><br />
   8:      <%= Html.Resource("Price") %> <%= Html.TextBox("Price", ViewData["Price"]) %><br />
   9:      <%= Html.Resource("Quantity") %> <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
  10:      <%= Html.SubmitButton("submitButton", Html.Resource("Save")) %>
  11:  <% } %>

Here's the Confirm view:

   1:  <%= Html.Resource("Thanks") %><br /><br />
   2:  <%= Html.Resource("CreateNew", Html.ActionLink<ProductsController>(c => c.Create(), 
   3:                             Html.Resource("ClickHere"))) %>

As you can see, I'm using a mixture of resource expressions both within the controller and view implementation. Here are the main implementations:

   1:  // default global resource
   2:  Html.Resource("GlobalResource, ResourceName")
   3:   
   4:  // global resource with optional arguments for formatting
   5:  Html.Resource("GlobalResource, ResourceName", "foo", "bar")
   6:   
   7:  // default local resource
   8:  Html.Resource("ResourceName")
   9:   
  10:  // local resource with optional arguments for formatting
  11:  Html.Resource("ResourceName", "foo", "bar")

As you can see, it supports both Global Resources and Local Resources. When working within your controller actions, only Global Resources work as we don't have a concept of a "local resource." The implementation for Html.Resource is actually a wrapper around the verbose method I previously mentioned. It does, however, take into consideration the expression syntax and the context of where the code is calling from to smartly determine the correct resource call to make. A gotcha in the codebase is that this code will only work with the WebFormViewEngine out of the box for local resources. The reason for this is that the code needs a way to find the associated virtual path for the view it's currently rendering using the view engine's own ViewLocator. Should you be using another View Engine, you'll have to modify the codebase to use it's ViewLocator. So, here's the code:

   1:  public static string Resource(this HtmlHelper htmlhelper, 
   2:                                string expression, 
   3:                                params object[] args)
   4:  {
   5:      string virtualPath = GetVirtualPath(htmlhelper);
   6:   
   7:      return GetResourceString(htmlhelper.ViewContext.HttpContext, expression, virtualPath, args);
   8:  }
   9:   
  10:  public static string Resource(this Controller controller, 
  11:                                string expression, 
  12:                                params object[] args)
  13:  {
  14:      return GetResourceString(controller.HttpContext, expression, "~/", args);
  15:  }
  16:   
  17:  private static string GetResourceString(HttpContextBase httpContext, 
  18:                                          string expression, 
  19:                                          string virtualPath, 
  20:                                          object[] args)
  21:  {
  22:      ExpressionBuilderContext context = new ExpressionBuilderContext(virtualPath);
  23:      ResourceExpressionBuilder builder = new ResourceExpressionBuilder();
  24:      ResourceExpressionFields fields = (ResourceExpressionFields)builder
  25:                                              .ParseExpression(expression, typeof(string), context);
  26:   
  27:      if (!string.IsNullOrEmpty(fields.ClassKey))
  28:          return string.Format((string)httpContext.GetGlobalResourceObject(
  29:                                                      fields.ClassKey, 
  30:                                                      fields.ResourceKey, 
  31:                                                      CultureInfo.CurrentUICulture),
  32:                              args);
  33:   
  34:      return string.Format((string)httpContext.GetLocalResourceObject(
  35:                                                      virtualPath, 
  36:                                                      fields.ResourceKey, 
  37:                                                      CultureInfo.CurrentUICulture), 
  38:                              args);
  39:  }
  40:   
  41:  private static string GetVirtualPath(HtmlHelper htmlhelper)
  42:  {
  43:      string virtualPath = null;
  44:      Controller controller = htmlhelper.ViewContext.Controller as Controller;
  45:   
  46:      if (controller != null)
  47:      {
  48:          WebFormViewEngine viewEngine = controller.ViewEngine as WebFormViewEngine;
  49:          if (viewEngine != null)
  50:          {
  51:              virtualPath = viewEngine.ViewLocator.GetViewLocation(
  52:                                                      new RequestContext(controller.HttpContext, 
  53:                                                               controller.RouteData), 
  54:                                                               htmlhelper.ViewContext.ViewName);
  55:          }
  56:      }
  57:   
  58:      return virtualPath;
  59:  }

And just so you know I'm not lying - here's the output in English and Spanish!

English Spanish

Since this example code is so lengthy, I've zipped up the main code to make things much easier for you to bring into your solution.

kick it on DotNetKicks.com


Currently rated 4.5 by 4 people

  • Currently 4.5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

May 24. 2008 02:39

For the actual translation, maybe I am allowed to do some self-advertising of my (free) translation tool: www.codeproject.com/.../ZetaResourceEditor.aspx

Uwe

May 26. 2008 10:20

Is there any reason you can't use

<%= GetLocalResourceObject("foo").ToString() %>

to obtain local resources? I tested it while looking for a way to get strongly typed access to the resources and it seems to work in an MVC app.

Harry

May 27. 2008 01:58

@Harry - that works too, but the reason I extended it is for the following

1. Verbocity of the method call (I want short calls)
2. The use with other view engines (your example depends upon a view from Page/UserControl)
3. I really like the way you define the expression like <%$ Resource: Foo, Bar %>, my method is agnostic of Global/Local resources because it encapsulates both.

matthaw

May 28. 2008 02:59

what's the best way get/set localization language (culture) as part of URL, not query string parameter?

thanks,
Al

Al

May 28. 2008 04:26

I would recommend passing it in the URL specifically...

/foo/en-US/bar
/foo/es-MX/bar

And your routing can interpret the language and pass it as a parameter (see Scott's latest MVC push for this example). You'll then just have to set the ui culture on the current thread. Another method is to pull the languages from the browser (via http headers) and use the first language as their default ui culture.

matthaw

July 8. 2008 17:00

I think this extension does not work inside masterpage with local resources.

Zygimantas

July 29. 2008 09:42

Hi... nice code! i've included it in a web i'm making. However it seems to be impossible to include local (view) resources in the controller... correct? In the view code you say <%= Html.Resource("Name") %>, but in the controller, when you do the error checking you pull the error message from a generic strings resource... what i would like to know if it is possible to do something like the following in the controller...

error += String.Format(this.Resource("Strings, GenericRequiredError"), this.View.Resource("Name"));

where GenericRequiredError is a string like "The {0} field is required"...

No idea if my question is clear... I hope so, and thanks for the informative article!

davidinbcn

July 30. 2008 21:38

Please post the source code sample for this. Thanks!

tl

September 4. 2008 21:08

The new MVC Preview 5 has broken this code. The controller no longer has a reference to the view engine, and the view engine no longer has a ViewLocator. I just started looking at preview 5 so I don't have a fix myself yet or I'd post it.

Paul Wideman

September 8. 2008 15:13

Here is an ugly and temporary implementation which worked for me. I found the virtual folder as Preview 5 finds the view itself, just replace the GetVirtualPath(HtmlHelper htmlhelper) as below.

Hopefully this will be replaced with the next releases of the MVC.

private static string GetVirtualPath(HtmlHelper htmlhelper)
{
string virtualPath = null;
Controller controller = htmlhelper.ViewContext.Controller as Controller;

if (controller != null)
{
string controllerName = controller.ToString();
controllerName = controllerName.Substring(controllerName.LastIndexOf(".") + 1).Replace("Controller", "");
string viewName = htmlhelper.ViewContext.ViewName;

string[] viewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};

foreach (string location in viewLocationFormats)
{
virtualPath = location.Replace("{0}", viewName).Replace("{1}", controllerName);
if (File.Exists(HttpContext.Current.Server.MapPath(virtualPath)))
{
break;
}
}
}

return virtualPath;
}

Kemal Eginci

September 24. 2008 09:04

Preview 5 clean GetVirtualPath method:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Compilation;
using System.Globalization;

namespace InternationalizationExample.Extensions
{
/// <summary>
/// Source: blog.eworldui.net/.../...T-MVC---Localization.aspx
///
/// Adapted by Maarten Balliauw
/// </summary>
public static class ResourceExtensions
{
public static string Resource(this HtmlHelper htmlhelper, string expression, params object[] args)
{
string virtualPath = GetVirtualPath(htmlhelper);

return GetResourceString(htmlhelper.ViewContext.HttpContext, expression, virtualPath, args);
}

public static string Resource(this Controller controller, string expression, params object[] args)
{
return GetResourceString(controller.HttpContext, expression, "~/", args);
}

private static string GetResourceString(HttpContextBase httpContext, string expression, string virtualPath, object[] args)
{
ExpressionBuilderContext context = new ExpressionBuilderContext(virtualPath);
ResourceExpressionBuilder builder = new ResourceExpressionBuilder();
ResourceExpressionFields fields = (ResourceExpressionFields)builder.ParseExpression(expression, typeof(string), context);

if (!string.IsNullOrEmpty(fields.ClassKey))
return string.Format((string)httpContext.GetGlobalResourceObject(
fields.ClassKey,
fields.ResourceKey,
CultureInfo.CurrentUICulture),
args);

return string.Format((string)httpContext.GetLocalResourceObject(
virtualPath,
fields.ResourceKey,
CultureInfo.CurrentUICulture),
args);
}

private static string GetVirtualPath(HtmlHelper htmlhelper)
{
string virtualPath = null;
Controller controller = htmlhelper.ViewContext.Controller as Controller;

if (controller != null)
{
ViewEngineResult result = FindView(controller.ControllerContext, htmlhelper.ViewContext.ViewName);
WebFormView webFormView = result.View as WebFormView;

if (webFormView != null)
{
virtualPath = webFormView.ViewPath;
}
}

return virtualPath;
}

private static ViewEngineResult FindView(ControllerContext controllerContext, string viewName)
{
// Result
ViewEngineResult result = null;

// Search only for WebFormViewEngine
WebFormViewEngine webFormViewEngine = null;
foreach (var viewEngine in ViewEngines.Engines)
{
webFormViewEngine = viewEngine as WebFormViewEngine;

if (webFormViewEngine != null)
break;
}

result = webFormViewEngine.FindView(controllerContext, viewName, "");
if (result.View == null)
{
result = webFormViewEngine.FindPartialView(controllerContext, viewName);
}

// Return
return result;
}
}
}

Maarten Balliauw

September 24. 2008 20:16

Why not just use Resources.<ClassName>.<ResourceKey>?

Steve Andrews

October 24. 2008 06:33

Everything works good except for resources which are stored in controls (.ascx files). GetVirtualPath method gets a reference for the aspx page on which a control is stored. How to get the reference to a control without overloading the helper metod like this public static string Resource(this HtmlHelper htmlhelper, string expression, string virtualPath, params object[] args) where virtual path comes directly from the ascx file?

Serg

Add comment


 

  Country flag

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]



Live preview

November 19. 2008 04:54



Copyright © 2000 - 2008 , Excentrics World