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 HttpHandler
s 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.
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. :)
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="HelveticSolutions.Resources.WebStyles,
HelveticSolutions.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="HelveticSolutions.Web.Internationalization.ResourceHandler,
HelveticSolutions.Web, Version=1.0.0.0, Culture=neutral"
validate="false"/>
<add path="*.css" verb="*"
type="HelveticSolutions.Web.Internationalization.ResourceHandler,
HelveticSolutions.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 HelveticSolutions.Web.Internationalization
{
public class ResourceHandler : IHttpHandler, IRequiresSessionState
{
#region members
private const string RESOURCEPATTERN =
@"<\x25{1}\x24{1}\s*Resources\s*:\s*(?<declaration >\w+\s*,\s*\w+)\s*%>";
private const string SETTINGSPATTERN =
@"<resourcesettings>(?>.|\n)+?resourceSettings>";
private CultureInfo defaultCulture;
#endregion
public delegate CultureInfo OnApplyCulture();
public static event OnApplyCulture ApplyCulture;
#region constructor
public ResourceHandler()
{
defaultCulture = Thread.CurrentThread.CurrentCulture;
}
#endregion
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.Buffer = false;
if (ApplyCulture != null)
{
CultureInfo culture = ApplyCulture() ?? defaultCulture;
if (culture != null)
{
Thread.CurrentThread.CurrentCulture =
CultureInfo.CreateSpecificCulture(culture.Name);
Thread.CurrentThread.CurrentUICulture = culture;
}
}
string physicalFilePath = context.Request.PhysicalPath;
string fileContent = string.Empty;
string convertedFile = string.Empty;
if (File.Exists(physicalFilePath))
{
using (StreamReader streamReader = File.OpenText(physicalFilePath))
{
fileContent = streamReader.ReadToEnd();
if (!string.IsNullOrEmpty(fileContent))
{
Dictionary<string,> locationTypes =
GetResourceLocationTypes(fileContent);
convertedFile =
ReplaceResourcePlaceholders(fileContent, locationTypes);
}
}
}
context.Response.ContentType = GetHttpMimeType(physicalFilePath);
context.Response.Output.Write(convertedFile);
context.Response.Flush();
}
#endregion
private static Dictionary<string,> GetResourceLocationTypes(string fileContent)
{
try
{
Match settingsMatch = Regex.Match(fileContent, SETTINGSPATTERN);
Dictionary<string,> resourceLocations = new Dictionary<string,>();
while (settingsMatch.Success)
{
string value = settingsMatch.Groups[0].Value.Replace("///",
string.Empty).Replace("//", string.Empty);
XElement settings = XElement.Parse(value);
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));
resourceLocations = new[] { resourceLocations, newLocations }
.SelectMany(dict => dict)
.ToDictionary(pair => pair.Key, pair => pair.Value);
settingsMatch = settingsMatch.NextMatch();
}
return resourceLocations;
}
catch (Exception ex)
{
return null;
}
}
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)
{
if (resourceMatch.Groups["declaration"] != null)
{
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();
if (!replacedResources.Contains(string.Concat(
resourceLocationName, resourceName)))
{
Type resourceLocationType;
if (resourceLocations.TryGetValue(resourceLocationName,
out resourceLocationType))
{
PropertyInfo resourcePropertyInfo =
resourceLocationType.GetProperty(resourceName);
if (resourcePropertyInfo != null)
{
string localizedValue =
resourcePropertyInfo.GetValue(null,
BindingFlags.Static, null, null, null).ToString();
outputString =
outputString.Replace(resourceMatch.Groups[0].Value,
localizedValue);
replacedResources.Add(string.Concat
(resourceLocationName, resourceName));
}
else
{
throw new ArgumentException("Resource name");
}
}
else
{
throw new ArgumentException("Resource location");
}
}
}
resourceMatch = resourceMatch.NextMatch();
}
return outputString;
}
private static Type GetTypeFromFullQualifiedString(string typeString)
{
string fullQualifiedAssemblyName =
typeString.Substring(typeString.IndexOf(",") + 1,
typeString.Length - 1 - typeString.IndexOf(",")).Trim();
string fullTypeName = typeString.Split(',')[0].Trim();
Assembly repositoryAssembly = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !string.IsNullOrEmpty(a.FullName) &&
a.FullName.Equals(fullQualifiedAssemblyName))
.FirstOrDefault();
if (repositoryAssembly == null)
{
repositoryAssembly =
AppDomain.CurrentDomain.Load(fullQualifiedAssemblyName);
}
return repositoryAssembly.GetTypes()
.Where(t => t.FullName.Equals(fullTypeName))
.FirstOrDefault();
}
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:
- Set the current thread culture.
- Load the requested file content from the file system.
- Extract resource configuration(s) using a Regular Expression.
- Try to load each configured resource type using Reflection.
- Extract resource place holders using a Regular Expression and replace them with the respective localized string using Reflection.
- 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()
{
string culture = Convert.ToString(Session["Culture"]);
if (!string.IsNullOrEmpty(culture))
{
return new CultureInfo(culture);
}
return Thread.CurrentThread.CurrentCulture;
}