Best Practices for Localization of ASP.NET MVC 3

[中文版]

Note: The original article is here: http://geekswithblogs.net/shaunxu/archive/2010/05/06/localization-in-asp.net-mvc-ndash-3-days-investigation-1-day.aspx. It was written by Shaun.

This article can be treated as an updated version of the original article mentioned above.

Download: 

Demo Project: zizhujy.com.zip (1.25 mb)


Abstract:

Localization is a common issue when we develop a world wide web application. The key point of making your application localizable is to separate the page content from your logic implementation. That means, when you want to display something on the page, never put them directly on the page file (or the backend logic). You should give the content a key which can be linked to the real content for the proper culture setting.

Last week I was implementing the localization on my ASP.NET MVC application. This is my first time to do it so I spent about 3 days for investigation, trying and come up with a final solution which only needs 1 day’s job. So let’s take a look on what I have done.

Localization supported by ASP.NET MVC

ASP.NET MVC was built on top of the ASP.NET runtime so all feature provided by ASP.NET can be used in MVC without any wheaks such as caching, session state and localization. In the traditional ASP.NET web form ages we were using the resource files to store the content of the application with different cultures and using the ResourceManager class to retrieve them which can be generated by Visual Studio automatically. In ASP.NET MVC they works well.

Let’s create a standard ASP.NET MVC application for an example. We can see all content are hard-written in the view pages and the controller classes.

Now what I need to do is to put all contents out of from the pages and the controllers. ASP.NET gives us a special folder named App_GlobalResources which contains the resource files for the content of all cultures. Just right-click the project in the solution explorer window and create the folder under the Add > Add ASP.NET Folders menu.

I created 2 global resource files for 2 cultures respectively: English and Chinese. The English would be the default culture of this application so I will create Application.resx file firstly, then Application.zh-CN.resx. The middle name “zh-CN” was the culture name of Chinese (People's Republic of China). If we need a Frech version in the future we can simply create Application.fr-FR.resx. The Visual Studio will help us to generate the accessing class for them.

image

Then let’s add some text information for the Application Level into the resource files. Here just 1 Application Name needed.

image

image

And then I created some other local resource files for all views, each view with 2 local resource files (English version and Chinese version). The screenshots are as below:

image

image

image

Then we change the view page by replacing the hardcoded text information with the related content in resources files. The screenshot is as below, notice that the values of title and message are from global resource file and local resource file respectively.

image

You can copy the source code in above screenshot from below:

@{
    ViewBag.Title = HttpContext.GetLocalResourceObject("~/Views/Home/Index.cshtml", "Title").ToString();
    ViewBag.Message = Resources.Application.Name;
}

<h2>@ViewBag.Message</h2>
<p>
    
</p>

 

You can also define ViewBag variables in controller class and which can be passed to the view page. For example, we can remove the code in the 3rd line of the above screenshot, and define ViewBag.Message in the HomeController Class file like as below screenshot shows:

image

You can copy the source code in above screenshot from below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using zizhujy.Attributes;

namespace zizhujy.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = Resources.Application.Name;

            return View();
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

Specify the culture through the URL

We had moved the content into the resource files but our application does not support localization since there’s no place we can specify the culture setting. In order to make it as simple as possible we will make the URL indicate the current selected culture, which means if my URL was http://localhost/en-US/Home/Index it will in English while http://localhost/zh-CN/Home/Index will in Chinese. The user can change the culture at any pages he’s staying, and also when he want to share the URL it will pass his culture setting as well.

In order to do so I changed the application routes, add a new route with a new partten named culture in front of the controller in the Global.asax.cs file in the project root folder.

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Localization", // 路由名称
                "{culture}/{controller}/{action}/{id}", // 带有参数的 URL
                new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // 参数默认值
                new { culture = @"\w{2}(?:-\w{2})?" } // 限制culture格式,只有en-US, en, zh, zh-CN, fr, fr-FR, … 等这样的字符才会被识别为有效的文化路径
            );

            routes.MapRoute(
                "Default", // 路由名称
                "{controller}/{action}/{id}", // 带有参数的 URL
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 参数默认值
            );

        }

