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 language 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. The website was in English and 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 resource files for 2 langauges: English and Chinese. The English would be the default language of this application so I will create Global.resx file firstly, then Global.zh.resx. The middle name ‘zh’ was the culture name of this language. If we need a French version in the future we can simply create Global.fr.resx. The Visual Studio will help us to generate the accessing class for them.
Then let’s move some content into the resource files. In the home page there are 3 places need to be changed: the title, message and the description. So we add 3 items in our 2 resource files.
The title and the description are defined in the view page so we will change the view page. It will load the content through the access class generated by Visual Studio.
1: <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
2:
3: <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
4: <% 1: : Resources.Global.HomeIndex_Title
%>
5: </asp:Content>
6:
7: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
8: <h2><% 1: : ViewData["Message"]
%></h2>
9: <p>
10: <% 1: : Resources.Global.Home_Index_Desc
%> <a href="http://asp.net/mvc" title='<%: Resources.Global.Home_Index_DescLink %>'>http://asp.net/mvc</a>
11: </p>
12: </asp:Content>
The message was defined in the controller class and passed to the view page through the ViewData so we also need to change the home controller as well.
1: public ActionResult Index()
2: {
3: ViewData["Message"] = Resources.Global.HomeIndex_Message;
4:
5: return View();
6: }
Specify the language 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 language setting. In order to make it as simple as possible we will make the URL indicate the current selected language, 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 language at any pages he’s staying, and also when he want to share the URL it will pass his language setting as well.
In order to do so I changed the application routes, add a new route with a new partten named lang in front of the controller.
1: public static void RegisterRoutes(RouteCollection routes)
2: {
3: routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
4:
5: routes.MapRoute(
6: "Localization", // Route name
7: "{lang}/{controller}/{action}/{id}", // URL with parameters
8: new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
9: );
10:
11: routes.MapRoute(
12: "Default", // Route name
13: "{controller}/{action}/{id}", // URL with parameters
14: new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
15: );
16:
17: }
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 language 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 language setting we should perform some logic before each action was executed. The ActionFilter would be a good solution in this scenario.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Mvc;
6: using System.Threading;
7: using System.Globalization;
8:
9: namespace ShaunXu.MvcLocalization
10: {
11: public class LocalizationAttribute : ActionFilterAttribute
12: {
13: public override void OnActionExecuting(ActionExecutingContext filterContext)
14: {
15: if (filterContext.RouteData.Values["lang"] != null &&
16: !string.IsNullOrWhiteSpace(filterContext.RouteData.Values["lang"].ToString()))
17: {
18: // set the culture from the route data (url)
19: var lang = filterContext.RouteData.Values["lang"].ToString();
20: Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
21: }
22: else
23: {
24: // load the culture info from the cookie
25: var cookie = filterContext.HttpContext.Request.Cookies["ShaunXu.MvcLocalization.CurrentUICulture"];
26: var langHeader = string.Empty;
27: if (cookie != null)
28: {
29: // set the culture by the cookie content
30: langHeader = cookie.Value;
31: Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
32: }
33: else
34: {
35: // set the culture by the location if not speicified
36: langHeader = filterContext.HttpContext.Request.UserLanguages[0];
37: Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
38: }
39: // set the lang value into route data
40: filterContext.RouteData.Values["lang"] = langHeader;
41: }
42:
43: // save the location into cookie
44: HttpCookie _cookie = new HttpCookie("ShaunXu.MvcLocalization.CurrentUICulture", Thread.CurrentThread.CurrentUICulture.Name);
45: _cookie.Expires = DateTime.Now.AddYears(1);
46: filterContext.HttpContext.Response.SetCookie(_cookie);
47:
48: base.OnActionExecuting(filterContext);
49: }
50: }
51: }
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 language 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 language setting in the RouteData I checked the cookie and set it if available. Otherwise I used the user language of the HttpRequest and set into the current thread.
Finally I set the language 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 language setting.
Then I applied the attribute on the home controller so that all actions will perform my localization logic.
1: namespace ShaunXu.MvcLocalization.Controllers
2: {
3: [HandleError]
4: [Localization]
5: public class HomeController : Controller
6: {
7: public ActionResult Index()
8: {
9: ViewData["Message"] = Resources.Global.HomeIndex_Message;
10:
11: return View();
12: }
13:
14: public ActionResult About()
15: {
16: return View();
17: }
18: }
19: }
Now if we start the application and add the language setting on the URL we can see the result.
Links for the language selection
Let the user change the language 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 language.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Routing;
6: using System.Web.Mvc;
7: using System.Web.Mvc.Html;
8: using System.Threading;
9:
10: namespace ShaunXu.MvcLocalization
11: {
12: public static class SwitchLanguageHelper
13: {
14: public class Language
15: {
16: public string Url { get; set; }
17: public string ActionName { get; set; }
18: public string ControllerName { get; set; }
19: public RouteValueDictionary RouteValues { get; set; }
20: public bool IsSelected { get; set; }
21:
22: public MvcHtmlString HtmlSafeUrl
23: {
24: get
25: {
26: return MvcHtmlString.Create(Url);
27: }
28: }
29: }
30:
31: public static Language LanguageUrl(this HtmlHelper helper, string cultureName,
32: string languageRouteName = "lang", bool strictSelected = false)
33: {
34: // set the input language to lower
35: cultureName = cultureName.ToLower();
36: // retrieve the route values from the view context
37: var routeValues = new RouteValueDictionary(helper.ViewContext.RouteData.Values);
38: // copy the query strings into the route values to generate the link
39: var queryString = helper.ViewContext.HttpContext.Request.QueryString;
40: foreach (string key in queryString)
41: {
42: if (queryString[key] != null && !string.IsNullOrWhiteSpace(key))
43: {
44: if (routeValues.ContainsKey(key))
45: {
46: routeValues[key] = queryString[key];
47: }
48: else
49: {
50: routeValues.Add(key, queryString[key]);
51: }
52: }
53: }
54: var actionName = routeValues["action"].ToString();
55: var controllerName = routeValues["controller"].ToString();
56: // set the language into route values
57: routeValues[languageRouteName] = cultureName;
58: // generate the language specify url
59: var urlHelper = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);
60: var url = urlHelper.RouteUrl("Localization", routeValues);
61: // check whether the current thread ui culture is this language
62: var current_lang_name = Thread.CurrentThread.CurrentUICulture.Name.ToLower();
63: var isSelected = strictSelected ?
64: current_lang_name == cultureName :
65: current_lang_name.StartsWith(cultureName);
66: return new Language()
67: {
68: Url = url,
69: ActionName = actionName,
70: ControllerName = controllerName,
71: RouteValues = routeValues,
72: IsSelected = isSelected
73: };
74: }
75:
76: public static MvcHtmlString LanguageSelectorLink(this HtmlHelper helper,
77: string cultureName, string selectedText, string unselectedText,
78: IDictionary<string, object> htmlAttributes, string languageRouteName = "lang", bool strictSelected = false)
79: {
80: var language = helper.LanguageUrl(cultureName, languageRouteName, strictSelected);
81: var link = helper.RouteLink(language.IsSelected ? selectedText : unselectedText,
82: "Localization", language.RouteValues, htmlAttributes);
83: return link;
84: }
85:
86: }
87: }
I created a class to store the information of the language links. This can be used to render a linkage for a language, and it also can be used if we need the selector it be an p_w_picpath linkage, dropdown list or anything we want as well.
The LanguageUrl 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 language part, then generate the URL of current page with that language so that it will render the same page with that language when the user clicked.
The LanguageSelectorLink method takes the responsible for rendering a full Html linkage for this language which we will use it for our simple exmaple.
We need the language select available in all pages so we should put the links in the master page.
1: <div id="logindisplay">
2: <%1: Html.RenderPartial("LogOnUserControl");
%>3:
4: <%1: : Html.LanguageSelectorLink("en-US", "[English]", "English", null)
%>5: <%1: : Html.LanguageSelectorLink("zh-CN", "[中文]", "中文", null)
%>6: </div>
Don’t forget to import the namespace of the SwitchLanguageHelper class on top of the master page otherwise the extension method will not work.