Pop quiz hotshot - you are building one of them massive single page web apps using something like Sencha where the vast majority of work is pushed into the JavaScript realm and some crazy nut wants everything localised or at least all text strings push to RESX files. What do you do? WHAT DO YOU DO?
Option 1
Well the most obvious option, and IMHO the most horrible, would be to convert all those JavaScript files into aspx's or cshtml's or whatever and embed the resource references directly into the files.
<%@ Import Namespace="Some.Resources" %>
var myWidget = new Widget({
title: '<%=Strings.DefaultWidgetTitle%>',
description: '<%=Strings.DefaultWidgetDescription%>'
});
myWidget.show(document.body);
This is far from nice. For one it kind of makes any static compression of the files either impossible or at best annoyingly fiddly. It also prevents any sort of quick client side caching unless you use some sort of VaryByCulture Output Caching strategy on the server side. Blegh.
Option 2
The next option, and certainly much better would be to pull out the direct <%= %> references and store them in another smaller file and reference them through a global JS object
<%-- DECALRED WITHIN THE MAIN HTML PAGE --%>
<script type="text/javascript">
var Strings = {
DefaultWidgetTitle: '<%=Strings.DefaultWidgetTitle%>',
DefaultWidgetDescription: '<%=Strings.DefaultWidgetDescription%>'
}
</script>
var myWidget = new Widget({
title: Strings.DefaultWidgetTitle,
description: Strings.DefaultWidgetDescription
});
myWidget.show(document.body);
This is nicer because it means you can compress and cache the JavaScript file globally without having to worry about different cultures etc. It does leave one annoyance though. The Strings object above it essentially boiler plate. The names are a 1:1 mapping of the resource file so we have introduced a layer of abstraction we have to write manually leaving us open to make some mistakes. Slightly less blegh, but still blegh.
Option 3
The solution I like the best is to provide a mechanism for serialising the Resource file into the equivalent JSON object. You get all the benefits of option 2 while not having to worry about having to write the mapping file. So lets keep the JS file from the second option and change the ASPX file
<%-- DECALRED WITHIN THE MAIN HTML PAGE --%>
<script type="text/javascript">
var Strings = <%= ResourceSerialiser.ToJson(typeof(Some.Resource.Strings)) %>
</script>
Now lets look at the magic behind this option - the JSON Serialiser
/// <summary>
/// Utility class that allows serialisation of .NET resource files (.resx)
/// into different formats
/// </summary>
public static class ResourceSerialiser
{
#region JSON Serialisation
/// <summary>
/// Converts a resrouce type into an equivalent JSON object using the
/// current Culture
/// </summary>
/// <param name="resource">The resoruce type to serialise</param>
/// <returns>A JSON string representation of the resource</returns>
public static string ToJson(Type resource)
{
CultureInfo culture = CultureInfo.CurrentCulture;
return ToJson(resource, culture);
}
/// <summary>
/// Converts a resrouce type into an equivalent JSON object using the
/// culture derived from the language code passed in
/// </summary>
/// <param name="resource">The resoruce type to serialise</param>
/// <param name="languageCode">The language code to derive the culture</param>
/// <returns>A JSON string representation of the resource</returns>
public static string ToJson(Type resource, string languageCode)
{
CultureInfo culture = CultureInfo.GetCultureInfo(languageCode);
return ToJson(resource, culture);
}
/// <summary>
/// Converts a resrouce type into an equivalent JSON object
/// </summary>
/// <param name="resource">The resoruce type to serialise</param>
/// <param name="culture">The culture to retrieve</param>
/// <returns>A JSON string representation of the resource</returns>
public static string ToJson(Type resource, CultureInfo culture)
{
Dictionary<string, string> dictionary = ResourceToDictionary(resource, culture);
return JsonConvert.SerializeObject(dictionary);
}
#endregion
/// <summary>
/// Converts a resrouce type into a dictionary type while localising
/// the strings using the passed in culture
/// </summary>
/// <param name="resource">The resoruce type to serialise</param>
/// <param name="culture">The culture to retrieve</param>
/// <returns>A dictionary representation of the resource</returns>
private static Dictionary<string, string> ResourceToDictionary(Type resource, CultureInfo culture)
{
ResourceManager rm = new ResourceManager(resource);
PropertyInfo[] pis = resource.GetProperties(BindingFlags.Public | BindingFlags.Static);
IEnumerable<KeyValuePair<string, string>> values =
from pi in pis
where pi.PropertyType == typeof(string)
select new KeyValuePair<string, string>(
pi.Name,
rm.GetString(pi.Name, culture));
Dictionary<string, string> dictionary = values.ToDictionary(k => k.Key, v => v.Value);
return dictionary;
}
}
Simple enough little class that is configurable by Culture etc. so you can pull different translation out on demand if needs be. Obviously it doesn't do anything around caching - SRP and all that stuff you know :-P
Yeah so I've used this on 2 projects already with great success so hopefully someone else finds it useful.