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.8 by 9 people

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

Comments

May 25. 2008 03: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 27. 2008 11: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 28. 2008 02: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 29. 2008 03:59

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

thanks,
Al

Al

May 29. 2008 05: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 9. 2008 18:00

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

Zygimantas

July 30. 2008 10: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 31. 2008 22:38

Please post the source code sample for this. Thanks!

tl

September 5. 2008 22: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 9. 2008 16: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 25. 2008 10: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 25. 2008 21:16

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

Steve Andrews

October 25. 2008 07: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

December 11. 2008 07:47

Actually, this is enough for the GetVirtualPath method to work in Beta 1:

private static string GetVirtualPath(HtmlHelper htmlHelper)
{
string virtualPath = null;
WebFormView view = htmlHelper.ViewContext.View as WebFormView;

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

Dan Lewi Harkestad

January 17. 2009 01:27

Like Steve said:

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

I don't really get this. Why would one use error-prone string keys all over the place when you can get compile-time safe versions for the exact same thing? (+ you'll spend a lot less time implementing things like HtmlHelper extension methods)

Gino

February 10. 2009 08:27

@Gino, Steve

Resources.<ClassName>.<ResourceKey> is good, but not all resource keys are known at compile time. For example, I have resources to localize enum types. The key is built at runtime from myEnum.GetType().Name + "_" + myEnum.ToString()...

Dominic

February 10. 2009 08:46

Then why not use Resources.<ClassName>.<ResourceKey> where possible and something like this:

public static string Localize(this HtmlHelper helper, Enum value)
{
return Localize(helper, value, <defaultResourceManager>);
}
public static string Localize(this HtmlHelper helper, Enum value, ResourceManager resourceManager) { ... }

or even

public static string Localize(this Enum value) {...}

for enums?

Gino

March 9. 2009 00:35

As of ASP.NET MVC RC2, this code does not compile.
It fails on 'controller.ViewEngine' and says 'System.Web.Mvc.Controller' does not contain a definition for 'ViewEngine' and no extension method 'ViewEngine' accepting a first argument of type 'System.Web.Mvc.Controller' could be found (are you missing a using directive or an assembly reference?)'

Avi

March 18. 2009 03:20

Here is my solution of control's(.ascx) resources problem:

private static string GetVirtualPath(HtmlHelper htmlhelper) {
string virtualPath = null;
TemplateControl tc = htmlhelper.ViewDataContainer as TemplateControl;

if (tc != null) {
virtualPath = tc.AppRelativeVirtualPath;
}

return virtualPath;
}

Omen

March 21. 2009 04:03

Been working on an MVC project which i want it to be multilingual. People really say resources are the best way for multilingual in ASP.NET MVC. Thats have i came to this post. I makes sense to me and i do understand more about resources thanks for the post

Bayram Çelik

March 24. 2009 23:21

We tried to localize here at work using your methods and now my colleague has a resource stuck in his pooper. Now what?!

Faffy Fuck

April 10. 2009 07:50

NIce, soooo 1 ?) while in an English screen, can the user go to a combo box on the screen choose to see the page in Spanish, and then have the page rendered in Spanish, 2 ?) how about changing to spanish and not lossing data?

Fred

April 21. 2009 22:09

very body another mee.

nakliyat

April 25. 2009 02:50

Yep, all implementations of GetVirtualPath() here simply do not work in ASP.NET MVC Release 1.

Dmitri

May 7. 2009 13:49

very nices article.Adding videos sounds like an interesting idea, although I've never done that before. Any recommendations for a screen recording software?

saç ekimi

May 15. 2009 22:21

I cannot get this to pick up browser language. I've tried CultureInfo.CurrentCulture but it always reports en-GB or en-US. Am I missing something?

Paul

May 25. 2009 16:06

public static string Localize(this HtmlHelper helper, Enum value, ResourceManager resourceManager) { ... }

or even

public static string Localize(this Enum value) {...}

for enums?

halı yıkama

May 25. 2009 16:07

how about changing to spanish and not lossing data?

ankarahalı yıkama

May 25. 2009 16:08

As of ASP.NET MVC RC2, this code does not compile.

ankara nakliyat

May 25. 2009 16:09



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

ankara nakliye

May 25. 2009 16:11

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

ankara evden eve nakliyat

May 25. 2009 16:12

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

izmir nakliyat

May 25. 2009 16:13

good article.

izmir evden eve nakliyat

May 25. 2009 16:13

thank you

digiturk

May 25. 2009 16:14

Under /Views/Products, create a "App_LocalResources", and 2 .resx files named "Create.aspx.resx" and "Confirm.aspx.resx".

digiturk başvuru

May 27. 2009 14:20

Something has happened to the download. When I grab it all the zip contains is 1 file (LocalizationHelpers.cs)

Jordan

June 4. 2009 08:02

Why not just use TemplateControl.GetLocalResourceObject/GetGlobalResourceObject in views or inherit ViewPage/ViewUserControl/ViewMasterPage and implement there something like:

public string GetLocalResourceString(string key)
{
var o = GetLocalResourceObject(key);
if (o != null)
{
return o.ToString();
}
return "?" + key + "?";
}

public string GetGlobalResourceString(string className, string key)
{
var o = GetGlobalResourceObject(className, key);
if (o != null)
{
return o.ToString();
}
return "?" + key + "?";
}

I think it's better than this approach which has major flaws when dealing with partial views and master pages.

Filip Kinsky

June 8. 2009 04:00

The light gray color of the font makes it hard to read... you should consider a better contrast

hugo

June 12. 2009 14:31

Can anyone of you tell me how to use extension in controller. I have tried it in the view and it works fine but not in the controller. In the view we create the resource file as index.aspx.resx but for controller what will be the name of the file and where we need to place the file.

Samoj Bhattarai

June 14. 2009 02:29

+1 for Filip Kinsky approach, works for master pages (and I guess controls to, but haven’t done that yet). Overall great discussion thanks everyone. Bit of a pain in the but GetLocalResourceObject() is protected, kinda makes sence but would have been nice to make it an extentsion method.

Keith

June 22. 2009 13:03

Hmmm. Must admit, Localization has slipped to the bottom of my list. This shows how easy it can be so might have to tackle that sooner rather than later.

Chris

July 2. 2009 09:36

A gotcha in the codebase is that this code will only work with the WebFormViewEngine out of the box for local resources<a href="http://www.ekintas.com" title="nakliyat">nakliyat</a>

nakliyat

Add comment


 

  Country flag

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



Live preview

July 3. 2009 21:44



Copyright © 2000 - 2009 , Excentrics World