Click here to Skip to main content
Click here to Skip to main content

Localizing WebSites, JavaScript and Assemblies with simple RESX files

, 10 Dec 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
History of globalization in ASP.NET and available options.

Introduction

Many of us have endured the problems that localization poses in website projects. Or projects in general.

There are many different ways to approach localization, and each usually has it's advantages and inconveniences.  

Let's walk through those options.

App_LocalResources   

Use classic App_LocalResources, and you get the flexibility of being able to organize your files quickly and efficiently. All it takes is to create a sub-folder underneath any file you want to localize called App_LocalResources, and start dropping .resx files in it while naming them the same as the file you want to localize, just ending with .resx for the base translation and {culture}.resx for each language specific translation.

Insert a key into your .resx called Title, and some value. 

And start using your translations:

<asp:Literal runat="server" Text="<%$Resources:Title %>" />
<asp:Button runat="server" Text="<%$Resources:Title %>" />

The Gotcha is that to make the magic work, <%$Resources:Title %> will only work on an element that is declared runat="server". So in many places you will be inserting our pretty little <asp:Literal /> and that can make for some really ugly syntax in certain situations.

Something more interesting you can do in projects that use .aspx files, is to use the less known meta:resourcekey.

<asp:TextBox runat="server" meta:resourcekey="MyInput" /> 

This allows you to do nice things like localize the full range of attributes of an element in a resx file while declaring only a single keyword in your .aspx file.

As you probably just guessed, the first part is the key identifier, and the second part is the name of the attribute you want to apply the value to. This is totally awesome as it saves a lot of ugly syntax from making it into your .aspx files.

And it can really be a savior with custom controls where you have tons of attributes. For example on special error handling controls: tooShortErrorMsg, tooLongErrorMsg, numericErrorMsg, notPrettyEnoughErrorMsg ..you get the idea.

Inconvienience:   

While the above is really cool, you have to set all the elements on which you want to use localization to runat="server".

You cannot use those translations inside code-behind code, nor inside classes or functions.

And you can't use them in JavaScript either ..unless you're willing to do this: 

<%@ Page Language="C#" ContentType="text/javascript" %>

var translations = {
    Title: "<asp:Literal runat="server" 
             Text="<%$Resources: Title %>" />"
};

Notice the ContentType declaration on the Page. Even though that's a nice trick to know, i really don't suggest you do that!

The above options are valid for both ASP.NET and MVC projects that use .aspx files instead of .cshtml files, since once we switch to .cshtml files we loose the ability to use runat="server".

App_GlobalResources 

Now this one is interesting in the fact that your translations are contained inside a single folder, instead of being spread out across your entire site. Not that that's a bad thing, since on many occasions it's quite practical to have your translations just one sub-folder away.

This folder sits at the root of your website.

Another interesting thing, is that App_GlobalResources actually generates compiled classes, and that means that you can stop using those magic runat="server" tags, and have access to real properties that you can even use in your code-behind.

<%= Resources.Common.Title %>

<asp:Literal runat="server" Text="<%$Resources: Common, Title %>" />

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    Response.Write(Resources.Common.Title);
}

That said, you can still use what applied above with the runat="server" tags, just now you have to declare in what namespace your translations are living in. 

You see, each file you add to App_GlobalResources will generate a class of the same name. Previously we added a file called common.resx, so we access that with Resources.Common.*, or <$Resources: Common, * %>

You can even do the little JavaScript trick, just now using real properties.

<%@ Page Language="C#" ContentType="text/javascript" %>

var translations = {
        Title: "<%= Resources.Common.Title %>"
};

By adding files of different names to App_GlobalResources you will generate just as many different classes, which is very convenient for organizing you translations by category (common, user, registration, etc) !

Inconvienience: 

App_GlobalResources was looking really good.

However by moving you files here, you just lost the possibility to do that cool meta:resourcekey trick. Actually this is because since App_GlobalResources compiles things to classes. Using a key identifier with punctuations (MyInput.Text) inside your .resx files becomes illegal syntax.

Localizing JavaScript files is still a bit of a bummer.

Even though App_GlobalResources allows you to organize your translations by class, you can add as many sub-folders as you like in there, still .NET will still only generate a flat namespace for you. Resources.Something, nothing more, nothing less. And this can be just fine in most cases, but in others can end up with hugely bloated .resx files or tons of KEY entries where one key is very similar to another but not quite so, etc.

And unfortunately, your translations are still locked to your website. Unless you are willing to add a reference of your website .dll to other assemblies. That is, if your website even has a .dll that's generated and sharable, which isn't the case with old style website projects.

A little something before moving on to dedicated assemblies

Now what if you could keep using the flexibility of App_LocalResources, while adding the advantages of App_GlobalResources?

Well, you can !

