ASP.NET MVC 3、JavaScript和jQuery中的全球化、国际化和本地化——第一部分

[原文发表地址] Globalization, Internationalization and Localization in ASP.NET MVC 3, JavaScript and jQuery - Part 1

[原文发表时间] 2011-05-26 02:56

有好几本有关Internationalization(il8n)的书值得一读,但是在一篇博客中放不下了,即使是9页长的博文。其实我想把它称之为Iñtërnâtiônàlizætiøn

不过在你创建多语言ASP.NET应用程序之前,你得知道一些基础的东西。首先让我们来统一一下一些基本的定义,因为这些术语常常会被互换使用。

· 国际化 (i18n) – 让你的应用程序支持多种语言和区域设置。

· 本地化 (L10n) – 让你的应用程序支持某一种特定语言/区域设置。

· 全球化 – 是国际化和本地化的结合

· 语言 – 比如,西班牙语,ISO代码“es”

· 区域设置 – 墨西哥。注意西班牙的西班牙语和墨西哥的西班牙语是不一样的,比如“es-ES”和“es-MX”

区域性和用户界面区域性

用户界面区域性是.NET基类库(BCL)的CultureInfo实例。它存在于Thread.CurrentThread.CurrentUICulture,如果你喜欢它,你可以手动设置:

    1: Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-MX");

The CurrentCulture是为日期,汇率等而设

    1: Thread.CurrentThread.CurrentCulture = new CultureInfo("es-MX");

不过,你得避免这类东西,除非你知道你自己在干什么而且有充分的理由。

用户的浏览器在接受语言HTTP标头中会报告他们的语言偏好,如下所示:

GET https://www.hanselman.com HTTP/1.1

Connection: keep-alive

Cache-Control: max-age=0

Accept-Encoding: gzip,deflate,sdch

Accept-Language: en-US,en;q=0.8

知道我为什么喜欢en-US和en了吧?我会让ASP.NET自动传递那些值,选择正确的区域性来启动线程。我需要对web.config做如下设置

    1: <SYSTEM.WEB>
    2:     <GLOBALIZATION culture="auto" uiculture="auto" enableclientbasedculture="true" />
    3: ...snip...

那一行就能实现我想要的。现在ASP.NET就会自动设置当前线程和当前用户界面线程的区域性了。

Pseudointernationalization的重要性

2005的时候我更新了John Robbin的 Pseudoizer(然后拼错了!),我把它运用到.NET4上。我发现用这个技术来创建可本地化的站点很简单,因为我时常在我的应用程序中把所有字符串改成其他语言,这就让我能认出在翻译字符串时遗漏的字符串。

你可以 点击此处下载.NET Pseudoizer

更新:我把Pseudoizer的资源放在Bitbucket。你可以克隆它,也可以发送请求或者制作自己的版本。

以下是在我早些时候写的博文中运行Pseudointernationalization时的例子:

    1: <DATA name="Accounts.Download.Title">
    2:   <VALUE>Transaction Download</VALUE>
    3: </DATA>
    4: <DATA name="Accounts.Statements.Action.ViewStatement">
    5:   <VALUE>View Statement</VALUE>
    6: </DATA>
    7: <DATA name="Accounts.Statements.Instructions">
    8:   <VALUE>Select an account below to view or download your available online statements.</VALUE>
    9: </DATA>

我可以用pseudoizer转换这些资源:

PsuedoizerConsole examplestrings.en.resx examplestrings.xx.resx

以下是结果:

    1: <DATA name="Accounts.Download.Title">
    2:   <VALUE>[Ŧřäʼnşäčŧįőʼn Đőŵʼnľőäđ !!! !!!]</VALUE>
    3: </DATA>
    4: <DATA name="Accounts.Statements.Action.ViewStatement">
    5:   <VALUE>[Vįęŵ Ŝŧäŧęmęʼnŧ !!! !!!]</VALUE>
    6: </DATA>
    7: <DATA name="Accounts.Statements.Instructions">
    8:   <VALUE>[Ŝęľęčŧ äʼn äččőūʼnŧ þęľőŵ ŧő vįęŵ őř đőŵʼnľőäđ yőūř äväįľäþľę őʼnľįʼnę şŧäŧęmęʼnŧş. !!! !!! !!! !!! !!!]</VALUE>
    9: </DATA>

