ASP.NET MVC 3 Internationalization

 

Introduction

If your website targets users from different parts of the world, these users might like to see your website content in their own language. Creating a multilingual website is not an easy task, but it will certainly allow your site to reach more audience. Fortunately, the .NET Framework already has components that support different languages and cultures.

We will build an ASP.NET MVC 3 web application that contains the following features:

  • It can display contents in different languages.
  • It autodetects the language from the user's browser.
  • It allows the user to override the language of their browser.

Globalization and Localization in ASP.NET

Internationalization involves Globalization and Localization. Globalization is the process of designing applications that support different cultures. Localization is the process of customizing an application for a given culture.

The format for the culture name is "<languagecode2>-<country/regioncode2>", where <languagecode2> is the language code and <country/regioncode2> is the subculture code. Examples include es-CL for Spanish (Chile) and en-US for English (United States).

ASP.NET keeps track of two culture values, the Culture and UICulture. The culture value determines the results of culture-dependent functions, such as the date, number, and currency formatting. The UICulture determines which resources are to be loaded for the page by the ResourceManager. The ResourceManager simply looks up culture-specific resources that is determined by CurrentUICulture. Every thread in .NET has CurrentCulture and CurrentUICulture objects. So ASP.NET inspects these values when rendering culture-dependent functions. For example, if current thread's culture (CurrentCulture) is set to "en-US" (English, United States), DateTime.Now.ToLongDateString() shows "Saturday, January 08, 2011", but if CurrentCulture is set to "es-CL" (Spanish, Chile) the result will be "sábado, 08 de enero de 2011".

How to Support Different Languages in ASP.NET MVC 3

There are two ways to incorporate different languages and cultures in ASP.NET MVC 3:

  1. By using resource strings in all our site views. (See part 2)
  2. By using different set of views for every language and locale.
  3. By mixing between 1 and 2

Which one is the best?
It depends on you. It is a matter of convenience. Some people prefer to use a single view for all languages because it is more maintainable. While others think replacing views content with code like "@Resources.Something" might clutter the views and will become unreadable. Recall that views have to be as simple as possible. If your views look fine with a lot of inline code, it's fine. But sometimes you have no choice in languages where layout has to be different like right-to-left languages. Perhaps, a mix of the two is the best. Anyway, in this example we will use the 2nd approach just to show a different way than the usual resource strings.

Views Naming Conventions

In order to create different views for every culture, we will append the culture name to the name of the view. For example, Index.cshtml (the default view), Index.es-CL.cshtml (Spanish, Chile), Index.ar-JO.cshtml (Arabic, Jordan). The view name that has no ending is considered the default culture. A default culture view will be rendered if the requested culture name is not implemented explicitly.

Globalizing our Web Site

We will create a new ASP.NET MVC 3 web application and globalize it step by step.

Click "File->New Project" menu command within Visual Studio to create a new ASP.NET MVC 3 Project. We'll create a new project using the "Internet Application" template.

ASP.NET MVC 3 Internationalization

Creating the Model

We'll need a model to create our web application. Add a class named "User" to the "Models" folder:

ASP.NET MVC 3 Internationalization

Internationalizing Validation Messages

Our model presented above contains no validation logic, and this is not the case in normal applications nowadays. We can use data annotation attributes to add some validation logic to our model. However, in order to globalize validation messages, we need to specify a few extra parameters. The "ErrorMessageResourceType" indicates the type of resource to look up the error message. "ErrorMessageResourceName" indicates the resource name to lookup the error message. Resource manager will pick the correct resource file based on the current culture.

Now modify the "Person" class and add the following attributes:


namespace MvcInternationalization.Models
{
     public  class Person
    {
        [Required(ErrorMessageResourceType= typeof(MyResources.Resources), 
                  ErrorMessageResourceName= " FirstNameRequired ")]
        [StringLength( 50, ErrorMessageResourceType =  typeof(MyResources.Resources), 
                          ErrorMessageResourceName =  " FirstNameLong ")]       
         public  string FirstName {  getset; }

        [Required(ErrorMessageResourceType =  typeof(MyResources.Resources), 
            ErrorMessageResourceName =  " LastNameRequired ")]
        [StringLength( 50, ErrorMessageResourceType =  typeof(MyResources.Resources), 
            ErrorMessageResourceName =  " LastNameLong ")]
         public  string LastName {  getset; }

