ASP.NET MVC 3、JavaScript和jQuery中的全球化、国际化和本地化——第一部分
[原文发表时间] 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)如下截屏所示:
然后我的验证实现那个,不会让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: });
这样就可以和验证一起使用了。。。
以下是[Range]使用的丹麦区域性:
我还可以设置必需的属性,来使用特定资源和名称,以及从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...
再看看这个:
注意:我正在研究如何为所有的字段设置新默认值,而不是逐个改写。我成功地改写了那些含“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类。
现在我们的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.
相关链接
· MSDN: 用非英语地区支持ASP.NET MVC 3 验证
· 可看的开放源项目: Daniel Crenna的全新国际化ASP.NET MVC项目,在GitHub上叫做“ il8n ” : https://github.com/danielcrenna/i18n
摘自他的网站:
· 全球分辨的界面;像大孩子一样本地化
· 本地化所有东西;视图,控制器,验证属性,甚至路径。
· SEO友好; 通过URL选择语言,目录语言设置恰当
· 自动; 无需路径改变,只要在你想本地化的地方使用域方法
智能; 知道什么时候托管,什么时候包住,离开,运行,基于il8n最好的应用。