Get insight to build your first Multi-Language ASP.NET MVC 5 Web Application

Download Demo from CodeProject - 3.9 MB

INTRODUCTION

This article explains how to create a simple Multi-Language ASP.NET MVC 5 Web application. The application will be able to deal with English (United States), Spanish and French languages. English will be the default language. Of course, it will be very easy to extend the solution for including new languages.

To begin with, it's assumed that readers have a basic knowledge on ASP.NET MVC 5 framework. Other related technologies such as jQuery will be slightly commented througout the article owing to they have been used to add new functionality. I will focus my explanations on MVC framework components. Anyway, I will try to explain how each component works or, at least, providing you with links to get more detailed information. It's not the goal of this article to explain each line of code for each technology or repeat explanations that are very well documented in other articles. My goal will be to explain the main features of our demo application at the same time as remember key issues to get better insight and understanding.

That being said, our demo application will have a HomeController with three basic typical actions such as IndexAbout and Contact. Besides, we will derive this controller from a BaseController, as we will see later, to provide every controller with common functionality. So, content rendered in Views called from HomeController will be localized according to default or user selected language.

BACKGROUND

From my view, when talking about Multi-Language ASP.NET MVC applications it would be necessary to take into account at least the following key issues:

  • Consider components to apply Globalizationand Localization.
  • Configure URL Routingfor Multi-Language purposes, especially bearing in mind SEO perspective. Regarding this, the most important issue is keeping distinct content on different URLs, never serving distinct content from the same URL.
 
Globalization and Localization

We must be able to set up the proper culture for each request being processed on current Thread running on controllers. So, we will create a CultureInfoobject and set the CurrentCulture and CurrentUICulture properties on the Thread (see more about here) to serve content based on appropriate culture. To do this, we will extract culture information fromUrl Routing.

CurrentCulture property makes reference to Globalization or how dates, currency or numbers are managed. Instead, CurrentUICulture governs Localization or how content and resources are translated or localized. In this article I'm going to focus on Localization.

CultureInfo class is instantiated based on a unique name for each specific culture. This code uses the pattern "xx-XX" where the first two letter stands for language and the second one for sublanguage, country or region (See more about here). In this demo application, en-USes-ES and fr-FR codes represent supporting languages English for United States, Spanish for Spain and French for France.

Having said that, here is a list of elements to be localizedaccording to culture:

  • Plain Texts.
    • We will translate texts by using Resource Files. In short, these files allow us to save content resources, mainly texts and images, based on a dictionary of key/value pairs. We will just employ these files to store texts, not images. Read more about at Microsoft Walkthrough.
  • Images.
    • We will localize images by extending UrlHelper class, contained in System.Web.Mvc.dll assembly. By means of extension methods inserted into this class, we will look for images within a previously-created structure of folders according to supported languages. Briefly explained, UrlHelper class contains methods to deal with URL addresses within a MVC application. In particular, we can obtain a reference to a UrlHelper class within a Razor View by making use of Url built-in property from WebViewPage class. See more about here.
  • Validation Messages from Client and Server code.
    • For translating Server Side Validation Messages we will employ Resource Files.
    • For translating Client Side Validation Messages we will override default messages. As we will make use of jQuery Validation Plugin 1.11.1 to apply client validation, we'll have to override messages from this plugin. Localized messages will be saved in separate files based on supported languages. So, to gain access to localized script files we will extend again UrlHelper class.
  • Localizing entire Views might be necessary according to requirements of our application. So, we'll consider this issue.
    • In this demo, English (United States) and Spanish languages will not make use of this option but, with demo purposes, French language will. So, we will create a new derived ViewEngine from default RazorViewEngine to achieve this goal. This new view engine will look for Views through a previously-created folder tree.
  • Other Script and CSS files.
    • For large applications, perhaps it would be necessary to consider localized scripts and CSS files. The same strategy chosen with image files might be used. We will not dive into this issue, simply take into account.
  • Localized content from back-end storage components such as databases.
    • We'll not work with databases in this demo application. The article would be too long. Instead, we'll assume that, if necessary, information about current culture set on Thread will be provided to database from Data Access Layer.This way, corresponding translated texts or localized images should be returned accordingly. At least, bear in mind this if you're planning use localized content from databases.