You may noticed that I added a new route rather than modifed the default route, and didn’t specify the default value of the {lang} pattern. It’s because we need the default route render the default request which without the culture setting such as http://localhost/ and http://localhost/Home/Index.

If I modied the default route, http://localhost/ cannot be routed; and the http://localhost/Home/Index would be routed to lang = Home, controller = Index which is incorrect.

Since we need the URL control the culture setting we should perform some logic before each action was executed. The ActionFilter would be a good solution in this scenario. So just add a folder named Attributes to project root, and then add a class named LocalizationAttribute.cs under this folder. The source code of LocalizationAttribute.cs is as below:

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

namespace zizhujy.Attributes
{
    public class LocalizationAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.RouteData.Values["culture"] != null &&
                !string.IsNullOrWhiteSpace(filterContext.RouteData.Values["culture"].ToString()))
            {
                // set the culture from the route data (url)
                var lang = filterContext.RouteData.Values["culture"].ToString();
                Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
            }
            else
            {
                // load the culture info from the cookie
                var cookie = filterContext.HttpContext.Request.Cookies["_culture"];
                var langHeader = string.Empty;
                if (cookie != null)
                {
                    // set the culture by the cookie content
                    langHeader = cookie.Value;
                    Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
                }
                else
                {
                    // set the culture by the location if not speicified
                    langHeader = filterContext.HttpContext.Request.UserLanguages[0];
                    Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
                }
                // set the lang value into route data
                filterContext.RouteData.Values["culture"] = langHeader;
            }

            // save the location into cookie
            HttpCookie cookieToCreate = new HttpCookie("_culture", Thread.CurrentThread.CurrentUICulture.Name);
            cookieToCreate.Expires = DateTime.Now.AddYears(1);
            filterContext.HttpContext.Response.SetCookie(cookieToCreate);

            base.OnActionExecuting(filterContext);
        }
    }
}

I created an attribute named LocalizationAttribute which inherited from the ActionFilterAttribute and overrided its OnActionExecuting method. I firstly checked the RouteData. If it contains the culture setting I will set it to the CurrentUICulture of the CurrentThread, which will indicate the resource manager (generated by Visual Studio based on the resource files) retrieve the related value.

If no culture setting in the RouteData I checked the cookie and set it if available. Otherwise I used the user culture of the HttpRequest and set into the current thread.

Finally I set the culture setting back to the route data so all coming actions would retrieve it and also saved it into the cookie so that next time the user opened the browser he will see his last culture setting.

Then I applied the attribute on the home controller so that all actions will perform my localization logic.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using zizhujy.Attributes;

namespace zizhujy.Controllers
{
    [Localization]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = Resources.Application.Name;

            return View();
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

Now if we start the application and add the culture setting on the URL we can see the result.

image

image

 

Links for the culture selection

Let the user change the culture through the URL would not be a good solution. We need to give them some links on top of the pages so that they can change it at any time. In ASP.NET MVC the simplest way is to create a HtmlHelper to render the links for each culture. You can put all your helper classes into the utility folder under the project root.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Threading;

namespace zizhujy.Utility
{
    public static class CultureSelectionHelper
    {
        public class Culture
        {
            public string Url { get; set; }
            public string ActionName { get; set; }
            public string ControllerName { get; set; }
            public RouteValueDictionary RouteValues { get; set; }
            public bool IsSelected { get; set; }

            public MvcHtmlString HtmlSafeUrl
            {
                get
                {
                    return MvcHtmlString.Create(Url);
                }
            }

        }