很酷吧?如果你常用RESX文件,那就熟悉一下Visual Studio和.NET SDK中的resgen.exe命令行工具吧。它已经包含您的系统中了。您可以在RESX XML基础文件格式和更人性化的文本名=值格式间随意切换,如下:

resgen /compile examplestrings.xx.resx,examplestrings.xx.txt

现在他们有了更好的名称=值格式(value format),正如我说的,我可以随意切换。

Accounts.Download.Title=[Ŧřäʼnşäčŧįőʼn Đőŵʼnľőäđ !!! !!!]

Accounts.Statements.Action.ViewStatement=[Vįęŵ Ŝŧäŧęmęʼnŧ !!! !!!]

Accounts.Statements.Instructions=[Ŝęľęčŧ äʼn äččőūʼnŧ þęľőŵ ŧő vįęŵ őř đőŵʼnľőäđ yőūř äväįľäþľę őʼnľįʼnę şŧäŧęmęʼnŧş. !!! !!! !!! !!! !!!]

在开发过程中,我喜欢把这个Pseudoizer步骤添加到我的持续集成生成中,或者作为预生成步骤。同时将资源设置为我不会创建的一种随机语言,比如波兰语(我很尊重波兰),所以我会制作examplestrings.pl.resx,然后通过将浏览器的用户语言从en-US改为pl-PL来测试我们的语言。

本地化回退

不同的语言占的空间也不一样。上帝保佑德国人,因为他们的字符串会比英语词组平均多占30%的空间。中文则会少占30%。Pseudoizer填充字符串来显示这些差别,希望你在布局时能把这个因素考虑在内。

.NET内的本地化(没有具化到ASP.NET Proper或者ASP.NET MVC)实行标准回退机制。这就意味着它会在要求场所寻找最具体的字符串,然后回退继续寻找,直到回到中性语言才结束(无论是什么)。这个回退是由常规命名来处理的。以下是一个有点年头,但仍然相当出色的ASPAlliance上的资源回退演示

比如,我们假设有三个资源。Resources.resx, Resources.es.resx, 和Resources.es-MX.resx

Resources.resx:
HelloString=Hello, what's up?

GoodbyeString=See ya!

DudeString=Duuuude!

Resources.es.resx:
HelloString=¿Cómo está?

GoodbyeString=Adiós!

Resources.es-MX.resx:
HelloString=¿Hola, qué tal?

假定这三个文件处于回退方案中。用户的浏览器请求es-MX。如果我们要HelloString,他就会获得最具化的那个。如果我们要GoodbyeString,我们没有与“es-MX”对等的内容,所以我们就筛选到“es”那里。如果我们要DudeString,我们连es字符串都没有,那我们就回退到中性的资源。

使用这个回退的基本概念,你可以最小化你本地化的字符串数量,让用户不仅拥有特定语言(language specific)字符串(西班牙语),还有本地(local)(墨西哥西班牙语)字符串。我知道这个例子有点傻,但是并不是真的代表西班牙和墨西哥殖民语言。

视图而非资源

如果你不喜欢资源的想法,当然你还是得处理一些资源的,你也可以为各种不同语言和区域选择视图。你可以像Brian Reiter等人一样构造自己的/视图文件夹。如果你接受上述的资源回退的想法,看上去就会很清楚,以下是Brian的例子:

/Views

/Globalization

/ar

/Home

/Index.aspx

/Shared

/Site.master

/Navigation.aspx

/es

/Home

/Index.aspx

/Shared

/Navigation.aspx

/fr

/Home

/Index.aspx

/Shared

/Home

/Index.aspx

/Shared

/Error.aspx

/Footer.aspx

/Navigation.aspx

/Site.master

正如你能让ASP.NET改变基于用户语言或者cookie的当前UI区域性,你也可以通过覆盖最喜欢的视图引擎来控制视图的选择。Brian在他的博客中用几行介绍了基于语言cookie的视图

