Saturday, November 24, 2012

Using T4 for localizing JavaScript resources based on .resx files

In our project we're using two languages: Dutch and English. Furthermore, the IT company who built the application is using a framework on top of EXT.NET and thus there is a lot of JavaScript involved. There are a couple of solutions out there for localizing JavaScript in an ASP.NET MVC application, and I think the most common is this one:
<%$Resources:Resource, FieldName %>
But that his obvious limitations as described here Use ASP.NET Resource strings from within javascript files, namely you cannot use inside the body nor in .js files. I used a slightly different approach to localize the JavaScript in or project, based on the discussion here keeping three goals in mind:
  • Having only 1 place where resources are managed ( namely the .resx files )
  • Support for multiple cultures
  • Leverage IntelliSense - allow for code completion
Because of the last point - I decided to go with code generation. Code generation of course normally comes down to T4 Text Templates. So - currently we're using a text template in the /Scripts folder which generates Resources.js ( marked as 'copy to output directory' ) which is published on Web Deploy. Here's an example:
var Resources = {
  Common: {},
 };

Resources.Common.Greeting = { 
 'nl-NL': 'Hallo',
 'en-GB': 'Hi'
 };
Which gives me IntelliSense in JavaScript as it gives me in C#:
In the master page I'm including the JavaScript file and set the global space var 'locale' using the thread culture on the server:
   

This enables us to use the resources like this:
var msg = Resources.Common.Greeting[locale];
alert(msg);
And now for the T4 template:
<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ output extension=".js"#>
<#
 var path = Path.GetDirectoryName(Host.TemplateFile) + "/../App_GlobalResources/";
 var resourceNames = new string[1]
 {
  "Common"
 };

#>
/**
* Resources
* ---------
* This file is auto-generated by a tool
* 2012 Jochen van Wylick
**/
var Resources = {
 <# foreach (var name in resourceNames) { #>
 <#=name #>: {},
 <# } #>
};
<# foreach (var name in resourceNames) { 
 var nlFile = Host.ResolvePath(path + name + ".nl.resx" );
 var enFile = Host.ResolvePath(path + name + ".resx" );
 ResXResourceSet nlResxSet = new ResXResourceSet(nlFile);
 ResXResourceSet enResxSet = new ResXResourceSet(enFile);
#>

<# foreach (DictionaryEntry item in nlResxSet) { #>
Resources.<#=name#>.<#=item.Key.ToString()#> = { 
 'nl-NL': '<#= ("" + item.Value).Replace("\r\n", string.Empty).Replace("'","\\'")#>',
 'en-GB': '<#= ("" + enResxSet.GetString(item.Key.ToString())).Replace("\r\n", string.Empty).Replace("'","\\'")#>'
 };
<# } #>
<# } #>
Find the code snippet here The resourceNames array takes the names of the resource files that I want included in the .js file. The solution is generic enough for our needs right now, and if you would want to include other languages - some changes would be required. The shortcomings of this solution are of course that the size of the .js file might become quite large. However, since it's cached by the browser, we don't consider this a problem for our application. However - this caching can also result in the browser not finding the resource called from code.

2 comments:

  1. Dear Jochen,

    What a joy to find your well thought generic solution!

    I've integrated it in my project and I'm happily taking advantage of strings stored in both .resx (*.pt-BR and .resx) files. No more duplication. DRY principle.

    I'm going to share your post here: http://stackoverflow.com/q/104022/114029


    Thanks and God bless your life,


    Leniel

    ReplyDelete
  2. Thanks Leniel, glad you like it, thank you for the feedback.

    ReplyDelete