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

Localization for Scripts (JavaScript) and Style Sheets in ASP.NET

, 19 May 2009
Rate this:
Please Sign up or sign in to vote.
Often JavaScript content and/or styles in ASP.NET need to be localized too. This article shows how this can be leveraged by using an HttpHandler.

Introduction

In ASP.NET applications, replacing strings by their respective resources often isn't enough to localize. Most of the applications nowadays extensively use JavaScript especially to utilize AJAX. Hence, it is likely that the JavaScript gets moved into a separate file in order to keep the markup neat.

This article shows how JavaScript and style sheet content can be localized using an HttpHandler.

Background

Note: This article is thought as a 'How to' more than a detailed description of all the technologies used. Hence, it is targeted at intermediate and advanced programmers.

The idea is that all requests for *.js and *.css files get processed by some custom written code. That's where HttpHandlers come in handy. The handler gets executed every time a file type which it is registered for gets requested. This registration is configured in the web.config file.

Our HttpHandler then replaces all resource place holders by their respective localized strings stored in a resource file.

Preparing Resources

If the resources are to be stored in a separate library, a couple of things need to be kept in mind.

In order to be able to access the resources, the access modifier of the neutral culture resource file wrapper class needs to be configured as public. This can be achieved by double clicking the resource from the Solution Explorer and then changing the 'Access Modifier' dropdown (as shown in the picture below) to Public.

LocalizationForJsAndCSS

If the resources live in the App_GlobalResources folder of the web application and the HttpHandler does not live within the web application project, the following line has to be added to the AssemblyInfo.cs file of the web application:

[assembly: InternalsVisibleTo("SmartSoft.Web")]

This makes all internal types of the web application visible for the declared assembly.

The specified string needs to correspond to the external library containing the HttpHandler. If that assembly is signed, it is mandatory that the fully qualified name is used including the public key token.

Preparing a JavaScript File

In order to know where resources shall be loaded from, the JavaScript file needs to have some extra information attached. The resources configuration has to be contained in XML as shown below being commented out. It doesn't matter where this definition is located within the file, and the file can contain multiple definitions too.

//<resourceSettings>
// <resourceLocation name="WebScriptResources" 
//    type="SmartSoft.Resources.WebScripts, 
//          SmartSoft.Resources, Version=1.0.0.0, Culture=neutral" />
//</resourceSettings>

The name defines the resource key used in the resource place holder(s) and the type is the fully qualified type name used to load the requested resource on runtime using Reflection.

The resource place holders can then be used everywhere in the file like this:

alert("<%$ Resources: WebScriptResources, TestAlert %>");

You may have noticed that the resource declaration is exactly the same as the standard one used by the .NET Framework. This helps if the file content gets extracted from an existing markup. Smile | :)

Preparing a CSS File

The resource configuration for a style sheet is exactly the same as described above for JavaScript files.

/* <resourceSettings>
  <resourceLocation name="WebStylesResources" 
      type="SmartSoft.Resources.WebStyles, 
            SmartSoft.Resources, Version=1.0.0.0, 
            Culture=neutral" />
</resourceSettings> */

And the place holders:

body {
    background-color: <%$ Resources: WebStylesResources, BodyBackgroundColor %>;
}

The web.config

A HttpHandler can be configured for any type of request. For the ResourceHandler, the following two entries need to be added for the *.js and *.css file extensions:

<system.web>
  <httpHandlers>
    <add path="*.js" verb="*" 
      type="SmartSoft.Web.Internationalization.ResourceHandler, 
            SmartSoft.Web, Version=1.0.0.0, Culture=neutral" 
            validate="false"/>
    <add path="*.css" verb="*" 
      type="SmartSoft.Web.Internationalization.ResourceHandler, 
            SmartSoft.Web, Version=1.0.0.0, Culture=neutral" 
            validate="false"/>
  </httpHandlers>
</system.web>

This causes the web server to re-route the requests to the HttpHandler.