Let's see some screenshot about our demo application:

Home page English(United States) version:

Home Page English ScreenShot

Home page Spanishversion:

It's a very simple application, but enough to get insight about multi-language key issues.

  • Home View page contains localized texts and images.
  • About View page just includes localized texts.
  • Contact View page contains also localized texts but it also includes a partial view with a form to post data and apply client and server validation over corresponding underlying model.
  • Shared Error View page will be translated as well.
  • A list of selecting flags are provided from layout view page.
 
URL Routing

First of all, we must accomplish with the fact of not serving different -language-based- content from the same URL. Configure appropiate URL Routing is mandatory to serve language-based content in accordance with different URLs. So, we will configure routing for including Multi-Language support by extracting specific culture from URL routing.

Our URLs addresses, on debug mode, will look like as it is shown below. I'm assuming that our demo application is being served on localhost with XXXXX port.

  • English (United States) language:
    • http://localhost:XXXXX/Home/Index or http://localhost:XXXXX/en-US/Home/Index
    • http://localhost:XXXXX/Home/About or http://localhost:XXXXX/en-US/Home/About
    • http://localhost:XXXXX/Home/Contact or http://localhost:XXXXX/en-US/Home/Contact
  • Spanish language:
    • http://localhost:XXXXX/es-ES/Home/Index
    • http://localhost:XXXXX/es-ES/Home/About
    • http://localhost:XXXXX/es-ES/Home/Contact
  • French language:
    • http://localhost:XXXXX/fr-FR/Home/Index
    • http://localhost:XXXXX/fr-FR/Home/About
    • http://localhost:XXXXX/fr-FR/Home/Contact

Furthermore, we will provide the user with a list of supporting languages in the Layout View Page. So, users always can get to the desired language by clicking on one of them. When users select a different language, we will use a Cookie to save this manual choice. The use of a Cookie might generate controversy. So, to use it or not is up to you. It's not a key point in the article. We will use it taking into account that we will never create Cookies from server side based on content of URL routing. So, if a given user never changes language manually, he will navigate in the language that he entered our website. Next time users get into our website, if the cookie exists, they will be redirected to the appropiate URL according to their last language selection. Anyway, remember again, never think of using only CookiesSession StateBrowser's Client user settings, etc. to serve different content from the same URL.

USING THE CODE

First steps to create our Multi-Language Application

I have taken the simple MVC 5 template given by Microsoft Visual Studio 2013 for starting to work, changing authentication options to No Authentication. As you can see below, the name of my demo application is MultiLanguageDemo. Then, I have rearranged folders as is shown below:

  • Notice folders under Contentdirectory. Personally, I like to have this structure for storing ImagesScriptsStylesand Texts. Each time you create a new directory and add classes to it, a new namespace is added by default with the folder's name. Take it into account. I have modified web.config in Views folder to include these new namespaces. Doing this, you can gain direct access to classes in these namespaces from Razor view code nuggets.

  • As en-US will be the default culture, it's necessary to configure web.config in accordance with:

  • We will use a custom GlobalHelperclass to include global common functionality such as reading current culture on Thread or default culture in web.config. Here is the code:
public class GlobalHelper
{
    public static string CurrentCulture
    {
        get
        {
            return Thread.CurrentThread.CurrentUICulture.Name;
        }
    }

    public static string DefaultCulture
    {
        get
        {
            Configuration config = WebConfigurationManager.OpenWebConfiguration("/");
            GlobalizationSection section = (GlobalizationSection)config.GetSection("system.web/globalization");
            return section.UICulture;
        }
    }     
}
 
Setting-up URL Routing

We'll have two routes, LocalizedDefaultand Default. We'll use langplaceholder to manage culture. Here is the code within RouteConfigclass in RouteConfig.cs file (see more about URL Routing):

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

        routes.MapRoute(
           name: "LocalizedDefault",
           url: "{lang}/{controller}/{action}",
           defaults: new { controller = "Home", action = "Index"},
           constraints: new {lang="es-ES|fr-FR|en-US"}
       );

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}",
            defaults: new { controller = "Home", action = "Index", lang = en-US }
        );
    }
}

