创建多语言网站——第1部分






4.75/5 (76投票s)
2004 年 8 月 16 日
10分钟阅读

537714

21394
扩展.NET现有的国际化功能,以创建灵活强大的多语言网站。首先,创建一个自定义资源管理器,然后创建自定义本地化服务器控件,以便轻松部署多语言功能。
目录
我要感谢Jean-Claude Manoli开发了他的C#代码格式化工具,我在这里的教程中使用了它。
引言
开发支持多种语言的网站可能是一个充满挑战且耗时的过程。使用标准的HTML页面,这需要为每种支持的语言创建和维护页面的重复版本,并将语言内容嵌入到HTML中,而这些内容又很难编辑。虽然随着ASP和PHP等脚本技术的引入,这个过程略有改进,但并没有节省多少开发或维护时间。对于那些必须开发多语言界面和应用程序的人来说,您会很高兴地知道ASP.NET使事情变得更加容易。
ASP.NET和.NET Framework附带了对多语言应用程序的支持,即资源文件、CultureInfo
类以及System.Globalization
和System.Resources.ResourceManager
命名空间。不幸的是,在目前的状况下,在ASP.NET应用程序中本地化内容仍然是一个繁琐的过程。然而,就像.NET中的其他一切一样,可用的对象模型和强大的功能使得扩展现有功能和开发新功能来支持更好的本地化变得轻而易举。
在第一部分中,我们将开发一个自定义资源管理器,它避免了.NET程序集资源文件的限制,并扩展了许多类以轻松支持本地化。在第二部分中,我们将花更多时间讨论创建多语言应用程序,特别是关注数据库实现和技术。
在本教程结束时,您应该能够以最少的工作量和维护量创建多语言应用程序,并能够轻松地稍后添加新语言。
开始之前
如果您不熟悉.NET中的国际化,请不要担心。本教程大部分跳过了.NET中已有的内容,而是讨论了使工作更轻松的替代方法。不过,您需要了解一些核心原则。(即使您不知道基础知识,也可以跳过本节并下载我创建的简单应用程序来展示核心功能,通过玩转它可能会是理解的最佳方式)。
基础知识
.NET中国际化的工作方式相当直接。内容存储在非常简单的XML文件中,称为资源文件。为每种支持的语言创建一个资源文件(以后可以添加更多)。应用程序编译时,资源文件会被嵌入到程序集中——默认资源文件嵌入到主程序集(.dll文件)中;特定语言的资源文件嵌入到它们自己的程序集中,称为卫星程序集。资源文件非常简单,看起来很像一个哈希表,它们有一个名称和一个值——名称对于所有资源文件都相同,而值是某个内容的特定语言翻译。本质上,这允许您使用System.Resources.ResourceManager
类执行诸如
1: UserNameLabel.Text = myResourceManager.GetString("Username");
2: UserNameValidator.ErrorMessage =
myResourceManager.GetString("RequiredUsername");
资源管理器将根据当前线程的CurrentCulture
值自动加载正确的资源文件——下一节将详细介绍。希望您已经看到了很多潜力。一些关键亮点是
- 内容被分离到简单的XML文件中。
- 每种支持的语言都有一个单独的XML文件。
- 加载值的代码相对简单且简短。
ResourceManager
类根据线程的CurrentCulture
值自动从正确的XML文件中检索内容。- 您可以轻松地拥有1个实际页面,支持N种语言。
区域设置
理解区域设置很重要,因为我们的新代码将使用它们——特别是System.Globalization.CultureInfo
类,以及遵循RFC 1766命名标准的区域设置名称值。基本上,您可以通过在构造函数中指定区域设置名称来创建一个新的CultureInfo
实例
1: CultureInfo c = new CultureInfo("en-US");
//creates a CultureInfo instance for American English
2: CultureInfo c = new CultureInfo("en-AU");
//creates a CultureInfo instance for Australian English
3: Cultureinfo c = new CultureInfo("he-IL");
//creates a CultureInfo instance for Israel Hebrew
一旦您拥有了CultureInfo
实例,就可以将其设置为当前线程的UIculture
,这将使我们上面的代码中的ResourceManager
自动从正确的XML资源文件中获取内容。
1: CultureInfo c = new CultureInfo("en-US");
//creates a CultureInfo instance for American English
2: System.Threading.Thread.CurrentThread.CurrentCulture = c;
//Will automatically format dates and such
3: System.Threading.Thread.CurrentThread.CurrentUICulture = c;
//Used by the ResourceManager to get the correct XML File
在第2部分中,我们将讨论确定加载哪个区域设置的方法,但目前,这可以像在QueryString中传递一个代码一样简单。例如,当存在lang=f
时,应使用法属加拿大区域设置。另一个关键因素是所有这些操作的地点。最简单也是最合乎逻辑的地方是Global.Asax的Begin_Request
。
下载示例应用程序
理解基础知识的最佳方法是玩一些代码。我创建了一个非常基础的VB.NET Web应用程序来演示基本原理。下载它并玩玩。查看3个资源文件的结构、index.aspx的代码隐藏以及global.asax中的代码。
为什么不直接使用现有功能?
虽然使用ASP.NET提供的工具开发多语言应用程序是可能的,但有许多限制使得这项任务不那么顺畅。一些关键问题是
- 资源文件被嵌入到[卫星]程序集中
- 资源文件无法返回强类型对象
- Web控件不易与资源文件关联
虽然列表看起来很小,但上述三个问题可能非常严重——第一个是最严重的。例如,由于资源文件被嵌入到程序集中,因此很难发布一个允许客户端灵活更改内容的产品——这是许多产品提供的功能。在我之前的工作中,每次翻译部门想更改一些文本时,我们都需要重新编译整个应用程序,停止20台Web服务器,并将.dll复制到bin文件夹——这是一个令人沮丧的过程。
构建更好的资源管理器
我们的第一个任务是构建一个更好的资源管理器,它不会导致我们的资源文件被嵌入到程序集中。这将允许在生产或客户端环境中轻松编辑资源文件。我们的核心功能将位于三个函数中
public
方法GetString
,应用程序将通过它来访问所需的资源。private
方法GetResource
,它从缓存中获取一个HashTable
,或者通过调用LoadResource
来获取。private
方法LoadResource
,它解析XML文件并将其存储到缓存中。
GetString()
1: public static string GetString( string key) {
2: Hashtable messages = GetResource();
3: if (messages[key] == null){
4: messages[key] = string.Empty;
5: #if DEBUG
6: throw new ApplicationException("Resource" +
" value not found for key: " + key);
7: #endif
8: }
9: return (string)messages[key];
10: }
该方法接受一个参数,即我们要获取的资源的键。然后,它使用GetResource
检索一个内容HashTable
,该方法具有区域设置感知功能,并返回正确的HashTable
。如果请求的键不存在,我们将在DEBUG模式下抛出异常,否则将简单地返回一个空字符串。
GetResource()
1: private static Hashtable GetResource() {
2: string currentCulture = CurrentCultureName;
3: string defaultCulture =
LocalizationConfiguration.GetConfig().DefaultCultureName;
4: string cacheKey = "Localization:" +
defaultCulture + ':' + currentCulture;
5: if (HttpRuntime.Cache[cacheKey] == null){
6: Hashtable resource = new Hashtable();
7:
8: LoadResource(resource, defaultCulture, cacheKey);
9: if (defaultCulture != currentCulture){
10: try{
11: LoadResource(resource, currentCulture, cacheKey);
12: }catch (FileNotFoundException){}
13: }
14: }
15: return (Hashtable)HttpRuntime.Cache[cacheKey];
16: }
GetResource()
方法稍微复杂一些。它的目标是检索一个HashTable
,可以通过键查找来检索值。该方法将首先检查HashTable
是否已加载并缓存[第5行]。如果是,它将直接从缓存中返回值。否则,它将使用LoadResource()
来解析适当的XML文件[第6-13行]。值得注意的是,“适当的XML文件”实际上是当前区域设置的XML文件与默认区域设置的XML文件的混合。默认区域设置在配置文件中指定[第3行],当前区域设置从当前线程的当前区域设置中检索[第2行]。
首先加载默认区域设置[第8行],然后加载当前区域设置[第11行]。这意味着如果一个键同时存在于两个XML文件中(大多数应该都存在),那么默认值将被特定区域设置的值覆盖。但如果它不存在于特定区域设置的值中,则将使用默认值。
LoadResource()
1: private static void LoadResource(Hashtable resource,
string culture, string cacheKey) {
2: string file =
LocalizationConfiguration.GetConfig().LanguageFilePath +
'\\' + culture + "\\Resource.xml";
3: XmlDocument xml = new XmlDocument();
4: xml.Load(file);
5: foreach (XmlNode n in xml.SelectSingleNode("Resource")) {
6: if (n.NodeType != XmlNodeType.Comment){
7: resource[n.Attributes["name"].Value] = n.InnerText;
8: }
9: }
10: HttpRuntime.Cache.Insert(cacheKey, resource,
new CacheDependency(file), DateTime.MaxValue, TimeSpan.Zero);
11: }
LoadResource
加载XML文件[第4行](它从我们的配置文件[第2行]中获取根路径),并简单地解析它,将值加载到我们的HashTable
中[第5-9行]。最后,HashTable
被存储在缓存中[第10行]。
其他增强功能
包装器
我们可以对我们的Resource Manager类进行一些小的增强。例如,我构建了英语和法语的双语网页。令人讨厌的是,在英语中,冒号总是紧跟在它所跟随的单词后面,但在法语中必须有一个空格。例如
Username: //English
Nom d'utilisateur : //French
这意味着冒号需要被本地化。我们可以简单地构建一个包装器,而不是使用GetString()
方法
1: public static string Colon {
2: get { return GetString("colon"); }
3: }
在我们的英语资源文件中,冒号将简单地是':
',而在法语中,它将有一个空格' :
'。
强类型资源
我们使用HashTable
而不是NameValueCollection
的原因是,Resource Manager类可以扩展为返回强类型对象。例如,您可能拥有本地化的帮助内容,它不仅仅是一个单独的值。它可能有一个标题、一个示例和帮助文本。虽然探索这一点超出了本文的范围(也许是第三部分??),但这种能力是存在的。
本地化控件
我们的下一个目标是通过扩展现有的服务器控件(文本框、标签、按钮)使其具有本地化意识,从而使我们在开发网站时更加轻松。我们首先创建一个简单的接口,我们的新控件将实现该接口。
ILocalized
1: public interface ILocalized{
2: string Key {get; set; }
3: bool Colon {get; set; }
4: }
ILocalized
定义了一个Key
属性,该属性将传递给我们的ResourceManager
的GetString()
方法。为了展示如何扩展这些类以满足您自己的需求,我还包含了一个布尔型的Colon
属性,它将告诉我们的控件是否应该在其值后面附加一个冒号。
LocalizedLiteral
1: public class LocalizedLiteral : Literal, ILocalized {
2: #region fields and properties
3: private string key;
4: private bool colon = false;
5:
6: public bool Colon {
7: get { return colon; }
8: set { colon = value; }
9: }
10:
11: public string Key {
12: get { return key; }
13: set { key = value; }
14: }
15: #endregion
16:
17:
18: protected override void Render(HtmlTextWriter writer) {
19: base.Text = ResourceManager.GetString(key);
20: if (colon){
21: base.Text += ResourceManager.Colon;
22: }
23: base.Render(writer);
24: }
25: }
我们将要使其具有本地化意识的第一个Web控件是经常使用的System.Web.UI.WebControls.Literal
。首先,我们让我们的类继承自Literal
控件并实现ILocalized
接口[第1行]。接下来,我们实现ILocalized
接口中定义的Key
和Colon
属性[第3-14行]。最后,我们重写我们基类Literal
的Render
方法,并使用ResourceManager
的GetString()
方法和Colon
属性来完全本地化我们的控件[第19-22行]。不要忘记稍后调用基类的Render()
方法,让它发挥作用[第23行]。
重复利用
您可以一次又一次地复制粘贴相同的代码,只需更改类名及其继承的类即可;例如,让我们创建一个本地化的按钮
1: public class LocalizedButton : Button, ILocalized {
2:
3: #region Fields and Properties
4: private string key;
5: private bool colon = false;
6:
7: public string Key {
8: get { return key; }
9: set { key = value; }
10: }
11:
12: public bool Colon {
13: get { return colon; }
14: set { colon = value; }
15: }
16:
17: #endregion
18:
19: protected override void Render(HtmlTextWriter writer) {
20: base.Text = ResourceManager.GetString(key);
21: if (colon){
22: base.Text += ResourceManager.Colon;
23: }
24: base.Render(writer);
25: }
26: }
请注意,只有两个粗体字已更改。
在需要时,您可以扩展功能。例如,当删除某些内容时,带有弹出JavaScript确认框的LinkButton
并不少见。我们可以通过创建第二个键属性轻松实现这一点
1: using System.Web.UI;
2: using System.Web.UI.WebControls;
3:
4: namespace Localization {
5: public class LocalizedLinkButton : LinkButton, ILocalized {
6: #region Fields and Properties
7: private string key;
8: private bool colon;
9: private string confirmKey;
10:
11: public string ConfirmKey {
12: get { return confirmKey; }
13: set { confirmKey = value; }
14: }
15: public string Key {
16: get { return key; }
17: set { key = value; }
18: }
19: public bool Colon {
20: get { return colon; }
21: set { colon = value; }
22: }
23: #endregion
24:
25: protected override void Render(HtmlTextWriter writer) {
26: if(key != null){
27: Text = ResourceManager.GetString(key);
28: if (colon) {
29: Text += ResourceManager.Colon;
30: }
31: }
32: if (confirmKey != null) {
33: Attributes.Add("onClick", "return confirm('" +
ResourceManager.GetString(confirmKey).Replace("'",
"\'") + "');");
34: }
35:
36: base.Render(writer);
37: }
38:
39: }
40: }
使用本地化控件
您像使用其他服务器控件一样使用本地化控件。首先,在页面上注册控件
1: <%@ Register TagPrefix="Localized"
Namespace="Localization" Assembly="Localization" %>
然后,无需编写任何代码,您就可以通过在设计器中拖放控件,或在HTML模式下通过键入来添加控件
1: <Localized:LocalizedLiteral id="passwordLabel"
runat="server" Key="password" Colon="True" />
2: <Localized:LocalizedButton id="login"
runat="server" colon="false" Key="login" />
下载
现在最好的方法是实际操作一些代码。我再次创建了一个示例站点(与之前的类似),但这次使用了我们新的Resource Manager类和本地化控件。您可能需要更改web.config中的languageFilePath
属性以指向正确的文件夹。
历史
- 2004年8月14日:初版
许可证
本文没有明确的许可附带,但可能包含文章文本或下载文件本身的使用条款。如有疑问,请通过下面的讨论区联系作者。您可以在此处找到作者可能使用的许可列表。