In case of using IIS as web server, the application mappings need to be extended for these two file extensions. A description of how to do that can be found here.

It is important that the option 'Verify that file exists' is not checked.

The ResourceHandler (HttpHandler)

Most of the ResourceHandler is pretty much self-explanatory. However, the ApplyCulture event may need some explanation.

Since there might be several simultaneous requests (hopefully), it is crucial that each and every request sets the culture according to the user's latest choice before replacing the resource place holders. In the attached example, the Session is used as storage to cache the user's language preference. Alternatively, it could be determined from the browser settings or somewhere else.

In any case, it is strongly recommended to register the ApplyCulture event during the session start as outlined under "The Global.asax file".

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using System.Web.SessionState;
using System.Xml.Linq;

namespace SmartSoft.Web.Internationalization
{
    /// <summary>
    /// A custom HTTP handler that replaces resource
    /// place holders by their respective localized values.
    /// </summary>
    public class ResourceHandler : IHttpHandler, IRequiresSessionState
    {
        #region members
        /// <summary>Regex pattern to extract
        /// the resource place holders.</summary>
        private const string RESOURCEPATTERN = 
          @"<\x25{1}\x24{1}\s*Resources\s*:\s*(?<declaration >\w+\s*,\s*\w+)\s*%>";

        /// <summary>Regex pattern to extract
        /// the resource location settings.</summary>
        private const string SETTINGSPATTERN = 
          @"<resourcesettings>(?>.|\n)+?resourceSettings>";

        /// <summary>Caches the default culture set
        /// when the handler got instantiated.</summary>
        private CultureInfo defaultCulture;
        #endregion

        public delegate CultureInfo OnApplyCulture();
        public static event OnApplyCulture ApplyCulture;

        #region constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="ResourceHandler" /> class.
        /// </summary>
        public ResourceHandler()
        {
            defaultCulture = Thread.CurrentThread.CurrentCulture;
        }
        #endregion

        #region IHttpHandler Members

