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 http://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最好的应用。

Comments (0)

Skip to main content