他还介绍了一些简单的jQuery,让用户能用cookie来覆盖语言,如下所示:

    1: var mySiteNamespace = {}
    2:  
    3: mySiteNamespace.switchLanguage = function (lang) {
    4:     $.cookie('language', lang);
    5:     window.location.reload();
    6: }
    7:  
    8: $(document).ready(function () {
    9:     // attach mySiteNamespace.switchLanguage to click events based on css classes
   10:     $('.lang-english').click(function () { mySiteNamespace.switchLanguage('en'); });
   11:     $('.lang-french').click(function () { mySiteNamespace.switchLanguage('fr'); });
   12:     $('.lang-arabic').click(function () { mySiteNamespace.switchLanguage('ar'); });
   13:     $('.lang-spanish').click(function () { mySiteNamespace.switchLanguage('es'); });
   14: });

我还是想把这个做成单客户端事件,可以使用数据语言或者HTML5属性(头脑风暴),如下所示:

    1: $(document).ready(function () {
    2:         $('.language').click(function (event) {
    3:             $.cookie('language', $(event.target).data('lang'));
    4:         })
    5: });

这下你大概明白了。你可以设置覆盖cookie,先检查一下,然后检查一下用户语言标头。这取决于你想要什么样的体验,然后需要在客户端和服务器中平衡。

全球化JavaScript验证

如果你用JavaScript和jQuery做许多客户端工作的话,你会需要熟悉一下jQuery全球插件的。你可能还想在NuGet 上通过“install-package jQuery.UI.i18n”为DataPicker和jQueryUI获取本地化文件。

看来不能通过JavaScript来询问浏览器的就是它想要哪种语言了。在HTTP标头有一个像这样的叫“接受语言(Accept-Language)”,是一个个加权的列表。

en-ca,en;q=0.8,en-us;q=0.6,de-de;q=0.4,de;q=0.2

我们想把这个值告诉jQuery和我们的朋友,所以我们需要用另一种方式从客户端进入,我推荐以下方法。

很低级的——使用Ajax

我们可以通过服务器上的简单控制器这么做:

    1: public class LocaleController : Controller {
    2:     public ActionResult CurrentCulture()  {
    3:         return Json(System.Threading.Thread.Current.CurrentUICulture.ToString(), JsonRequestBehavior.AllowGet);
    4:     }
    5: }

然后从客户端调用它,让jQuery进行计算,请确保在客户端有你想支持的文化的全球化库。我从GitHub上下载了全部700个jQueryGlob。然后我就可以做一个快捷的Ajax调用,从服务器动态获取信息。我还以脚本形式放进了我想支持的区域,比如/Scripts/globinfo/jquery.glob.fr.js。你还可以建立一个动态解析器,动态加载这些,或者在它们以完整blob形式出现在Google或微软CDN上时进行全部加载。

    1: <SCRIPT>
    2:     $(document).ready(function () {
    3:         //Ask ASP.NET what culture we prefer
    4:         $.getJSON('/locale/currentculture', function (data) {
    5:             //Tell jQuery to figure it out also on the client side.
    6:             $.global.preferCulture(data);
    7:         });
    8:     });
    9: </SCRIPT>

这其实没什么,我还要做个小的JSON调用。这可能属于其他像自定义META标签这类。

不那么低级的—— Meta标签

为什么不把这个标头的值放到页面的META标签里,从那里获取呢?这样就不用多余的AJAX调用,我还能像以前一样继续使用jQuery了。我会创建一个HTML帮助程序以在主布局页面使用。以下就是HTML帮助程序。使用的是当前线程,是我们之前往web.config添加设置时,自动设置好的。

    1: namespace System.Web.Mvc
    2: {
    3:     public static class LocalizationHelpers
    4:     {
    5:         public static IHtmlString MetaAcceptLanguage<T>(this HtmlHelper<T> html)
    6:         {
    7:             var acceptLanguage = HttpUtility.HtmlAttributeEncode(Threading.Thread.CurrentThread.CurrentUICulture.ToString());
    8:             return new HtmlString(String.Format("<META content=\ name=\ {0}\?? accept-language\??>",acceptLanguage));
    9:         }
   10:     }
   11: }