        /// <summary>
        /// Gets a value indicating whether another request can use
        /// the <see cref="T:System.Web.IHttpHandler" /> instance.
        /// </summary>
        /// <returns>
        /// True if the <see cref="T:System.Web.IHttpHandler"/>
        /// instance is reusable; otherwise, false.
        public bool IsReusable
        {
            get { return true; }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler
        /// that implements the <see cref="T:System.Web.IHttpHandler" /> interface.
        /// </summary>
        /// <param name="context">
        /// An <see cref="T:System.Web.HttpContext" /> object that
        /// provides references to the intrinsic server objects (for example, Request, 
        /// Response, Session, and Server) used to service HTTP requests.
        /// </param>
        public void ProcessRequest(HttpContext context)
        {
            context.Response.Buffer = false;

            // Retrieve culture information from session
            if (ApplyCulture != null)
            {
                CultureInfo culture = ApplyCulture() ?? defaultCulture;
                if (culture != null)
                {
                    // Set culture to current thread
                    Thread.CurrentThread.CurrentCulture = 
                           CultureInfo.CreateSpecificCulture(culture.Name);
                    Thread.CurrentThread.CurrentUICulture = culture;
                }
            }

            string physicalFilePath = context.Request.PhysicalPath;
            string fileContent = string.Empty;
            string convertedFile = string.Empty;

            // Determine whether file exists
            if (File.Exists(physicalFilePath))
            {
                // Read content from file
                using (StreamReader streamReader = File.OpenText(physicalFilePath))
                {
                    fileContent = streamReader.ReadToEnd();

                    if (!string.IsNullOrEmpty(fileContent))
                    {
                        // Load resource location types
                        Dictionary<string,> locationTypes = 
                            GetResourceLocationTypes(fileContent);

                        // Find and replace resource place holders
                        convertedFile = 
                           ReplaceResourcePlaceholders(fileContent, locationTypes);
                    }
                }
            }

            context.Response.ContentType = GetHttpMimeType(physicalFilePath);
            context.Response.Output.Write(convertedFile);
            context.Response.Flush();
        }

        #endregion

        /// <summary>
        /// Gets the resource location types defined as resource settings in a string.
        /// </summary>
        /// <param name="fileContent">The file content.</param>
        /// <returns>A <see cref="Dictionary{TKey,TValue}"/>
        /// containing all resource location types.</returns></returns />
        private static Dictionary<string,> GetResourceLocationTypes(string fileContent)
        {
            try
            {
                // Attempt to extract the resource settings from the file content
                Match settingsMatch = Regex.Match(fileContent, SETTINGSPATTERN);
                Dictionary<string,> resourceLocations = new Dictionary<string,>();

                while (settingsMatch.Success)
                {
                    // Get matched string and clean it up
                    string value = settingsMatch.Groups[0].Value.Replace("///", 
                           string.Empty).Replace("//", string.Empty);
                    XElement settings = XElement.Parse(value);

                    // Load resource location assemblies and reflect the resource type
                    Dictionary<string,> newLocations = 
                      settings.Elements("resourceLocation")
                        .Where(el => el.Attribute("name") != null && 
                               el.Attribute("type") != null)
                        .ToDictionary(
                        el => el.Attribute("name").Value,
                        el => GetTypeFromFullQualifiedString
				(el.Attribute("type").Value));

                    // Merge resource location dictionaries
                    resourceLocations = new[] { resourceLocations, newLocations }
                        .SelectMany(dict => dict)
                        .ToDictionary(pair => pair.Key, pair => pair.Value);

                    // Find next regex match
                    settingsMatch = settingsMatch.NextMatch();
                }

                return resourceLocations;
            }
            catch (Exception ex)
            {
                // Attempt to read resource settings failed
                // TODO: Write to log
                return null;
            }
        }

        /// <summary>
        /// Replaces the resource placeholders.
        /// </summary>
        /// <param name="fileContent">Content of the file.</param>
        /// <param name="resourceLocations">The resource locations.</param>
        /// <returns>File content with localized strings.</returns>
        private static string ReplaceResourcePlaceholders(string fileContent, 
                IDictionary<string,> resourceLocations)
        {
            string outputString = fileContent;
            Match resourceMatch = Regex.Match(fileContent, RESOURCEPATTERN);
            List<string> replacedResources = new List<string >();

            while (resourceMatch.Success)
            {
                // Determine whether a valid match was found
                if (resourceMatch.Groups["declaration"] != null)
                {
                    // Extract resource arguments -> always two
                    // arguments expected: 1. resource location name, 2. resource name
                    string[] arguments = 
                      resourceMatch.Groups["declaration"].Value.Split(',');

                    if (arguments.Length < 2)
                    {
                        throw new ArgumentException("Resource declaration");
                    }

                    string resourceLocationName = arguments[0].Trim();
                    string resourceName = arguments[1].Trim();

                    // Determine whether the same resource has been 
		  // already replaced before
                    if (!replacedResources.Contains(string.Concat(
                                 resourceLocationName, resourceName)))
                    {
                        Type resourceLocationType;
                        if (resourceLocations.TryGetValue(resourceLocationName, 
                                                          out resourceLocationType))
                        {
                            PropertyInfo resourcePropertyInfo = 
                               resourceLocationType.GetProperty(resourceName);
                            if (resourcePropertyInfo != null)
                            {
                                // Load resource string
                                string localizedValue = 
				resourcePropertyInfo.GetValue(null, 
                                  	BindingFlags.Static, null, null, null).ToString();

                                // Replace place holder
                                outputString = 
                                  outputString.Replace(resourceMatch.Groups[0].Value, 
                                                       localizedValue);

                                // Cache replaced resource name
                                replacedResources.Add(string.Concat
					(resourceLocationName, resourceName));
                            }
                            else
                            {
                                throw new ArgumentException("Resource name");
                            }
                        }
                        else
                        {
                            throw new ArgumentException("Resource location");
                        }
                    }
                }

                // Find next regex match
                resourceMatch = resourceMatch.NextMatch();
            }

            return outputString;
        }

        /// <summary>
        /// Determines and returns the <see cref="Type" />
        /// of a given full qualified type string.
        /// </summary>
        /// <param name="typeString">The full qualified type string. E.g. 
        /// 'SampleLibrary.SampleType, SampleLibrary,
        ///      Version=1.0.0.0, Culture=neutral'.</param>
        /// <returns>The determined <see cref="Type"/>.</returns>
        private static Type GetTypeFromFullQualifiedString(string typeString)
        {
            // Extract resource manager type name and
            // full qualified assembly name from string
            string fullQualifiedAssemblyName = 
              typeString.Substring(typeString.IndexOf(",") + 1, 
                         typeString.Length - 1 - typeString.IndexOf(",")).Trim();
            string fullTypeName = typeString.Split(',')[0].Trim();

            // Determine whether assembly is already loaded
            Assembly repositoryAssembly = AppDomain.CurrentDomain.GetAssemblies()
                    .Where(a => !string.IsNullOrEmpty(a.FullName) && 
                        a.FullName.Equals(fullQualifiedAssemblyName))
                    .FirstOrDefault();

            if (repositoryAssembly == null)
            {
                // Load assembly
                repositoryAssembly = 
                  AppDomain.CurrentDomain.Load(fullQualifiedAssemblyName);
            }

            // Attempt to load type
            return repositoryAssembly.GetTypes()
                .Where(t => t.FullName.Equals(fullTypeName))
                .FirstOrDefault();
        }

        /// <summary>
        /// Gets the HTTP MIME type.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>        
        /// <returns>The HTTP MIME type.</returns>
        private static string GetHttpMimeType(string fileName)
        {
            string fileExtension = new FileInfo(fileName).Extension;

            switch (fileExtension)
            {
                case "js":
                    return "application/javascript";
                case "css":
                    return "text/css";
                default:
                    return "text/plain";
            }
        }
    }
}

Processing content:

