Setup a multi-language website using ASP.NET MVC

Errore 403 - forbidden dopo aver pubblicato una applicazione ASP.NET MVC su IIS 7: come risolvere

Since the first release of the .NET framework developers are given the chance to easily configure any kind of project - be it a Website, a Web Application, a Windows Forms or XPF/XAML client and such - in order to support multiple languages. This can be achieved using the well-known Resource Files (.resx) approach. We won't explain them here (if you're interested, read the official walkthrough), but we'll remember a couple key concepts. A Resource Files is basically a key/value array of content resources (mostly images and text) for each supported language. All the developers have to do is to create a .resx file for the main language (let's say english) and another one for each of these languages using the same name plus the ISO 639-1 two-letters language code of the language itself, i.e.:

  • Global.resx file to store text and images for english, assuming it'll be our default & fallback language.
  • file to store text and images for italian language
  • file to store text and images for german language

and so on. Once we did that, we'll only have to write our code using the key specified in these files instead of the actual content (if you don't know how, read the walkthrough above): ASP.NET will look up the keys in our Resource Files, starting from the one with the Localization closest to the one set for the current thread and then going backwards until it founds something to show.

Kickass feature, indeed: let's see how we can use it to build our very own multi-language MVC ASP.NET Web Application.

Resource Files in MVC

The first question would be: where do we put the .resx files? The answer is all but granted: our first choice would be adding the  App_GlobalResources ASP.NET Folder to our project, just like we always did since .NET Framework 1.1: where else?


You could be surprised, but if you're working with MVC this is not the right choice. If you want to know why, you can read the whole story in this great article by K. Scott Allen sul tema. To summarize it, let's just say that the Framework will compile that folder in a separate assembly, thus making them unaccessible to our ControllersUnit Tests, etc.: we don't want this, that's why it's better to just place them in a separate, dedicated standard folder which we'll just call  /Resources/ .

Once we added there our .resx files there's another thing we need to do: we need to change some default settings by opening each file's Properties window:


First thing we need to do is to change the Custom Tool, which is the code-generator engine used by the Framework to build the Resource File strongly-typed class. The default tool, ResXFileCodeGenerator, would generate a private class, which is not what we want. That's why we'll replace it with the PublicResXFileCodeGenerator, which will be able to generate a public class with public methods & properties: just like what we need.

Second settings to change is the Custom Tool Namespace: default value, an empty string, means no namespace, which is far from ideal. That's why we'll replace it with something like Resources, so the custom tool will generate a code consistent with our folder structure: our resources will be located in the /Resources/  folder and will also have the Resources namespace: that's good enough.

Notice that these small modifications will be required for each and every .resx we'll add to our project: you cannot setup these values as default, but you can still duplicate your Resource Files so you'll always have these settings set, since each .resx copy comes with the same properties as the source file.

How to serve the proper language to each Request

As we said before, .NET Framework automatically select the Resource Files closest to the localization of the actual thread, which is the one that will process the http request and serve the proper http response accordingly. The localization info are stored in the response thread's CultureInfo object, which is usually set against the language specified by the request's browser/client. Meaning that we'll be able to serve the same contents in different languages - depending by the browser's client language settings - upon the same URL.

Same URL, different content. Would that be a proper behaviour? We could be tempted to say yes - after all, allowing each user to read the exact same URL in their home/native language seems like a good thing. Problem is, from a SEO perspective it's a complete failure.

Now we should ask ourselves La domanda che dobbiamo porci è: si tratta di un comportamento corretto? Da un punto di vista di funzionalità, sicuramente si: è indubbio che leggere una pagina in italiano o in inglese a seconda della localizzazione dell'utente possa essere molto comodo. Il problema è che da un punto di vista SEO si tratta di un fallimento completo. Chiunque abbia un minimo di infarinatura nel campo sa bene che è opportuno fare in modo che ogni traduzione di ciascuna pagina disponga di una URL specifica: se avete dei dubbi in proposito potete chiarirvi le idee su questa breve guida di Google che illustra una serie di best-practices da utilizzare per gestire siti multi-lingua, della quale ci limiteremo a citare la frase più significativa: Keep the content for each language on separate URLs.

