代码改变世界

本地化与Atlas对于本地化的支持

2006-09-22 19:18 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏
  周三我参与的项目的Beta版终于发布了,这个项目是对今年2月已经在美国上线的产品进行本地化工作。很奇怪,在Production环境中使用下来感觉比想象中要好很多,忐忑的心情放轻松了不少。虽然我那个项目没有用到Atlas,不过也就趁这个机会,简单讲一下本地化和Atlas对于本地化的支持吧。

一、什么是本地化

本地化,英文是Localization,缩写为L10n(与之类似的还有Globalization - G11n和Internationalization - I17n),其工作是将一个市场的产品进行某种方式的修改,目的在于向另一个市场的推广。一般来说,这两种市场的语言环境是不同的,因此“翻译”似乎就是本地化工作的重中之重,其重要程度已经达到了让某些人误以为本地化工作完全就是一个翻译工作。其实不然。正像刚才所说的,本地化是为了让一个产品在另一个市场得到推广,因此它需要做的事情还不少:

首先当然最重要的就是文字的翻译。一个产品会有大量的文字信息,在不同的市场中,对于这些文字自然应该翻译成符合该市场语言。翻译后语言应该要保持语句流畅和意义明确。需要注意的是,这里的“文字信息”不单指产品中的“文本”,图片中的文字和以图片方式显示的文字也一样需要被翻译。在ASP.NET中,对于这方面的本地化工作有着很好的支持。

其次是环境的转换。打个比方,在产品中很可能会有关于人的图片,则在美国市场应该显示为美国人,而在中国大陆市场则必须换作中国人——当然,如果用香港台湾韩国日本人可以吗?这……

还要注意的是语言习惯和环境属性。不同的市场往往会有不同的语言习惯和环境属性,比如时间格式、数字格式、货币单位、距离单位、时区、距离单位与标准长度单位的比等等。同样的信息在不同市场的显示出来时会有很大的不同。这里大部分的属性在.NET Framework的System.Globalization.CultureInfo类有描述。而本文也着重于Atlas对于这一点的支持。

不同的市场往往还需要运用不同的样式。姑且不说市场不同语言之间文字的阅读方向会不同,就拿同样是从左往右的两种不同语言,在样式上就会有所不同。比如拿英语与中文来说,首先他们如果用相同的字号,则中文会显得偏小。如果放大一些文字那么文字与边框,边框与边框,文字与图片之间的距离就会显得偏小,也需要一并改变。英文字母所组成的文章会有参差的感觉,而中文汉字基本上都是方块字,同样是文章汉字给人的感觉会比较密。单个英语单词中间不能换行,而中文任意两个汉字之间都能换行,其限制主要在在标点符号上。诸如此类等等。如果是大规模的产品,会涉及到许多产品的本地化工作,会专门成立一个小组用来研究各种语言文字下的样式,并提供开发人员全面的参考。这样保证了同一个,或者同一系列的产品,在不同市场会应用不同样式,却又保持了风格的美观和统一。

本地化工作还包括开发市场特定功能。不同的市场在很多地方会不相同,比如当地的法律限制了有些功能无法发布,有些功能必须修改和标注等等。为了更好地迎合用户,也有可能会为本地用户开发出新的功能。

其他例如还有广告商,赞助商等等,这里就不多说了。


二、Atlas对于本地化的支持

Atlas对于本地化工作的支持主要体现在客户端的Sys.CultureInfo对象。打开Atlas.js文件,可以发现如下的代码:
Sys.CultureInfo =
{
    
"Name" : "en-US"
    
"NumberFormat" : { ... }, 
    
"DateTimeFormat" : { ... }
};

代码比较长也比较无聊,因此就不完整贴出了。Sys.CultureInfo对象对于日期和时间的格式有着良好的支持,Atlas的客户端代码对于Date和Number的扩展都是基于Sys.CultureInfo对象的定义,具体代码也可以在Atlas.js文件中看到。那么Atlas是如何支持别的Culture的呢?我们新建一个aspx页面,加入一个ScriptManager,代码如下:
<atlas:ScriptManager ID="ScriptManager1" runat="server" />

然后在浏览器中打开这张页面。查看网页的源文件,可以发现这么一段代码:
<script src="atlasglob.axd" type="text/javascript"></script>

