Click here to Skip to main content
15,881,139 members
Articles / Web Development / HTML

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

Rate me:
Please Sign up or sign in to vote.
4.97/5 (29 votes)
13 Dec 2016CPOL20 min read 109.7K   6.1K   33   34
This article explains how to create a simple Multi-Language ASP.NET MVC 5 Web Application. I'll show how to translate texts, localize images or entire views as well as how to deal with URL routing to support several languages.

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 Index, About 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 Globalization and Localization.
  • Configure URL Routing for 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 CultureInfo object 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 from Url 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-US, es-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 localized according 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 Spanish version:

Home Page Spanish ScreenShot

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 Cookies, Session State, Browser'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:

Image 3

  • Notice folders under Content directory. Personally, I like to have this structure for storing Images, Scripts, Styles and 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.

Image 4

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

Image 5

  • We will use a custom GlobalHelper class 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, LocalizedDefault and Default. We'll use lang placeholder to manage culture. Here is the code within RouteConfig class 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, Default route will be used to match URLs without specifying explicit culture. Therefore, we'll configure it for using default culture. Notice how lang is set to en-US culture in defaults param.

On the other hand, LocalizedDefault route is configured to use specific culture on URLs. Besides, lang param is restricted to be included in supporting languages es-ES, fr-FR or en-US. Notice how this is configured by setting constraints param 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;
    }

}

BaseController class overrides OnActionExecuting method. 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, BaseController use SetCurrentCultureOnThread private function. First, a new CultureInfo class is created based on specific culture passed as param. Finally, CurrentUICulture and CurrentCulture properties from current Thread are 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 key is a string identifying a given resource and value is the translated text or localized image. Internally, all this information is saved in XML format and compiled dynamically by Visual Studio Designer.

Resource Files have a RESX extension. So, in this demo we will create three different Resource Files for 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 RGlobal is 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 RGlobal files for English and Spanish languages:

Resource File English

Resource File Spanish

  • 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.About, RGlobal.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 Images folder under Content directory.

Content Image Directory Tree

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 GetImage method extension in UrlHelper class for selecting localized images. This static method will be contained in UrlHelperExtensions static 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 UrlHelper by adding a new GetImage method. This method will allow us to look for localized images under Images directory. 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.

A typical call within a template View would be:

<img src="@Url.GetImage("Welcome.jpg")" alt="@RGlobal.Welcome"/>

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

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 ModelState that contains the state of the model will be set as incorrect. In code, this is equal to set IsValid property of ModelState to false. Consequently, ModelState dictionary 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 Model to 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.1 and 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.config file as is shown in figure below:

Web.Config Views Root

You can also enable/disable Client Validation directly from Views by means of inherited Html property from System.Web.Mvc.WebViewPage class. As it is shown at figure below, Html property within a View returns a HtmlHelper object that contains EnableClientValidation and EnableUnobtrusiveJavaScript methods. Once Client Validation is enabled, HtmlHelper class is allowed to write client validation code automatically. 

Client Validation Setting in Views

In our demo application we're employing jQuery Validation Plugins to perform validationSo, default messages are shown in English but 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.

Scripts Directory Tree for Client Validation

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

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)
{
    <script src="@Url.GetScript("jquery.validate.extension.js")" defer></script>
}

As I did before, I have extended UrlHelper class to add a new method GetScript for 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 View without filling up any required field, we obtain the following validation messages accoding to English and Spanish languages.

Validation messages for English language:

Image 13

Validation messages for Spanish languages:

Image 14

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

Contact Partial View

This partial view contains a simple form to post data. If this partial view is rendered from a Get method on http request, a form will be shown. Instead, if it's rendered after a Post request sending data, result from Post will 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 French culture. Views for this language will be different. What do we need to reach this goal? To begin with, we need to create new specific Views for this language. Secondly we must be able to reference these Views when French culture 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:

Views Directory Tree

Notice fr-FR subdirectory under Home directory. It will contain specific Views for French culture. Views directly under Home directory will be used for Default and Spanish culture. 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 Views based 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<string> arViewLocationFormats = 
                new string[] { "~/Views/{1}/" + lang + "/{0}.cshtml" };
           ICollection<string> arBaseViewLocationFormats = new string[] { 
                @"~/Views/{1}/{0}.cshtml", 
                @"~/Views/Shared/{0}.cshtml"};
           this.ViewLocationFormats = arViewLocationFormats.Concat(arBaseViewLocationFormats).ToArray();
        }

        public static string CurrentCulture
        {
            get { return _currentCulture; }
        }
    }
}
</string></string></string>

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 SetCurrentCulture method. This method set a new location to look for views based on lang param. This new path is inserting at first position in the array of locations to search. and the array of strings is saved in ViewLocationFormats property. Besides, MultiLanguageViewEngine will 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_Start method 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 OnActionExecuting method on our BaseController class. I remind you this method is always called before any method on controller is invoked.

Let's see Application_Start method 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 OnActionExecuting method in BaseController class 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 ViewEngines class 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.

Image 17

To handle user selections we'll include javascript code. To begin with, I have created a javascript file multiLanguageDemo.js to 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:

Image 18

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, javascript code 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 Web with .Net Framework 4.5 and MVC 5. Other main components used have been jQuery JavaScript Library v1.10.2, jQuery Validation Plugin 1.11.1, jQuery 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.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Spain Spain
Telecommunication Engineer by University of Zaragoza, Spain.

