ASP.NET 中脚本(JavaScript)和样式表的本地化






4.75/5 (14投票s)
通常 JavaScript 内容和/或 ASP.NET 中的样式也需要本地化。本文将展示如何通过使用 HttpHandler 来实现这一点。
引言
在 ASP.NET 应用程序中,用相应的资源替换字符串通常不足以实现本地化。如今,大多数应用程序广泛使用 JavaScript,特别是为了利用 AJAX。因此,为了保持标记整洁,JavaScript 很可能会被移到一个单独的文件中。
本文将介绍如何使用 HttpHandler
对 JavaScript 和样式表内容进行本地化。
背景
注意:本文侧重于“如何操作”,而不是对所用技术进行详细描述。因此,它主要面向中级和高级程序员。
其思路是让所有对 *.js 和 *.css 文件的请求都由一些自定义代码进行处理。这时 HttpHandler
就派上用场了。每当请求一个它已注册的文件类型时,该处理程序就会执行。这种注册在 web.config 文件中进行配置。
我们的 HttpHandler
随后会用存储在资源文件中的相应本地化字符串替换所有资源占位符。
准备资源
如果资源要存储在一个单独的库中,有几点需要注意。
为了能够访问资源,中性文化资源文件包装器类的访问修饰符需要配置为 public
。可以通过双击解决方案资源管理器中的资源,然后将“访问修饰符”下拉列表(如下图所示)更改为 **Public** 来实现这一点。
如果资源位于 Web 应用程序的 App_GlobalResources 文件夹中,并且 HttpHandler
不位于 Web 应用程序项目中,则必须将以下行添加到 Web 应用程序的 AssemblyInfo.cs 文件中
[assembly: InternalsVisibleTo("SmartSoft.Web")]
这将使 Web 应用程序的所有内部类型对声明的程序集可见。
指定的字符串需要与包含 HttpHandler
的外部库匹配。如果该程序集已签名,则必须使用完全限定名称,包括公钥标记。
准备 JavaScript 文件
为了知道从哪里加载资源,JavaScript 文件需要附带一些额外的信息。资源配置必须以 XML 形式包含,如下面注释掉的部分所示。此定义在文件中的位置并不重要,文件也可以包含多个定义。
//<resourceSettings>
// <resourceLocation name="WebScriptResources"
// type="SmartSoft.Resources.WebScripts,
// SmartSoft.Resources, Version=1.0.0.0, Culture=neutral" />
//</resourceSettings>
name
定义了资源占位符中使用的资源键,而 type
是在运行时使用反射加载请求资源的完全限定类型名称。
然后就可以在文件中的任何地方像这样使用资源占位符了
alert("<%$ Resources: WebScriptResources, TestAlert %>");
您可能已经注意到,资源声明与 .NET Framework 标准使用的声明完全相同。如果您从现有标记中提取文件内容,这将非常有用。:)
准备 CSS 文件
样式表的资源配置与上面为 JavaScript 文件描述的完全相同。
/* <resourceSettings>
<resourceLocation name="WebStylesResources"
type="HelveticSolutions.Resources.WebStyles,
HelveticSolutions.Resources, Version=1.0.0.0,
Culture=neutral" />
</resourceSettings> */
以及占位符
body {
background-color: <%$ Resources: WebStylesResources, BodyBackgroundColor %>;
}
web.config
可以为任何类型的请求配置 HttpHandler
。对于 ResourceHandler
,需要为 *.js 和 *.css 文件扩展名添加以下两个条目
<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>
这会导致 Web 服务器将请求重路由到 HttpHandler
。
如果使用 IIS 作为 Web 服务器,则需要为这两个文件扩展名扩展应用程序映射。有关如何执行此操作的说明,请参阅此处。
“验证文件是否存在”选项未选中是重要的。
ResourceHandler (HttpHandler)
ResourceHandler
的大部分内容都相当容易理解。但是,ApplyCulture
事件可能需要一些解释。
由于可能有多个并发请求(希望如此),因此每个请求在替换资源占位符之前都必须根据用户的最新选择设置文化,这一点至关重要。在附件的示例中,Session
被用作存储来缓存用户的语言偏好。或者,也可以从浏览器设置或其他地方确定。
在任何情况下,都强烈建议在会话开始时注册 ApplyCulture
事件,如“Global.asax 文件”部分所述。
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
{
/// <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>
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";
}
}
}
}
处理内容
- 设置当前线程的文化。
- 从文件系统中加载请求的文件内容。
- 使用正则表达式提取资源配置。
- 尝试使用反射加载每个配置的资源类型。
- 使用正则表达式提取资源占位符,并使用反射将其替换为相应的本地化字符串。
- 返回修改后的文件内容。
Global.asax 文件
如前所述,在会话开始时,应注册 ResourceHandler
的 ApplyCulture
事件。这使得 Web 应用程序能够处理来自具有不同文化设置的用户的多个并发请求。
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;
}