        public static Culture CultureUrl(this HtmlHelper helper, string cultureName, string cultureRouteName = "culture", bool strictSelected = false)
        {
            // set the input culture to lower
            cultureName = cultureName.ToLower();
            // retrieve the route values from the view context
            var routeValues = new RouteValueDictionary(helper.ViewContext.RouteData.Values);
            // copy the query strings into the route values to generate the link
            var queryString = helper.ViewContext.HttpContext.Request.QueryString;
            foreach (string key in queryString)
            {
                if (queryString[key] != null && !string.IsNullOrWhiteSpace(key))
                {
                    if (routeValues.ContainsKey(key))
                    {
                        routeValues[key] = queryString[key];
                    }
                    else
                    {
                        routeValues.Add(key, queryString[key]);
                    }
                }
            }
            var actionName = routeValues["action"].ToString();
            var controllerName = routeValues["controller"].ToString();
            // set the culture into route values
            routeValues[cultureRouteName] = cultureName;
            // generate the culture specify url
            var urlHelper = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);
            var url = urlHelper.RouteUrl("Localization", routeValues);
            // check whether the current thread ui culture is this culture
            var currentCultureName = Thread.CurrentThread.CurrentUICulture.Name.ToLower();
            var isSelected = strictSelected ? currentCultureName == cultureName : currentCultureName.StartsWith(cultureName);

            return new Culture() { Url = url, ActionName = actionName, ControllerName = controllerName, RouteValues = routeValues, IsSelected = isSelected };

        }

        public static MvcHtmlString CultureSelectionLink(this HtmlHelper helper, string cultureName, string selectedText, string unselectedText, IDictionary htmlAttributes, string cultureRouteName = "culture", bool strictSelected = false)
        {
            var culture = helper.CultureUrl(cultureName, cultureRouteName, strictSelected);
            var link = helper.RouteLink(culture.IsSelected ? selectedText : unselectedText, "Localization", culture.RouteValues, htmlAttributes);
            return link;
        }
    }
}

I created a class to store the information of the culture links. This can be used to render a linkage for a culture, and it also can be used if we need the selector it be an image linkage, dropdown list or anything we want as well.

The CultureUrl method takes the main responsible for generating the information that can be used in the selector such as the URL, RouteValues, etc. It loads the RouteData and query string from the incoming request and swich the culture part, then generate the URL of current page with that culture so that it will render the same page with that culture when the user clicked.

The CultureSelectionLink method takes the responsible for rendering a full Html linkage for this culture which we will use it for our simple exmaple.

We need the culture select available in all pages so we should put the links in the shared layout page.

Firstly we created a partial page called _CultureSelectionPartial.cshtml that only contains the culture selector, and place it under the ~/Views/Shared folder of project root.

@using zizhujy.Utility

@Html.CultureSelectionLink("en-US", "[English(United States)]", "English(United States)", null)
@Html.CultureSelectionLink("zh-CN", "[中文(简体)]", "中文(简体)", null)

Don’t forget to import the namespace of the CultureSelectionHelper class on top of the _CultureSelectionPartial page otherwise the extension method will not work.

The source code is quoted out of the _Layout.cshtml file in the ~/Views/Shared folder.

@Html.Partial("_LogOnPartial") @Html.Partial("_CultureSelectionPartial")

image

Summary

In this post I explained about how to implement the localization on an ASP.NET MVC web application. I utilized the resource files as the container of the localization information which provided by the ASP.NET runtime.

The localization information can be stored in any places. In this post I just use the resource files which I can use the ASP.NET localization support classes. But we can store them into some external XML files, database and web services. The key point is to separate the content from the usage. We can isolate the resource provider and create the relevant interface to make it changable and testable.

Download: 

Demo Project: zizhujy.com.zip (1.25 mb)

Comments (3) -

  • I've been testing it out for a while and so far there has been no unwelcome surprises <br />Karen recently posted..Social Bookmarking and How It Affects Your Bounce Rate
  • Truly good site thank you so much for your time in publishing the posts for all of us to learn about.
  • 903770 8661I like this weblog  really significantly so significantly excellent  information  . 995152

Add comment

Loading