我在主布局页面上使用的帮助程序如下:

    1: <HTML>
    2: <HEAD>
    3:      
    4:      
    5:     <LINK href="@Url.Content(" type=text/css rel=stylesheet ~ Content site.css?)?>
    6:     <SCRIPT src="@Url.Content(" type=text/javascript ~ jquery-1.5.1.min.js?)? Scripts></SCRIPT>   1:     2:     <SCRIPT src="@Url.Content(" type=text/javascript ~ Scripts jquery.glob.fr.js?)? globinfo>   1: </SCRIPT>    2:     <SCRIPT src="@Url.Content(" type=text/javascript ~ Scripts modernizr-1.7.min.js?)?>   1: </SCRIPT>   2:     <SCRIPT src="@Url.Content(" type=text/javascript ~ Scripts jquery.global.js?)?></SCRIPT>
    7:     @Html.MetaAcceptLanguage()
    8:  
    9: ...

HTML结果如下所示。注意这个META标签会和目录语言或者lang=属性有所区别,因为它是解析过的HTTP标头的一部分,ASP.NET会决定我们的当前区域性,再移到客户端。

    1: <HTML>
    2: <HEAD>
    3:      
    4:      
    5:     <LINK href="/Content/Site.css" type=text/css rel=stylesheet>
    6:     <SCRIPT src="/Scripts/jquery-1.5.1.min.js" type=text/javascript></SCRIPT>   1:     2:     <SCRIPT src="/Scripts/globinfo/jquery.glob.fr.js" type=text/javascript>   1: </SCRIPT>    2:     <SCRIPT src="/Scripts/modernizr-1.7.min.js" type=text/javascript>   1: </SCRIPT>   2:     <SCRIPT src="/Scripts/jquery.global.js" type=text/javascript></SCRIPT>
    7:     <META content=en-US name=accept-language>