在浏览器里访问这个atlasglob.axd文件就可以查看这个“文件”的内容。它也是一段和上面相似的Sys.CultureInfo代码,只是内容会有所不同(如果相同的话那么访问atlasglob.axd?c=zh-cn就可以看到zh-cn的Sys.CultureInfo内容)。它后与Atlas.js文件加载,因此它的内容会覆盖Atlas.js文件中的定义。这就是Atlas对于本地化的支持。

在ScriptManager中有一个属性EnableScriptGlobalization,默认值为True。如果不需要这个功能,则可以把它设为False,那么页面就不会产生前面的<script />元素,加快加载速度。事实上,ScriptManager判断是否加载atlasglob.axd的逻辑会先判断EnabledScriptGlobalization的值,如果是True,并且Request.UserLanguages[0]不是“en-us”(en-us的Sys.CultureInfo已经被包含在Atlas.js文件中),则会在页面中引用atlasglob.axd文件。

那么atlasglob.axd文件是什么呢?它是如何工作的呢?首先,如果使用在web.config中可以看到下面这样定义:
<httpHandlers>
    
<add verb="*" path="atlasglob.axd" type="Microsoft.Web.Globalization.GlobalizationHandler" validate="false"/>
</httpHandlers>

这个定义表示将atlasglob.axd文件交由Microsoft.Web.Globalization.GlobalizationHandler处理,我们要它如何运行的,就必须看一下那个类的代码了。它的ProcessRequest方法代码:
 1public void ProcessRequest(HttpContext context)
 2{
 3      string culture = context.Request.QueryString["c"];
 4
 5      if ((culture == null&& (context.Request.UserLanguages != null))
 6      {
 7            culture = context.Request.UserLanguages[0];
 8      }

 9
10      if (culture == null)
11      {
12            culture = "en-us";
13      }

14
15      AtlasCultureInfo info = AtlasCultureInfo.Create(culture);
16      string script = JavaScriptObjectSerializer.Serialize(info);
17      context.Response.Write("Sys.CultureInfo = ");
18      context.Response.Write(script);
19      context.Response.Write(";");
20}

首先先查看Query String里“c”的值(因此前面通过atlasglob.axd?c=zh-cn可以访问到zh-cn的信息),如果没有提供c的值,那么则会查看Request对象里UserLanguages数组的第一个值。那么什么是UserLanguage?它反映了客户端的设定,我们打开IE,点击菜单“工具——Internet选项”,再点击“语言”按钮,会弹出语言首选项窗口,如图:


在这里可以添加删除UserLanguage,比如现在UserLanguage[0]是“zh-cn”。在上面的ProcessRequest的方法中,如果没有Query String和UserLanguage的设定,则默认使用en-us。接着将culture值传入AtlasCultureInfo的静态方法Create得到一个AtlasCultureInfo对象。然后将对象序列化输出,就得到了我们看到的结果:一个Sys.CultureInfo的定义。

AtlasCultureInfo的静态方法Create会使用用户传入的cultureName字符串,根据System.Globalization.CultureInfo类的静态方法CreateSpecificCulture得到一个CultureInfo对象。可以看出,这个cultureName必须代表了一个specific culture(例如zh-CN和en-GB),否则就会抛出异常(关于invariant culture,neutral culture和specific culture的区别和联系可以参考MSDN相关内容)。然后根据得到的CultureInfo对象使用AtlasCultureInfo的私有构造函数得到一个AtlasCultureInfo对象,这个对象会把传入的CultureInfo对象的Name,NumberFormat和DateTimeFormat复制给自己的相关属性。因此,序列化后的信息就是我们前面从atlasglob.axd访问得到的数据了。


三、为Atlas的本地化的支持自定义Culture Detection规则

分析了Atlas对于本地化的支持,是不是发现还缺了点什么?默认Handler会使用用户UserLanguage设定,那么如果用户没有UserLanguage设定该怎么办呢?如果我们要根据访问页面的Query String,用户当前的Cookie设定,或者用户帐户设定又该怎么办呢?如果我们只是想把默认的Culture设为zh-cn而不是en-us又该怎么办呢?其实我们需要的只是将自己的Culture Detection规则告诉Atlas。

还好我们能从atlasglob.axd的Query String下手。首先,我们先将ScriptManager的EnableScriptGlobalization属性设成False,以避免ScriptManager自动生成对于atlasglob.axd文件的script加载。代码如下:
<atlas:ScriptManager ID="ScriptManager1" runat="server" EnableScriptGlobalization="false" />

接着,我们可以用自己的方式在网页里添加对于atlasglob.axd的script引用了。注意,引用必须在Atlas.js的script引用(在网页源文件可以看到是一个对WebResource.axd的script引用)之后才能生效。当然最简单的办法就是通过ScriptManager使用RegisterScriptReference方法添加引用了。比如我们需要在Form_Load里注册,代码如下:
protected void Page_Load(object sender, EventArgs e)
{
    
string cultureName = GetUserCulture();
    
this.ScriptManager1.RegisterScriptReference("atlasglob.axd?c=" + cultureName);
}

打开网页后查看页面的源代码,就能发现下面的xml-script:
<script type="text/xml-script">
    
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005">
        
<references>
            
<add src="atlasglob.axd?c=en-GB" />
        
</references>
        
<components />
    
</page>
</script>

那么我们就来检验了它有没有生效,在浏览器地址栏里输入“javascript:alert(Sys.CultureInfo.Name)”并回车,弹出的MessageBox就显示了目前Sys.CultureInfo对象的名称。如图:


如果当前页面无法访问到ScriptManager对象,那么使用下面的代码也能完成相同的功能:
protected void Page_Load(object sender, EventArgs e)
{
    
string cultureName = GetUserCulture();
    ScriptManager.GetCurrent(
this.Page).RegisterScriptReference("atlasglob.axd?c=" + cultureName);
}

ScriptManager类的静态方法GetCurrent是取出作为参数的System.Web.UI.Page对象里的ScriptManager实例。当然,它的前提是当前Page对象里有ScriptManager对象。在Atlas类库里大量使用了这个方法,各个控件(比如UpdatePanel)都是通过这个方法获得当前页面的ScriptManager对象,然后通过各种逻辑改变页面的行为。


四、更好地控制Atlas本地化支持

我们已经做到了自定义的Culture Detection,足够了吗?事实上,这样的做法还有比较明显的缺陷。首先它将Culture Detection的逻辑放在了页面中,很可能会在多个地方出现类似的代码,不利于维护,我们应该集中的处理这段逻辑。再者,如果我们还想更进一步地操作Atlas的本地化支持,该怎么做呢?比如,我们希望修改部分的CultureInfo信息,甚至完全自定义,又该怎么办呢?

我们自然可以完全放弃atlasglob.axd而使用自己生成的Scripts,但是我更倾向于在Atlas的基础上作。毕竟,atlasglob.axd的使用是ScriptManager的built-in功能,我们不妨就在这个基础上扩展。我们知道,atlasglob.axd自身没有任何功能,因为它不是一个文件,根本不存在。唯一起作用的是Microsoft.Web.Globalization.GlobalizationHandler类,它生成了我们需要的所有CultureInfo代码。因此,我们为什么不使用自定义的Handler?

写这样一个Handler非常简单,唯一要关注的只有ProcessRequest方法。这样,我们就可以将所需culture在Handler里得到,然后和上面贴出的GlobalizationHandler.ProcessRequest代码完全一样地输出即可。

唔?AtlasCultureInfo是internal类,外界无法访问?没有关系,那么我们自己写一个:
public class JeffzAtlasCultureInfo
{
    
public DateTimeFormatInfo DateTimeFormat;
    
public string Name;
    
public NumberFormatInfo NumberFormat;

    
public JeffzAtlasCultureInfo(CultureInfo culture)
    
{
        
this.Name = cultureInfo.Name;
        
this.NumberFormat = cultureInfo.NumberFormat;
        
this.DateTimeFormat = cultureInfo.DateTimeFormat;
    }

}

然后在ProcessRequest方法里使用即可。

如果我们有特殊的要求,想修改部分Sys.CultureInfo信息又该怎么办呢?当然我们可以自己定义一个类并序列化输出,但是CultureInfo还是有不少信息,编写起来有着不小的工作量,而我们往往只需要做一点点修改。因此我推荐在输出代码的最后输出额外的修改代码即可。例如:
public void ProcessRequest(HttpContext context)
{
    
// the original output
    context.Response.Write("Sys.CultureInfo.NumberFormat.CurrencySymbol = '@';");
}

这样,Sys.CultureInfo对象的内容就会被修改了,非常地方便。

当我们写好了自己的Handler,只需再web.config文件中把Handler注册给atlasglob.axd文件即可。剩下的工作,就交给ScriptManager去处理吧。