On one hand, Defaultroute will be used to match URLs without specifying explicit culture. Therefore, we'll configure it for using default culture. Notice how langis set to en-US culture in defaultsparam.

On the other hand, LocalizedDefaultroute is configured to use specific culture on URLs. Besides, langparam is restricted to be included in supporting languages es-ESfr-FR or en-US. Notice how this is configured by setting constraintsparam in MapRoute method. This way we'll cover all previously-established routes.

Configuring Controllers to serve proper based-language content

As I said before, to switch culture is necessary to create a CultureInfo object to set the CurrentCulture and CurrentUICulture properties on the Thread that processes each http request sent to controllers. Using MVC 5, there are several ways of achieving this. In this case, I will create an abstract BaseController class from which the rest of controllers will be derived . The BaseController will contain common functionality and will override OnActionExecuting method from System.Web.Mvc.Controller class. The key point about OnActionExecuting method is to be aware of it is always called before a controller method is invoked.

At last, simply saying that another way of getting this would be by means of Global Action Filters instead of using a base class. It's not considered in this example, but bearing it in mind if you like more.

Let's have a look at our BaseController class code:

 public abstract class BaseController : Controller
 {
    private static string _cookieLangName = "LangForMultiLanguageDemo";

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        string cultureOnCookie = GetCultureOnCookie(filterContext.HttpContext.Request);
        string cultureOnURL = filterContext.RouteData.Values.ContainsKey("lang") 
            ? filterContext.RouteData.Values["lang"].ToString() 
            : GlobalHelper.DefaultCulture;
        string culture = (cultureOnCookie == string.Empty) 
            ? (filterContext.RouteData.Values["lang"].ToString()) 
            : cultureOnCookie;
        
        if (cultureOnURL != culture)
        {
            filterContext.HttpContext.Response.RedirectToRoute("LocalizedDefault", 
            new { lang=culture,
                    controller = filterContext.RouteData.Values["controller"],
                    action = filterContext.RouteData.Values["action"]
            });
            return;
        }

        SetCurrentCultureOnThread(culture);

        if (culture != MultiLanguageViewEngine.CurrentCulture)
        {
            (ViewEngines.Engines[0] as MultiLanguageViewEngine).SetCurrentCulture(culture);
        }

        base.OnActionExecuting(filterContext);
    }

    private static void SetCurrentCultureOnThread(string lang)
    {
        if (string.IsNullOrEmpty(lang))
            lang = GlobalHelper.DefaultCulture;
        var cultureInfo = new System.Globalization.CultureInfo(lang);
        System.Threading.Thread.CurrentThread.CurrentUICulture = cultureInfo;
        System.Threading.Thread.CurrentThread.CurrentCulture = cultureInfo;
    }

    public static String GetCultureOnCookie(HttpRequestBase request)
    {
        var cookie = request.Cookies[_cookieLangName];
        string culture = string.Empty;
        if (cookie != null)
        {
            culture= cookie.Value;
        }
        return culture;
    }

}

BaseControllerclass overrides OnActionExecutingmethod. Then, we get information about specific culture from URL Routing and Cookies. If there's no cookie, culture on Thread will be set from Url Routing. Otherwise, if a final user has selected manually a language and then a cookie exists, the http response will be redirected to the corresponding route containing language stored in cookie.

Additionally, to set current culture on Thread, BaseControlleruse SetCurrentCultureOnThreadprivate function. First, a new CultureInfo class is created based on specific culture passed as param. Finally, CurrentUICultureand CurrentCultureproperties from current Threadare assigned with previously created CultureInfo object.

 
Dealing with Plain Texts

To translate plain texts, we will use Resource Files. These are a great way of storing texts to be translated. The storage is based on a dictionary of key/value pairs, where keyis a string identifying a given resource and value is the translated textor localized image. Internally, all this information is saved in XML format and compiled dynamically by Visual Studio Designer.

Resource Files have a RESXextension. So, in this demo we will create three different Resource Filesfor the default culture. One for storing global texts, RGlobal.resx, another for general error messages, RError.resx, and the last for storing messages related to Home Controller, RHome.resx. I like to create this structure of resource files in my projects, normally including one resource file for each controller, but you can choose another way if you prefer.