It's a bit tedious, but quite simple actually. All you have to do, is select the .resx that represents the base culture, choose properties, and change the "Custom Tool" to "PublicResXFileCodeGenerator"

And now you can magically do this:

<%= WebApplication1.App_LocalResources.Default_aspx.Title %>

OK, the namespace isn't very pretty, so an additional step you can take is to set the "Custom Tool NameSpace" so something like "MyCompany", which allows you to invoke it with:

<%= MyCompany.Default_aspx.Title %>

You still lose the ability to do the meta:resourcekey trick, and you still have the problem associated with sharing your translations with other projects. If you do want to share your website .dll with another project you will also have to set "Build Action" to "Embedded Resource".

This tells the compiler to embed the translations inside the generated dll. And you don't even have to push your .resx files to your production web-server anymore, since they are now directly inside the final DLL.

However as you saw, it can take quite a bit of manual action here if your site has hundreds of localized files, not to mention unforeseen errors that can occur if ever you forget to change those settings.

Standalone Assembly

So how about we just put all our translations inside a standalone .dll, where we can organize our translations with as many namespaces as we like, and gain the possibility to share that .dll with our website, and all our other projects too ?

Like this we can use our translations in our website to localize our web pages, in our assemblies to return error messages, localize properties or ViewModels. And it even works perfectly in .cshtml Razor files, since everything is compiled.

Now you can do all this:

<%= Resources.Models.User.Pseudo %>
 
@Resources.Models.User.Pseudo
 
public string Validate(User user)
{
    if (user == null) {
        return Resources.Models.User.NotExists;
    }
    
    return string.Empty;
}

Nice !

But .. another gotcha here is that like above you will have to set the "Custom Tool" to "PublicResXFileCodeGenerator", or you translations will be marked as private by .NET by default. And you still can't do the meta:resourcekey trick, and you still have the same problems with JavaScript.

However your translations are now in a standalone assembly where you can efficiently organize your namespaces, not to mention the great advantage you have to be able to share those translations with all your other projects, as well as being able to use them just as well in html, code-behind, functions & classes.

Let's go for the Ultra-Kill !

As you've seen we have quite a bit of options at our disposal when it comes to localization, each with their advantages and inconveniences.

So can we actually have something perfect, or almost perfect ? Well, sort of. But only if you are willing to code up a custom solution.

Enter: T4 Text Templating

T4 is a nice little technology that actually just boils down to a fancy .bat file with a .tt extension. And instead of executing DOS commands, you can actually instantiate the entire .NET framework.

Then ..you just start doing WriteLines().

While simple at base, it enables you spit out text, and have that text saved as a .cs file (or any extension you like). You can for example parse a .js file, apply minification to it, and spit the result out to a .min.js file.

In my case, I'm going to be getting a listing of all those .resx files we have in our project, instantiate a ResourceManager for each one to be able to extract the KEY and some extra information from them, and then spit out a nice big .cs file that contains all the namespaces, classes, and keys, in the form of properties so we can combine the best of all the worlds mentioned above.

In addition, since we are already starting to do custom code, we might as well add a bunch of goodies to it, so we can do things we would have never been able to do using any of the above mentioned methods.

For example i often want to format an error message with a variable. Other times there are words in the middle of my translations that i want to replace, but with the above solutions this would mean that I would have to split my text into multiple lines to be able to insert a Pseudo into the middle of a phrase for example. Why not just do "Hello {0}, how are you doing" ?

Also what happens when you have domain names, or brand names in your translations because you are working on some generic project, and then want to use those same translations in a different project for another customer ? Duplicate all the translations.. Why not just write "Sign up with {DOMAIN} and get the benefits of being a {BRAND} member for only {0}/month !", and then replace {DOMAIN} & {BRAND} depending on which customer we are dealing with ?

Oh, and our big big problem from the very start. Localized JavaScript ! We've seen how nasty every single solution up to now has been ..can we please just have a our translations in JSON format whenever we need them ?

T4 makes all this, and much more is possible, downside is you have to code it yourself. 

Of course since I'm writing about this, means I've already done most of the heavy lifting for you. And all that's left, is to add a reference of T4ResX to your "standalone assembly" that contains your .resx files

Then you just need to open the file, followed by a CTRL+S to trigger the build process, and T4ResX.cs will be created.

This code contains quick access to our regular translations, and helper methods to access all the extra goodies mentioned above.

Localized JavaScript now becomes easy, because the generated code contains a small method that (via reflection) is capable of searching our assembly for properties, and then returning them in the form of a Dictionary<>.

Dictionary<string, Dictionary<string, string>> result = MyAssembly.Resources.Common.GetAsDictionary();

Now once we have that Dictionary<namespace, Dictionary<key, value>, we just need a way to serialize it to JSON, and return the result.