Passionate Software Architect, MCPD, MCAD, MCP. Over 10 years on the way building software.

Mountain Lover, Popular Runner, 3km 8'46, 10km 31'29, Half Marathon 1h08'50, Marathon 2h27'02. I wish I could be able to improve, but It's difficult by now

Comments and Discussions

 
Questioninstead of resource file why not data table? Pin
Zipadie Doodah28-Feb-24 10:32
Zipadie Doodah28-Feb-24 10:32 
QuestionWhat Changes are required in MultiLanguageViewEngine for localized views with in Areas Pin
kuldeepleo26-Apr-20 23:54
kuldeepleo26-Apr-20 23:54 
I have made changes in MultiLanguageViewEngine -

ICollection<string> arAreaViewLocationFormats =
                 new string[] { "~/Areas/{2}/Views/{1}/" + lang + "/{0}.cshtml" };
            ICollection<string> arAreaBaseViewLocationFormats = new string[] {
                @"~/Areas/{2}/Views/{1}/{0}.cshtml",
                @"~/Areas/{2}/Views/Shared/{0}.cshtml"};

            this.AreaViewLocationFormats= arAreaViewLocationFormats.Concat(arAreaBaseViewLocationFormats).ToArray();

            ICollection<string> arViewLocationFormats =
                 new string[] { "~/Views/{1}/" + lang + "/{0}.cshtml" };
            ICollection<string> arBaseViewLocationFormats = new string[] {
                @"~/Views/{1}/{0}.cshtml",
                @"~/Views/Shared/{0}.cshtml"};

            this.ViewLocationFormats= arViewLocationFormats.Concat(arBaseViewLocationFormats).ToArray();


But this code only works for localized views which have the same culture as default.
For other cultures the localized views (only those which is with in area) are not getting picked.

From yellow screen it seems view engine is not searching view with in areas for other cultures.
QuestionRenderPartial Pin
Member 1077454628-Aug-19 5:15
Member 1077454628-Aug-19 5:15 
AnswerSolution AJAX Pin
rudolf cruz6-Jun-19 8:35
rudolf cruz6-Jun-19 8:35 
QuestionJesonResult Pin
rudolf cruz6-Jun-19 8:00
rudolf cruz6-Jun-19 8:00 
Questionhow to add custom font with resources Pin
ko ko maung18-Mar-19 4:32
professionalko ko maung18-Mar-19 4:32 
QuestionAjax seems will redirect to xx-XX/controller/action which lead to parameter missing Pin
siew peng29-Nov-18 15:39
siew peng29-Nov-18 15:39 
AnswerRe: Ajax seems will redirect to xx-XX/controller/action which lead to parameter missing Pin
vahid borandeh12-Mar-20 3:19
vahid borandeh12-Mar-20 3:19 
GeneralRe: Ajax seems will redirect to xx-XX/controller/action which lead to parameter missing Pin
vahid borandeh12-Mar-20 11:21
vahid borandeh12-Mar-20 11:21 
QuestionThe pages are loaded twice and the parameter values are lost Pin
zZMaxZz13-Nov-18 6:07
zZMaxZz13-Nov-18 6:07 
AnswerRe: The pages are loaded twice and the parameter values are lost Pin
Member 784441717-Nov-18 3:12
Member 784441717-Nov-18 3:12 
GeneralRe: The pages are loaded twice and the parameter values are lost Pin
siew peng29-Nov-18 15:45
siew peng29-Nov-18 15:45 
AnswerRe: The pages are loaded twice and the parameter values are lost Pin
vahid borandeh12-Mar-20 3:20
vahid borandeh12-Mar-20 3:20 
QuestionMissing Script tree client validation Image Pin
Vinod Kumar Sabbavarapu4-Jul-18 4:58
Vinod Kumar Sabbavarapu4-Jul-18 4:58 
QuestionAttribute Routes Pin
Kirill Borunov16-Apr-18 2:04
professionalKirill Borunov16-Apr-18 2:04 
QuestionLocalized views are not displayed Pin
Member 1010336515-Apr-18 7:35
Member 1010336515-Apr-18 7:35 
AnswerRe: Localized views are not displayed Pin
Member 133987332-May-18 3:25
Member 133987332-May-18 3:25 
Questionhow to change dir for arabic Pin
Member 1305152713-Apr-18 20:12
Member 1305152713-Apr-18 20:12 
QuestionRelated with Model Pin
Member 130500766-Apr-18 1:27
Member 130500766-Apr-18 1:27 
QuestionMultiLanguageViewEngine NullPointerException Pin
Alessandro Bellone11-Jan-18 6:14
Alessandro Bellone11-Jan-18 6:14 
QuestionWrong controller handler Pin
squizzy9-Jan-18 20:28
squizzy9-Jan-18 20:28 
QuestionLocalized views are not displayed Pin
roman-f4-Jan-18 0:11
roman-f4-Jan-18 0:11 
BugRe: Localized views are not displayed Pin
Member 1043531917-Feb-18 2:29
Member 1043531917-Feb-18 2:29 
GeneralRe: Localized views are not displayed Pin
Member 133987332-May-18 3:26
Member 133987332-May-18 3:26 
AnswerRe: Localized views are not displayed Pin
Member 135903697-Jun-18 8:08
Member 135903697-Jun-18 8:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.