This basically means that we can't just set the thread language taking into accont things like:

  • the user's browser language
  • cookie values (if set)
  • user session values (if present)
  • any other Request-based field (HEADER, POST data, etc.) different from the URL

But we need to build a proper URL-based localization mechanism, where each language answers to its very own set of URL patterns, such as:

  • (for default language contents - english in our example)
  • (for all italian contents)
  • (for all german contents)

... And so on. Once we do that, we're free to use some or all the above info to route the requests we receive to the most suited language - i.e. sending the english browsers to the english contents and stuff like that. This basically means that we will use the URL to set the thread localization, and any other info - browser language, cookie values and such - to choose the user's default route.

Let's see how we can build our Web Application to make sure it will behave in such way.

Set-up a Multi-Language Route

First thing we have to do is to setup an ASP.NET Route to handle these kind of requests and store the localization-related info. If our web-application implements the standard ASP.NET MVC route-pattern, such as {controller}/{action}/{id}, you can use the following example:

You'll need to put this route in the  /App_Start/RouteConfig.cs file just before the last one, which is the pre-defined route{controller}/{action}/{id}. If you're Web Application used a different routing approach, you'll need to adopt a similar, yet suitable strategy according to that.

The main goal of this "localized" Route is to isolate and store a   lang  variable corresponding to the requested language. This information, if present, will be used to set the localization of the current Thread so it will use the corresponding Resource Files.

Using a LocalizationAttribute to handle Multi-Language Requests

Now that we have a Localization Route to catch our multi-language URL calls and store our language info in a handy variable, we need to find a way to programmatically handle it. To fullfill this task we can create a LocalizationAttribute by creating a new LocalizationAttribute.cs class: the attribute will be executed upon each Action and it will set the current Thread's Culture and the UICulture with the value stored into the lang variable by the route.

This class can be put everywhere in our Web Application, such as a /Classes/  or  /AppCode/ folder. Once we add it to our project we also need to make sure it will be executed upon each request/Actions: we can do that by registering it as a Global Filter by using the RegisterGlobalFilters method in the  /App_Start/FilterConfig.cs  class:

The marked line shows the line we need to add to the default implementation to make our LocalizationAttribute kick in upon each and every response. We can - and actually should - also choose a default Localization - "en" in our sample, corresponding to the English language: that will ensure that we'll serve the default language for any URL not containing Localization info, i.e. any time the lang variable will be set to null because the request will be handled by a route other than our LocalizationRoute.


We don't need to do anything else, except creating the relevant .resx files for any language we want to support. As soon as we do that, we'll be able to test the results of our work by calling the following pages of our Web Application:

  • , seeing our contents in the default language (which is english in our example).
  • , seeing our contents in italian language (with a fallback to the default language if not present).
  • , seeing our contents in german language (with a fallback to the default language if not present).

... and so on.


About Ryan

IT Project Manager, Web Interface Architect and Lead Developer for many high-traffic web sites & services hosted in Italy and Europe. Since 2010 it's also a lead designer for many App and games for Android, iOS and Windows Phone mobile devices for a number of italian companies. Microsoft MVP for Development Technologies since 2018.

View all posts by Ryan

50 Comments on “Setup a multi-language website using ASP.NET MVC”

  1. If your default language is English, I am curious why you have both and ? Why not simply default to ‘en’ language i.e. ? Are there SEO reasons or MVC best practices for having both URLs? It would seem that this setup would lead to duplicate content and hurt SEO.

    1. Hi Alex,

      thanks for your reply. Actually my default language is Italian, that’s why you found the /en/ reference in the above post. I understand that it could be counterintuitive in an english-written post, so I changed it a little following your advice: now I clearly stated that the default language is english, and I also changed the example URLs accordingly. As a side note, I can absolutely confirm that there aren’t SEO reasons or MVC best practices for having the same language content served by different URLs, as it would definitely hurt SEO: that was the point of the whole post and I hope that now everything is clear enough.

      Thank you for helping me writing a better post.

      1. Thank you for the post!

        After the update, see the conclusion section, is referenced as English.

        Thanks again.

  2. Pingback: Change IIS default language on a working Windows Server
  3. Pingback: ASP.NET - Insert HTML code in Resource Files (.resx)
    1. Yes, you can insert something like that in an action method:

      Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(yourLanguageString);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

    1. Hello, thanks for the comment and sorry for the delay. Please see my answer to hayim S regarding this.

  4. Hi Ryan,
    Great and practical post for the multi-lingual issue.
    I have a question: I implemented your suggestion and it mostly works great.
    my problem is that for some links created by razor (Html.ActionLink, Url.Action)
    the href is without the language prefix. For example ,when I am in mysite/es, I
    get the Spanish copy I put it the resx file, but some links on the page are still : mysite/somepage
    and not mysite/es/somepage. I hope it makes sense,
    any thought would be appreciated.

    1. Hello, thanks for the comment and sorry for the delay. The most simple way to achieve what you need is using the wonderful RouteLocalization project at the following link:

      or via NuGet:

      – RouteLocalization.Mvc (for MVC)

      – RouteLocalization.WebApi (for WebApi).

      Here’s a great guide about how to use/implement that:

      I’ll update this post accordingly.

      Let me know if you manage to solve your issue.

  5. Pingback: Multi Language MVC | Porão do AJ
  6. Hey Rayan, Congrats on this great post.
    Just when Creating the new route, need to add a constraint to the default route generated by visual studio, so the id param would be accepted as an integer (or some other modifications), otherwise
    the route “{controller}/{action}/{id}” will be selected instead of “{lang}/{controller}/{action}” with an id optional.

    1. Hi to you and hanks for the support! :)

      Regarding your suggestion, I am fairly sure that if you add my route before the default one, just like I said in the post, mine will always trigger first, as it will be processed first.

  7. this is great but for some reason, on one of my project that I copied from RBAC (, the default routing was not working. I had to modify it this way:
    defaults: new { lang = “en”, controller = “Home”, action = “Index”, id = UrlParameter.Optional }

    1. That’s not recommended tho, since you won’t be able to properly handle browser redirection for the default URLs by forcing that route attribute (see my above answer to Jostein Torseter for details).

      If you see the above


      implementation (first line of the


      method) you can clearly see that, if the lang routeValue is not present, the default one is used instead (which should be “en” in your scenario). I would put a breakpoint on that line and take a look on what happens there.

  8. Ok, so I have tried to set this up according to the instructions (great post btw, thanks!), but I am having a logical issue. I have set up my view with a test string, and on //localhost/Home/Test it shows my default language (norwegian). Also, if i navigate to //localhost/en/Home/Test i get the english version. Fine so far. But how will the browser know where to direct me in the first place? The site address never contains the “en”, and it does not redirect me automatically based on the language of my browser. I might be missing something obvious here, but how do I make sure a user with a browser set to english will end up on the /en/ pages?

    1. This method doesn’t automatically “redirect” the browser to its default language, but you can easily do that by issuing a conditional redirect inside the




      method, assuming these two conditions are true:

      1) there is no explicit


      set, meaning that the browser is poking the “default” url without a language being specified: if that’s not the case, you should probably respect that since it most likely is an explicit choice issued by the client.

      2) the Thread.CurrentThread.CurrentUICulture (which you can use to fetch the browser’s default language from server-side in .NET) is different from the default one (which is


      in my sample).

      If these two conditions are both true, you can most likely put together something like the following:

      filterContext.Result = new RedirectToRouteResult(
                      new RouteValueDictionary 
                          { "lang", Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName }, 
                          // todo: iterate the rest of filterContext.RouteData.Values here

      … or something like that.

      Let me know if that’s enough or if you need a full working code sample.

  9. Hey Ryan,

    Great article, but I’m running into an issue myself. I’m unable to get this working with DataAnnotations. I’m setting the annotation properly, ie..

    [Required(ErrorMessageResourceType = typeof(Resources.SharedRegistration), ErrorMessageResourceName = “ERROR_EMAIL_REQ”)]

    ..but I’m always getting my default language back from this. It’s acting like these values are set before the filter changes the culture.

    Any thoughts on what I’m doing wrong? Thanks!!

    (Edit: Note that I’m not using RouteLocalization)

    1. Follow up to my own question. After doing some digging, my suspicions were correct. OnActionExecuting of a Filter Attribute is too late for some parts of .NET

      I eventually stumbled upon this article, adjusted where the Culture gets set, and my DataAnnotations started working again.


      1. Hi Rob,
        great catch! Updated my article accordingly:

        I’ve also mentioned you there :) Thanks for sharing this.

  10. Hello Ryan,

    I followed your tutorial and it works great so far.
    I have but one question. I have 3-4 language menus in the _Header file of my application.
    Is there a way (and how) to create a Html or Url.Action to change the language of the site, but redirect the user to the same page he was when he clicked the languageChange button?

    In which controller should i put this method? I tried putting it in the HomeController, but i could’t redirect the user to the previous page he was and changing the ‘lang’ parameter

    1. Hello Fanorius, thanks for your question. I just added two additional posts with C# code samples to do exactly what you want.



      Feel free to check them out!

  11. Pingback: Html.ActionLink Extension Method for multi-language Routes in NET MVC
  12. Pingback: Url.Action Extension Method for multi-language Routes in NET MVC
  13. Hello Ryan.
    That’s really good! Thanks a lot for sharing this information.

    But still, i have one questionissue.
    I am unable to use attribute routing. Just always getting 404 error.
    Currently URLs are generated in form “/en/Home/About”, “/en/Home/Contact”, etc.
    How it could be achieved to get URLs like “/en/About”, “en/Contact”, etc?

    I’ve played with RoutePrefix, Route for each ActionResult, etc (routes.MapMvcAttributeRoutes(); was in RouteConfig) but result was the same all the time – 404.

    Also i’ve slightly amended your described solution to following, to get automatic redirection to required language:

    public IController Create(RequestContext requestContext, Type controllerType)
    string language = “en”;
    List supportedLanguages = new List() {“en”,”de”,”es” };
    if(requestContext.RouteData.Values[“lang”] == null)
    language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
    string redirectString = requestContext.HttpContext.Request.RawUrl == “/”? “/” + language : “/” + language + requestContext.HttpContext.Request.RawUrl;
    language = requestContext.RouteData.Values[“lang”].ToString();

    //Get the culture info of the language code
    CultureInfo culture = CultureInfo.GetCultureInfo(language);
    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;

    return DependencyResolver.Current.GetService(controllerType) as IController;

    Thank you in advance for suggestions.

    1. Hi again.
      Looks like I’ve found root cause and it’s solution.

      First of all, when Route attribute was applied to Action, when i was clicking on the link (which was correct, like “/en/About”), my requestContext.RouteData.Values was without “lang” key, and instead there was “MS_DirectRouteMatches” key with another RouteData object in it’s value, which containing “lang” key and it’s correct value. As result, “requestContext.RouteData.Values[“lang”] == null” was returning “True” and i was redirected to /en/en/About (language was in URL 2 times).
      After searching for “MS_DirectRouteMatches”, an answer was found:

      So final function now is following, and works correctly (while it, maybe, not very elegant :) ):

      public IController Create(RequestContext requestContext, Type controllerType)
      string language = “en”;
      List supportedLanguages = new List() {“en”,”de”,”es” };
      string requestLanguage = requestContext.RouteData.Values[“lang”] as string;
      if(requestLanguage == null)
      if (requestContext.RouteData.Values.ContainsKey(“MS_DirectRouteMatches”))
      RouteData routeData = ((IEnumerable)requestContext.RouteData.Values[“MS_DirectRouteMatches”]).First();
      requestLanguage = routeData.Values[“lang”] as string;
      if (requestLanguage == null)
      language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
      string redirectString = requestContext.HttpContext.Request.RawUrl == “/”? “/” + language : “/” + language + requestContext.HttpContext.Request.RawUrl;
      language = requestLanguage;

      //Get the culture info of the language code
      CultureInfo culture = CultureInfo.GetCultureInfo(language);
      Thread.CurrentThread.CurrentCulture = culture;
      Thread.CurrentThread.CurrentUICulture = culture;

      return DependencyResolver.Current.GetService(controllerType) as IController;

      Now i can use routing attribute as following:
      public ActionResult About()

      But at the same time i have another minor issue with ActionResult Index()
      I was trying to decorate as following (commenting and uncommenting it in different combinations):
      public ActionResult Index()

      But in this case i am getting Index URL generated without currently selected language, just “/”, and if i am clicking it, selected language then reset to CultureInfo language, or, getting 404 error.
      Without any decoration for this Action – i am getting correct URL, for example “/en”, or other currently selected language, and no 404’s. Any ideas how to solve it?

    2. Hello Alex,

      To achieve such result I would do the following (assuming your About and Contact ActionMethods are in HomeController.cs):

      name: “DefaultLocalized”,
      url: “{lang}/{action}/{id}”,
      constraints: new { lang = @”(w{2})|(w{2}-w{2})” }, // en or en-US
      defaults: new { controller = “Home”, action = “Index”, id = UrlParameter.Optional }

      However, since you’re using AttributeRouting, you won’t need that.

      Regarding the auto-redirection to browser-default language, your approach seems quite overkill to me: I would do something like the following instead (adding the “lang” to route value if it isn’t set).

      public class LanguageCodeAttribute: ActionFilterAttribute
      private string[] _AcceptedLangCodes = new[] { “en”, “it” };
      public string[] AcceptedLangCodes { get { return _AcceptedLangCodes; } set { _AcceptedLangCodes = value; } }

      private string _DefaultLangCode = “it”;
      public string DefaultLangCode { get { return _DefaultLangCode; } set { _DefaultLangCode = value; } }

      private string _RouteDataKey = “lang”;
      public string RouteDataKey { get { return _RouteDataKey; } set { _RouteDataKey = value; } }

      // This checks the current langauge code. if there’s one missing, it defaults it.
      public override void OnActionExecuting(ActionExecutingContext filterContext)
      // Determine the language.
      if (filterContext.RouteData.Values[RouteDataKey] == null || !AcceptedLangCodes.Contains(filterContext.RouteData.Values[RouteDataKey]))
      // Add or overwrite the language code value.
      if (filterContext.RouteData.Values.ContainsKey(RouteDataKey)) filterContext.RouteData.Values[RouteDataKey] = DefaultLangCode;
      else filterContext.RouteData.Values.Add(RouteDataKey, DefaultLangCode);


      I’m using the ActionFilterAttribute approach here but you can do the same with the IControllerActivator approach you’re using instead. Anyway, in the above sample I’m forcing the DefaultLangCode here if there’s none, but you could go with CultureInfo.CurrentCulture.TwoLetterISOLanguageName instead to achieve what you want.

      Last but not least, in case you need a centralized way to generate CultureInfo-aware ActionLinks or URLs, I would suggest you reading my other 2 recent posts about Html.ActionLink and Url.Action, adding an overload to skip the Controller part in the url generation.

        1. Changed DNS tonight :) It will take some hours. If you’re in a hurry, add the following to your /system32/drivers/etc/hosts file:

      1. Hi Ryan.
        Regarding firs part of my question everything is clear – i am using AttributeRouting.

        But regarding second part (see my comment from September 16, 11:54 PM) – why my approach look so bad for you? What is the difference between adding language in front of URL and then doing redirection, and your approach with RouteData values?

        1. There’s nothing bad in your approach: never said that :) I just thought it was quite overkill, as you are basically doing the same thing twice: since I’ve already put up an engine that uses routeData I would just stick to that instead, until I can for all language-related issues… but it was just my thought.

  14. Are you using both LocalizedControllerActivator and the LocalizationAttribute class?
    Cause when im trying to only use the LocalizedControllerActivator class I get an error saying :

    “The given filter instance must implement one or more of the following filter interfaces: System.Web.Mvc.IAuthorizationFilter, System.Web.Mvc.IActionFilter, System.Web.Mvc.IResultFilter, System.Web.Mvc.IExceptionFilter, System.Web.Mvc.Filters.IAuthenticationFilter.”

  15. Ryadel, your tutorial is really good and helpful. However i would really appreciate if you could add a test project which would be really handy.

  16. Hi Ryadel,
    Thanks for your article, i would like to make a website multiple language and route like you site ( i saw the way you do that it seem easy. Please guide me step by step in mvc 5. Thank you a lot.

    1. when i did like your article, after that how to i want to get language in url? if i do like HttpContext.Current.Request[“lang”]; it always null. Pleases help me

      1. You won’t find it in the request, because it’s not a GET or POST parameter: it’s a route value.

        You can get the current language by using Thread.CurrentThread.CurrentCulture, since this is where we set it once retrieved by the URL.

        Or, if you have a requestContext available, you can pull it off by calling requestContext.RouteData.Values[“lang”].

  17. Hi there, I’d like to know how to change the URL when the user clicks on the language button to change it. As of now I send the “returnUrl” from the view but my default url for example will be something like root/, no locale specified as a default one has been specified in the routing setup.

    Now I need to be able to insert the desired locale into the url prior to redirecting the user to the other language version…. Would someone have an idea of how to accomplish it?

    Thanks in advance!

  18. It was giving me an ‘object reference not set’ because requestContext.RouteData.Values[“lang”] is null when there is no labguage set in the URL.

    changed it to:
    string lang = (requestContext.RouteData.Values[“lang”] != null) ? requestContext.RouteData.Values[“lang”].ToString() : _DefaultLanguage;

    1. Hello, that’s quite strange because null values are handled by my code (using the ?? coalesce operator)… Can you tell me the line you needed to change?

  19. Hello Ryadel! Great post, thank you very much! I managed to make it working using /en/test and /pt-br/test for example but I have one question still… Is there a way to “keep” the current language so all the pages that I access after changing the language will be opened with the selected language? With a Dropdown for lang selection, for example. Know what I mean?

    1. Hey Matheus, you can definitely setup a cookie in the browser client and then retrieve that value using Get-Cookie (if it does exist) to valorize the DefaultLanguage within the LocalizationAttribute OR within the LocalizedControllerActivator, depending of the path you’ve chosen to follow: it would be very simple to implement.

      Regarding the Set-Cookie part, I think that a good place to do that would be whenever the language fetched from the route value is different from DefaultLanguage. Please let me know if that’s enough to help you!

    2. Well, I think I finally found it. For those who are looking for it too, here it is:

      1. Yeah: you will notice that it does that by setting a client-side cookie in the same exact way I just wrote in the answer below :)

  20. I implemented your method and in general it works fine. I used the LocalizedControllerActivator version.

    My problem is that my actionlinks do not work properly. I can switch the language but then it adds just another {lang}.

    Action-Link – First click:
    Action-Link 2 – Second click:

    My Action-Link Code:
    @Html.ActionLink(“EN”, “Index”, “Home”, null, new { @class = “linkImgGB”, @alt = “English Version”}, CultureInfo.GetCultureInfo(“en”))

    How can I fix this problem? Any idea?
    Thanks in advance!

    1. Have the same issue. I also used Html and url extension with no success. Did you find the solution?

    2. Url.Action(“Action”, “Controller”) works for me.
      instead of Url.Action(“Action”, “Controller”, null, System.Globalization.CultureInfo.CurrentCulture)
      do not know why?

  21. Thank you very much, it was the simplest and most adequate way I found to implement, it helped a lot in my service.

Leave a Reply

Your email address will not be published. Required fields are marked *

The reCAPTCHA verification period has expired. Please reload the page.

This site uses Akismet to reduce spam. Learn how your comment data is processed.