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

Introduction

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.
  • Global.it.resx file to store text and images for italian language
  • Global.de.resx 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?

App_GlobalResources

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   .

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:

Resx.Properties

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   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. Everyone who knows something about the field knows that you need a different URL for each localized version of the same page. If you have doubts about that, we suggest you to carefully read this  brief Google guide about SEO featuring some useful best-practices, the most important one being the following: 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:

  • http://www.example.com/page (for default language contents – english in our example)
  • http://www.example.com/it/page (for all italian contents)
  • http://www.example.com/de/page (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   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     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   or   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    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.

Using a LocalizedControllerActivator instead of a LocalizationAttribute

IMPORTANT: A couple commenters made me recently realize that setting the culture in the OnActionExecuting method might be too late in some cases, due to how MVC lifecycle actually works. A common example of this is when the model metadata is being created by ASP.NET MVC right after a POST request, which is something that gets processed before any ActionFilter – including ours! When this happens, a lot of stuff (such as the DataAnnotations values) is handled using the default culture.

Luckily enough, we can easily overcome this by creating a LocalizedControllerActivator instead of the LocalizationAttribute in the following way:

And implement it in the following way (Global.asax.cs or Startup.cs, depending if you’re using the OWIN startup pattern or not):

This is the most reliable implementation to handle this issue, regardless if we’re working with DataAnnotations or not.

Source: StackOverflow (many thanks to s.ermakovitch for sharing this and to Rob LeGood for pointing it out).

Translating MVC Routes

There’s still a major issue we need to address yet: we need a good plan to translate/localize the URLs generated by the Razor helpers such as  ,   and so on. There are two ways we can choose to do that:

  • Implementing a custom   that will accept an additional     object (using   if/when ) and use it to prepend a   built against the   property value.
    EDIT: by popular demand, we recently added a sample implementation of this technique by using two extension methods to enhance the Html.ActionLink and Url.Action so they can accept a CultureInfo object (or just use the one provided by the application’s CurrentCulture).
  • Using the great RouteLocalization project by Dresel, available at this link, that can be used to do that and also a lot more interesting stuff such as “fully-translated” routes. You can also grab it via NuGet with the following names:
    •   (for MVC)
    •   (for WebApi)

If you just need to prepend the culture code to your URLs you might just stick to the former option and save some valuable time: if you don’t know how to get the relevant stuff done, just ask for help in this post comments and we’ll be more than happy to provide a working code sample. If you want more flexibility, such as being able to do   for English and   for Italian, you should really give the latter one a try. Here’s a great guide about how to use/implement it:

Conclusions

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:

  • http://www.example.com/page , seeing our contents in the default language (which is english in our example).
  • http://www.example.com/it/page , seeing our contents in italian language (with a fallback to the default language if not present).
  • http://www.example.com/de/page , seeing our contents in german language (with a fallback to the default language if not present).

… and so on.

 

RELATED POSTS

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.

View all posts by Ryan