For other supporting languages we will create resource files with names RGlobal.es-ES.resx, RError.es-ES.resx, RHome.es-ES.resx (Spanish) and RGlobal.fr-FR.resx, RError.fr-FR.resx and RHome.fr-FR.resx (French). Note the cultural code for each name. Here is our Resource Files tree:

Content Directory Tree

The most important points to know about are:

  • When you create a resource file for the default culture such as RGlobal.resx file, an internal class called RGlobalis auto-generated by Visual Studio. Using the designer, you should change the Access Modifier to public for using it in the solution. Let's have a look at our RGlobalfiles for English and Spanishlanguages:

 

  • Resources for each specific culture are compiled in separate assemblies, saved in different subdirectories according to culture and named as AssemblyName.resources.dll. In our case names will be MultiLanguageDemo.resources.dll
  • Once specific culture is set on Thread, the runtime will choose the assembly accordingly.
  • Individual resources can be consumed for controllers or other classes by concatenating the resource file name with the keyword. For instance RGlobal.AboutRGlobal.AppName, etc.
  • To use individual resources inside views with Razor syntax you just have to add code such as @RHome.Title@RHome.Subtitle or @RHome.Content.
 
Dealing with Images

As I said before, we will just store texts in Resource Files, although images might be saved too. Personally, I prefer to save images in another way. Let's have a look at our Imagesfolder under Contentdirectory.

As you can see, a specific folder has been created for each culture. Images not requiring localization will be saved directly in Images folder as France.jpg or Spain.jpg. These files just contain flags for showing and selecting languages and therefore they don't require localization. The rest of images requiring localization will be stored separately. For instance, welcome.jpg file, under en-US subdirectory contains a picture with text "Welcome", instead welcome.jpg file under es-ES subdirectory contains a drawing with text "Bienvenido".

Having said that, let's go on with our GetImagemethod extension in UrlHelperclass for selecting localized images. This static method will be contained in UrlHelperExtensionsstatic class inside a file called UrlHelperExtensions.cs under Extensions folder. Here is the code:

public static class UrlHelperExtensions
{
    public static string GetImage(this UrlHelper helper, 
        string imageFileName, 
        bool localizable=true)
    {
        string strUrlPath, strFilePath = string.Empty;
        if (localizable)
        {
            /* Look for current culture */
            strUrlPath = string.Format("/Content/Images/{0}/{1}", 
                GlobalHelper.CurrentCulture, 
                imageFileName);
            strFilePath = HttpContext.Current.Server.MapPath(strUrlPath);
            if (!File.Exists(strFilePath))
            {   /* Look for default culture  */
                strUrlPath = string.Format("/Content/{0}/Images/{1}", 
                GlobalHelper.DefaultCulture, 
                imageFileName);
            }
            return strUrlPath;
        }

        strUrlPath = string.Format("/Content/Images/{0}", imageFileName);
        strFilePath = HttpContext.Current.Server.MapPath(strUrlPath);
        if (File.Exists(strFilePath))
        {   /* Look for resources in general folder as last option */
            return strUrlPath;
        }

        return strUrlPath;
    }
}

We'll extend UrlHelperby adding a new GetImagemethod. This method will allow us to look for localized images under Imagesdirectory. We just need to call the method by passing to it the proper image filenames. There's another boolean param to set whether image is localized. If so, method will look for results inside the corresponding subdirectory based on current culture and if not encountered will try with default culture and general folder in that order. Anyway, first search should be enough if everything is well-configured.

Url is a property of System.Web.Mvc.WebViewPage class, from which all Razor Views are derived. This property returns a UrlHelperinstance. This way, we can gain access to our GetImagemethod.

 
Dealing with Validation Messages

We'll consider both server and client validation. To apply localization to server side validation we'll use Resource Files whereas to client validation we'll create a structure of directories similar to what we did with images. Then, we'll create new script files to override default messages according to supporting languages and we'll extend UrlHelper class to gain access to these new files.

Server Validation

Server validation is usually executed on controllers over models. If validation is not correct, model state dictionary object ModelStatethat contains the state of the model will be set as incorrect. In code, this is equal to set IsValidproperty of ModelStateto false. Consequently, ModelStatedictionary will be filled up with validation messages according to input fields, global validations, etc. these messages should be translated.

