Skip to content

6. Інтернаціоналізація подань

Тут ми розглянемо проблему інтернаціоналізації переглядів. Це складна проблема, докладний опис якої можна знайти в наступній статті Скотта Гансельмана:

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

Спочатку наведемо його визначення різних термінів, пов’язаних з інтернаціоналізацією подань:

Інтернаціоналізація (i18n)
забезпечення підтримки додатком різних мов та локалей
Локалізація (l10n)
забезпечення підтримки додатком конкретної пари «мова/локаль»
Глобалізація
поєднання Internationalisation та Localisation
Мова
мова, якою розмовляють — позначається кодом ISO (fr: французька, es: іспанська, en: англійська, ...)
Локаль
варіант мови — також позначається кодом ISO (en_GB: британська англійська, en_US: американська англійська, ...)

Розглянемо цю проблему на першому прикладі.

6.1. Локалізація дійсних чисел

У попередній формі введення даних можна помітити аномалію:

 

Для дійсного числа ми ввели [0,3], але воно не було прийняте. Потрібно ввести [0.3]:

 

Отже, очікуваний формат — це англосаксонський, а не французький. Пошукавши в Інтернеті, можна знайти рішення. Ось одне з них.

Дії [GET] та [POST] стають такими:


    // Дія13-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 адаптуйте версію jQuery відповідно до вашої версії Visual Studio.

  • У рядку 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) {
    //Використовуйте плагін глобалізації для аналізу значення        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});

// під час завантаження документа
$(document).ready(function () {
  var culture = 'fr-FR';
  Globalize.culture(culture);
});

У рядку 1 я вказав, де було знайдено цей скрипт. Я не буду намагатися його пояснити, оскільки сам його не розумію. JavaScript іноді буває досить незрозумілим. У рядках 4, 9, 15 використовується об’єкт [Globalize]. Він надається бібліотекою JQuery Globalization, яку можна отримати за допомогою [NuGet]:

  • у [1], керуйте пакетами [NuGet] проекту [Exemple-03];
  • у [2] перегляньте пакети в режимі онлайн;
  • у [3] введіть термін [globalization];
  • у [4] встановіть пакет [Globalize] з проєкту JQuery.

Після встановлення пакета [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));
}

...

// під час завантаження документа
$(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, пов’язаних із документом.

Тепер ми можемо протестувати новий додаток:

 

Тепер можна ввести [0,3] як число з дійсною частиною, чого раніше зробити не можна було. Однак виникає інша аномалія:

 

У наведеному вище прикладі перевірка на стороні клієнта дозволяє ввести [11.2] у англосаксонській нотації. Це значення не приймається на стороні сервера під час перевірки форми:

 

Потрібно ввести [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");

Ці дві інструкції можуть бути в конструкторі кожного контролера додатка. Але можна також винести цей код, спільний для всіх контролерів, у окремий модуль. Ми підемо цим шляхом.

Створюємо два нових контролери:

  
  • [I18NController] буде базовим класом для всіх контролерів, що використовують інтернаціоналізацію;
  • [SecondController] — це приклад контролера, похідного від [I18NController].

Код контролера [I18NController] такий:


using System.Threading;
using System.Web;
using System.Web.Mvc;

namespace Exemples.Controllers
{
  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // отримується контекст поточного запиту
      HttpContext httpContext = HttpContext.Current;
      // перевіряємо запит на наявність параметра [lang]
      // шукаємо його серед параметрів URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // шукаємо його серед відправлених параметрів
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // шукається в сесії користувача
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // перший параметр заголовка HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // культура fr-FR
        langue = "fr-FR";
      }
      // встановлюємо мову в сесії
      httpContext.Session["lang"] = langue;
      // змінюємо локалі потоку            
      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; }

    // перевірка
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // список помилок
      List<ValidationResult> résultats = new List<ValidationResult>();
      // одна й та сама помилка для всіх
      string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();

      // Дата 1
      if (Date1.Date <= DateTime.Now.Date)
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
      }
      // Електронна пошта 1
      try
      {
        new MailAddress(Email1);
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
      }
      // Регулярний вираз 1
      try
      {
        DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }

      // виводимо список помилок
      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].

Щоб створити ключ [infoRequise] та пов’язане з ним значення у файлі [MyResources], слід виконати такі дії:

Вводимо ключ у полі [1], значення — у [2], область дії ресурсу — у [3].

Залишається уточнити ще один момент: простір імен класу [MyResources]. Він визначається у властивостях файлу [MyResources.resx]:

У файлі [1] ми визначаємо простір імен класу [MyResources], який буде створено на основі файлу ресурсів [MyResources.resx]. Повернімося до розглянутого інтернаціоналізованого рядка:


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

Оператор typeof очікує на клас, у даному випадку — клас [MyResources]. Щоб його було знайдено, необхідно імпортувати його простір імен у клас [ViewModel14]:


using Exemple_03.Resources;

Щоб клас [MyResources] став видимим, проект має бути згенерований хоча б один раз після створення файлу ресурсів [MyResources]. Код цього класу можна переглянути у файлі [MyResources.Designer.cs]:

  

При подвійному клацанні на цьому файлі відкривається код класу [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], і це спричиняє помилки, які важко зрозуміти, якщо забути змінити цю область дії.

Чому тепер три файли ресурсів?

  

Ми створили файл [MyResources.resx]. Це кореневий ресурс. Далі ми створюємо стільки файлів ресурсів [MyResources.locale.resx], скільки є локалей (мов), які потрібно обробляти. Тут ми обробляємо французьку мову [fr-FR] та американську англійську [en-US]. Коли поточна локаль не є ані [fr-FR], ані [en-US], використовується кореневий ресурс [MyResources.resx].

Остаточний вміст [MyResources.resx] є таким:

 

Повідомлення будуть французькою мовою, якщо локаль не буде розпізнана. Кінцевий вміст файлу [MyResources.fr-FR.resx] є ідентичним і отримується шляхом простого копіювання файлу.

Кінцевий вміст файлу [MyResources.en-US.resx] також отримується шляхом копіювання файлу, а потім змінюється наступним чином:

 

Повернемося до подання [ViewModel14] та його методу [Validate]:


    // перевірка
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // список помилок
      List<ValidationResult> résultats = new List<ValidationResult>();
      // однакове повідомлення про помилку для всіх
      string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();

      // Дата 1
      if (Date1.Date <= DateTime.Now.Date)
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
      }
...
      // виводиться список помилок
      return résultats;
}

У рядку 7 показано, як отримати повідомлення з файлу ресурсів [MyResources]. Тут ми хочемо отримати повідомлення, пов’язане з ключем [infoIncorrecte], у поточній культурі:

  • MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : отримує об’єкт, пов’язаний із ключем [infoIncorrecte] у файлі ресурсів [MyResources.en-US.resx];
  • ми бачили, що контролер [I18NController] встановлює поточну культуру в сесії, пов’язаній із ключем [lang]. Отже, поточну культуру можна отримати за допомогою System.Web.HttpContext.Current.Session["lang"] as string;
  • ресурс отримується з типом [object]. Щоб отримати повідомлення про помилку, до нього застосовується метод [ToString].

6.4. Інтернаціоналізація подання [Action14Get.cshtml]

Ми модифікуємо вигляд форми наступним чином:

  

@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>
<!-- вибір мови -->
@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).

У міжнародних повідомленнях слід також врахувати ті, що відображаються фреймворком перевірки на стороні клієнта. Для цього слід використовувати бібліотеки JQuery у рядках 17–19. Ми використовуємо файли JQuery для обох культур, які ми підтримуємо: [fr-FR] та [en-US]. Крім того, можливо, ви пам’ятаєте, що у поданні [Action13Get] використовувався такий скрипт JavaScript [myscripts.js]:


// під час завантаження документа
$(document).ready(function () {
  var culture = 'fr-FR';
  Globalize.culture(culture);
});

Тепер значення «culture» більше не є фіксованим ([fr-FR]), а варіюється. Тому ці рядки тепер генеруються самим поданням [Action14Get] у рядках 21–26. Ці шість рядків будуть включені до сторінки HTML, що надсилається клієнту.

  • рядок 23: змінна JavaScript [culture] ініціалізується з поточним культурним налаштуванням потоку запиту, що обробляється. Можливо, ви пам’ятаєте, що вона була ініціалізована конструктором класу [I18NController]:

      // встановлення мови для сеансу
      httpContext.Session["lang"] = langue;
      // зміна культурного контексту потоку            
      Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

Якщо поточна культура — [en-US], то скрипт JavaScript, вбудований у сторінку HTML, набуває такого вигляду:


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

Як уже зазначалося, функція [$(document).ready] виконується після завершення завантаження сторінки браузером. Її виконання призведе до встановлення мови фреймворку валідації на стороні клієнта. З мовою [en-US] повідомлення про помилки фреймворку будуть англійською мовою і братимуться з файлу ресурсів [MyResources.en-US.resx]. Ми розглянемо, як саме.

Тепер розглянемо рядки 57–65:


<!-- вибір мови -->
@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: форма надсилається до дії [Lang] контролера [Second]. Наразі ми не бачимо жодного значення, яке могло б бути надіслане;
  • рядки 6 і 7: клік на посиланнях викликає виконання функції JavaScript [postForm]. Де знаходиться ця функція? У скрипті [myscripts2.js], на який є посилання в рядку 20 подання:

Її вміст такий:


function postForm(lang, url) {
  // отримано другу форму документа
  var form = document.forms[1];
  // додаємо до нього прихований атрибут lang
  var hiddenField = document.createElement("input");
  hiddenField.setAttribute("type", "hidden");
  hiddenField.setAttribute("name", "lang");
  hiddenField.setAttribute("value", lang);
  // додавання прихованого поля до форми
  form.appendChild(hiddenField);
  // додаємо до нього прихований атрибут url
  var hiddenField = document.createElement("input");
  hiddenField.setAttribute("type", "hidden");
  hiddenField.setAttribute("name", "url");
  hiddenField.setAttribute("value", url);
  // додаємо приховане поле до форми
  form.appendChild(hiddenField);
  // відправка
  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) {
    //Використовуйте плагін «Глобалізація» для аналізу значення        
    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]. Тож нам потрібно визначити дію [Lang] у контролері [SecondController]. Вона буде такою:


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);
      }

      // мова
      [HttpPost]
      public RedirectResult Lang(string url)
      {
        // клієнта перенаправляють на URL
        return new RedirectResult(url);
      }

    }
  • рядок 18: дія реагує лише на [POST];
  • рядок 19: вона отримує лише параметр із назвою [url];
  • рядок 22: вона відповідає клієнту, щоб той перенаправився на цей URL.

Але що сталося з параметром із назвою [lang]? Тепер слід згадати, що контролер [SecondController] походить від класу [I18NController] (рядок 1 нижче). Саме цей контролер керує параметром [lang]:


  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // отримується контекст поточного запиту
      HttpContext httpContext = System.Web.HttpContext.Current;
      // перевіряється запит на наявність параметра [lang]
      // його шукають серед параметрів URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // шукаємо його серед відправлених параметрів
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // шукається в сесії користувача
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // перший параметр заголовка HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // культура fr-FR
        langue = "fr-FR";
      }
      // встановлюємо мову в сесії
      httpContext.Session["lang"] = langue;
      // змінюємо локалі потоку            
      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()
    {
      // отримано контекст поточного запиту
      HttpContext httpContext = System.Web.HttpContext.Current;
      // перевіряємо запит на наявність параметра [lang]
      // шукаємо його серед параметрів URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // шукаємо його серед відправлених параметрів
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // шукається в сесії користувача
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // перший параметр заголовка HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // культура fr-FR
        langue = "fr-FR";
      }
      // встановлюємо мову в сесії
      httpContext.Session["lang"] = langue;
      // змінюємо локалі потоку            
      Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}

Цього разу параметр [lang] буде знайдено в сесії за рядком 18. Припустимо, що його значенням є [en-US]. Отже, ця культура стає культурою потоку виконання запиту (рядки 33–34). Повернемося до дії [Action14Get]:


      // Дія14-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] та її календар:

  

Можна помітити, що календар є французьким, тоді як культура сторінки — [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] дата продовжує запитуватися у французькому форматі дд/мм/рррр (20/11/2013), тоді як американський формат — мм/дд/рррр (10/21/2013). Ми спробуємо вирішити ці дві проблеми за допомогою нового виду та нової моделі виду.

JQuery UI — це проект, похідний від проекту JQuery, який надає компоненти для форм, зокрема календар. Цей календар можна інтернаціоналізувати. Саме це ми й продемонструємо.

Для початку додамо [JQuery UI] до нашого проєкту.

Після встановлення JQuery та UI у проєкті з’являються нові елементи:

  • у [1] — бібліотека [JQuery UI] у звичайній та мінімізованій версіях;
  • у [2] — таблиця стилів [JQuery UI];

Календар JQuery UI за замовчуванням на англійській мові. Для його інтернаціоналізації потрібно додати скрипти, які можна знайти у файлі URL [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>
  }
  <!-- вибір мови -->
  @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], з ідентифікатором [Date1] та іменем [Date1];
  • рядок 19: коли завантаження сторінки завершиться, функція JQuery UI [datepicker] буде застосована до елемента з ідентифікатором [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; }

    // конструктор
    public ViewModel15()
    {
      // Поточна локаль
      Culture = HttpContext.Current.Session["lang"] as string;
      cultureInfo=new CultureInfo(Culture);
      // Регіональні налаштування календаря JQuery
      Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
      // формат дати
      FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
    }



    // перевірка
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // Список помилок
      List<ValidationResult> résultats = new List<ValidationResult>();
      // однакове повідомлення про помилку для всіх
      string errorMessage = MyResources.ResourceManager.GetObject("infoIncorrecte", cultureInfo).ToString();
...
      // Регулярний вираз 1
      try
      {
        DateTime.ParseExact(Regexp1, FormatDate, cultureInfo);
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }

      // виводимо список помилок
      return résultats;

    }

    // поля, що не відповідають шаблону дії
    public string Culture { get; set; }
    public string Regionale { get; set; }
    public string StrDate1 { get; set; }
    public string FormatDate { get; set; }

    // локальні дані
    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] змінюються наступним чином:

Ми майже готові. Додаємо дію [Action15] до контролера [SecondController]:


      // Дія15
      public ViewResult Action15(FormCollection formData)
      {
        // метод HTTP
        string method = Request.HttpMethod.ToLower();
        // шаблон
        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);
        }
        // відображення
        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. Conclusion

Як бачимо, тема інтернаціоналізації додатка є досить складною...