web 2.0

ASP.NET MVC 5 Internationalization



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 5 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).

Anyway, Internationalization is often abbreviated to "I18N". The abbreviation takes the first and last letters and the number of letters between them, so 18 stands for the number of letters between the first "I" and the last "N". The same applies to Globalization (G11N), and Localization (L10N).

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".

Now, let’s review the terms used so far:

  • Globalization (G11N): The process of making an application support different languages and regions.
  • Localization (L10N): The process of customizing an application for a given language and region.
  • Internationalization (I18N): Describes both globalization and localization.
  • Culture: It is a language and, optionally, a region.
  • Locale: A locale is the same as a culture.
  • Neutral culture: A culture that has a specified language, but not a region. (e.g. "en", "es")
  • Specific culture: A culture that has a specified language and region. (e.g. "en-US", "en-GB", "es-CL")

Why do we need a region? Isn’t a language alone enough?

You might not need a region at all. It is true that English in the United States is not the same as English in the United Kingdom but if your application just shows English text readable to people from these English-speaking countries, you will not need a region. The problem arises when you need to deal with numbers, dates, and currencies. For example, compare the following output for two different Spanish-speaking regions (Chile, Mexico):

int value = 5600;

Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("es-CL");

Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("es-MX");

// Output
26-07-2011 // Date in es-CL, Spanish (Chile)
$5.600,00 // Currency in es-CL, Spanish (Chile)

26/07/2011 // Date in es-MX, Spanish (Mexico)
$5,600.00 // Currency in es-MX, Spanish (Mexico)

You can notice the difference in date and currency format. The decimal separator in each region is different and can confuse people in the other region. If a Mexican user types one thousand in their culture "1,000", it will be interpreted as 1 (one) in a Chilean culture website. We mainly need regions for this type of reasons and not much for the language itself.

How to Support Different Languages in ASP.NET MVC

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


  1. By using resource strings in all our site views.
  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 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. Some project requirements force developers to implement different views per language. But sometimes you have no choice where layout has to be different like right-to-left languages. Even if you set dir="rtl", this may not be enough in real applications unless the project’s UI layout is really simple. Perhaps, a mix of the two is the best. Anyway, for this example, it makes sense to use resources since we won’t have any issue with the layout for the Spanish, English, and Arabic languages that we will use.

How can ASP.NET guess the user’s language?

On each HTTP request, there is a header field called Accept-Language which determines which languages the user’s browser supports:

        Accept-Language: en-us,en;q=0.5

This means that my browser prefers English (United States), but it can accept other types of English. The "q" parameter indicates an estimate of the user’s preference for that language. You can control the list of languages using your web browser.

Internet Explorer


Globalizing our Web Site

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

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

Creating the Model

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


public class Person
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public string Email { get; set; }
        public string Biography { get; set; }


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:


    public class Person
        [Display(Name = "FirstName", ResourceType = typeof(Resources.Resources))]    
        [Required(ErrorMessageResourceType = typeof(Resources.Resources),
                  ErrorMessageResourceName = "FirstNameRequired")]
        [StringLength(50, ErrorMessageResourceType = typeof(Resources.Resources),
                          ErrorMessageResourceName = "FirstNameLong")]
        public string FirstName { get; set; }
        [Display(Name = "LastName", ResourceType = typeof(Resources.Resources))]    
        [Required(ErrorMessageResourceType = typeof(Resources.Resources),
                  ErrorMessageResourceName = "LastNameRequired")]
        [StringLength(50, ErrorMessageResourceType = typeof(Resources.Resources),
                          ErrorMessageResourceName = "LastNameLong")]
        public string LastName { get; set; }
        [Display(Name = "Age", ResourceType = typeof(Resources.Resources))]    
        [Required(ErrorMessageResourceType = typeof(Resources.Resources),
                  ErrorMessageResourceName = "AgeRequired")]
        [Range(0, 130, ErrorMessageResourceType = typeof(Resources.Resources),
                       ErrorMessageResourceName = "AgeRange")]
        public int Age { get; set; }
        [Display(Name = "Email", ResourceType = typeof(Resources.Resources))]    
        [Required(ErrorMessageResourceType = typeof(Resources.Resources),
                  ErrorMessageResourceName = "EmailRequired")]
        [RegularExpression(".+@.+\\..+", ErrorMessageResourceType = typeof(Resources.Resources),
                                         ErrorMessageResourceName = "EmailInvalid")]
        public string Email { get; set; }
        [Display(Name = "Biography", ResourceType = typeof(Resources.Resources))]    
        public string Biography { get; set; }


Localizing Data Annotations Validation Messages

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 "Resources".

Now right click on "Resources" 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:

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.resx" and add the following names and values like below:

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:

We need to reference "Resources" 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 "Resources" project from Projects tab.


We need to extract the English text from all the views and move it to the resource files. Here is a trick, instead of typing the namespace name each time (e.g. "@Resources.Resources.LogOn "), we can add this namespace to the views Web.config and type "@Resources.LogOn" instead. Open the Web.config file under the views folder.

Determining Culture

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 public place. 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:

    public class BaseController : Controller
        protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
            string cultureName = null;
            // Attempt to read the culture cookie from Request
            HttpCookie cultureCookie = Request.Cookies["_culture"];
            if (cultureCookie != null)
                cultureName = cultureCookie.Value;
                cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ? 
                        Request.UserLanguages[0] :  // obtain it from HTTP header AcceptLanguages
            // Validate culture name
            cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
            // Modify current thread's cultures            
            Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
            Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
            return base.BeginExecuteCore(callback, state);

Make sure all controllers in this project inherit from this BaseController. The base controller checks if the cookie exists, and sets the current thread cultures 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.