In this example I'm going to show how translate validation messages originated from Data Annotations. In MVC projects is very common to configure server validations by using classes contained in System.ComponentModel.DataAnnotations. Let's see an example.

This is the code related to Contact Modelto be applied to Contact View:

namespace MultiLanguageDemo.Models
{
    [MetadataType(typeof(ContactModelMetaData))]
    public partial class ContactModel
    {
        public string ContactName { get; set; }
        public string ContactEmail { get; set; }
        public string Message { get; set; }
    }

    public partial class ContactModelMetaData
    {
        [Required(ErrorMessageResourceName = "RequiredField", 
        ErrorMessageResourceType = typeof(RGlobal))]
        [Display(Name = "ContactName", ResourceType = typeof(RHome))]
        public string ContactName { get; set; }

        [Required(ErrorMessageResourceName = "RequiredField", 
        ErrorMessageResourceType = typeof(RGlobal))]
        [Display(Name = "ContactEmail", ResourceType = typeof(RHome))]
        [DataType(DataType.EmailAddress)]
        public string ContactEmail { get; set; }

        [Required(ErrorMessageResourceName = "RequiredField", 
        ErrorMessageResourceType = typeof(RGlobal))]
        [Display(Name = "Message", ResourceType = typeof(RHome))]
        public string Message { get; set; }
    }
}

On one hand, we have a ContactModel class with three simple properties. On the other hand, we have a ContactModelMetaData class used to apply validations over ContactModel and to set further functionality or metadata in order to show labels related to fields, data types, etc.

Regarding validation we are configuring all model fields as Required. So, to enforce localization, it's necessary to reference the auto-generated class associated with a Resource File. It is done by means of ErrorMessageResourceType property. We also have to configure, the keyword name related to the corresponding validation message that we want to show. It is done by using ErrorMessageResourceName property. This way, messages from Resource Files -being selected automatically based on culture- will be returned accordingly.

 
Client Validation

By using Client Validation is possible to execute validation in clients avoiding unnecessary requests to controllers. We'll make use of this feature by means of jQuery Validation Plugin 1.11.1and jQuery Validation Unobtrusive Plugin.References to these files are auto-generated when you start a new MVC 5 project by using Microsoft Visual Studio MVC 5 template project. You can enable Client Validation in web.configfile as is shown in figure below:

You can also enable/disable Client Validation directly from Viewsby means of inherited Htmlproperty from System.Web.Mvc.WebViewPage class. As it is shown at figure below, Htmlproperty within a Viewreturns a HtmlHelperobject that contains EnableClientValidationand EnableUnobtrusiveJavaScriptmethods. Once Client Validation is enabled, HtmlHelperclass is allowed to write client validation code automatically.

In our demo application we're employing jQuery Validation Pluginsto perform validation.So, default messages are shown in Englishbut we need to supply translated messages for all supporting languages. To achieve this, we will extend the plugin. First, we'll create a directory tree as it's shown at picture below.

Then, for each supported language we'll create a javascriptfile to override default messages according to culture running on current Thread. Here is the code related to Spanishlanguage:

jQuery.extend(jQuery.validator.messages, {
  required: "Este campo es obligatorio.",
  remote: "Por favor, rellena este campo.",
  email: "Por favor, escribe una dirección de correo válida",
  url: "Por favor, escribe una URL válida.",
  date: "Por favor, escribe una fecha válida.",
  dateISO: "Por favor, escribe una fecha (ISO) válida.",
  number: "Por favor, escribe un número entero válido.",
  digits: "Por favor, escribe sólo dígitos.",
  creditcard: "Por favor, escribe un número de tarjeta válido.",
  equalTo: "Por favor, escribe el mismo valor de nuevo.",
  accept: "Por favor, escribe un valor con una extensión aceptada.",
  maxlength: jQuery.validator.format("Por favor, no escribas más de {0} caracteres."),
  minlength: jQuery.validator.format("Por favor, no escribas menos de {0} caracteres."),
  rangelength: jQuery.validator.format("Por favor, escribe un valor entre {0} y {1} caracteres."),
  range: jQuery.validator.format("Por favor, escribe un valor entre {0} y {1}."),
  max: jQuery.validator.format("Por favor, escribe un valor menor o igual a {0}."),
  min: jQuery.validator.format("Por favor, escribe un valor mayor o igual a {0}.")
});