        [Required(ErrorMessageResourceType =  typeof(MyResources.Resources), 
            ErrorMessageResourceName =  " AgeRequired ")]        
        [Range( 0130, ErrorMessageResourceType =  typeof(MyResources.Resources), 
            ErrorMessageResourceName =  " AgeRange ")]                
         public  int Age {  getset; }

        [Required(ErrorMessageResourceType =  typeof(MyResources.Resources), 
            ErrorMessageResourceName =  " EmailRequired ")]
        [RegularExpression( " .+@.+\\..+ ", ErrorMessageResourceType =  typeof(MyResources.Resources), 
            ErrorMessageResourceName =  " EmailInvalid ")]        
         public  string Email {  getset; }

         public  string Biography {  getset; }
    }
}

Because we need to perform data validation on our model using Data Annotations, we will have to add translated resource strings for every culture our site will support. In this case, English, Spanish, and Arabic.

We will store resource files in a separate assembly, so we can reference them in other project types in the future.

Right click on the Solution and then choose the "Add->New Project" context menu command. Choose "Class Library" project type and name it "MyResources".

Now right click on "MyResources" project and then choose "Add->New Item" context menu command. Choose "Resource File" and name it "Resources.resx". This will be our default culture (en-US) since it has no special endings. Add the following names and values to the file like below:

ASP.NET MVC 3 Internationalization

ASP.NET MVC 3 Internationalization

Remember to mark the resource's access modifier property to "public", so it will be accessible from other projects.

Now create a new resource file and name it "Resources.es-CL.resx" and add the following names and values like below:

ASP.NET MVC 3 Internationalization

Now, do the same for the Arabic version. You may not be able to enter the correct strings by keyboard because your OS may not be configured to accept Arabic. However, you can download the files from the link at the top. Anyway, the resource file is included for reference:

ASP.NET MVC 3 Internationalization

We need to reference "MyResources" project from our web application, so that we can read the resource strings right from our web site. Right click on "References" under our web project "MvcInternationalization", and choose the "MyResources" project from Projects tab.

Determining Culture

How do we determine which version of a view to return to the end user?
How do we know which culture does the user want?

There is a header field called "Accept-Language" that the browser sends on every request. This field contains a list of culture names (language-country) that the user has configured in their browser. The problem is that this culture may not reflect the real user's preferred language, such as a computer in a cybercafé. We should allow the user to choose a language explicitly and allow them even to change it. In order to do this sort of things, we need to store the user's preferred language in a store, which can be perfectly a cookie.

We will create a base controller that inspects the cookie contents first, if there is no cookie, we will use the "Accept-Language" field sent by their browser. Create a controller and name it "BaseController" like below:

namespace MvcInternationalization.Controllers
{
     public  class BaseController : Controller
    {
        
         protected  override  void OnActionExecuted(ActionExecutedContext filterContext)
        {
             //  Is it View ?
            ViewResultBase view = filterContext.Result  as ViewResultBase;
             if (view ==  null//  if not exit
                 return;

             string cultureName = Thread.CurrentThread.CurrentCulture.Name;  //  e.g. "en-US"  //  filterContext.HttpContext.Request.UserLanguages[0];  //  needs validation return "en-us" as default            

            
//  Is it default culture? exit
             if (cultureName == CultureHelper.GetDefaultCulture())
                 return;

            
             //  Are views implemented separately for this culture?  if not exit
             bool viewImplemented = CultureHelper.IsViewSeparate(cultureName);
             if (viewImplemented ==  false)
                 return;
            
             string viewName = view.ViewName;

             int i =  0;

             if ( string.IsNullOrEmpty(viewName))
                viewName = filterContext.RouteData.Values[ " action "] +  " . " + cultureName;  //  Index.en-US
             else  if ((i = viewName.IndexOf( ' . ')) >  0)
            {
                 //  contains . like "Index.cshtml"                
                viewName = viewName.Substring( 0, i +  1) + cultureName + viewName.Substring(i);
            }
             else
                viewName +=  " . " + cultureName;  //  e.g. "Index" ==> "Index.en-Us"

            view.ViewName = viewName;

            filterContext.Controller.ViewBag._culture =  " . " + cultureName;

             base.OnActionExecuted(filterContext);
        }


         protected  override  void ExecuteCore()
        {
             string cultureName =  null;
             //  Attempt to read the culture cookie from Request
            HttpCookie cultureCookie = Request.Cookies[ " _culture "];
             if (cultureCookie !=  null)
                cultureName = cultureCookie.Value;
             else
                cultureName = Request.UserLanguages[ 0];  //  obtain it from HTTP header AcceptLanguages

            
//  Validate culture name
            cultureName = CultureHelper.GetValidCulture(cultureName);  //  This is safe



            
//  Modify current thread's culture            
            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureName);
            Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureName);
            
            
             base.ExecuteCore();
        }



    }
}

