解析Asp.net中资源本地化的实现
本文将从Asp.net实现资源全球化和本地化的基本概念入手,阐述在Asp.net1.1和Asp.net2.0中实现全球化和本地化的步骤、方法。
一.基本概念
1.为什么要实现资源的本地化?
我们的站点可能为全球各个国家和地区的人所浏览,每个国家和地区的人都有自身的语言文化特点。就拿咱们伟大的祖国为例,中国大陆用简体中文,港澳台则使用繁体中文。另外各个国家对于货币、数字、日历等信息的表达格式各有不同,我们国家多使用年月日的格式,而美国则是月日年。诸如此类的区别林林总总,我也就不多举例了。为了给我们的网站浏览者更好的用户体验,我们应该提供一个全球化的解决方案,只要用户选择了他的语言和区域,站点就按照他的语言文化习惯来展现页面信息,这个过程可以叫做本地化。
2.区域性、固定区域性、非特定区域性、特定区域性
区域性名称遵循 RFC 1766 标准,格式为“<languagecode2>-<country/regioncode2>”,其中 <languagecode2> 是从 ISO 639-1 派生的由两个小写字母构成的代码,<country/regioncode2> 是从 ISO 3166 派生的由两个大写字母构成的代码。例如,美国英语为“en-US”。在双字母语言代码不可用的情况中,将使用从 ISO 639-2 派生的三字母代码;例如,三字母代码“div”用于使用 Dhivehi 语言的区域。某些区域性名称带有指定书写符号的后缀;例如“-Cyrl”指定西里尔语书写符号,“-Latn”指定拉丁语书写符号。举例:
区域性名称 |
区域性标识符 |
语言-国家/地区 |
zh-CN |
0x0804 |
中文-中国 |
zh-TW |
0x0404 |
中文-台湾 |
zh-CHS |
0x0004 |
简体中文 |
zh-CHT |
0x |
繁体中文 |
en |
0x0009 |
英语 |
en-US |
0x0409 |
英语-美国 |
en-GB |
0x0809 |
英语-英国 |
uz-UZ-Cyrl |
0x0843 |
乌兹别克语(西里尔语)- 乌兹别克斯坦 |
uz-UZ-Latn |
0x0443 |
乌兹别克语(拉丁)- 乌兹别克斯坦 |
固定区域性不区分区域性。可以使用空字符串 ("") 按名称或者按区域性标识符 0x
CultureInfo Invc = CultureInfo.InvariantCulture;这两行代码的作用相同,目的是获得固定区域性实例。
比如你现在要对一个DateTime的实例dateTime执行dateTime.ToString()方法。这个方法实际是使用你当前线程的CurrentCulture作为默认的区域性,根据这个区域性将日期实例转化为相应的字符串形式。那么如果我们此时不需要它按照线程或系统的区域性进行ToString操作,那么我们应该用这个方法dateTime.ToString(“G”, CultureInfo.InvariantCulture)或者dateTime.ToString(“G”, DateTimeFormatInfo.InvariantInfo)。
非特定区域性是与某种语言关联但不与国家/地区关联的区域性。特定区域性是与某种语言和某个国家/地区关联的区域性。例如,“en”是非特定区域性,而“en-US”是特定区域性。注意,“zh-CHS”(简体中文)和“zh-CHT”(繁体中文)均为非特定区域性。
区域性有层次结构,即特定区域性的父级是非特定区域性,而非特定区域性的父级是 InvariantCulture。CultureInfo类的Parent属性将返回与特定区域性关联的非特定区域性。如果特定区域性的资源在系统中不存在,或因其它原因不可用,则使用非特定区域性的资源;如果非特定区域性的资源也不可用,那么使用主程序集中嵌入的资源。
3.实现本地化常用的类型、属性和方法
CultureInfo类表示有关特定区域性的信息,包括区域性的名称、书写体系和使用的日历,以及有关对常用操作(如格式化日期和排序字符串)提供信息的区域性特定对象的访问。CultureInfo类的实例化一般有两个途径,如下所示:
CultureInfo culture = CultureInfo. CreateSpecificCulture (name);
CultureInfo culture = new CultureInfo(name);
二者的区别是,使用第一种方法,只能创建固定区域性或特定区域性的CultureInfo实例。如果name为空字符串,则建立固定区域性的实例,如果name为非特定区域性,那么建立name 关联的默认特定区域性的 CultureInfo实例。第二种方法,则是建立一个name所指定的区域性的CultureInfo实例,它可以是固定的,非特定的或特定区域性的。
Thread类的CurrentCulture属性用来获取或设置当前线程的区域性。它必须被设置为特定区域性。Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");如果Thread.CurrentThread.CurrentCulture = new CultureInfo("en ");就会报错!
Thread类的CurrentUICulture属性用来获取或设置资源管理器使用的当前区域性以便在运行时查找区域性特定的资源。这里的资源管理器可以关联为ResourceManger类。
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
ResourceManger类可以查找区域性特定的资源,当本地化资源不存在时提供代用资源,并支持资源序列化。常用的ResourceManager的构造函数是public ResourceManager(string,Assembly)。其含义是初始化 ResourceManager类的新实例,它使用指定的根名称从给定的Assembly中查找资源文件。所谓根名称是例如名为“MyResource.en-US.resources”的资源文件的根名称为“MyResource”。在根名称的表达中可以加上命名空间,如“MyWebSite.Resource.UserFolder. MyResource”。而Assembly可以是需要调用资源文件的页面所在的Assembly,如typeof(MyPage).Assembly。ResourceManager类的GetString方法用来获得资源文件中的指定键的值。举例:当已设置了线程的CurrentUICulture属性之后按如下方法。
ResourceManager rm = new ResourceManager("items", Assembly.GetExecutingAssembly());
String str = rm.GetString("welcome");
如果想按照指定的区域性来获得资源则按照如下写法:
ResourceManager rm = new ResourceManager("items", Assembly.GetExecutingAssembly());
CultureInfo ci = Thread.CurrentThread.CurrentCulture;
String str = rm.GetString("welcome",ci);
二.在Asp.net1.1中实现资源本地化
首先应在网站项目WebTest中建立一个Resource文件夹,在这个文件夹中存放整个项目公用的资源文件。比如我们建立了以下三个资源文件:MyResource.en.resx,MyResource.en-US.resx,MyResource.zh-CN.res。每个资源文件中都有两个键值对,键值为State和Address。在需要使用资源文件的页面MyPage.aspx中调用资源文件,如下所示:
Thread.CurrentThread.CurrentCulture= CultureInfo.CreateSpecificCulture("zh-CN");
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
ResourceManager rm = new ResourceManager("WebTest.Resource.MyResource", typeof (MyPage).Assembly);
Label1.Text = rm.GetString("State");
Label2.Text = rm.GetString("Address");
好了,这个时候Label1和Label2就按照MyResource.zh-CN.resx文件中的规定显示“州”和“地址”。以上是一个最基本最简单的本地化方法,这里隐含着一些问题,我们来逐一解决并优化该方法。
1. 如何获得用户的默认区域性
通过用户浏览器“属性”->“语言”选项里的设置,取最上面那条作为用户的默认语言。
CultureInfo cultureInfo = CultureInfo.CreateSpecificCulture(Request.UserLanguages[0]);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = cultureInfo;
一般情况下,设定CurrentCulture和CurrentUICulture具有相同的区域性,当然也可以不相同,比如你规定CurrentCulture为en-US,而CurrentUICulture为zh-CN。那么这样造成的效果是,页面中货币、日期等信息都按照美国英语的格式显示,而需要从资源文件中取值的内容,资源管理器会从MyResource.zh-CN.resx文件里获得。
如果你的站点页面上并没有提供让用户选择语言的功能,那么也就是默认按照用户浏览器设置的区域性进行显示,因此你就可以把上述代码放在Global.asax.cs文件的Application_BeginRequest方法中。这样每次用户对页面发出请求时,我们的程序都会首先进行区域性设置。
2. 记住用户的区域性设置
通过会话可以记住浏览者的区域性设置或选择。但是这个操作不能在Global.asax.cs文件中Application_BeginRequest方法中进行,因为那时会话还处于不可用状态。如果你的站点并没有提供让用户选择语言的功能,那么你也没什么必要记住用户的区域性设置,只要按照上面介绍的在Global.asax.cs文件中Application_BeginRequest方法里设置一下就可以了,不影响性能。这主要可以避免用户在中途突然改变了浏览器中语言的设置,而网站仍按照会话中存储的区域性为用户显示页面内容的冲突。
如果你提过了让用户选择语言的功能,那显然要在页面程序中使用会话来记录用户的区域性选择。因为从客户端到服务器段的每次请求,服务器段都会开启一个新的线程进行处理和响应。如果你的程序没有记住客户的选择,那么只能按照默认的区域性进行响应。
3. 资源管理器如何查找指定区域性的相应资源文件?
在执行取值操作时,也就是执行ResourceManager类的GetString方法时,资源管理器会按照当前线程的CurrentUICulture属性去寻找相对应的资源文件。有如下几种情况:
(1). 比如当前CurrentUICulture对应的区域性是en-US,那么首先找MyResource.en-US.resx是否存在,如存在则从中取值;如不存在,则看MyResource.en.resx是否存在。
(2). 比如当前CurrentUICulture对应的区域性是en,因为en是非特定区域性的,那么首先找其默认关联的特定区域性en-US的资源文件MyResource.en-US.resx是否存在,如存在则从中取值;如不存在,则看MyResource.en.resx是否存在。
(3). 比如当前CurrentUICulture对应的区域性是en-GB,那么首先找资源文件MyResource.en-GB.resx,如不存在,则看MyResource.en.resx是否存在,如存在则从中取值;如也不存在,则看en关联的默认特定区域性en-US的资源文件MyResource.en-US.resx是否存在,如果此时MyResource.en-US.resx不存在,但是MyResource.en-CA.resx存在,则程序依然会抛出找不到合适资源文件的异常。
因此我们可以总结一下,当前线程CurrentUICulture对应的是特定区域性时,资源管理器优先查找此特定区域性对应的资源文件,如果没找到,则去找其非特定区域性的资源文件,如果还没找到,再去找其非特定区域性关联的默认区域性的资源文件。当前线程CurrentUICulture对应的是非特定区域性时,资源管理器优先查找此非特定区域性对应的默认特定区域性的资源文件是否存在,如果不存在,则去看此非特定区域性对应的资源文件是否存在,如果也不存在则抛出异常。
4.如何处理未提供本地化支持的区域性?
如果站点没有提供相应的资源文件支持用户默认的区域性,那么必须将其当前线程的CurrentUICulture转化为你站点默认的区域性,比如en-US或zh-CN。转化的时机有两个:
一是当你在获得Request.UserLanguages[0]时,用其与配置文件中预先设定的被支持的区域性进行比较,如果确认其为不被支持的,那么立刻设置CurrentUICulture为默认区域性。
二是在使用ResourceManager的GetString方法进行取值的时候,使用try catch结构,捕获MissingManifestResourceException异常,在异常处理中,首先将CurrentUICulture设为默认区域性,之后再重新使用GetString取值。
5. 通过Web.config设定站点默认的culture和uiCulture
<globalization requestEncoding="utf-8" responseEncoding="utf-8" uiCulture="zh-CN" culture="en-US"/>
如上所示:规定站点的默认culture为en-US(此处必须为特定区域性),uiCulture为zh-CN。
当然你也可以在每个页面的Page标签中进行逐页设定:<@Page Culture=“zh-CN” UICulture=“en”>。这里就不管web.config是如何设置的,页面会按照Page标签的设定进行显示。
三.在Asp.net2.0 中实现资源本地化
Asp.net2.0中为资源本地化提供了更加多样的实现方法。我这里着重谈其与Asp.net1.1中的不同之处。
1.通过Web.config设定站点默认的culture和uiCulture
在Asp.net1.1中使用web.config文件进行站点区域性设定的方法已经讲过了,而在Asp.net2.0中其更加灵活。通常,您会想要站点中的所有页面都符合相同的区域性设置。只需按如下所示在web.config中,为globalization元素的UICulture 和 Culture(区域性)属性分配一个站点范围的“auto”值, 注意这个“auto”值在Asp.net1.1中是不被接受的。<globalization uiCulture="auto" culture="auto" /> auto的意义在于ASP.NET 通过检查浏览器发送的 HTTP 标题获取到的用户首选区域性设置,并使用这个区域性设置站点的默认区域性,即当前线程的CurrentUICulture和CurrentCulture属性。
除了自动设置以外,您还可以为 Asp.net 指定一个站点的默认区域性: <globalization uiCulture="auto:zh-CN" culture="auto:zh-CN" /> 注意:只有当ASP.NET无法找到 HTTP 标题来确定用户的首选区域性,比如浏览器的“属性”->“语言”中没有任何区域性设置完全是空的时候,auto后面设定的默认区域性才会生效。
在web.config中进行了globalization配置之后,你的应用程序不需要写任何代码,线程的CurrentUICulture和CurrentCulture就会按照在globalization元素中设置的uiCulture和culture属性值获得区域性设置。如果没有进行globalization配置,则线程的CurrentUICulture和CurrentCulture就会默认为en-US。
2.使用Web.config文件跟踪用户的区域性选择
在Asp.net1.1中,那些提供了区域性选择的站点,一般使用会话来记录用户的选择,以便在用户每次对站点发出请求时,都按照用户选择的区域性对显示内容进行本地化。在Asp.net2.0中提供了另一个方法,那就是使用web.config文件来跟踪用户的区域性选择。
您可以在web.config文件中添加一个名为 LanguagePreference 的基于字符串的配置文件属性来支持匿名识别用户区域性的功能。请注意anonymousIdentification元素的enabled属性必须为“true”,否则匿名识别功能就不可用。
<anonymousIdentification enabled="true"/>
<profile>
<properties>
<add name="LanguagePreference" type="string" defaultValue="auto" allowAnonymous="true" />
</properties>
</profile>
下面我将阐述在Asp.net2.0中如何针对LanguagePreference属性编程。首先,可以写一个PageBase类,它继承自System.Web.UI.Page,并作为站点中所有页面类的基类。这么做的目的其实很简单,就是为了将各个页面中一些共同的处理过程提炼出来放到基类中,以减少代码重复,提高可维护性。然后在PageBase类中写如下代码:protected override void InitializeCulture()
{
base.InitializeCulture();
string LanguagePreference = ((ProfileCommon)this.Context.Profile).LanguagePreference;
//该用户首次访问本站,Profile.LanguagePreference为空时,识别用户浏览器的语言设置
if(string.IsNullOrEmpty(LanguagePreference))
{
if (this.Context.Request.UserLanguages != null)
{
LanguagePreference = this.Context.Request.UserLanguages[0];
((ProfileCommon)Context.Profile).LanguagePreference = LanguagePreference;
}
}
else
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(LanguagePreference);
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(LanguagePreference);
}
}
System.Web.UI.Page类的InitializeCulture方法是在Asp.net2.0中新加的,它为当前线程设置Culture和UICulture。页面生命周期已被设计为InitializeCulture方法先于页面的Init和Load运行。在上述代码中,首先使用((ProfileCommon)this.Context.Profile).LanguagePreference;获得当前LanguagePreference配置文件属性的值,判断其是否为空,也就是是否已经为用户保存了区域性设置。如果为空,则从Http头中获取用户的首选区域性设置,并通过((ProfileCommon)Context.Profile).LanguagePreference = LanguagePreference;保存用户的首选区域性设置。如果不为空,说明已经保存了用户的区域性设置,那么使用这个区域性设置当前线程的CurrentUICulture和CurrentCulture属性。
如果Web.config中定义了<globalization uiCulture="auto" culture="auto" />,那么可以将上述代码简化为:protected override void InitializeCulture()
{
base.InitializeCulture();
string LanguagePreference = ((ProfileCommon)this.Context.Profile).LanguagePreference;
if(!string.IsNullOrEmpty(LanguagePreference))
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(LanguagePreference);
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(LanguagePreference);
}
else
{
((ProfileCommon)Context.Profile).LanguagePreference = Thread.CurrentThread.CurrentCulture.Name;
}
}
如果在站点中提供了让用户选择区域性的功能,比如在站点的母版页中放了一个选择语言的列表,那么可以通过以下语句来记住用户对区域性的选择:
protected void lstLanguage_SelectedIndexChanged(object sender,EventArgs e)
{
if (lstLanguage.SelectedValue != "Auto") //默认选项是Auto
{
Profile.LanguagePreference = lstLanguage.SelectedValue;
}
else
{
Profile.LanguagePreference = null;
}
Response.Redirect(Request.Url.AbsolutePath);
}
注意Response.Redirect(Request.Url.AbsolutePath);这行代码,因为事件处理代码是在Page_Load之后执行的,要是想让页面迅速发生变化必须执行重定向操作。
3. 在Asp.net2.0中使用资源文件
在站点中建立全局资源文件的时候,VS.Net2005会自动建立一个App_GlobalResources文件夹专门来存放全局资源文件。所谓全局资源文件,也就是给站点中多个页面文件或母版页使用的资源文件。假设我们创建名为MyResource.resx和MyResource.zh-cn.resx的文件。在程序中我们可以使用以下代码来获得资源文件中的值:this.lblCountry.Text = Resources.MyResource.Country;
其中Country是资源文件中的键。显然,这比Asp.net1.1中从资源文件获取值要容易很多。
这里有两个问题需要注意:第一,在创建一组具有相同根名称的资源文件时,没有区域性标示的文件必须建立,比如MyResource.resx是必须有的,其它如MyResource.en-gb.resx和MyResource.zh-cn.resx的建立是根据需要的。如果不建立MyResource.resx只建立了MyResource.zh-cn.resx等,则上述代码中的Resources命名空间下就不会出现MyResource,因此上述代码编译无法通过。MyResource.resx中应该存放站点默认语言的内容,以备在找不到与当前线程CurrentUICulture匹配的本地化资源文件或在本地化资源文件中找不到相应键值时使用。Asp.net是以MyResource.resx文件中的键为准,假如在MyResource.resx中不存在Country键,而在MyResource.zh-cn.resx中存在Country键,那么上述代码在编译时也会报错。第二,Asp.net在找不到相应区域的本地化资源时,不会报告任何异常,会自动从MyResource.resx文件中获取值,但并不改变当前线程的CurrentUICulture。
在站点中建立局部资源文件的时候,VS.Net2005会自动建立一个App_LocalResources文件夹专门来存放局部资源文件。所谓局部资源文件,也就是给站点中单一页面文件使用的资源文件。它的命名方式一般是Default.aspx.resx和Default.aspx.zh-cn.resx。现在我在Default资源文件中添加三个键Language、lblNavigation.Text和lblNavigation.ForeColor。其中我为Default.aspx.resx的lblNavigation.ForeColor设置blue,为Default.aspx.zh-cn.resx的lblNavigation.ForeColor设置red。在页面文件中Default.aspx中从局部资源文件里获得内容的方法如下有两种:
(1). <asp:Label ID="lblLanguage" runat="server" Text="<%$ Resources:Language %>"></asp:Label>
(2). <asp:Label ID="lblNavigaion" runat="server" meta:resourcekey="lblNavigation"></asp:Label>
使用第一种方法时要注意使用符号$。使用第二种方法更加灵活,它可以一次性地为控件的很多属性设定值。
在这里仍然有问题需要注意:页面默认的局部资源文件必须被建立,比如Default.aspx.resx是必须的,而Default.aspx.zh-cn.resx则根据需要。如果你不建立默认的局部资源文件,而在页面中却要使用局部资源文件时,当使用第一种方法进行绑定时,出编译错误;当使用第二种方法进行绑定时,不会出编译错误,但是这些属性的设置全都没起作用,如同没写一样。
4.显示本地化图像
显示本地化图像也是Asp.net2.0的新功能。在Asp.net2.0中资源文件已经不仅限于string类型的键值对组合,它可以保存多种类型的文件。利用这一功能可以实现图像的本地化。其实所谓本地化图像,无非就是将给不同区域性准备的图像放到不同的本地化资源文件中去。比如将LitwareSlogan.jpg放到MyResource.resx中,把LitwareSlogan.cn.jpg放到MyResource.zh-cn.resx中。
当不同本地化版本的全局资源文件中含有本地化版本的图像文件时,您可以自定义一个名为 MyLocalImage.ashx 的处理程序文件,基于用户的语言首选项来有条件地进行加载,代码如下所示。
页面中的调用方法:
<asp:Image ID="Image1" runat="server" ImageUrl="~/ MyLocalImage.ashx" />
MyLocalImage.ashx 的处理程序的写法:
public class MyLocalImage : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
context.Response.ContentType = "image/png";
string LanaguageReference = ((ProfileCommon)context.Profile).LanguagePreference;
if (!string.IsNullOrEmpty(LanaguageReference))
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(LanaguageReference);
}
Bitmap bm = Resources.Litware.LitwareSlogan;
MemoryStream image = new MemoryStream();
bm.Save(image,ImageFormat.Png);
context.Response.BinaryWrite(image.GetBuffer());
}
}