CultureHelper Class

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


    public static class CultureHelper
        // Valid cultures
        private static readonly List<string> _validCultures = new List<string> { "af", "af-ZA", "sq", "sq-AL", "gsw-FR", "am-ET", "ar", "ar-DZ", "ar-BH", "ar-EG", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY", "ar-MA", "ar-OM", "ar-QA", "ar-SA", "ar-SY", "ar-TN", "ar-AE", "ar-YE", "hy", "hy-AM", "as-IN", "az", "az-Cyrl-AZ", "az-Latn-AZ", "ba-RU", "eu", "eu-ES", "be", "be-BY", "bn-BD", "bn-IN", "bs-Cyrl-BA", "bs-Latn-BA", "br-FR", "bg", "bg-BG", "ca", "ca-ES", "zh-HK", "zh-MO", "zh-CN", "zh-Hans", "zh-SG", "zh-TW", "zh-Hant", "co-FR", "hr", "hr-HR", "hr-BA", "cs", "cs-CZ", "da", "da-DK", "prs-AF", "div", "div-MV", "nl", "nl-BE", "nl-NL", "en", "en-AU", "en-BZ", "en-CA", "en-029", "en-IN", "en-IE", "en-JM", "en-MY", "en-NZ", "en-PH", "en-SG", "en-ZA", "en-TT", "en-GB", "en-US", "en-ZW", "et", "et-EE", "fo", "fo-FO", "fil-PH", "fi", "fi-FI", "fr", "fr-BE", "fr-CA", "fr-FR", "fr-LU", "fr-MC", "fr-CH", "fy-NL", "gl", "gl-ES", "ka", "ka-GE", "de", "de-AT", "de-DE", "de-LI", "de-LU", "de-CH", "el", "el-GR", "kl-GL", "gu", "gu-IN", "ha-Latn-NG", "he", "he-IL", "hi", "hi-IN", "hu", "hu-HU", "is", "is-IS", "ig-NG", "id", "id-ID", "iu-Latn-CA", "iu-Cans-CA", "ga-IE", "xh-ZA", "zu-ZA", "it", "it-IT", "it-CH", "ja", "ja-JP", "kn", "kn-IN", "kk", "kk-KZ", "km-KH", "qut-GT", "rw-RW", "sw", "sw-KE", "kok", "kok-IN", "ko", "ko-KR", "ky", "ky-KG", "lo-LA", "lv", "lv-LV", "lt", "lt-LT", "wee-DE", "lb-LU", "mk", "mk-MK", "ms", "ms-BN", "ms-MY", "ml-IN", "mt-MT", "mi-NZ", "arn-CL", "mr", "mr-IN", "moh-CA", "mn", "mn-MN", "mn-Mong-CN", "ne-NP", "no", "nb-NO", "nn-NO", "oc-FR", "or-IN", "ps-AF", "fa", "fa-IR", "pl", "pl-PL", "pt", "pt-BR", "pt-PT", "pa", "pa-IN", "quz-BO", "quz-EC", "quz-PE", "ro", "ro-RO", "rm-CH", "ru", "ru-RU", "smn-FI", "smj-NO", "smj-SE", "se-FI", "se-NO", "se-SE", "sms-FI", "sma-NO", "sma-SE", "sa", "sa-IN", "sr", "sr-Cyrl-BA", "sr-Cyrl-SP", "sr-Latn-BA", "sr-Latn-SP", "nso-ZA", "tn-ZA", "si-LK", "sk", "sk-SK", "sl", "sl-SI", "es", "es-AR", "es-BO", "es-CL", "es-CO", "es-CR", "es-DO", "es-EC", "es-SV", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PY", "es-PE", "es-PR", "es-ES", "es-US", "es-UY", "es-VE", "sv", "sv-FI", "sv-SE", "syr", "syr-SY", "tg-Cyrl-TJ", "tzm-Latn-DZ", "ta", "ta-IN", "tt", "tt-RU", "te", "te-IN", "th", "th-TH", "bo-CN", "tr", "tr-TR", "tk-TM", "ug-CN", "uk", "uk-UA", "wen-DE", "ur", "ur-PK", "uz", "uz-Cyrl-UZ", "uz-Latn-UZ", "vi", "vi-VN", "cy-GB", "wo-SN", "sah-RU", "ii-CN", "yo-NG" };
        // Include ONLY cultures you are implementing
        private static readonly List<string> _cultures = new List<string> {
            "en-US",  // first culture is the DEFAULT
            "es", // Spanish NEUTRAL culture
            "ar"  // Arabic NEUTRAL culture
        /// <summary>
        /// Returns true if the language is a right-to-left language. Otherwise, false.
        /// </summary>
        public static bool IsRighToLeft()
            return System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.IsRightToLeft;

        /// <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 GetImplementedCulture(string name)
            // make sure it's not null
            if (string.IsNullOrEmpty(name))
                return GetDefaultCulture(); // return Default culture
            // make sure it is a valid culture first
            if (_validCultures.Where(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Count() == 0)
                return GetDefaultCulture(); // return Default culture if it is invalid
            // if it is implemented, accept it
            if (_cultures.Where(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Count() > 0)
                return name; // accept it
            // 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)  
            var n = GetNeutralCulture(name);
            foreach (var c in _cultures)
                if (c.StartsWith(n))
                    return c;
            // else 
            // It is not implemented
            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[0]; // return Default culture
        public static string GetCurrentCulture()
            return Thread.CurrentThread.CurrentCulture.Name;
        public static string GetCurrentNeutralCulture()
            return GetNeutralCulture(Thread.CurrentThread.CurrentCulture.Name);
        public static string GetNeutralCulture(string name)
            if (!name.Contains("-")) return name;
            return name.Split('-')[0]; // Read first part only. E.g. "en", "es"


We should populate "_cultures" manually. The "_cultures" dictionary stores the list of culture names our site supports. The nice part of this utility class is that it serves similar cultures. For example, if a user is visiting our site from the United Kingdom (en-GB), a culture which is not implemented in our site, he or she will see our site in English using "en-US" (English, United States). This way, you don't have to implement all cultures unless you really care about currency, date format, etc. One important thing to mention is that we added all specific cultures for the Spanish language. For example, we are implementing "es" Spanish in general without any region where a date looks like this "30/07/2011". Now suppose a nerd is coming from Chile (es-CL), it would be very nice display dates in their culture format (30-07-2011) instead of the neutral one. It does not matter if we don’t have a resource file for any of these cultures. ResourceManager can pick a neutral culture when it cannot find a specific culture file, this automatic mechanism is called fallback.


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:


    public class HomeController : BaseController
        public ActionResult Index()
            return View();
        public ActionResult Index(Person per)
            return View();
        public ActionResult SetCulture(string culture)
            // Validate input
            culture = CultureHelper.GetImplementedCulture(culture);
            // Save culture in a cookie
            HttpCookie cookie = Request.Cookies["_culture"];
            if (cookie != null)
                cookie.Value = culture;   // update cookie value
                cookie = new HttpCookie("_culture");                
                cookie.Value = culture;
                cookie.Expires = DateTime.Now.AddYears(1);
            return RedirectToAction("Index");


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:

Click OK and replace the existing view if any. 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:


@model MvcInternationalization.Models.Person
    ViewBag.Title = Resources.AddPerson;
    var culture = System.Threading.Thread.CurrentThread.CurrentUICulture.Name.ToLowerInvariant();
@helper selected(string c, string culture)
    if (c == culture)
@using(Html.BeginForm("SetCulture", "Home"))
        <div class="control-group">
            <div class="controls">
                <label for="en-us">
                    <input name="culture" id="en-us" value="en-us" type="radio" @selected("en-us", culture) /> English
        <div class="control-group">
            <div class="controls">
                <label for="es">
                    <input name="culture" id="es" value="es" type="radio" @selected("es", culture) /> Español
        <div class="control-group">
            <div class="controls">
                <label for="ar">
                    <input name="culture" id="ar" value="ar" type="radio" @selected("ar", culture) /> العربية
@using (Html.BeginForm()) 
    <div class="form-horizontal">        
        <hr />
        <div class="form-group">
            @Html.LabelFor(model => model.FirstName, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.FirstName)
                @Html.ValidationMessageFor(model => model.FirstName)
        <div class="form-group">
            @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.LastName)
                @Html.ValidationMessageFor(model => model.LastName)
        <div class="form-group">
            @Html.LabelFor(model => model.Age, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Age)
                @Html.ValidationMessageFor(model => model.Age)
        <div class="form-group">
            @Html.LabelFor(model => model.Email, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email)
                @Html.ValidationMessageFor(model => model.Email)
        <div class="form-group">
            @Html.LabelFor(model => model.Biography, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Biography)
                @Html.ValidationMessageFor(model => model.Biography)
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="@Resources.Create" class="btn btn-default" />
@section Scripts {
    <script type="text/javascript">
        (function ($) {
            $("input[type = 'radio']").click(function () {
                $(this).parents("form").submit(); // post form


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.

Of course, we should not forget about partial views too


@using Microsoft.AspNet.Identity
@if (Request.IsAuthenticated)
    using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
    <ul class="nav navbar-nav navbar-right">
            @Html.ActionLink(User.Identity.GetUserName(), "Manage", "Account", routeValues: null, htmlAttributes: new { title = "Manage" })
        <li><a href="javascript:document.getElementById('logoutForm').submit()">@Resources.LogOff</a></li>
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink(Resources.Register, "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink(Resources.LogOn, "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>


Left-to-right or Right-to-left

HTML supports rtl languages too, so we need to make sure our main HTML tag has the appropriate direction. Modify the _Layout.cshtml file to look like the following:


 <!DOCTYPE html>
<html lang="@CultureHelper.GetCurrentNeutralCulture()" dir="@(CultureHelper.IsRighToLeft() ? "rtl" : "ltr")">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - ASP.NET MVC Internationalization</title>
    @Styles.Render("~/Content/css" + (CultureHelper.IsRighToLeft() ? "-rtl" : ""))
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                @Html.ActionLink("ASP.NET MVC Internationalization", "Index", "Home", null, new { @class = "navbar-brand" })
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">                    
    <div class="container body-content">
        <hr />
    @Scripts.Render("~/bundles/bootstrap" + (CultureHelper.IsRighToLeft() ? "-rtl" : ""))
    @RenderSection("scripts", required: false)


Now we need to provide basically two sets of CSS and JS files: One for left-to-right languages and one for right-to-left languages. Because MVC default template is using Bootstrap, we need to install the RTL version of bootstrap files. For this, open the package manager console (aka Nuget) by choosing Tools -> Library Package Manager -> Package Manager Console and type:

Install-Package Twitter.Bootstrap.RTL

Make sure you have the two files bootstrap-rtl.js and bootstrap-rtl.css

We need to create two bundles: one for RTL and one for LTR. Modify the file BundleConfig.cs and add the following:


           bundles.Add(new ScriptBundle("~/bundles/bootstrap-rtl").Include(

            bundles.Add(new StyleBundle("~/Content/css-rtl").Include(


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.




How to store culture in the URL instead of a cookie?

There can be different reasons why you want the culture to be part of your website url (such as search engine indexing). Anyway. First let's fist define the culture to be part of our routes. Edit RouteConfig.cs like below:


        public static void RegisterRoutes(RouteCollection routes)
                name: "Default",
                url: "{culture}/{controller}/{action}/{id}",
                defaults: new {culture = CultureHelper.GetDefaultCulture(), controller = "Home", action = "Index", id = UrlParameter.Optional }


Notice the we used the default culture in case it is missing. Now modify the base controller:


        protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
            string cultureName = RouteData.Values["culture"] as string; 

            // Attempt to read the culture cookie from Request
            if (cultureName == null)               
                cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ? Request.UserLanguages[0] : null; // obtain it from HTTP header AcceptLanguages

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

            if (RouteData.Values["culture"] as string != cultureName) {
                // Force a valid culture in the URL
                RouteData.Values["culture"] = cultureName.ToLowerInvariant(); // lower case too

                // Redirect user

            // Modify current thread's cultures            
            Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
            Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

            return base.BeginExecuteCore(callback, state);


The final step is to modify the SetCulture action in HomeController.cs


        public ActionResult SetCulture(string culture)
            // Validate input
            culture = CultureHelper.GetImplementedCulture(culture);
            RouteData.Values["culture"] = culture;  // set culture

            return RedirectToAction("Index");

NOTE: To force the default culture appear in the URL, simply set the default value for culture in RouteConfig.cs to string.Empty


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 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:


  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 resource files that contain translation of all string messages. (e.g. Resources.resx, Resources.es.resx, Resources.ar.resx, etc )
  4. Update views to use localized text.
  5. Localize javascript files.


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

Tags: , , , ,

ASP.NET | MVC | Website Optimization


@nonintanon Thailand, on 1/14/2011 6:41:44 PM Said:


Thanks for sharing. This is the technique I'm using as well. So you give me strong feeling that I'm walking through the right path Laughing

raamshalakha United States, on 2/21/2011 5:12:16 AM Said:


It is great supporting tool, some days ago my development team was asking about that tool , i could not tell this. I bookmarked and mail it, we will use it...thanks for help.

NerdGround Czech Republic, on 3/9/2011 2:01:28 AM Said:


I've missed this feature a lot in MVC 2, I used my custom internationalization based on xml with caching.

Rais Hussain Islamic Republic of Pakistan, on 5/30/2011 3:03:10 AM Said:

Rais Hussain

Nice article, I would be thankful to you if you would post the article about ASP.NET MVC 3 Internationalization using Database, because if I would need more languages and website like fifa then what technique should I use.

Thank you in advance, I hope you would help me out in this regard.

Nadeem United States, on 6/1/2011 3:28:47 PM Said:


@Rais Hussain,
You can implement as many languages as you want without using databases, you can use resource files. Of course, you can use databases but I do not recommend this approach since you might run into scalability issues.

I hope this helps!!

jim Russia, on 7/1/2011 12:53:08 AM Said:


Thanks for your prompt reply.Because your code is based on views than people including me think that it only works with views.

Marce; Netherlands, on 7/5/2011 2:51:26 AM Said:


Besides the views I like the approach. What I am missing here is the URL effect.

www.website.com/es for Spain
www.website.com/nl for Netherlands

How do you deal with this?

Adrichal United States, on 8/3/2011 11:46:20 PM Said:


I have missed this feature a lot in MVC 2, I used my custom internationalization based on xml with caching.

Nadeem Afana United States, on 8/7/2011 9:23:29 AM Said:

Nadeem Afana

H. MacCreight,
That was an example only. Check my next post afana.me/.../...c-internationalization-part-2.aspx

tanden bleken United States, on 8/22/2011 5:56:32 AM Said:

tanden bleken

I'm building a multi language web application for my daily job atm so this comes in real handy. Thank you for this Nadeem, it solves some questions/problems i had.

Chaussures d'enfant France, on 8/26/2011 3:45:26 AM Said:

Chaussures d'enfant

Good article.Congratulations to you. I put it in my bookmarks

Chris Netherlands, on 8/28/2011 11:03:47 AM Said:


Does anybody now where I find cues on how to create my own localized version? changing CultureHelper to False is not working for me

Nadeem Afana United States, on 8/28/2011 11:06:10 AM Said:

Nadeem Afana

See part 2

Bill Yeager United States, on 11/19/2011 7:34:18 AM Said:

Bill Yeager

Nice article....

Is there any way to do globalize the validation messages that are coming from a Model which is in a separate dll?

I tried creating a reference to my presentation tier project in my model project, but it just didn't work out. From my view, it seems to be only working if you have the Model in the same project.

Nadeem United States, on 11/19/2011 7:58:45 AM Said:


Bill Yeager,
Yes, of course you can use validation messages from a different library as long as you specify the data annotation correctly.

Bill Yeager United States, on 11/19/2011 8:31:17 AM Said:

Bill Yeager

I have a separate dll that is a Model and it returns validation messages properly (via DataAnnotations) with no problem when I'm editing on the presentation tier.

However, when I try to implement your section on Internationalizing Validation Messages in my separate model dll, I can't get a reference to the Resources object.

I have your MyResources project in the same solution as my presentation tier project. It compiles just fine. I then add my presentation solution dll to my Model project as a reference.

The Resources object does not show up in the intellisense (in my model class) when I prefix it with my presentation tier project name (MyProjectName) and then the "." (for the intellisense).

What do I need to do to have the Resources show up?

Thanks a lot for your help.

Bill Yeager United States, on 11/19/2011 11:48:03 AM Said:

Bill Yeager

After setting the reference in my separate model solution, the only namespaces that come up for "MyProject" with the "." intellisense are:


The "MyResources" project does not show up in the intellisense (which is compiled into the "MyProject" project.

On the presenation tier where I globalized the site (as I said before), I simply put a "using" statement in the Views and the intellisense would pop up for me to place the specific resource text object. This is not happening with my dll. I've got to be missing something....

Nadeem Afana United States, on 11/19/2011 2:14:41 PM Said:

Nadeem Afana

Bill Yeager,
You need to add a reference of your resources project or DLL to the Model project. If it's not showing up then did you set the resources Access modifier to PUBLIC like above ??

Robert Slaney Australia, on 12/14/2011 1:34:06 PM Said:

Robert Slaney

We use a custom resource provider to load our resource strings from a database, using the globalization element to define the resourceProviderFactoryType.

The ErrorMessageResourceName attribute REQUIRES that an ErrorMessageResourceType be defined otherwise it throws an exception.

Further more, the type supplied MUST be a resx as internally the Validation attribute it uses REFLECTION to enumerate all the STATIC properties.  I can't even create a dynamic object and pretend that static properties exist as this doesn't work with reflection calls.

This means I cannot reconcile the two approaches.  Any advice ?

Nadeem Afana United States, on 12/15/2011 7:30:45 AM Said:

Nadeem Afana

Robert Slaney
Try and define properties that are not dynamic. Why should your properties be dynamic ?!

Robert Slaney Australia, on 12/15/2011 12:14:16 PM Said:

Robert Slaney

I don't have a class that has a property for each resource key like the generated resx.

I use classes that inherit from IResourceProvider and ResourceProviderFactory to resolve a resource string from the given culture.

public override IResourceProvider CreateGlobalResourceProvider(string classKey)
public object GetObject(string resourceKey, CultureInfo culture)

These are incompatible with the DataAnnotations validation attributes

Nadeem Afana United States, on 12/15/2011 12:37:51 PM Said:

Nadeem Afana

Robert Slaney,
I know that Data Annotations localization is pretty limited. One way would be using alternate validation mechanisms like custom validation. Or maybe inheriting from standard validation attributes like here http://bit.ly/sZRMH9

Fayez Tunisia, on 2/27/2012 2:31:46 AM Said:


Hello and Thank you for sharing this article!
I have a strange problem in translation on a production server.
I posted my question here stackoverflow.com/.../translation-different-from-culture-and-ui-culture-in-asp-mvc-3-site

Can you please take a look at it when you have time?
Thank you.

Nadeem United States, on 2/27/2012 9:00:23 AM Said:


I think that CurrentUICulture  is overwritten with a different value from ASP.NET due to some other configuration. I also think the event Application_AuthenticateRequest is not the appropriate one for setting the CurrentUICulture, use another one preferrably. Try to override the

protected override void OnActionExecuted(ActionExecutedContext filterContext)


Fayez Tunisia, on 2/28/2012 5:23:35 AM Said:


Thank you for your answer.
If I would like to set the culture according to the DNS host.
Where can I set it in the code? (asp mvc 3)
Suppose I have this method:
string getLang() {return Request.Url.Host.Contains(".co.uk") ? "en-GB" : "fr-FR"; }

Nadeem United States, on 2/28/2012 9:31:02 AM Said:



The same method I gave you above. Ideally you should set the Thread cultures in this method. You can access the host in this way:

string host = filterContext.HttpContext.Request.Url.Host;

Charles Daitem France, on 2/28/2012 9:45:43 PM Said:

Charles Daitem

Thanks Nadeem, I had the same problem as Fayez and now it's fixed. Smile

Fayez Tunisia, on 2/28/2012 10:04:21 PM Said:


Thank you very much M. Afana! That worked perfectly. Have a nice day.

Jacob United States, on 4/9/2012 4:47:31 AM Said:


What is the most optimum way to incorporate different languages and cultures in ASP.NET MVC 3? Which one most people find the easiest to implement?


Abdalla Nayer Egypt, on 5/15/2012 6:11:22 AM Said:

Abdalla Nayer

Thank You for your code more than perfect,,, i just have one question i am using views and partialviews and it doesnot work with @html.partial, how can i get it work with @html.partial thank you Smile

Nadeem United States, on 5/20/2012 8:32:34 AM Said:


Abdalla Naye,
Could you please provide more details ??

Alberto Menchaca Mexico, on 6/22/2012 5:38:22 AM Said:

Alberto Menchaca

Hi, great work. Very easy to implement and to adapt to projects.
Thanks for sharing this on your blog Smile

Matt Johnson United States, on 10/22/2013 7:01:45 PM Said:

Matt Johnson

Great article!  But it would have been nice if you had shown usage of TimeZoneInfo, DateTime.UtcNow and DateTimeOffset instead using DateTime.Now.

Since DateTime.Now gives the local time of the server, it's almost never appropriate for a web site.  Especially one that is focused on internationalization.

Please see codeofmatt.com/.../the-case-against-datetime-now on my blog.

Suraj Deshpande United States, on 10/22/2013 7:04:27 PM Said:

Suraj Deshpande

Excellent article, very well explained.

It would be nice if you post a link to source code.


Suraj Deshpande United States, on 10/22/2013 7:09:42 PM Said:

Suraj Deshpande

Ignore my previous comment, I got the download source code link.

Cheers and thanks for great article.

Nadeem United States, on 10/22/2013 7:16:07 PM Said:


@Matt Johnson,
Time zones are a whole a topic by themselves. If I get enough comments I promise I will write a whole post about them!

Daria Portugal, on 10/23/2013 5:59:55 PM Said:


Hello Nadeem very good project you made, I use you approach but modificate some, and I put it here, if somebody interesting, I change you hyperlink of languages for images and I moved it to _Layout:

<a href="@Url.Action("SetCulture", "Home", new {culture="en-us"})" >
                        <img src="@Url.Content("../images/pt.png")" width="35" height="35" alt="Português" /></a>

        <a href="@Url.Action("SetCulture", "Home", new {culture="ru"})" >
                        <img src="@Url.Content("../images/ru.png")" width="35" height="35" alt="Português" /></a>

I have just one question, could you help me and make the example, how to put the language from cookies to URL and config it in RouteConfig? Thank you.

Nadeem United States, on 10/23/2013 6:59:19 PM Said:


I updated the post. Now it shows how to do that. Check the last part.


Anas Ghanem , on 10/24/2013 8:40:18 AM Said:

Anas Ghanem

Great information.
Just wanted to mention that you use CultureInfo.TextInfo.IsRightToLeft property to check if you are working with RTL or LTR cutlure. No need to hardcode the "ar/he".

Daria Portugal, on 10/24/2013 3:37:20 PM Said:


Thank you very much Nadeem, everything is work fantastic Smile
One more question, the user could change the languages on any page, but after change of language the user is redirected to Index page, is it possible redirect the user on a same page? I.e. if the user change the language on About page, just the language is changing, but he still see the About page.

Nadeem United States, on 10/24/2013 4:57:05 PM Said:


Try the following for the links:

 <a href="@Url.RouteUrl(new RouteValueDictionary(ViewContext.RouteData.Values.ToDictionary(r => r.Key, r => r.Key == "culture" ? "es" :  r.Value)))">

Spanish </a>

biwen People's Republic of China, on 10/24/2013 5:14:12 PM Said:


good job!

Daria Portugal, on 10/25/2013 6:59:52 AM Said:


Thank you very much Nadeem, great job....everything work fantastic!!!! Smile

Mohammad Denmark, on 11/14/2013 10:52:25 AM Said:


Salam aleikum Nadeem!
First of all I think this article is the best on the Internet that deals with Internationalization and how to create a multilingual website. I have searched and searched but have not been able to find an article that deals with the subject as good as you have done. You even managed to get the seo optimized url handling, nice Smile
As opposed to others I believe using multiple views in this scenario is the correct way to go, even if it go against the dry principle. As I see it using multiple views will make it possible to style rtl and ltr oriented languages. This would in practice be impossible to achieve in a perfect manner when using one view. Its better to ditch the dry principle and get the job done correctly then to stick with a principle that does not fit the reality in this case.

Again thanks for the best article on the internet that shows how to create a multilingual web site

Marau France, on 11/23/2013 8:25:17 PM Said:


Great article.

This works fine but how can I load resource files dynamicly ?
I want to provide original resource files instead of the compiled version to a client so he can modify any text with a simple resource editor.
Is there a way to do that ?

marau France, on 11/24/2013 7:41:12 PM Said:


I saw the article, but I thought there was a way to do it like in classic ASP sites. (Resx files are in the folder App_LocalResources and can be modified via a simple tools like ZetaRessourceEditor).

Nadeem United States, on 11/27/2013 6:53:34 AM Said:


You can use any XML editor to edit the resources if they are XML.

alex United States, on 12/4/2013 11:46:47 AM Said:


Great article!  I was able to use a lot of the techniques you described and they work great except for one small problem...  

Has anyone had a problem with the culture not being set on the login page in an MVC4 app?  I've stepped trhough the code a dozen times, and it reads/writes the cookie but for some reason the language doesn't change on that page.  Every other page is fine after I log in.

Husnain Abbas Islamic Republic of Pakistan, on 12/8/2013 5:28:08 PM Said:

Husnain Abbas

thanks for share this . it is very easy to understand .

Akshay Srinivasan India, on 1/5/2014 9:48:46 PM Said:

Akshay Srinivasan

There is some missing code which is there in the downloadable code.  It is in the resources.resx.cs file where some properties are defined which are not shown in the article this was throwing me off with errors.  If you can include all the code steps required would be helpful.  Otherwise thanks a lot for the article.  Also by any chance can you show the same or similar example for MVC 4 VS 2012 and with some database action going on to save multinational data in SQL Server using ADO.NET entity framework 5.
Thanks a lot for the article it helps but more would help even more Smile

Daniel United Kingdom, on 1/7/2014 3:03:04 PM Said:


Interesting article Nadeem. I'm confused by a lot of the earlier comments, however, disagreeing with multiple views as opposed to resource strings. This  approach uses resource strings for localization, did you change the article or something? It's even stated early on when discussing the 3 options that it will use resources. Am I missing something?

Mark Meuer United States, on 1/9/2014 7:20:02 AM Said:

Mark Meuer

Thank you so much for providing this example!  It is extremely helpful.

I do have a question about setting the culture in BeginExecuteCore of the base controller.  Why did you not do this in the Initialize method?  The MS docs for Initialize even mention that as the appropriate place to set the culture. (msdn.microsoft.com/.../...itialize(v=vs.100).aspx)

Thanks again for making this very useful post.

Leon United States, on 1/25/2014 11:04:51 PM Said:


Hi Nadeem, thank you for your good stuff. For cookie in URL, I am encountering a problem when I apply [Authorize] on a controller, the language code cannot be set properly (404 error), I tried to correct authentication node in web.config, still no luck, please advise.
Changes made in web.config.
<authentication mode="Forms">
      <forms loginUrl="~/Default/Account/Login" timeout="2880" />

رها Iran, on 1/26/2014 4:36:02 AM Said:


Hi Nadeem Afana
thanks for sharing this great tut
but when In the second step I add the :
and Error messageType......
the file all filled with Error and when I Build the app all person Calss is Error
and in next step when I right-click on Refrences I don't see any Choose menu to choose the Resources
Please help me additional tut about this greate web app and register form
Thanks again
whit the best regard :

Nadeem United States, on 1/26/2014 1:32:28 PM Said:


Did you make sure that the access modifier of the resources is set to Public. You also need to add a reference to the project that contains the resources. Make sure to select the Projects tab first.

Nadeem United States, on 1/26/2014 2:18:53 PM Said:


In order to use TempData if needed.

Nadeem United States, on 1/26/2014 2:21:20 PM Said:


"For cookie in URL"
Cookies and URLs are two separate things.

Leon United States, on 1/26/2014 3:13:28 PM Said:


Hi Nadeem, sorry for confusing you, 'For cookie in URL' should be corrected to 'How to store culture in the URL instead of a cookie?', I tried to store culture in URL, I am encountering a problem when I apply [Authorize] on a controller, if a unauthenticated user tried to visit a page needs authentication, the user will be taken to '~/Account/Login' page which cause 404 due to default route has been changed, I tried to correct authentication node in web.config, still no luck, I believe there is a better solution, please advise.
Changes made in web.config.
<authentication mode="Forms">
      <forms loginUrl="~/Default/Account/Login" timeout="2880" />

رها Iran, on 1/26/2014 7:28:15 PM Said:


Hi @Nadeem
And really thanks for you're reply
I downloaded you're complete project and when I open it in visual studio it begin to download some additonal package .but you didn't add any additional package into you're project in first step of you're post
what are them that we must add to our MvcInternationalization project in first step
please help me by there name ?
so I can add them to my project
thanks Dear Nadeem

Nadeem United States, on 1/27/2014 4:56:27 AM Said:


I see. Have you tried this:

<authentication mode="Forms">
     <forms loginUrl="~/en-us/Account/Login" timeout="2880" />

Nadeem United States, on 1/27/2014 5:01:01 AM Said:


I removed the packages on purpose to reduce the attachment size. Once you build the project, they will be downloaded automatically for you.

Abouldahab Egypt, on 2/11/2014 9:49:19 PM Said:


thanks a lot Nadeem it is great article your blog now is my favorite i hope it help me to learn more and more
thanks again Smile

Scott United States, on 2/17/2014 10:00:18 PM Said:


Thanks for the great post, Nadeem.

I'm curious why the check in CultureHelper for existence of the culture name in the _validCulture list is necessary?

If the name doesn't match an implemented culture, then it'd be checked against a language fallback (which seems reasonable), and if no match is found, use the default culture.

Benjamin Germany, on 2/20/2014 4:32:43 AM Said:


Thanks for the article.
I included the code step by step in my project. But the website uses the strings from the Resources.resx (standard culture) file every time. If I switch the language button the page is reloading with exactly the same strings. The cookie includes the selected correct culture but the strings are the default ones. Where could be the error? The modifiers are public, the reference of the web project to the resource Project is set.

Thank you for your help!

Nadeem United States, on 2/24/2014 6:53:07 AM Said:


It is only for gaining a little bit of performance. There is no need to start comparing strings if the value is invalid.

Nadeem United States, on 2/24/2014 4:04:35 PM Said:


Did you add your cultures to the  _cultures list ?

Benjamin Germany, on 2/25/2014 9:22:53 PM Said:


Yes, that wasn't the problem.
I named my files resource.resx (with german strings) and resource.en.resx (with english strings) and set the default culture to german "de-DE". Because he could't find a resource file with he used every time the german one. that the switch of the language wasn't working I don't know.
But I fixed my Problem and renamed my resource files to resources.resx (english and fallback) and resources.de.resx (german) and sets the default culture in the culture helper to "de-DE". Now it's working.

Achmad Indonesia, on 3/5/2014 8:29:29 PM Said:


I have a model that put in different project from web project i.e:
Project.Library.Data -> used for define model and table (using Entity Framework)
Project.Mvc.Web -> This is a web application project, and I add "Project.Library.Data" to this web application project  to references,

If I want to change display name, what I have to do is create partial class,
and create metadata class inside of it. And for internalization case I use your method.

But when I run the web application, display name of my property doesn't change to internalization case. It's just default display name. Is that any problem to put the defined model in another project not in same project?

Gabriela Belgium, on 3/10/2014 10:58:11 PM Said:


Hello Nadeem Smile
I've implemented your solution several times and I have a problem on my login page.
On the login page the languages are not changing, but if I log in everything works fine.
This is surely due to the authorizations in my web.config.
I haven't found a solution to it.

Can you post your web.config file please??

In my web.config I have this:
    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2880" />

      <deny users="?" />

Thank you!

Nadeem United States, on 3/13/2014 4:47:32 AM Said:


See the attachment.

Nadeem United States, on 3/13/2014 4:51:12 AM Said:


Try  using  a metadata class and attach it to your entity class via the MetadataTypeAttribute msdn.microsoft.com/.../...tadatatypeattribute.aspx

Gabriela Belgium, on 3/13/2014 4:55:18 AM Said:


Thank you Nadeem.

I see in your web.config file that you have no security:  
<authentication mode="None" />

Actually that was my problem, with no security the app worked fine, with the security
      <deny users="?" />
it didn't worked.

Finally  I've found the solution, I've deleted the authentification mode and the authorization and I've annotated all the controllers with [Authorize].

Thank you for the tutorial it was really helpful.

Ahmed Sakr Egypt, on 3/17/2014 12:10:36 AM Said:

Ahmed Sakr

Thank you very much

Nadeem United States, on 3/23/2014 3:45:51 PM Said:


Can  you clarify more? Did you follow all the instructions on this page?


Ingen Speciell United Kingdom, on 3/27/2014 1:43:40 AM Said:

Ingen Speciell

Thanks for a great article! Just wondering if there's any way to update the text in the resx file without having to compile when they're in a separated project like this?

Nadeem United States, on 3/27/2014 4:49:26 PM Said:


Unfortunately, you have to compile your resources project. However, you may not need to compile your main project if you are just updating views.

Patrick United States, on 3/28/2014 7:25:40 AM Said:


Hey Nadeem,

Thanks for the write up.  I think this is one of the better implementations and explanations I have seen.

Quick question.  Why do you use "BeginExecuteCore" vs. a different Controller method like "OnActionExecuting"?  Is there any benefit for one of these over the other (or one not listed here)?


Evgeny Russia, on 3/30/2014 10:01:36 PM Said:


Thank you for this very usefull article. I've applied it to my project and it works good. But I faced with the same [Authorize] problem like @Leon. The changing web.config as you stated does not work for me. I found that in my case login url can be modified in Startup.Auth.cs. I Changed it to "LoginPath = new PathString("/en-US/Account/Login")" and now it works.
But there is stiil one problem. If a user first select a defferent culture and then go to authorized page he will be redirected to login page in default (en-US) culture, not culture he is selected before. It is not user friendly. User of course can authorize by link "Log in" and it will be in proper culture. But I would like the automate redirection work the same way. Could you advise how to fix it?

Nadeem United States, on 4/3/2014 5:41:58 PM Said:


You need to set the culture as early as possible in the call stack so that it becomes available. You can use the Initialize method.

Nadeem United States, on 4/4/2014 3:39:53 PM Said:


You need to specify the culture as part of the return url.

Juan Pablo Spain, on 4/14/2014 7:52:28 PM Said:

Juan Pablo

Very good solution, especially the method in BaseController, using BeginExecuteCore, perfect!



Andre Tella Brazil, on 4/18/2014 12:31:40 PM Said:

Andre Tella

Hi Nadeem, great article you wrote! Thanks.
I've changed Age from int to decimal in order to test decimal validation in pt-br culture (using "," as decimal separator) and i got "The field age must be a number" validation message.
How do i get this validation working? Am i missing something?
Best Regards.

Nadeem United States, on 4/19/2014 3:06:52 PM Said:


@Andre Tella,
The message "The field {0} must be a number." is hard coded in the MVC framework .resx file. I believe you can change it but it's not that easy. See this stackoverflow.com/.../the-field-must-be-a-number-how-to-change-this-message-to-another-language

sam Canada, on 4/20/2014 11:51:54 AM Said:


How to use a satellite assembly per culture?

Andre Tella Brazil, on 4/20/2014 12:44:53 PM Said:

Andre Tella

Thanks Nadeem for your answer.
I dont't want to change the message. I think the message should not be shown because the number i entered used a comma as decimal separator (standard in pt-br culture) like 12,5. I did everything the same way your article explained, the culture is set in the right way and everything works good except the message that is still shown when i enter numbers like the one above.

Nadeem United States, on 4/21/2014 7:35:43 AM Said:


@Andre Tella,
I see. That is because jQuery validation by default does not consider i18n. See the sections Client Side and Unobtrusive Client-Side Validation in afana.me/.../...c-internationalization-part-2.aspx

BTH Netherlands, on 4/29/2014 7:10:55 PM Said:


Nadeem, can you extend your sample with money fields and date times? I have searched the internet for a good sample that includes money editing and in the same time handles culture in the right way.

Nadeem United States, on 5/2/2014 4:49:30 AM Said:


Satellite assemblies are implicitly supported by the default ResourceManager. You just need to create a separate assembly per culture. See this msdn.microsoft.com/en-us/library/21a15yht.aspx

winicius Brazil, on 5/2/2014 9:00:49 AM Said:


great tutorial,

but i ran into a problem, when i dont specify the culture on the url( eg: server:port/controller) i get redirected to the home controller.
is there anyway i can change it to redirect to the specified controller with the default culture?
my RouteConfig is the same as yours

Tiar Indonesia, on 5/16/2014 6:33:39 AM Said:


Hello. Very useful tutorial. I've implement this on my project and it's working well. As for database internationalization, it would make sense if we want to implement it on our contents. For static labels, using resources is faster.

Tiar Indonesia, on 5/16/2014 9:39:59 AM Said:


Hello. I have a problem on form internationalization. The display name used for each field always using default language (en-US) even though I set the language (the one on the cookie) to id-ID (it still show "Remember Me?" instead of "Ingat Saya?"). Any idea why?

here is my model's field:
[Display(Name = "RememberMe", ResourceType = typeof(LangResources))]
public bool RememberMe { get; set; }

and here is the view:
<div class="checkbox">
    @Html.CheckBoxFor(m => m.RememberMe)
    @Html.LabelFor(m => m.RememberMe)

Nadeem United States, on 5/18/2014 3:16:28 PM Said:


Make sure you update _cultures in CultureHelper.

Nadeem United States, on 5/18/2014 3:18:15 PM Said:


See afana.me/.../...re-strings-in-database-or-xml.aspx for database implementation.

Tiar Indonesia, on 5/19/2014 10:46:39 PM Said:


Well ... I'm not sure if I'm understand. Where do exactly I need to update the _cultures?

If I'm not mistaken, it was a list that contain all languages I want to support with the top most is the default, right? So if I never change that set, will I need to update it?

I've try to match your example on above post. Any html text were successfully translated to loaded resources, except for Model specific text like LabelFor, etc (which always use default resources). I wonder where I went wrong Smile

Nadeem United States, on 5/20/2014 10:48:31 AM Said:


Just add the culture names your site supports to _cultures.

Also, make sure you add a Display attribute on your property RememberMe. See the class Person above.

Saqib Ahmad Islamic Republic of Pakistan, on 6/2/2014 8:31:11 PM Said:

Saqib Ahmad

Thanks for the Help, great article

sam United Kingdom, on 6/15/2014 9:38:51 PM Said:


Hi Nadeem,

Am trying to add Persian support I have added a resource file called Resources-fa.resx and added option in _culture called "fa", but when I click on the option it does not read resex the layout changes to rtl though.

Can you help please.

Nadeem United States, on 6/16/2014 5:20:26 PM Said:


Make sure you set the access modifier to Public of the resource assembly.

The resource file name should be  Resources.fa.resx, not Resources-fa.resx

sam United Kingdom, on 6/16/2014 9:16:16 PM Said:


Thanks bro, the naming was the problem Smile

Asshiah France, on 7/6/2014 9:17:56 PM Said:



I have the exact same problem as Evgeny and Leon:

Thank you for this very usefull article. I've applied it to my project and it works good. But I faced with the same [Authorize] problem like @Leon. The changing web.config as you stated does not work for me. I found that in my case login url can be modified in Startup.Auth.cs. I Changed it to "LoginPath = new PathString("/en-US/Account/Login")" and now it works.
But there is still one problem. If a user first select a different culture and then go to an authorized page he will be redirected to login page in default (en-US) culture, not culture he has selected before. It is not user friendly. User of course can authorize by link "Log in" and it will be in proper culture. But I would like the automate redirection to work the same way. Could you advise how to fix it?

As stated above, I can define my login page path in Startup.Auth.cs as I am using ASP.NET Identity with OWIN/KATANA. The thing is I don't want to specify a login url containing an hardcoded culture (the default one) because I updated your code to enable prefered language autodetection if no culture is specified by the user yet. I want the login page to have this feature too. So I can't hardcode the culture in the login path.

I know this is asking a lot. Your article already helped me a lot. Thanks for that. But if you have any idea I'd like to hear it.^^

Nadeem United States, on 7/7/2014 4:49:55 AM Said:


It is hard to tell without source code. Can you post a project source code somewhere where I can reproduce the issue ?

Nadeem United States, on 7/7/2014 4:53:46 AM Said:


Did you make sure your Login link actually have the right culture in it?

Asshiah France, on 7/7/2014 8:25:18 PM Said:


I think i found a solution to my problem, I can use the OnApplyRedirect method of the OWIN cookie authentication middleware (in the Startup.Auth.cs) to intercept the redirection to the login page (this method only deals with redirections comming from the OWIN cookie middleware). I define the login path with a token that I replace dynamically in this method. For this I use the actual thread culture that was set right in your BaseController just before the login redirection happens :

        public void ConfigureAuth(IAppBuilder app)
            app.UseCookieAuthentication(new CookieAuthenticationOptions
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/CultureToken/Account/Login"),
                Provider = new CookieAuthenticationProvider
                    OnApplyRedirect = ctx =>
                        if (CultureHelper.useURL)
                            var culture = Thread.CurrentThread.CurrentCulture;

                            if (ctx.RedirectUri.Contains("CultureToken"))
                                ctx.RedirectUri = ctx.RedirectUri.Replace("CultureToken", culture.Name);

Ishaq India, on 7/7/2014 8:55:52 PM Said:



Create a class in the filters Folder with this code:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
internal sealed class LocalizedAuthorizeAttribute : AuthorizeAttribute
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        string language = filterContext.RouteData.Values["lang"] == null ? "en-us" : filterContext.RouteData.Values["lang"].ToString();
        filterContext.Result =
        new RedirectResult


Then Use it like this:

    public ActionResult Create()
        return View();

Nadeem United States, on 7/8/2014 4:24:41 PM Said:


Please upload a whole project via a downloadable link, not just the attribute class.

Dan France, on 7/18/2014 9:52:26 PM Said:


@Nadeem: Thks gonna use this for my app now.


I  thought the same : Instead of a base controller and repeating it each time, we could make a filter.

And register this filter Globally (to make it apply to all Controllers of the application) in Global.asax.cs :

        protected void Application_Start()
            RegisterGlobalCustomFilters(GlobalFilters.Filters);    $


        public static void RegisterGlobalCustomFilters(GlobalFilterCollection filters)
            filters.Add(new LocalizedAuthorizeAttribute ());



kevin obrian France, on 7/19/2014 2:39:57 AM Said:

kevin obrian

Thank you so much for sharing that very well detailed post

DevMec Algeria, on 7/20/2014 9:17:38 PM Said:


Very nice artcile,

I have a question, is there any way to enable user choosing his language by clicking in the right of the navbar. for example, a flag icon with the DefaultCulture text.

An then, after click, a modal popup appear with a list of implemented languages.

Thanks in advance

Nadeem United States, on 7/21/2014 4:44:46 AM Said:


Yes, that is easy. See afana.me/.../...c-internationalization-part-2.aspx to see how it is done.

Add comment

(Will show your Gravatar icon)

  Country flag

Click to change captcha
  • Comment
  • Preview