I'm taken for granted that jQuery and jQuery Validation Plugin are loaded before these files are. Anyway, for referencing these files from a view that require client validation, we have to use the following code:

@Scripts.Render("~/bundles/jqueryval")
@if (this.Culture != GlobalHelper.DefaultCulture)
{
 
}

As I did before, I have extended UrlHelperclass to add a new method GetScriptfor searching localized script files. Then, I make use of it by referencing jQuery.validate.extension.js file right after loading jQuery Validation plugin, but only if current culture is different from default one.

As a consequence of all previously-mentioned, when we try to send our Contact Viewwithout filling up any required field, we obtain the following validation messages accoding to Englishand Spanishlanguages.

Validation messages for English language:

Validation messages for Spanish languages:

At last, here is a code snippet from _Contactpartial view in _Contact.cshtml file:

This partial view contains a simple form to post data. If this partial view is rendered from a Getmethod on http request, a form will be shown. Instead, if it's rendered after a Postrequest sending data, result from Postwill be displayed. Nothing more to say, except if you want to dive into source code I'm using a Post-Redirect-Get pattern to do this (see more about).

Focusing on validation, I'd like to point out that when client validation is activated, some methods from HtmlHelper class such as ValidationMessageFor (see picture above), are enabled to write html code to manage validation for each input field according to annotations in model metadata classes. For simple validations you don't need to do anything else.

 
Dealing with Localizing Entire Views

So far, we have achieved pretty much everything regarding localization for not very complex large applications. These might demand new features such as localizing Entire Views. That is, Views must be very different for each culture. Therefore, we need to add new features to our application. I'm going to apply this case to the Frenchculture. Views for this language will be different. What do we need to reach this goal? To begin with, we need to create new specific Viewsfor this language. Secondly we must be able to reference these Viewswhen Frenchculture is selected. At last, all previously explained should work well too. Let's see how we can accomplish all of this.

First, we'll create a directory tree under Views directory as is shown below:

Notice fr-FR subdirectory under Homedirectory. It will contain specific Views for Frenchculture. Views directly under Home directory will be used for Defaultand Spanishculture. If there were more controllers than Home Controller, the same strategy should be taken.

At this point, we have to supply a way of selecting template Viewsbased on culture. For this, we will create a custom ViewEngine derived from RazorViewEngine (more about here). We'll call this engine MultiLanguageViewEngine. Briefly explained, view engines are responsible for searching, retrieving and rendering views, partial views and layouts. By default there are two view engines pre-loaded when you run a MVC 5 Web application: Razor and ASPX View Engine. However, we can remove them or add new custom view engines, normally in Application_Start method in Global.asax file. In this case, we'll unload pre-existing default view engines to add our MultiLanguageViewEngine.It will do the same as RazorViewEngine but additionally and according to culture will look up for specific subdirectories containing localized entire view templates. Let's have a look at code stored in MultiLanguageViewEngine.cs file under App_code folder:

namespace MultiLanguageDemo
{
    public class MultiLanguageViewEngine : RazorViewEngine
    {
        private static string _currentCulture = GlobalHelper.CurrentCulture;

        public MultiLanguageViewEngine()
            : this(GlobalHelper.CurrentCulture){
        }

        public MultiLanguageViewEngine(string lang)
        {
            SetCurrentCulture(lang);
        }

        public void SetCurrentCulture(string lang)
        {
           _currentCulture = lang;
           ICollection arViewLocationFormats = 
                new string[] { "~/Views/{1}/" + lang + "/{0}.cshtml" };
           ICollection arBaseViewLocationFormats = new string[] { 
                @"~/Views/{1}/{0}.cshtml", 
                @"~/Views/Shared/{0}.cshtml"};
           this.ViewLocationFormats = arViewLocationFormats.Concat(arBaseViewLocationFormats).ToArray();
        }

        public static string CurrentCulture
        {
            get { return _currentCulture; }
        }
    }
}