  1. Set the current thread culture.
  2. Load the requested file content from the file system.
  3. Extract resource configuration(s) using a Regular Expression.
  4. Try to load each configured resource type using Reflection.
  5. Extract resource place holders using a Regular Expression and replace them with the respective localized string using Reflection.
  6. Return modified file content.

The Global.asax File

As mentioned above, at the session start, the ApplyCulture event of the ResourceHandler should be registered. This enables the web application to handle multiple simultaneous requests from users with different culture settings.

protected void Session_Start(object sender, EventArgs e)
{
    ResourceHandler.ApplyCulture += ResourceHandler_ApplyCulture;
}

private CultureInfo ResourceHandler_ApplyCulture()
{
    // Retrieve culture information from session
    string culture = Convert.ToString(Session["Culture"]);

    // Check whether a culture is stored in the session
    if (!string.IsNullOrEmpty(culture))
    {
        return new CultureInfo(culture);
    }

    // No culture info available from session -> pass on
    // the currently used culture for this session.
    return Thread.CurrentThread.CurrentCulture;
}

License

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

About the Author

Michael Ulmann
Architect Helvetic Solutions
Australia Australia
MCAD, MCPD Web Developer 2.0, MCPD Enterprise Developer 3.5
My company: www.helveticsolutions.com
Hopp Schwiiz Smile | :)

Comments and Discussions

 
GeneralGood Job Man! 5 from me :) PinmemberMoim Hossain10-Jun-10 9:22 
GeneralRe: Good Job Man! 5 from me :) PinmemberMichael Ulmann19-Jun-10 0:25 

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 | Mobile
Web04 | 2.8.140709.1 | Last Updated 19 May 2009
Article Copyright 2009 by Michael Ulmann
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid