Posts tagged with globalization

Localizing MVC for ASP.NET views and master pages

Microsoft’s MVC for ASP.NET is still under serious development but at the moment support for localization is a little weak. Here’s one approach that works with the 04/16 source-drop.

LocalizingWebFormViewLocator class

This class helps by trying to identify language-specific versions of views, user controls and master-pages where they exist, falling back to the generic one where necessary.

public class LocalizingWebFormViewLocator : ViewLocator {
  public LocalizingWebFormViewLocator() : base() {
    ViewLocationFormats = new[] { "~/Views/{1}/{0}.{2}aspx", "~/Views/{1}/{0}.{2}ascx", "~/Views/Shared/{0}.{2}aspx", "~/Views/Shared/{0}.{2}ascx" };
    MasterLocationFormats = new[] { "~/Views/{1}/{0}.{2}master", "~/Views/Shared/{0}.{2}master" };
  }

  protected override string GetPath(RequestContext requestContext, string[] locationFormats, string name) {
    string foundView = FindViewLocation(locationFormats, requestContext, name, CultureInfo.CurrentUICulture.Name + ".");
    if (String.IsNullOrEmpty(foundView))
      foundView = FindViewLocation(locationFormats, requestContext, name, "");
    return foundView;
  }

  protected string FindViewLocation(string[] locationFormats, RequestContext requestContext, string name, string cultureSuffix) {
    string controllerName = requestContext.RouteData.GetRequiredString("controller");
    foreach (string locationFormat in locationFormats) {
      string viewFile = string.Format(CultureInfo.InvariantCulture, locationFormat, name, controllerName, cultureSuffix);
      if (HostingEnvironment.VirtualPathProvider.FileExists(viewFile))
        return viewFile;
      }
    return null;
  }
}

Using the class

To use the class you must set the ViewLocator on the WebFormViewEngine to a new instance of LocalizingWebFormViewLocator (either in the constructor or in your common controller subclass) and ensure that any master pages are specified on the RenderView calls to ensure the localized version is detected.

public class HomeController : Controller {
  public HomeController() {
    ((WebFormViewEngine)ViewEngine).ViewLocator = new LocalizingWebFormViewLocator();
  }

  public ActionResult Index() {
    return RenderView("Index", "Site");
  }

  public ActionResult About() {
    return RenderView("About", "Site");
  }
}

You must also ensure the thread’s current UI culture is set. The easiest way to do this is to specify the following in your web.config file’s system.web section which will pick it up automatically from the user’s browser settings via the HTTP language-accept header.

<globalization responseEncoding="UTF-8" requestEncoding="UTF-8" culture="auto" uiCulture ="auto" />

MVC for ASP.NET default page in pseudo-Japanese via the Babelfish

Then all you need to do is create views and master pages that have the culture name appended between the name and .aspx, e.g:

  • /Views/Home/Index.aspx (common fall-back for this view)
  • /Views/Home/Index.ja.aspx (Japanese view)
  • /Views/Home/Index.en-GB.aspx (British English view)
  • /Views/Shared/Site.Master (common fall-back for this masterpage)
  • /Views/Shared/Site.ja.Master (Japanese masterpage)

Caveats

There are some limitations to this solution:

Only primary language is attempted

Only the user’s primary language specified in their browser is attempted despite browsers having a complete list in order of preference. Ideally we would scan down this entire list before giving up but that would need more code and there is the issue of whether scanning for several languages across several folders could be too much of a performance hit.

Specifying the masterpage on RenderView

It would be nice if you didn’t have to specify the masterpage on render view but if you do not then the ViewLocator never gets called to resolve the actual masterpage address. This may be for backward compatibility within MVC.

Creating files in Visual Studio

Visual Studio 2008 seems to get a little confused if you create a Index.ja.aspx or Site.ja.aspx – whilst the files are created okay the names are not and you will need to adjust the class names to ensure they don’t conflict and make sure the opening declaration on the .aspx file points to the right code-behind page and inherits from the correct name.

Of course the beauty of this approach is you can mix-and-match using dedicated views where required and localising labels in the fall-back view when it isn’t.

[)amien

Localizing .NET web applications

It seems that globalization often makes the wish list of many a web site until the client realizes professional quality translations require a significant investment of time and money.

On occasion however smaller web applications with their limited vocabulary are prime targets for localization globalization and it can be quite feasible to translate the couple of hundred strings involved.

Here’s a very brief whirlwind overview of what’s involved.

Create the default language resource file

Create a new folder inside your App_GlobalResources folder to contain your language resource files. Then create a new resource file (e.g. Localization\Language.resx) to use when no translation exists for the user’s preferred language.

Detect the user’s browser settings and switch

In .NET 1.1 this required a couple of lines of code in your global.asa.cs:

public void Application_BeginRequest(object sender, EventArgs e) {
    Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(Request.UserLanguages[0]);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(Request.UserLanguages[0]);
}

Update

Sander Rijken points out that .NET 2.0 lets you do this with a line in your web.config <system.web> section instead:

<globalization culture="auto" uiCulture="auto" />

or indeed at individual page level with:

<@ Page ... Culture="auto" UICulture="auto">

Localize the classes

For every bit of code that sets a string that will end up on the user’s display you now have to move that into the language resource file and replace the code with a reference to it. So if for example you had;

if (name.Length == 0) error.Text = "Please enter a name";

Then you move “Please enter a name” into the resource file and give it a sensible key such as NameBlankError and modify the above line to read;

if (name.Length == 0) error.Text = Resources.Language.NameBlankError;

There is a Resource Refactoring Tool to do this for you now! Just right-click the string, choose Extract to resource and fill in the blanks.

Localize the pages

Unlike the WinForms designer the WebForms one doesn’t support multi-language so you’re instead forced to do it by hand. One way is to remove all the text from the page and place it into the resource language file.

Then create a private void Localize() method in each page that simply looks something like;

public void Localize() {
    Title = Resources.Language.LoginPageTitle;
    loginButton.Text = Resources.Language.LoginPageLoginButton;
    reminderButton.Text = Resources.Language.LoginPageReminderButton;
}

Obviously you need to call this from the page, I find that calling it from Page_PreRender works a treat.

One disadvantage to this technique is your page itself ends up looking very blank in the designer or duplicates text that soon gets out of date. You could avoid this by leaving the default-language text in the page and not calling localize if you are running in that language. Be sure to put “\***” or something in the default language resource file for it though so that if it’s missing for other languages you immediately spot the missing text during testing.

Don’t treat types as strings

If you have a number, treat it as a number and pass it around as a number. The same applies to dates etc.

If you need to pass over to SQL etc. then use a parameterized query, they’re fast and will take care of all the regionalization stuff for you!

To get those pesky strings in and out of the correct types see the following extra steps!

Always use .ToString() to format output

Almost all basic .NET types include locale-aware formatting and so keep an eye on the ToString methods. Remember even numbers are formatted differently across the globe. 1,234.00 in England and the US becomes 1.234,00 in various parts of Europe.

Be very careful of outputting currencies. .NET won’t convert the amount for you but you could easily find yourself with the wrong currency symbol and therefore a totally different price!

Always use .TryParse to read input

When accepting information from users hand over that string to TryParse for it to try and work out what’s going on. It will helpfully return a boolean indicating if it did the job okay – if not time to use that localized error-message.

Auf wiedersehen!

[)amien