To begin with, notice how MultiLanguageViewEngine inherits from RazorViewEngine. Then, I have added a constructor for getting supporting languages. This constructor will set new locations where looking for localized entire views by making use of new SetCurrentCulturemethod. This method set a new location to look for views based on langparam. This new path is inserting at first position in the array of locations to search. and the array of strings is saved in ViewLocationFormatsproperty. Besides, MultiLanguageViewEnginewill return the specific culture used for setting this property.

That being said, how to deal with MultiLanguageViewEngine? First, we'll create a new instance of this view engine in Application_Startmethod in Global.asax file. Secondly, we'll switch current culture for the custom view engine right after setting culture on Thread. More in detail, we'll override OnActionExecutingmethod on our BaseControllerclass. I remind you this method is always called before any method on controller is invoked.

Let's see Application_Startmethod in Global.asax file:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new MultiLanguageViewEngine());
}

Bolded code shows how to unload collection of pre-load view engines and how to load our new MultiLanguageViewEngine.

Now, let'see again OnActionExecutingmethod in BaseControllerclass focusing on this:

 protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    string cultureOnCookie = GetCultureOnCookie(filterContext.HttpContext.Request);
    string cultureOnURL = filterContext.RouteData.Values.ContainsKey("lang")
          ? filterContext.RouteData.Values["lang"].ToString() 
          : GlobalHelper.DefaultCulture;
    string culture = (cultureOnCookie == string.Empty) 
           ? (filterContext.RouteData.Values["lang"].ToString()) 
          : cultureOnCookie;
    
    if (cultureOnURL != culture)
    {
        filterContext.HttpContext.Response.RedirectToRoute("LocalizedDefault", 
            new { lang=culture,
                    controller = filterContext.RouteData.Values["controller"],
                    action = filterContext.RouteData.Values["action"]
            });
        return;
    }

    SetCurrentCultureOnThread(culture);
    
    if (culture != MultiLanguageViewEngine.CurrentCulture)
    {
        (ViewEngines.Engines[0] as MultiLanguageViewEngine).SetCurrentCulture(culture);
    }
    

    base.OnActionExecuting(filterContext);
}

Bolded code above shows how to update our custom view engine. If current culture on Thread, stored in culture variable,is different from current culture in MultiLanguageViewEngine, our new engine is updated to be synchronized with Thread. We gain access to MultiLanguageViewEngine through Engines collection property of ViewEnginesclass with zero index. Take into account that we unloaded pre-loaded view engines in global.asax file to add only MultiLanguageViewEngine. So, it is in the first place.

 
Switching languages from User Interface

As it is shown in previous screenshots, our demo application will have a list of flags to switch language, placed in the lower right corner. So, this functionality will be included in  _Layout.cshtml file. This file will contain the layout for every view in the project.

On one hand, here is a excerpt of html code to render flags. It is a simple option list to show flags representing supporting languages. Once a language is set, the selected flag will be highlighted with a solid green border.

To handle user selections we'll include javascript code. To begin with, I have created a javascript file multiLanguageDemo.jsto include common functionality to the application. Basically, this file contains functions to read and write cookies. It is based on "namespace pattern" (see more about here) Needless, this file is contained in Scripts folder.

Once a user clicks an option, a cookie with the selected language will be created. After this, the page will be reloaded to navigate to the corresponding URL based on specified language. Here is the jQuery code to get this:

You must notice use of MultiLanguageDemo.Cookies.getCookie to read cookie value and MultiLanguageDemo.Cookies.SetCookie to set value on cookie. Besides, when some flag is clicked, javascriptcode sets an active-lang class to the selected flag, captures language from data-lang attribute and reload view page.

POINTS OF INTEREST

I have had a great time building this little demo application. Anyway, it has been hard to try to explain in detail somethings not in my mother tongue. Sorry about.

ENVIRONMENT

This demo has been developped using Microsoft Visual Studio 2013 for the Webwith .Net Framework 4.5 and MVC 5. Other main components used have been jQuery JavaScript Library v1.10.2,jQuery Validation Plugin 1.11.1jQuery Validation Unobtrusive Plugin and Bootstrap v3.0.0.

HISTORY

It's the first version of the article. In later reviews I would like to add some improvements about providing the application with some examples using both globalization issues and localized content from databases objects such as tables, stored procedures, etc.

Add comment