So, now we just need to setup a little helper method in our website that will return the dictionary as JSON so you can start doing script src = , and enjoy the benefits of real localized JavaScript.

First we need a way to return JSON, we can do this by creating a new class in our project and declaring a little extension method as follows:

using System.Web.Script.Serialization;
 
/// <summary>
/// Mark as partial, like this we can create supplementary extension methods in other files,
/// instead of filling this file with tons of junk until it becomes unreadable.
/// 
/// Yeah, i know the namespace is missing, it's on purpose so we can access our extension method from AnyWhere.
/// </summary>
public static partial class ExtensionMethods
{
    /// <summary>
    /// Make it static and initialized, we can reuse it when we need.
    /// </summary>
    public static readonly JavaScriptSerializer JavaScriptSerializer = new JavaScriptSerializer();
 

    /// <summary>
    /// Our nice JSON helper method
    /// </summary>
    public static string ToJson(this object value)
    {
        return JavaScriptSerializer.Serialize(value);
    }
}

And in our controller (if using MVC that is), setup a little helper function:

public ActionResult GetNameSpaceAsJs(string ns)
{
    return JavaScript(string.Format("var T4ResX = {{ Localization: {0}}};", 
        Localization.Utilities.GetResourcesByNameSpace(ns).ToJson()));
}

Now all we need to do, is add this to our HTML, and we have localized JavaScript!

<script src="http://www.codeproject.com/home/GetNameSpaceAsJs?ns=OurNamespace"></script> 

>Of course I could have auto-generated all the above mentioned helper methods dynamically, but i do not wish to have a reference to System.Web inside the .tt file at the moment. And the helpers are relatively quick to setup manually.

Almost forgot!

How do you actually switch between cultures in your website ?

I'm not gonna go into the full details because the sample project has some fun stuff for you to play with. Nonetheless i'll list up your options:

Web.config

<System.Web><Globalization>
  • We can declare the culture by default for the user interface (uiCulture)
  • And the low level part of the site which impacts date/time/mathematical functions (culture)
  • We can also set the culture to auto-detect (uiCulture="auto:en")
    • This detects the preferred culture of the user's browser
  • There's also an attribute called EnableClientBasedCulture which extracts the culture from the HTTP Accept-Language Header of the client browser. But i find it less reliable than setting the first 2 options individually.
    • In code you can read Accept-Language via a shortcut: Request.UserLanguages
    • But again, you can run into problems here.. 
Page
  • On the @Page element we can declare UICulture and/or Culture
Code
  • We can set the Thread of the application to whatever culture we like
    • System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("en")
  • Like in @Page above you can set UICulture and/or Culture inside your OnLoad() event too Page.UiCulture = "en";
  • By overriding InitializeCulture() inside a Page
    • Actually setting cultures in Pages is a little bit broken, so this is the preferred method in those cases.
  • In MVC we have the ability to add custom Filter ?? attributes to our controllers
    • For example [LocalizedAttribute]
      • It's nice, but you can run into problems, like low level things not being translated, because the attribute kicks in too late.
HttpModule
  • We can create a custom HttpModule that sets the Thread of the application to whatever culture we need, based on various input variables: QueryString, Cookies, User Preferences, etc.. 
    • This is also the most performance efficient method, and triggers early enough so that everything in your site gets localized
    • Well ..this, and the web.config method 

Hope reading this wasn't too tedious and that you have fun with your future localizations Wink | <img src=

License

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

Share

About the Author

Robert Hoffmann
Software Developer (Senior) Index Multimedia
France France
Internet & Technology related professional since 1994. Passionate about WEB 2.0 and Community/Social networking related type Websites, or anything that is tech related to bridging the gap between the User and his everyday Multimedia Experience.
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
QuestionToJson() reference PinmemberMember 84253812-Nov-13 10:28 
AnswerRe: ToJson() reference PinmemberRobert Hoffmann12-Nov-13 10:37 
GeneralRe: ToJson() reference PinmemberMember 84253812-Nov-13 11:25 
GeneralRe: ToJson() reference PinmemberRobert Hoffmann12-Nov-13 11:30 
GeneralRe: ToJson() reference PinmemberMember 84253812-Nov-13 12:36 
GeneralRe: ToJson() reference PinmemberRobert Hoffmann12-Nov-13 12:37 
GeneralMy vote of 5 PinmemberPrabu ram10-Dec-12 18:37 
GeneralRe: My vote of 5 PinmemberRobert Hoffmann10-Dec-12 22:33 
GeneralMy vote of 5 Pinmembertanweer akhtar10-Dec-12 18:26 
GeneralRe: My vote of 5 PinmemberRobert Hoffmann10-Dec-12 22:33 
Thanks !

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411022.1 | Last Updated 10 Dec 2012
Article Copyright 2012 by Robert Hoffmann
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid