Skip to content

6. 视图国际化

在此我们将探讨视图国际化的问题。这是一个复杂的问题,Scott Hanselman 在以下文章中对此进行了很好的阐述:

[http://www.hanselman.com/blog/GlobalizationInternationalizationAndLocalizationInASPNETMVC3JavaScriptAndJQueryPart1.aspx]

首先,让我们回顾一下他对视图国际化相关术语的定义:

国际化 (i18n)
使应用程序支持不同的语言和区域设置
本地化 (l10n)
使应用程序支持特定的语言/区域设置组合
全球化
国际化和本地化的结合
语言
所使用的语言——由 ISO 代码指定(fr:法语,es:西班牙语,en:英语,……)
区域设置
语言的变体——同样由 ISO 代码标识(en_GB:英式英语,en_US:美式英语,……)

让我们通过一个例子来解决这个问题。

6.1. 实数的局域化

你可能会注意到前面的输入表单中存在一个异常:

Image

对于实数,我们输入了 [0,3],但系统未接受。你必须输入 [0.3]:

Image

因此,系统期望的格式是英式格式,而非法式格式。网上快速搜索后发现了一些解决方案。以下是一种。

[GET] 和 [POST] 操作变为如下形式:


    // Action13-GET
    [HttpGet]
    public ViewResult Action13Get()
    {
      return View("Action13Get", new ViewModel11());
}


    // Action13-POST
    [HttpPost]
    public ViewResult Action13Post(ViewModel11 modèle)
    {
      return View("Action13Get", modèle);
}

[Action13Get.cshtml] 视图与 [Action12Get.cshtml] 视图完全相同,仅 JavaScript 脚本有所不同:


<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action13Get</title>
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
...
  <script type="text/javascript" src="~/Scripts/myscripts.js"></script>
 
</head>

注意:第 5 行,请根据您使用的 Visual Studio 版本调整 jQuery 版本。

  • 第 9 行,我们添加了一个脚本 [ myscripts.js ]。内容如下:

// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseFloat(value));
}
 
$.validator.methods.date = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseDate(value));
}
 
jQuery.extend(jQuery.validator.methods, {
  range: function (value, element, param) {
    //Use the Globalization plugin to parse the value        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});
 
// au chargement du document
$(document).ready(function () {
  var culture = 'fr-FR';
  Globalize.culture(culture);
});

我在第 1 行标注了该脚本的位置。我不会尝试解释它,因为我也不太明白。JavaScript 有时确实有些晦涩难懂。在第 4、9 和 15 行中,使用了一个 [Globalize] 对象。该对象由 jQuery Globalization 库提供,可通过 [NuGet] 获取:

  • 在 [1] 中,管理 [Example-03] 项目的 [NuGet] 包;
  • 在 [2] 中,在线浏览包;
  • 在 [3] 中,输入关键词 [globalization];
  • 在 [4] 中,为 JQuery 项目安装 [Globalize] 包。

安装 [Globalize] 包后,[Scripts] 文件夹中会出现一个新文件夹:

  • 在 [1] 中,已创建名为 [globalize] 的文件夹,其中包含主脚本 [globalize.js];
  • 在 [2] 中,主脚本 [globalize.js] 由针对特定语言和区域设置的脚本进行补充;
  • 在 [3] 中,包含针对法语以及比利时 (BE)、加拿大 (CA)、法国 (FR)、瑞士 (CH)、卢森堡 (LU) 和摩纳哥 (MC) 地区设置的专用脚本。

[globalize.js] 脚本以及我们的文化脚本 [globalize.culture.fr-FR.js] 必须包含在 [Action13Get.cshtml] 页面的脚本列表中:


<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action13Get</title>
...
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/myscripts.js"></script>
</head>

  • 第 5 行:[globalize] 脚本;
  • 第 6 行:[globalize.culture.fr-FR.js] 脚本;
  • 第 7 行:[myscripts.js] 脚本;

让我们仔细看看最后这个脚本:


// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseFloat(value));
}
 
...
 
// au chargement du document
$(document).ready(function () {
  var culture = 'fr-FR';
  Globalize.culture(culture);
});

第 10–13 行将客户端区域设置为 [fr-FR]:

  • 第 10 行:当包含该脚本的文档被浏览器完全加载后,jQuery 的 [ready] 函数会被执行;
  • 第 11–12 行:将客户端区域设置为 [fr-FR]。要使此功能生效,文件 [globalize.culture.fr-FR.js] 必须包含在与该文档关联的 JavaScript 脚本列表中。

现在我们可以测试这个新应用程序了:

Image

现在我们可以输入 [0.3] 作为实数,这是之前无法做到的。然而,我们遇到了另一个问题:

Image

在上文中,客户端验证允许我们使用英制记法输入 [11.2]。但在提交表单时,服务器端不接受该值:

Image

我们必须输入 [11,2],这样在客户端和服务器端都能正常工作。在客户端,不应接受盎格鲁-撒克逊记法。这应该是有办法实现的……

现在让我们来探讨视图的国际化。我们将延续前一个表单的示例,提供法语和英语两种语言版本。

6.2. 管理区域设置

视图的语言由 [Thread.CurrentThread.CurrentUICulture] 对象控制。若要在 [fr-FR] 文化环境中显示页面,我们编写如下代码:

Thread.CurrentThread.CurrentUICulture=new CultureInfo("fr-FR");

本地化(日期、数字、货币、时间等)由 [Thread.CurrentThread.CurrentCulture] 对象控制。与之前写的内容类似,我们编写如下代码:

Thread.CurrentThread.CurrentCulture=new CultureInfo("fr-FR");

这两条语句可以放在应用程序中每个控制器的构造函数中。不过,我们也可能希望将这段所有控制器共有的代码提取出来。我们将采用这种做法。

我们创建两个新的控制器:

Image

  • [I18NController] 将作为所有使用国际化的控制器的基类;
  • [SecondController] 是从 [I18NController] 派生出的一个示例控制器。

[I18NController] 控制器的代码如下:


using System.Threading;
using System.Web;
using System.Web.Mvc;
 
namespace Exemples.Controllers
{
  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // retrieve the context of the current query
      HttpContext httpContext = HttpContext.Current;
      // examine the query for the [lang] parameter
      // look for it in the URL parameters
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // look for it in the posted parameters
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // search for it in the user's session
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1st header parameter HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // culture fr-FR
        langue = "fr-FR";
      }
      // put your tongue in session
      httpContext.Session["lang"] = langue;
      // changing thread cultures            
      Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
    }
  }
}

  • 第 7 行:[I18NController] 继承自 [Controller] 类;
  • 第 7 行:该类被声明为 [abstract] 以防止直接实例化:它只能通过派生来使用;
  • 第 9 行:每次实例化从 [I18NController] 派生的控制器时,都会执行该类的构造函数;
  • 第 12 行:我们获取当前由控制器处理的 HTTP 请求的上下文;
  • 第 15 行:我们假设语言由 [lang] 参数设定,该参数可能位于不同位置。我们将按以下顺序进行搜索:
    • 第 15 行:在 URL 参数 [?lang=en-US] 中,
    • 第 19 行:在提交的参数 [lang=de] 中,
    • 第 24 行:在用户的会话中,
    • 第 29 行:在 HTTP 客户端发送的语言偏好设置中,
    • 第 26 行:如果未找到任何信息,则将区域设置为 [fr-FR];
  • 第 37 行:我们将区域设置存储在会话中。后续请求将从此处获取该设置。用户可通过在 GET 或 POST 请求的参数中包含该设置来更改它;
  • 第 39–40 行:我们为当前请求处理完成后将显示的视图设置语言环境。

[SecondController] 控制器将如下所示:


using Exemple_03.Models;
using Exemples.Controllers;
using System.Web.Mvc;
 
namespace Exemple_03.Controllers
{
    public class SecondController : I18NController
    {
      // Action14-GET
      [HttpGet]
      public ViewResult Action14Get()
      {
        return View("Action14Get", new ViewModel14());
      }
 
      // Action14-POST
      [HttpPost]
      public ViewResult Action14Post(ViewModel14 modèle)
      {
        return View("Action14Get", modèle);
      }
    }
}

  • 第 7 行:[SecondController] 继承自 [I18NController]。这确保了待显示视图的区域设置已初始化;
  • 第 13 行:我们使用视图模型 [ViewModel14],稍后将对此进行介绍;
  • 第 13 行和第 20 行:视图 [Action14Get.cshtml] 显示表单。

6.3. 视图模型 [ViewModel14] 的国际化

视图模型 [ViewModel14] 如下所示:


using Exemple_03.Resources;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net.Mail;
 
namespace Exemple_03.Models
{
  public class ViewModel14 : IValidatableObject
  {
 
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [Display(ResourceType = typeof(MyResources), Name = "chaineaumoins4")]
    [RegularExpression(@"^.{4,}$", ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    public string Chaine1 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "chaineauplus4")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [RegularExpression(@"^.{1,4}$", ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    public string Chaine2 { get; set; }
 
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [Display(ResourceType = typeof(MyResources), Name = "chaine4exactement")]
    [RegularExpression(@"^.{4,4}$", ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    public string Chaine3 { get; set; }
 
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [Display(ResourceType = typeof(MyResources), Name = "entier")]
    public int Entier1 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "entierentrebornes")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [Range(1, 100, ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    public int Entier2 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "reel")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    public double Reel1 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "reelentrebornes")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [Range(10.2, 11.3, ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    public double Reel2 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "email")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [EmailAddress(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte", ErrorMessage="")]
    public string Email1 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "date1")]
    [RegularExpression(@"\s*\d{2}/\d{2}/\d{4}\s*", ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    public string Regexp1 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "date2")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [DataType(DataType.Date)]
    public DateTime Date1 { get; set; }
 
    // validation
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // error list
      List<ValidationResult> résultats = new List<ValidationResult>();
      // the same error msg for all
      string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
 
      // Date 1
      if (Date1.Date <= DateTime.Now.Date)
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
      }
      // Email1
      try
      {
        new MailAddress(Email1);
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
      }
      // Regexp1
      try
      {
        DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }
 
      // return the list of errors
      return résultats;
    }
  }
}

此模型是前一个模型 [ViewModel11] 的国际化版本。我们将描述第一个属性第一个属性的国际化机制。其余属性遵循相同的机制。


    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }

在之前的模型 [ViewModel11] 中,这些代码如下:


[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }

在国际化版本中,第 1 行中要显示的文本被放置在资源文件中。此处,该文件名为 [MyResources.resx](typeof),并已放置在项目根目录下。它被称为资源文件。

我们在此创建了三个资源文件:

  • [MyResources]:当当前语言环境没有资源时使用的默认资源;
  • [MyResources.fr-FR]:针对 [fr-FR] 语言环境的资源;
  • [MyResources.en-US]:适用于 [en-US] 语言环境的资源;

要创建资源文件,请按照以下步骤操作 [1, 2, 3]:

这将生成资源文件 [MyResources2.resx]。双击该文件后,您将看到以下页面:

资源文件是一个字典,其中包含键及其对应的值。请在 [1] 中输入键,在 [2] 中输入值,并在 [3] 中输入资源作用域。为了使这些资源可读,它们必须具有 [Public] 作用域。让我们回到这一行:


    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]

  • [ErrorMessageResourceType]:指代资源文件。参数 [typeof] 即文件名。该文件在编译过程中会被转换为类,其二进制文件将包含在项目程序集之中。因此,最终 [MyResources] 即为资源类的名称;
  • [ErrorMessageResourceName = "infoRequise"]: 指代资源文件中的一个键。最终,该行代码表示要显示的错误消息是 [MyResources] 文件中与 [infoRequise] 键关联的值。

要在 [MyResources] 文件中创建 [infoRequise] 键及其关联值,请按以下步骤操作:

在 [1] 中输入键名,在 [2] 中输入值,并在 [3] 中输入资源作用域。

还有一点需要说明:[MyResources] 类的命名空间。该命名空间在 [MyResources.resx] 文件的属性中定义:

在 [1] 中,我们定义了将从 [MyResources.resx] 资源文件创建的 [MyResources] 类的命名空间。让我们回到之前分析过的国际化行:


[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]

typeof 运算符期望接收一个类,在本例中即 [MyResources] 类。为了能够找到该类,必须将其命名空间导入到 [ViewModel14] 类中:


using Exemple_03.Resources;

要使 [MyResources] 类可见,自 [MyResources] 资源文件创建以来,项目必须至少构建过一次。该类的代码可在 [MyResources.Designer.cs] 文件中查看:

Image

双击此文件,即可访问 [MyResources] 类的代码:


namespace Exemple_03.Resources {
    using System;
 
 
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    public class MyResources2 {
 
  ...
         public static string infoRequise {
            get {
                return ResourceManager.GetString("infoRequise", resourceCulture);
            }
        }
    }
}

  • 第 1 行:类命名空间;
  • 第 11 行:[infoRequise] 键已成为 [MyResources] 类的静态属性。可通过 [MyResources.infoRequise] 这种写法访问它。此外,请注意该属性具有 [public] 作用域。若无此声明,则无法访问该属性。 这一点必须牢记,因为默认作用域是 [internal],若忘记修改作用域,可能会导致难以理解的错误。

为什么现在有三个资源文件?

Image

我们创建了 [MyResources.resx],这是根资源文件。接下来,我们需要根据需要管理的语言环境(语言)数量,创建相应数量的 [MyResources.locale.resx] 资源文件。 这里我们处理的是法语 [fr-FR] 和美式英语 [en-US]。当当前语言环境既不是 [fr-FR] 也不是 [en-US] 时,将使用根资源文件 [MyResources.resx]。

[MyResources.resx] 的最终内容如下:

Image

当无法识别语言环境时,消息将显示为法语。文件 [MyResources.fr-FR.resx] 的最终内容与之完全相同,只需复制该文件即可获得。

[MyResources.en-US.resx] 的最终内容也是通过复制该文件获得的,然后按以下方式进行修改:

Image

让我们回到 [ViewModel14] 视图及其 [Validate] 方法:


    // validation
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // liste des erreurs
      List<ValidationResult> résultats = new List<ValidationResult>();
      // le même msg d'erreur pour tous
      string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
 
      // Date 1
      if (Date1.Date <= DateTime.Now.Date)
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
      }
...
      // on rend la liste des erreurs
      return résultats;
}

第 7 行演示了如何从 [MyResources] 资源文件中获取一条消息。在此,我们希望获取当前区域设置下与键 [infoIncorrecte] 关联的消息:

  • MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")):从资源文件 [MyResources.en-US.resx] 中检索与键 [infoIncorrecte] 关联的对象;
  • 我们看到 [I18NController] 控制器通过 [lang] 键在会话中设置了当前区域设置。因此,可以通过 System.Web.HttpContext.Current.Session["lang"] as string 获取当前区域设置;
  • 资源被检索为 [object] 类型。为了获取错误消息,我们对其应用 [ToString] 方法。

6.4. 视图 [Action14Get.cshtml] 的国际化

我们按以下方式更新表单显示视图:

Image


@model Exemple_03.Models.ViewModel14
@using Exemple_03.Resources
@{
  Layout = null;
}
 
<!DOCTYPE html>
 
<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action14Get</title>
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
  <script type="text/javascript" src="~/Scripts/myscripts2.js"></script>
  <script>
    $(document).ready(function () {
      var culture = '@System.Threading.Thread.CurrentThread.CurrentCulture';
        Globalize.culture(culture);
      });
  </script>
 
</head>
<body>
  <h3>Formulaire ASP.NET MVC - Internationalisation</h3>
  @using (Html.BeginForm("Action14Post", "Second"))
  {
    <table>
      <thead>
        <tr>
          <th>@MyResources.type</th>
          <th>@MyResources.value</th>
          <th>@MyResources.error</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>@Html.LabelFor(m => m.Chaine1)</td>
          <td>@Html.EditorFor(m => m.Chaine1)</td>
          <td>@Html.ValidationMessageFor(m => m.Chaine1)</td>
        </tr>
...
      </tbody>
    </table>
    <p>
      <input type="submit" value="Valider" />
    </p>
  }
</body>
</html>
<!-- choice of language -->
@using (Html.BeginForm("Lang", "Second"))
{
  <table>
    <tr>
      <td><a href="javascript:postForm('fr-FR','/Second/Action14Get')">Français</a></td>
      <td><a href="javascript:postForm('en-US','/Second/Action14Get')">English</a></td>
    </tr>
  </table>
}

注意:第 14 行——请确保 jQuery 版本与您的 Visual Studio 版本匹配。

让我们从最简单的部分开始,即第 36–38 行。它们使用了我们刚才描述的 [MyResources] 类的静态属性。要访问 [MyResources] 类,必须导入其命名空间(第 2 行)。

在国际化消息中,还必须包含客户端验证框架显示的消息。为此,请使用第 17 至 19 行中的 jQuery 库。我们为支持的两个语言环境使用了相应的 jQuery 文件:[fr- FR] 和 [en-US]。此外,您可能还记得,[Action13Get] 视图使用了以下 JavaScript 脚本 [myscripts.js]:


// document loading
$(document).ready(function () {
  var culture = 'fr-FR';
  Globalize.culture(culture);
});

现在,语言环境不再仅仅是 [fr-FR];它会发生变化。因此,第 21 至 26 行代码现在由 [Action14Get] 视图本身生成。这六行代码将被包含在发送给客户端的 HTML 页面中。

  • 第 23 行:JavaScript 变量 [culture] 被初始化为处理请求的线程的当前区域设置。您可能还记得,该变量是由 [I18NController] 类的构造函数初始化的:


      // on met la langue en session
      httpContext.Session["lang"] = langue;
      // on modifie les cultures du thread            
      Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

如果当前区域设置为 [en-US],则嵌入在 HTML 页面中的 JavaScript 脚本变为:


  <script>
    $(document).ready(function () {
      var culture = 'en-US';
        Globalize.culture(culture);
      });
</script>

我们之前已经提到,[$(document).ready] 函数会在浏览器完成页面加载后执行。该函数的执行将设置客户端验证框架的语言环境。使用 [en-US] 语言环境时,框架的错误消息将显示为英文,并来自 [MyResources.en-US.resx] 资源文件。下面我们将具体了解其实现方式。

现在让我们来分析第 57–65 行:


<!-- language selection -->
@using (Html.BeginForm("Lang", "Second"))
{
  <table>
    <tr>
      <td><a href="javascript:postForm('fr-FR','/Second/Action14Get')">Français</a></td>
      <td><a href="javascript:postForm('en-US','/Second/Action14Get')">English</a></td>
    </tr>
  </table>
}

这是第二个表单;第一个表单位于第31至53行。该表单在页面底部显示以下链接:

  • 第2行:表单数据将提交至[Second]控制器中的[Lang]操作。目前,我们尚未看到任何可提交的值;
  • 第 6 行和第 7 行:点击这些链接会触发 JavaScript 函数 [postForm] 的执行。该函数位于何处?就在视图第 20 行引用的脚本 [myscripts2.js] 中:

其内容如下:


function postForm(lang, url) {
  // on récupère le deuxième formulaire du document
  var form = document.forms[1];
  // on lui ajoute l'hidden attribute lang
  var hiddenField = document.createElement("input");
  hiddenField.setAttribute("type", "hidden");
  hiddenField.setAttribute("name", "lang");
  hiddenField.setAttribute("value", lang);
  // ajout du champ caché dans le formulaire
  form.appendChild(hiddenField);
  // on lui ajoute l'hidden attribute url
  var hiddenField = document.createElement("input");
  hiddenField.setAttribute("type", "hidden");
  hiddenField.setAttribute("name", "url");
  hiddenField.setAttribute("value", url);
  // ajout du champ caché dans le formulaire
  form.appendChild(hiddenField);
  // soumission
  form.submit();
}
 
// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseFloat(value));
}
 
$.validator.methods.date = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseDate(value));
}
 
jQuery.extend(jQuery.validator.methods, {
  range: function (value, element, param) {
    //Use the Globalization plugin to parse the value        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});

第 22–40 行与前一个示例中使用的 [myscripts.js] 脚本中的内容相同。此处不再赘述。当点击语言链接时执行的 [postForm] 函数位于第 1–20 行:

  • 第 1 行:该函数接受两个参数,[lang] 表示用户选择的语言环境,[url] 表示语言环境更改完成后客户端浏览器应重定向到的 URL。这两个参数在调用时指定:

<td><a href="javascript:postForm('fr-FR','/Second/Action14Get')">Français</a></td>
<td><a href="javascript:postForm('en-US','/Second/Action14Get')">English</a></td>
  • 第3行:我们获取文档中第二个表单的引用;
  • 第5-8行:我们通过编程方式创建该标签
<input type="hidden" value="xx-XX"/>

其中 [xx-XX] 是该函数 [lang] 参数的值;

  • 第 10 行:同样通过代码,我们将该字段添加到第二个表单中。最终,该字段的行为就如同它从一开始就存在于第二个表单中一样。因此,其值将会被提交。这正是我们想要的效果;
  • 第 11–17 行:我们对一个标签重复相同的操作
<input type="hidden" value="url"/>

,其中 [url] 是该函数 [url] 参数的值;

  • 第 19 行:现在提交了第二个表单。提交到哪个 URL?

我们需要回到 [Action14Get.cshtml] 页面中第二个表单的代码:


@using (Html.BeginForm("Lang", "Second"))
{
...
}

因此,表单将提交至 URL [/Second/Lang]。接下来,我们需要在 [SecondController] 控制器中定义一个 [Lang] 操作。具体如下:


public class SecondController : I18NController
    {
      // Action14-GET
      [HttpGet]
      public ViewResult Action14Get()
      {
        return View("Action14Get", new ViewModel14());
      }
 
      // Action14-POST
      [HttpPost]
      public ViewResult Action14Post(ViewModel14 modèle)
      {
        return View("Action14Get", modèle);
      }
 
      // language
      [HttpPost]
      public RedirectResult Lang(string url)
      {
        // we redirect the client to url
        return new RedirectResult(url);
      }
 
    }
  • 第 18 行:该操作仅响应 [POST] 请求;
  • 第 19 行:它仅获取名为 [url] 的参数;
  • 第 22 行:它指示客户端重定向到此 URL。

但名为 [lang] 的参数去哪儿了?我们必须记住,[SecondController] 控制器继承自 [I18NController] 类(见下文第 1 行)。正是这个控制器处理 [lang] 参数:


  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // on récupère le contexte de la requête courante
      HttpContext httpContext = System.Web.HttpContext.Current;
      // on examine la requête à la recherche du paramètre [lang]
      // on le cherche dans les paramètres de l'URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // on le cherche dans les paramètres postés
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // on le cherche dans la session de l'utilisateur
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1er paramètre de l'entête HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // culture fr-FR
        langue = "fr-FR";
      }
      // on met la langue en session
      httpContext.Session["lang"] = langue;
      // on modifie les cultures du thread            
      Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}

在本示例中,[lang] 参数是通过引用传递的。因此,它将在第 13 行被获取,在第 31 行存储到会话中,并在第 33–34 行用于更新当前线程的区域设置。

接下来会发生什么?让我们重新审视这些链接:


<td><a href="javascript:postForm('fr-FR','/Second/Action14Get')">Français</a></td>
<td><a href="javascript:postForm('en-US','/Second/Action14Get')">English</a></td>

重定向 URL 为 [/Second/Action14Get]。因此将执行 [Action14Get] 操作:


public class SecondController : I18NController
    {
      // Action14-GET
      [HttpGet]
      public ViewResult Action14Get()
      {
        return View("Action14Get", new ViewModel14());
      }
...
}

此前,[I18NController] 类的构造函数会被执行:


  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // on récupère le contexte de la requête courante
      HttpContext httpContext = System.Web.HttpContext.Current;
      // on examine la requête à la recherche du paramètre [lang]
      // on le cherche dans les paramètres de l'URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // on le cherche dans les paramètres postés
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // on le cherche dans la session de l'utilisateur
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1er paramètre de l'entête HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // culture fr-FR
        langue = "fr-FR";
      }
      // on met la langue en session
      httpContext.Session["lang"] = langue;
      // on modifie les cultures du thread            
      Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}

这次,第 18 行将在会话中找到 [lang] 参数。假设其值为 [en-US]。因此,该区域设置将成为执行请求的线程的区域设置(第 33–34 行)。让我们回到 [Action14Get] 操作:


      // Action14-GET
      [HttpGet]
      public ViewResult Action14Get()
      {
        return View("Action14Get", new ViewModel14());
}

第 5 行,将创建视图模型 [ViewModel14] 的一个实例:


  public class ViewModel14 : IValidatableObject
  {
 
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [Display(ResourceType = typeof(MyResources), Name = "chaineaumoins4")]
    [RegularExpression(@"^.{4,}$", ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    public string Chaine1 { get; set; }
....

由于当前线程的区域设置为 [en-US],因此将使用 [MyResources.en-US.resx] 文件。因此,错误消息将显示为英文。

一旦 [ViewModel14] 模型被实例化,[Action14Get.cshtml] 视图就会显示出来:


@model Exemple_03.Models.ViewModel14
@using Exemple_03.Resources
@using System.Threading
@{
  Layout = null;
}
 
<!DOCTYPE html>
 
<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action14Get</title>
  ...
  <script>
    $(document).ready(function () {
      var culture = '@Thread.CurrentThread.CurrentCulture';
        Globalize.culture(culture);
      });
  </script>
 
</head>
<body>
  <h3>Formulaire ASP.NET MVC - Internationalisation</h3>
  @using (Html.BeginForm("Action14Post", "Second"))
  {
    <table>
      <thead>
        <tr>
          <th>@MyResources.type</th>
          <th>@MyResources.value</th>
          <th>@MyResources.error</th>
        </tr>
      </thead>
      <tbody>
        <tr>
...
        </tr>
<tr>

由于当前线程的区域设置为 [en-US],页面第 15–20 行中嵌入的脚本为:


  <script>
    $(document).ready(function () {
      var culture = 'en-US';
        Globalize.culture(culture);
      });
  </script>

这确保了验证框架将使用美国格式(日期、货币、数字等)。出于同样的原因,第30至32行中的消息将从资源文件 [MyResources.en-US.resx] 中获取,因此将显示为英文。

6.5. 执行示例

以下是一些执行示例:

  • [1] 中的表单为法语版本;[2] 中的表单为英语版本。

  • 在[3]中,客户端的错误信息现已改为英文。

如果查看页面的源代码,我们会发现这些错误消息已被嵌入到页面中,这意味着它们是由 ASP.NET 视图 [Action14Get] 及其视图模型 [ViewModel14] 生成的:


        <tr>
          <td><label for="Reel1">Real number</label></td>
          <td><input class="text-box single-line" data-val="true" data-val-number="The field Real number must be a number." data-val-required="Required data" id="Reel1" name="Reel1" type="text" value="0" /></td>
          <td><span class="field-validation-valid" data-valmsg-for="Reel1" data-valmsg-replace="true"></span></td>
        </tr>
        <tr>
          <td><label for="Reel2">Real number in range [10.2-11.3]</label></td>
          <td><input class="text-box single-line" data-val="true" data-val-number="The field Real number in range [10.2-11.3] must be a number." data-val-range="Invalid data" data-val-range-max="11.3" data-val-range-min="10.2" data-val-required="Required data" id="Reel2" name="Reel2" type="text" value="0" /></td>
          <td><span class="field-validation-valid" data-valmsg-for="Reel2" data-valmsg-replace="true"></span></td>
</tr>

6.6. 日期国际化

国际化是一个复杂的问题。让我们来看看 [Date1] 属性及其日历:

Image

我们可以看到,尽管页面的区域设置为 [en-US],但日历却是法语日历。在 HTML5 中,有一个 [lang] 属性,允许您设置页面或页面组件的语言。然后,我们可以在 [Action14Get.cshtml] 视图中编写以下代码:


@model Exemple_03.Models.ViewModel14
@using Exemple_03.Resources
@using System.Threading
@{
  Layout = null;
  var lang = Session["lang"] as string;  
}
 
<!DOCTYPE html>
 
<html lang="@lang">
<head>
...

  • 第 6 行:从会话中获取区域设置;
  • 第 11 行:我们将页面的 [lang] 属性设置为该值。

测试表明,即使页面以英语显示,日历仍保持法语界面。表单中的另一个日期字段也存在问题:

在[1]中,日期仍按法语格式 dd/mm/yyyy(20/11/2013)进行请求,而美式格式应为 mm/dd/yyyy(10/21/2013)。我们将尝试通过新的视图和视图模型来解决这两个问题。

jQuery UI 是从 jQuery 项目衍生出的一个项目,它提供了包括日历在内的表单组件。该日历支持本地化,这正是我们将要演示的内容。

首先,让我们将 [jQuery UI] 添加到项目中。

安装 jQuery UI 后,项目中会出现新的元素:

  • 在 [1] 中,包含 [jQuery UI] 库的普通版和压缩版;
  • 在 [2] 中,[JQuery UI] 样式表;

jQuery UI 日历默认使用英语。若要实现国际化,您需要添加位于 [https://github.com/jquery/jquery-ui/tree/master/ui/i18n] 网址下的脚本:

若要将 jQuery UI 日历设置为法语,请将上述 [jquery.ui.datepicker-fr.js] 文件的内容复制到项目的 [Scripts] 文件夹中。

新视图 [Action15.cshtml] 的代码是通过复制之前的视图 [Action14.cshtml] 并对其进行修改获得的。我们仅展示修改部分:


@model Exemple_03.Models.ViewModel15
@using Exemple_03.Resources
@using System.Threading
@{
  Layout = null;
}
 
<!DOCTYPE html>
 
<html lang="@Model.Culture">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action15</title>
...
  <link rel="stylesheet" href="~/Content/themes/base/jquery-ui.css" />
  <script type="text/javascript" src="~/Scripts/jquery-ui-1.10.3.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.ui.datepicker-fr.js"></script>
  <script>
    $(document).ready(function () {
      var culture = '@Thread.CurrentThread.CurrentCulture';
      Globalize.culture(culture);
      $("#Date1").datepicker($.datepicker.regional['@Model.Regionale']);
    });
  </script>
</head>
<body>
  <h3>@MyResources.titre</h3>
  @using (Html.BeginForm("Action15", "Second"))
  {
    <table>
...
        <tr>
          <td>@Html.LabelFor(m => m.Date1)</td>
          <td>@Html.TextBox("Date1", Model.StrDate1)</td>
          <td>@Html.ValidationMessageFor(m => m.Date1)</td>
        </tr>
      </tbody>
    </table>
    <p>
      <input type="submit" value="Valider" />
    </p>
  }
  <!-- language selection -->
  @using (Html.BeginForm("Lang", "Second"))
  {
    <table>
      <tr>
        <td><a href="javascript:postForm('fr-FR','/Second/Action15')">Français</a></td>
        <td><a href="javascript:postForm('en-US','/Second/Action15')">English</a></td>
      </tr>
    </table>
  }
</body>
</html>

注意:第 16 行,请将 jQuery UI 版本调整为您下载的版本。

  • 第 15 行:引用 jQuery UI 样式表;
  • 第 16 行:引用下载的 jQuery UI 版本;
  • 第 17 行:引用我们刚刚下载的法语日历脚本;
  • 第 34 行:[Html.TextBox] 方法将在此处生成一个 [input] 标签,类型为 [text],ID 为 [Date1],名称为 [Date1];
  • 第 19 行:页面加载完成后,jQuery UI 的 [datepicker] 函数将应用于 id 为 [Date1] 的元素,即第 34 行中的元素。该函数确保当用户将焦点移至 [Date1] 输入框时,会弹出日历供其选择日期。 [datepicker] 函数接受一个指定日历语言的参数。变量 [@Model.Regionale] 必须设置为:
  • 'fr' 表示法语日历,
  • '' 表示英文日历;

前一个视图 [Action15.cshtml] 的模型将是以下 [ViewModel15] 模型:

其代码基于 [ViewModel14] 模型稍作修改。此处仅展示修改部分:


using Exemple_03.Resources;
...
using System.Web;
 
namespace Exemple_03.Models
{
  [Bind(Exclude = "Culture,Regionale,StrDate1,FormatDate")]
  public class ViewModel15 : IValidatableObject
  {
 
...
    [Display(ResourceType = typeof(MyResources), Name = "date1")]
    [RegularExpression(@"\s*\d{2}/\d{2}/\d{4}\s*", ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoIncorrecte")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    public string Regexp1 { get; set; }
 
    [Display(ResourceType = typeof(MyResources), Name = "date2")]
    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
    [DataType(DataType.Date)]
    public DateTime Date1 { get; set; }
 
    // manufacturer
    public ViewModel15()
    {
      // Culture of the moment
      Culture = HttpContext.Current.Session["lang"] as string;
      cultureInfo=new CultureInfo(Culture);
      // Regional calendar JQuery
      Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
      // date format
      FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
    }
 
 
 
    // validation
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // error list
      List<ValidationResult> résultats = new List<ValidationResult>();
      // the same error msg for all
      string errorMessage = MyResources.ResourceManager.GetObject("infoIncorrecte", cultureInfo).ToString();
...
      // Regexp1
      try
      {
        DateTime.ParseExact(Regexp1, FormatDate, cultureInfo);
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }
 
      // return the list of errors
      return résultats;
 
    }
 
    // fields outside the action model
    public string Culture { get; set; }
    public string Regionale { get; set; }
    public string StrDate1 { get; set; }
    public string FormatDate { get; set; }
 
    // local data
    private CultureInfo cultureInfo;
  }
}

与之前的模型 [ViewModel14] 相比,我们增加了四个属性:

  • 第 60 行:视图的区域设置,'fr-FR' 或 'en-US'。该区域设置在第 26 行的构造函数中初始化;
  • 第 61 行:jQuery 日历的区域文化,'fr' 表示法语日历,'' 表示英语日历。该字段由构造函数第 29 行初始化;
  • 第 63 行:来自第 15 行的日期格式:'dd/MM/yyyy' 表示法语日期,'MM/dd/yyyy' 表示英语日期。该字段由构造函数第 31 行初始化;
  • 第 62 行:在 [Date1] 输入字段中显示的字符串。该字段将由操作进行初始化;
  • 第 47 行:日期 [Regexp1] 现根据当前区域设置的格式进行验证。

[Regionale] 和 [FormatDate] 属性的值位于 [MyResources] 资源文件中。法语资源文件 [MyResources] [MyResources.fr-FR] [1] 和英语资源文件 [2] 的更改如下:

我们已经准备就绪。我们在控制器 [SecondController] 中添加一个操作 [Action15]:


      // Action15
      public ViewResult Action15(FormCollection formData)
      {
        // method HTTP
        string method = Request.HttpMethod.ToLower();
        // model
        ViewModel15 modèle = new ViewModel15();
        if (method == "get")
        {
          modèle.StrDate1 = "";
        }
        else
        {
          TryUpdateModel(modèle, formData);
          modèle.StrDate1 = modèle.Date1.ToString(modèle.FormatDate);
        }
        // view display
        return View("Action15", modèle);
}

  • 第 2 行:[Action15] 方法同时处理 [GET] 和 [POST] 请求。在后一种情况下,提交的值将通过 [formData] 参数获取;
  • 第 5 行:获取请求的 HTTP 方法;
  • 第 7 行:创建待显示的视图模板(表单);
  • 第 8–11 行:如果是 [GET] 请求,则将 [Date1] 输入字段初始化为空字符串;
  • 第 12–16 行:如果是 [POST] 请求:
    • 第 14 行:使用提交的值初始化模型;
    • 第 15 行:将 [Date1] 输入字段初始化为一个字符串,该字符串是 [Date1] 的值,并根据当前区域设置进行格式化(法语日期格式为 [dd/MM/yyyy],英语日期格式为 [MM/dd/yyyy]);
  • 第 18 行:使用其模板显示 [Action15.cshtml] 视图。

让我们进行一些测试:

  • 在 [1] 中,当页面为法语时显示法语日历;
  • 在 [2] 中,当页面为英文时显示英文日历;
  • 在 [3] 中,当页面为法语时显示法语格式的日期;
  • 在 [4] 中,当页面为英文时,显示相同日期的英文格式;

6.7. 结论

如我们所见,应用程序国际化是一个复杂的话题……