One of ASP.NET's big strengths has always been its powerful support for localization. Happily, ASP.NET MVC applications are able to take advantage of that same strength, though there are a couple of hurdles you'll need to jump through to get everything working in perfect order. I recently spent some time working out these details, and thought I would share an overview of what I found along with some step-by-step instructions to help anyone else who might be looking for examples of how to implement localization support in their ASP.NET MVC applications.
Localization and the .NET Framework
The .NET Framework makes use of Resource, or .resx, files that are named by convention against the different languages that you want to support. So, for example, if you had the following files:
The first .resx file would contain resources, or definitions, for the default language or culture on your server (English, or en-US, in my case). The second .resx file would be used for any French (fr) speakers, while the third .resx file is more specific and will only be used against French-speaking Canadians (or fr-CA).
Each .resx file, in turn, consists of name-value pairs that allow different “translations” of the value of a shared Name (or key) to be defined or supplied in different files. For example, if I had a key or Name of “FavoriteFruit” in my HomePage.resx, the value might be "Apple". But in the HomePage.fr.resx file, the key “FavoriteFruit” would still exist, but the defined value would be “Une Pomme.”
Then to make these name-value pairs more accessible and even easier to use, the .NET Framework actually makes it very easy to compile .resx files down to actually classes, or objects, that can be accessed as if they contained static members. Even better, by assigning a language (or a language and culture) to the thread accessing these classes, the .NET Framework is able to magically pull back translated versions of the Names or keys being requested. All in all, this is quite slick, and it even provides support for IntelliSense when done correctly.
Localization ASP.NET MVC Websites
Though you can run into some issues with output caching (that I'll be looking into in the next few days), the easiest way to assign a language (and culture) to an ASP.NET thread is just to let tell ASP.NET to do this automatically. To do so, just add or modify your web.config (in the <system.web> section) as follows:
With that change in place, ASP.NET will detect the Accept-Language header defined by visitors to your site, and bind the specified language and culture for transparent use by your executing threads.
Once you've set the wheels in motion, you're then ready to start creating localization resources for your views, along with resources for any controller or model needs that you might have. At this point, you've got a number of options available to you. What follows is an approach I've found to work so far for my needs.
For localizing views, I've found that the best process is to use local resources (or resources contained within the same assembly). This allows translations and localization definitions to be kept locally - and in the same folders as views themselves. So, for example, if I have a Home controller, I'll therefore also (by MVC convention) have a "Home” folder in my Views folder.
To localize views in that folder, I prefer adding a new App_LocalResources ASP.NET Special Folder to my "Home" views folder. Then, for each view, or page, I can just create a new .resx file to keep things simple, isolated, and easy to manage. And, of course, for each view or page, I'll also need a corresponding .resx file for each language (or language-culture) that I'm going to support.
This approach may not work perfectly in every scenario, but so far I've found that it works well. There are a couple of tricks to getting it working though. So here's a step-by-step outline of what you'll need to do to get localization support for your views using Local Resources:
- Add a new App_LocalResources to your Views folder in question. (Right-click in Visual Studio's Solution Explorer, and then use the Add sub-menu option.)
- To your newly added App_LocalResources folder, add a new item. I'll be adding a .resx file for my Index.aspx view. So I'll specify Index.resx as my file name (which is a tiny bit different than what you'd do if this were an ASP.NET WebForms application).
- In the .resx file, add a new Name-Value pair. I'll just use Name: "GoesGreatWithCrackers" and Value: "Cheese".
- Save Changes, and then close your .resx file.
- Now click it in the Solution Explorer, and make sure that your Properties window is open.
- In the Properties window, set the Build Action property to Embedded Resource. Then set the Custom Tool Property to: "PublicResXFileCodeGenerator". (You can also do this by re-opening the .resx file, and flipping the drop-down for the Access Modifier at the top of the .resx designer to Public and saving/closing your file.) This, however, is the key to getting the .NET Framework to compile your .resx file as part of a class. (And, note that at this point we're “upgrading” our ASP.NET Local Resources to Global resources, so that both Unit Testing becomes easier AND to help make IntelliSense work as needed.)
- Now set the Custom Tool Namespace Property for your .resx file (in the Properties window) to something that will be easy enough to reference. I like specifying "Home" (or "Views") here because that make it easier to access my localization strings/resources from IntelliSense. Whatever you do, just pick something simple, and make it something that you can use consistently across your site.
- Now add a new .resx file to this same App_LocalResources folder. I'm going to create one for French speakers, so I'm going to name mine Index.fr.resx.
- In the Index.fr.resx file, add a new Name-Value pair with Name: "GoesGreatWithCrackers" and Value: "Du Fromage".
- Save and close the Index.fr.resx file.
- Now select it in the Properties Window, and go in and set its properties to all PERFECTLY match the properties you specified for the previous file.
- Double-check that all Properties for your two .resx files are the same.
- Build your Application. (IntelliSense can't 'see' these values until they're compiled. )
- Now open up your Home.aspx view. Add a new line as follows (and note that you get IntelliSense as you start typing the namespace to your Resource's namespace):
<p>Goes well with Crackers: <%= Home.Index.GoesGreatWithCrackers %>.</p>
- F5 or debug/launch your ASP.NET MVC web page and navigate to your /Home/Index view. You should see that it has filled everything out as needed in your default language.
- In your browser, change your preferred language to French. In FireFox, for example, select Tools > Options, then on the Content tab, select the Choose button to specify which language you want. Make sure that once you specify a French variant that you then push Move Up to make it the first language or entry in the list of language preferences. Save your changes, and reload the page. You should now be seeing the dynamically requested value in French.
And, just note that while you can push localized images up into .resx files, pulling them out in a web application can be painful, so I'd recommend just keeping different file paths as needed, or something else. Likewise, in a real application, we wouldn't just be localization a single word - but the entire page (so you'd probably want a .resx file for each Master Page on your site, and so on).
Localizing Controller and Model Details
Once you've localized your views, it's time to look at localizing aspects of your model (such as validation errors) as well as controller details that might be sent out to end-users (like processing errors or notifications).
To handle these needs, I've found that using satellite assemblies makes the most sense. To create a satellite assembly with localization resources:
- Add a new Project to your Solution. Make sure it's just a plain-old Windows Class Library. I'm going to name mine SiteResources.
- I prefer to add a two folders, one called Validation, the other called Errors. But you can organize resources however seems best to you. In this example, I'll be making a folder called Controllers since I'll be showing a lame example of specifying how to define controller messages to users using the default MVC template.
- In your folder, add a new .resx file. I'm going to call mine Home.resx (and I'm putting it in my Controllers folder).
- Add a new Name called "Message" with the Value of "Welcome to ASP.NET MVC!" Save your .resx file and close it.
- Then go through and set the properties for this file as follows (using the Properties window):
Build Action: Embedded Resource (should already be set)
Custom Tool: PublicResXFileCodeGenerator (same as for Views)
Custom Tool Namespace: Messages (or something similar - just don't use "Controllers" or you'll collide with default namespaces in your MVC project).
- Add another .resx file, this time called Home.fr.resx. Add a new Name of "Message" with a value of "ASP.NET MVC vous souhaite la bienvenue!". Save your changes, close the file, and then set all of the properties as needed.
- Build your Satellite Assembly.
- Add a reference to your Satellite Assembly project in your MVC site.
- Assuming that you're using a default ASP.NET MVC template/project for your site, go into the Controllers folder and open up HomeController.cs.
- In the Index() Action method, replace the statically defined string ("Welcome to ASP.NET MVC!") with a reference to the welcome message in your satellite assembly, like so:
ViewData\\["Message"\\] = Messages.Home.Message;
- Recompile your project and preview it in your browser. It should, by default, display the welcome message in French. Then change your language preferences back to English (or your native language) and you'll get the English version.
By taking the approach of keeping error and validation messages in satellite assemblies, you won't run the risk of colliding with your unit tests—something you'll want to consider.
Of course, speaking of unit tests, you'll probably want to have a couple of tests around to make sure that localization is working. Happily, that's quite easy to pull off. Simply add a reference in your Tests project to your satellite resource assembly, and from there things are pretty easy.
To verify that resource files are available, you just need to create tests that compile the following lines (I created a distinct test for each line):
string goesGreatWithCrackers = Home.Index.GoesGreatWithCrackers;
string message = Messages.Home.Message;
If those tests don't compile, then you're missing references, or something has gone wrong. Either way, you can't validate that localization resources are available.
Then, to test that you can see values in a given language, you just need to arrange, act, and then assert using the following approach:
string culture = "fr-FR";
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfoByIetfLanguageTag(culture);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfoByIetfLanguageTag(culture);
string frenchGoesGreatWithCrackers = Home.Index.GoesGreatWithCrackers;
Assert.AreEqual("Du Fromage", frenchGoesGreatWithCrackers);
That's really all there is to it (though you'll likely want to create another test to make sure that controller/model resources are working correctly too).
Getting localization just right can depend upon a number of factors. In this article I've just outlined a couple of step-by-step approaches that will get you going with the basics. Consequently, you'll want to do your own testing and planning about what specific approaches you'll want to take. But hopefully this article has gotten you started. And, if for some reason you can't get the steps I've outlined above working, just drop me a line at [email protected], and I'll be happy to email you a sample project I've created. Happy coding.