The base controller checks if the cookie exists, and sets the current thread culture to that cookie value. Of course, because cookie content can be manipulated on the client side, we should always validate its value using a helper class called "CultureHelper". If the culture name is not valid, the helper class returns the default culture. After that, when we call "Return" in controller action methods, the view name is modified by the base controller behind the scenes to reflect the correct culture in cookie or header field. This way we make sure that the user gets the right content based on their culture.

CultureHelper Class

The CultureHelper is basically a utility that allows us to store culture names we are implementing in our site:

ASP.NET MVC 3 Internationalization

namespace MvcInternationalization.Utility
{
     public  static  class CultureHelper
    {
         //  Include ONLY cultures you are implementing as views
         private  static  readonly  Dictionary<String,  bool> _cultures  =  new Dictionary< string, bool> {
            { " en-US "true},   //  first culture is the DEFAULT
            { " es-CL "true},
            { " ar-JO "true}
        };


         ///   <summary>
        
///  Returns a valid culture name based on "name" parameter. If "name" is not valid, it returns the default culture "en-US"
        
///   </summary>
        
///   <param name="name"> Culture's name (e.g. en-US) </param>
         public  static  string GetValidCulture( string name)
        {
             if ( string.IsNullOrEmpty(name))
                 return GetDefaultCulture();  //  return Default culture

             if (_cultures.ContainsKey(name))
                 return name;

             //  Find a close match. For example, if you have "en-US" defined and the user requests "en-GB", 
            
//  the function will return closes match that is "en-US" because at least the language is the same (ie English)            
             foreach( var c  in _cultures.Keys)
                 if (c.StartsWith(name.Substring( 02)))
                     return c;

            
             //  else             
             return GetDefaultCulture();  //  return Default culture as no match found
        }


         ///   <summary>
        
///  Returns default culture name which is the first name decalared (e.g. en-US)
        
///   </summary>
        
///   <returns></returns>
         public  static  string GetDefaultCulture()
        {
             return _cultures.Keys.ElementAt( 0);  //  return Default culture

        }


         ///   <summary>
        
///   Returns "true" if view is implemented separatley, and "false" if not.
        
///   For example, if "es-CL" is true, then separate views must exist e.g. Index.es-cl.cshtml, About.es-cl.cshtml
        
///   </summary>
        
///   <param name="name"> Culture's name </param>
        
///   <returns></returns>
         public  static  bool IsViewSeparate( string name)
        {
             return _cultures[name];
        }

    }
}

We should populate "_cultures" manually. The "_cultures" dictionary stores the list of culture names our site supports. The first parameter indicates culture name (e.g. en-US), the second parameter indicates whether we are implementing separate views for that culture. If the second parameter is false, the default view is used (ie the one that has no special ending).

The nice part of this utility class is that it serves similar languages. For example, if a user is visiting our site from Argentina (es-ar), a culture which is not implemented in our site, he or she will see our site in Spanish using "es-cl" (Spanish, Chile) instead of English language. This way, you don't have to implement all cultures unless you really care about currency, date format, etc.

Controllers

Visual Studio has created a controller named "HomeCotnroller" for us, so we'll use it for simplicity. Modify the "HomeController.cs" so that it looks like below:

namespace MvcInternationalization.Controllers
{
     public  class HomeController : BaseController
    {
        [HttpGet]
         public ActionResult Index()
        {
          
             return View();
        }

        [HttpPost]
         public ActionResult Index(Person per)
        {
                  return View();
        }

         public ActionResult SetCulture( string culture)
        {
             //  Validate input
            culture = CultureHelper.GetValidCulture(culture);

             //  Save culture in a cookie
            HttpCookie cookie = Request.Cookies[ " _culture "];
             if (cookie !=  null)
                cookie.Value = culture;    //  update cookie value
             else
            {

                cookie =  new HttpCookie( " _culture ");
                cookie.HttpOnly =  false//  Not accessible by JS.
                cookie.Value = culture;
                cookie.Expires = DateTime.Now.AddYears( 1);
            }
            Response.Cookies.Add(cookie);

             return RedirectToAction( " Index ");
        }

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

        



    }
}