现在我可以从客户端用相似的代码获取了。我想对此做一些改进,以支持JS动态加载,不过偏好区域性(preferCulture)不是智能的,而且需要加载资源来做决定。我想做一个方法来告诉我偏好区域性,这样我就能根据需求加载资源了。

    1: <SCRIPT>   1:     2:     $(document).ready(function () {   3:         //Ask ASP.NET what culture we prefer, because we stuck it in a meta tag   4:         var data = $("meta[name='accept-language']").attr("content")   5:         //Tell jQuery to figure it out also on the client side.   6:         $.global.preferCulture(data);   7:     });</SCRIPT>

所以呢?现在我在客户端,我的验证和JavaScript都更智能一些了。一旦客户端的jQuery了解你的当前的偏好区域性,你就可以开始使用jQuery的智能功能了。确认你先使用的是没有区域性针对的数据值,然后在用户可见的时候进行转化。

    1: var price = $.format(123.789, "c");
    2: jQuery("#price").html('12345');
    3: var date = $.format(new Date(1972, 2, 5), "D");
    4: jQuery("#date").html(date);
    5: var units = $.format(12345, "n0");
    6: jQuery("#unitsMoved").html(units);

现在,你可以在ASP.NET MVC中应用这些想法来验证了。

全球化的jQuery隐式验证

在上述代码基础上,我们可以实现验证的全球化,这样我们就更能理解如何管理像5,50,在法国实际是5.50这样的值了。有很多验证方法可以供你使用,以下是数字的解析。

    1: $(document).ready(function () {
    2:     //Ask ASP.NET what culture we prefer, because we stuck it in a meta tag
    3:     var data = $("meta[name='accept-language']").attr("content")
    4:     //Tell jQuery to figure it out also on the client side.
    5:     $.global.preferCulture(data);
    6:  
    7:     //Tell the validator, for example,
    8:     // that we want numbers parsed a certain way!
    9:     $.validator.methods.number = function (value, element) {
   10:         if ($.global.parseFloat(value)) {
   11:             return true;
   12:         }
   13:         return false;
   14:     }
   15: });

如果我把用户语言设置为偏好法语(fr-FR)如下截屏所示:

clip_image001

然后我的验证实现那个,不会让5.50显示为值,但是会允许5,50。如下列模型所示:

    1: public class Example
    2: {
    3:     public int ID { get; set; }
    4:     [Required]
    5:     [StringLength(30)]
    6:     public string First { get; set; }
    7:     [Required]
    8:     [StringLength(30)]
    9:     public string Last { get; set; }
   10:     [Required]
   11:     public DateTime BirthDate { get; set; }
   12:     [Required]
   13:     [Range(0,100)]
   14:     public float HourlyRate { get; set; } 
   15: }

我将会看到这个验证错误,因为客户端知道我们更喜欢以小数点作为分隔符。

注意: 对我来说,和jQuery验证对话的[Range]属性不支持全球化,并没有调入到本地化方法中,所以不能解决.和,小数问题。我通过在jQuery中覆盖range方法修复了这个问题,如下所示,强制它使用全球实行的parseFloat。感谢Kostas在评论中提供这个信息

    1: jQuery.extend(jQuery.validator.methods, {
    2:     range: function (value, element, param) {
    3:         //Use the Globalization plugin to parse the value
    4:         var val = $.global.parseFloat(value);
    5:         return this.optional(element) || (val >= param[0] && val <= param[1]);
    6:     }
    7: });

这样就可以和验证一起使用了。。。

clip_image002

以下是[Range]使用的丹麦区域性:

clip_image003

我还可以设置必需的属性,来使用特定资源和名称,以及从ExampleResources.resx文件中本地化,如下所示:

    1: public class Example
    2: {
    3:     public int ID { get; set; }
    4:     [Required(ErrorMessageResourceType=typeof(ExampleResources),
    5:               ErrorMessageResourceName="RequiredPropertyValue")]
    6:     [StringLength(30)]
    7:     public string First { get; set; }
    8: ...snip...

再看看这个:

clip_image004

注意:我正在研究如何为所有的字段设置新默认值,而不是逐个改写。我成功地改写了那些含“PropertyValueInvalid”和“PropertyValueRequired”关键词的资源文件,然后在Global.asax中设置了这些值,但是有的就不行了。

    1: DefaultModelBinder.ResourceClassKey = "ExampleResources";
    2: ValidationExtensions.ResourceClassKey = "ExampleResources";

我会继续研究的。

动态本地化jQuery DataPicker

既然我知道当前jQuery UI culture是什么,我可以使用它来动态加载我需要的DataPicker资源。我从Scott Kirkland上安装了MvcHtml5TemplatesNuGet库,所以我的输入类型是“日期时间”,我添加了这小段JavaScript。我们支不支持日期?我们是非英语使用者吗?如果是,就去获取正确的DataPicker脚本,通过当前区域性获取区域设置来设置默认信息。

    1: //Setup datepickers if we don't support it natively!
    2: if (!Modernizr.inputtypes.date) {
    3:     if ($.global.culture.name != "en-us" && $.global.culture.name != "en") {
    4:         var datepickerScriptFile = "/Scripts/globdatepicker/jquery.ui.datepicker-" + $.global.culture.name + ".js";
    5:         //Now, load the date picker support for this language 
    6:         // and set the defaults for a localized calendar
    7:         $.getScript(datepickerScriptFile, function () {
    8:             $.datepicker.setDefaults($.datepicker.regional[$.global.culture.name]);
    9:         });
   10:     }
   11:     $("input[type='datetime']").datepicker();
   12: }

然后我们设置所有的输入为 类型=日期时间(datetime)。如果你喜欢你还可以使用CSS类。

clip_image005

现在我们的jQuery DataPicker就是法语的了。

从右到左 (body=rtl)

对于像阿拉伯语和希伯来语这样从右读到左的语言,你需要改变你想翻页的元素dir=属性。常常你要改变根元素或者用CSS来改变。如下所示:

    1: div {
    2:    direction:rtl;
    3: }

关键还是有一个通用的策略,无论是为RTL语言做自定义布局文件还是使用CSS或HTML帮助程序来翻页共享的布局。同行们常常把方向定在资源中,根据具体情况拉出ltr或者rtl值。

总结

全球化是很难的,而且需要实际想法和分析。当前的JavaScript提供的是不断在演化之中的。

很多东西都能做成样板文件或者自动的文件,但很大程度上是移动的目标。我目前正在探索NuGet包,他们为你设置所有这些内容,或者做一个“文件|新项目”模板,其中所有最好的应用设置都在其中了。亲爱的读者,你想要哪个呢?

完整脚本

以下是我正在做的“完整”工作脚本,可以移入自己的文件。这是还在完善过程中的工作。所以如果有什么错误,请多包含,毕竟我还在学习JavaScript。

    1: <script>
    2:         $(document).ready(function () {
    3:             //Ask ASP.NET what culture we prefer, because we stuck it in a meta tag
    4:             var data = $("meta[name='accept-language']").attr("content")
    5:  
    6:             //Tell jQuery to figure it out also on the client side.
    7:             $.global.preferCulture(data);
    8:  
    9:             //Tell the validator, for example,
   10:             // that we want numbers parsed a certain way!
   11:             $.validator.methods.number = function (value, element) {
   12:                 if ($.global.parseFloat(value)) {
   13:                     return true;
   14:                 }
   15:                 return false;
   16:             }
   17:  
   18:             //Fix the range to use globalized methods
   19:             jQuery.extend(jQuery.validator.methods, {
   20:                 range: function (value, element, param) {
   21:                     //Use the Globalization plugin to parse the value
   22:                     var val = $.global.parseFloat(value);
   23:                     return this.optional(element) || (val >= param[0] && val <= param[1]);
   24:                 }
   25:             });
   26:  
   27:             //Setup datepickers if we don't support it natively!
   28:             if (!Modernizr.inputtypes.date) {
   29:                 if ($.global.culture.name != 'en-us' && $.global.culture.name != 'en') {
   30:  
   31:                     var datepickerScriptFile = "/Scripts/globdatepicker/jquery.ui.datepicker-" + $.global.culture.name + ".js";
   32:                     //Now, load the date picker support for this language 
   33:                     // and set the defaults for a localized calendar
   34:                     $.getScript(datepickerScriptFile, function () {
   35:                         $.datepicker.setDefaults($.datepicker.regional[$.global.culture.name]);
   36:                     });
   37:                 }
   38:                 $("input[type='datetime']").datepicker();
   39:             }
   40:  
   41:         });
   42:     </script>view raw gistfile1.js This Gist brought to you by GitHub. 
   43:     <script>
   44:         $(document).ready(function () {
   45:             //Ask ASP.NET what culture we prefer, because we stuck it in a meta tag
   46:             var data = $("meta[name='accept-language']").attr("content")
   47:  
   48:             //Tell jQuery to figure it out also on the client side.
   49:             $.global.preferCulture(data);
   50:  
   51:             //Tell the validator, for example,
   52:             // that we want numbers parsed a certain way!
   53:             $.validator.methods.number = function (value, element) {
   54:                 if ($.global.parseFloat(value)) {
   55:                     return true;
   56:                 }
   57:                 return false;
   58:             }
   59:  
   60:             //Fix the range to use globalized methods
   61:             jQuery.extend(jQuery.validator.methods, {
   62:                 range: function (value, element, param) {
   63:                     //Use the Globalization plugin to parse the value
   64:                     var val = $.global.parseFloat(value);
   65:                     return this.optional(element) || (val >= param[0] && val <= param[1]);
   66:                 }
   67:             });
   68:  
   69:             //Setup datepickers if we don't support it natively!
   70:             if (!Modernizr.inputtypes.date) {
   71:                 if ($.global.culture.name != 'en-us' && $.global.culture.name != 'en') {
   72:  
   73:                     var datepickerScriptFile = "/Scripts/globdatepicker/jquery.ui.datepicker-" + $.global.culture.name + ".js";
   74:                     //Now, load the date picker support for this language 
   75:                     // and set the defaults for a localized calendar
   76:                     $.getScript(datepickerScriptFile, function () {
   77:                         $.datepicker.setDefaults($.datepicker.regional[$.global.culture.name]);
   78:                     });
   79:                 }
   80:                 $("input[type='datetime']").datepicker();
   81:             }
   82:  
   83:         });

    </script>view raw gistfile1.js This Gist brought to you by GitHub.

相关链接

· 微软中的jQuery全球化插件

· 全球化ASP.NET MVC隐性验证

· 用jQuery探索Globalization

· MSDN: 用非英语地区支持ASP.NET MVC 3 验证

· 可看的开放源项目: Daniel Crenna的全新国际化ASP.NET MVC项目,在GitHub上叫做“ il8n https://github.com/danielcrenna/i18n

摘自他的网站:

· 全球分辨的界面;像大孩子一样本地化

· 本地化所有东西;视图,控制器,验证属性,甚至路径。

· SEO友好; 通过URL选择语言,目录语言设置恰当

· 自动; 无需路径改变,只要在你想本地化的地方使用域方法

智能; 知道什么时候托管,什么时候包住,离开,运行,基于il8n最好的应用。