Wednesday 24 February 2010

Javascript Localization using .Net Resource files and jQuery

As a web developer I'm shifting more and more code to the client, and a few weeks ago an interesting situation came up on a multi-lingual project. ie wanted to include translations for validation messages in jQuery code, but stay with the great .Net resource file model.
What I came up with was a pattern using JQuery AJAX and JSON to download the resource file to the browser. The translations are available using the same object notation available on the server eg
// -- Localisation --
var localResources;
var globalResources;

//Sample JSON for javascript resource values eg {TrackDetail:{HideHelp:"Hide Help", ShowHelp:"Show Help"}}
//Usage e.g: alert(localResources.TrackDetail.HideHelp);

//Load Localisation values into variables so that they can be available on the client
//Note that these values are loaded asynchronously, the code in the function will not run until the call has completed.
$.getJSON('Localisation.ashx?files=TrackDetail.aspx.resx&local=true', function(data) { localResources = data});
$.getJSON('Localisation.ashx?files=Errors.resx,Strings.resx', function(data) { globalResources = data});
The code on the server reads the local or global resource file and streams the contents back as a JSON formatted object. Note that I opted to roll my own JSON serialization code instead of using one of the built-in serializers.
Imports System.Web
Imports System.Web.Services
Imports System.Xml
Imports System.Resources
Imports System.Reflection

Public Class Localisation
  Implements System.Web.IHttpHandler

  Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
    Dim files As String = context.Request.QueryString("files")
    Dim local As String = context.Request.QueryString("local")
    Dim isLocal As Boolean
    Dim folder As String = "App_GlobalResources"
  
    context.Response.ContentType = "text/javascript"

    'Write out file as object
    context.Response.Write("{")

    'Determine if local resource file
    If local IsNot Nothing Then
      isLocal = CBool(local)
      If isLocal Then folder = "App_LocalResources"
    End If
    If files Is Nothing OrElse files.Length = 0 Then Throw New ArgumentException("Parameter 'files' was not provided in querystring.")
  
    Dim flag As Boolean = False
    For Each file As String In files.Split(",")
      If flag Then context.Response.Write(",")

      Dim className As String = file.Split(".")(0)
    
      'Write the class (name of the without any extensions) as the object
      context.Response.Write(className)
      context.Response.Write(":{")

      'Open the resx xml file
      Dim filePath As String = context.Server.MapPath("~\" & folder & "\" & file)
      Dim document As New XmlDocument()
      Dim flag2 As Boolean = False
      document.Load(filePath)

      Dim nodes As XmlNodeList = document.SelectNodes("//data")

      For Each node As XmlNode In nodes

        'Write out the comma seperator
        If flag2 Then context.Response.Write(",")

        Dim attr As XmlAttribute = node.Attributes("name")
        Dim resourceKey As String = attr.Value
        context.Response.Write(resourceKey)
        context.Response.Write(":""")

        'Write either the local or global value
        If isLocal Then
        context.Response.Write(HttpContext.GetLocalResourceObject(String.Format("~/{0}",    file.Replace(".resx", "")), resourceKey)) 'Has to be full path to the .aspx page
        Else
          context.Response.Write(HttpContext.GetGlobalResourceObject(className, resourceKey))
        End If
        context.Response.Write("""")

        'Flag that we need a comma seperator
        flag2 = True
      Next

      context.Response.Write("}")
      flag = True
    Next
  
    'End file
    context.Response.Write("}")
  End Sub

  Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
    Get
      Return True
    End Get
  End Property
End Class