The "SetCulture" action allows the user to change their current culture and stores it in a cookie called "_culture". We are not restricted to cookies, we could instead save the culture name in Session or elsewhere, but cookies are really lightweight since they do not take any type of space on server side.

Creating a View Template

Now we will implement the View associated with the HomeController's Index action. First delete the existing Index.cshtml file under "Views/Home" folder. Now to implement the view right-click within the "HomeController.Index()" method and select the "Add View" command to create a view template for our home page:

ASP.NET MVC 3 Internationalization

We will need to modify some of the settings above. Choose the option "Create a strongly-typed view" and choose the "Person" class we created before. Also, choose the "Create" menu item from the "Scaffold template" drop-down box.

When we click the "Add" button, a view template of our "Create" view (which renders the form) is created. Modify it so it looks like below:

ASP.NET MVC 3 Internationalization

The javascript code simply post back the form to set the culture. The "selected" helper is used to mark the appropriate culture radio button as checked.

Now make two copies of "Index.cshtml" and rename them to "Index.es-CL.cshtml" and "Index.ar-JO.cshtml". These latter views represent the localized versions of Index.cshtml for two different cultures, so we can add whatever is necessary inside them. Make them look like below:

ASP.NET MVC 3 Internationalization

ASP.NET MVC 3 Internationalization
Spanish view

ASP.NET MVC 3 Internationalization
Arabic view

Of course, for simple partial views like "_LogOnPartial.cshtml" and which are not referenced by controllers, we can use resource strings perfectly.

ASP.NET MVC 3 Internationalization
Arabic view

Try It Out

Run the website now. Notice that client side validation is working nicely. Click on radio buttons to switch between cultures, and notice how right-to-left language is showing correctly. Using separate views allowed us to control how to position elements, and have made our views clean and readable.

ASP.NET MVC 3 Internationalization
English

ASP.NET MVC 3 Internationalization
Spanish

ASP.NET MVC 3 Internationalization
Arabic

Client-Side localization

What about client-side scripts?

For client-side, we should worry mainly about numbers, date and time, and messages since these change from culture to culture. There are many ways to implement client-side localization. But here are two common options:

  1. Creating standalone localized javascript files for every culture and language.
  2. Creating a standard common javascript file for all cultures by sticking to Microsoft Ajax Library.

For (1), we follow the same convention for views and resource files. For example, for the file "myscript.js", you need to create "myscript.es-CL.js", "myscript.ar-JO.js", etc. We can reference the javascript files easily from our views by appending culture name to the javascript file :

<script src="@Url.Content("~/Scripts/myscript" + ViewBag._culture + ".js")" type="text/javascript"></script>

The variable "_culture" is already defined in the base controller and works nicely by ignoring default culture (returns null in this case).

Even if you want to use Microsoft Ajax Library, you may still need separate javascript files that define text messages to the end user. You can define a literal object that contains the list of messages, or if you are using separate views for every culture, you can use inline javascript instead of separate javascript files.

Summary

Building a multilingual web application is not an easy task. but it's worth it especially for web applications targeting users from all over the world, something which many sites do. It is true that globalization is not the first priority in site development process, however, it should be well planned early in the stage of development so it can be easily implemented in the future. Luckily, ASP.NET supports globalization and there are plenty of .NET classes that are handy. We have seen how to create an ASP.NET MVC 3 application that supports 3 different languages, including a right-to-left one, which requires a different UI layout. Anyway, here is a summary of how to globalize a site in ASP.NET MVC 3:

  1. Add a base controller from which all controllers inherit. This controller will intercept the view names returned and will adjust them depending on the current culture set.
  2. Add a helper class that stores the list of culture names that the site will support.
  3. Create a single view or set of views for every culture and language.
  4. Create resource files that contain translation of all string messages. (e.g. Resources.resx, Resources.es-CL.resx, Resources.ar-JO.resx, etc )
  5. Localize javascript files.

I hope this help!
Any questions or comments are welcome!


 

 

 

 

Trackback:

http://afana.me/post/aspnet-mvc-internationalization.aspx

 

你可能感兴趣的:(asp.net)