6. Internacjonalizacja widoków
W tym miejscu omówimy problem internacjonalizacji widoków. Jest to złożona kwestia, której dobry opis można znaleźć w poniższym artykule Scotta Hanselmana:
[http://www.hanselman.com/blog/GlobalizationInternationalizationAndLocalizationInASPNETMVC3JavaScriptAndJQueryPart1.aspx]
Na początek przytoczymy jego definicje różnych terminów związanych z internacjonalizacją widoków:
Internacjonalizacja (i18n) | sprawienie, by aplikacja obsługiwała różne języki i ustawienia regionalne |
Lokalizacja (l10n) | zapewnienie, że aplikacja obsługuje konkretną parę językowo-lokalizacyjną |
Globalizacja | połączenie Internationalisation i Localisation |
Język | język używany – oznaczony kodem ISO (fr: francuski, es: hiszpański, en: angielski, ...) |
Ustawienia regionalne | wariant języka – oznaczony również kodem ISO (en_GB: angielski brytyjski, en_US: angielski amerykański, ...) |
Rozpocznijmy od pierwszego przykładu.
6.1. Lokalizacja liczb rzeczywistych
W poprzednim formularzu wprowadzania danych można zauważyć nieprawidłowość:
![]() |
W przypadku liczby rzeczywistej wpisaliśmy [0,3], ale nie zostało to zaakceptowane. Należy wpisać [0.3]:
![]() |
Oczekiwany format to zatem format anglosaskie, a nie francuski. Szukając w Internecie, można znaleźć rozwiązania. Oto jedno z nich.
Akcje [GET] i [POST] zmieniają się na następujące:
// Akcja13-GET
[HttpGet]
public ViewResult Action13Get()
{
return View("Action13Get", new ViewModel11());
}
// Action13-POST
[HttpPost]
public ViewResult Action13Post(ViewModel11 modèle)
{
return View("Action13Get", modèle);
}
Widok [Action13Get.cshtml] jest identyczny z widokiem [Action12Get.cshtml], z wyjątkiem skryptów 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>
Uwaga: w wierszu 5 należy dostosować wersję pliku jQuery do posiadanej wersji programu Visual Studio.
- W wierszu 9 dodaliśmy skrypt [myscripts.js] . Oto jego treść:
![]() |
// 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) {
//Użyj wtyczki Globalization do analizy wartości
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
// podczas ładowania dokumentu
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
W wierszu 1 wskazałem, gdzie znaleziono ten skrypt. Nie będę próbował go wyjaśniać, ponieważ sam go nie rozumiem. Javascript bywa czasem niezrozumiały. W wierszach 4, 9 i 15 wykorzystywany jest obiekt [Globalize]. Jest on dostarczany przez bibliotekę JQuery Globalization, którą można uzyskać za pomocą [NuGet]:
![]() |
![]() |
- w [1] należy zarządzać pakietami [NuGet] z projektu [Exemple-03];
- w [2] przejrzyj pakiety online;
- w [3] wpisz hasło [globalization];
- w [4] zainstaluj pakiet [Globalize] z projektu JQuery.
Po zainstalowaniu pakietu [Globalize] w folderze [Scripts] pojawi się nowa gałąź:
![]() |
- w [1] utworzono folder [globalize] zawierający główny skrypt [globalize.js];
- w [2] główny skrypt [globalize.js] został uzupełniony o skrypty specyficzne dla danego języka i ustawień regionalnych;
- w [3] skrypty specyficzne dla języka francuskiego wraz z ustawieniami regionalnymi (wariantami) belgijskimi (BE), kanadyjskie (CA), francuskie (FR), szwajcarskie (CH), luksemburskie (LU), monakijskie (MC).
Skrypt [globalize.js] oraz skrypt dotyczący naszej kultury [globalize.culture.fr-FR.js] muszą znaleźć się na liście skryptów zawartych na naszej stronie [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>
- wiersz 5: skrypt [globalize];
- wiersz 6: skrypt [globalize.culture.fr-FR.js];
- wiersz 7: skrypt [myscripts.js];
Wróćmy do tego ostatniego skryptu:
// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
return this.optional(element) ||
!isNaN(Globalize.parseFloat(value));
}
...
// podczas ładowania dokumentu
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Wiersze 10–13 ustawiają kulturę po stronie klienta na [fr-FR]:
- wiersz 10: funkcja JQuery [ready] jest wykonywana, gdy dokument, w którym znajduje się skrypt, został w całości załadowany przez przeglądarkę;
- wiersze 11–12: ustawia się kulturę po stronie klienta na [fr-FR]. W tym celu plik [globalize.culture.fr-FR.js] musi zostać dołączony do listy skryptów JavaScript powiązanych z dokumentem.
Teraz możemy przetestować nową aplikację:
![]() |
Możemy teraz wpisać [0,3] jako liczbę rzeczywistą, czego wcześniej nie można było zrobić. Pojawia się jednak kolejna anomalia:
W powyższym przykładzie walidacja po stronie klienta pozwala na wpisanie [11.2] w notacji anglosaskiej. Wartość ta nie jest akceptowana po stronie serwera podczas walidacji formularza:
Należy wpisać [11,2], a wtedy działa to zarówno po stronie klienta, jak i serwera. Po stronie klienta notacja anglosaska nie powinna być akceptowana. To powinno być możliwe...
Przejdźmy teraz do kwestii internacjonalizacji widoków. Będziemy kontynuować na przykładzie poprzedniego formularza, udostępniając go w dwóch językach: francuskim i angielskim.
6.2. Zarządzanie kulturą
Język widoków jest kontrolowany przez obiekt [Thread.CurrentThread.CurrentUICulture]. Aby wyświetlić strony w kulturze [fr-FR], należy wpisać:
Lokalizacja (daty, liczby, waluty, godziny itp.) jest kontrolowana przez obiekt [Thread.CurrentThread.CurrentCulture]. Podobnie jak w poprzednim przykładzie, należy wpisać:
Te dwie instrukcje mogłyby znajdować się w konstruktorze każdego kontrolera aplikacji. Można jednak również wyodrębnić ten wspólny dla wszystkich kontrolerów kod. Pójdziemy tą drogą.
Tworzymy dwa nowe kontrolery:
![]() |
- [I18NController] będzie klasą nadrzędną dla wszystkich kontrolerów wykorzystujących internacjonalizację;
- [SecondController] to przykładowy kontroler wywodzący się z klasy [I18NController].
Kod kontrolera [I18NController] wygląda następująco:
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace Exemples.Controllers
{
public abstract class I18NController : Controller
{
public I18NController()
{
// pobierany jest kontekst bieżącego żądania
HttpContext httpContext = HttpContext.Current;
// sprawdzamy żądanie pod kątem obecności parametru [lang]
// szukamy go wśród parametrów URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// szuka się go wśród przesłanych parametrów
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// szuka się go w sesji użytkownika
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// pierwszy parametr nagłówka HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// kultura fr-FR
langue = "fr-FR";
}
// ustawiamy język w sesji
httpContext.Session["lang"] = langue;
// zmiana ustawień regionalnych wątku
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
}
}
- wiersz 7: [I18NController] wywodzi się z klasy [Controller];
- wiersz 7: klasa jest zadeklarowana jako [abstract], aby uniemożliwić jej bezpośrednią instancjonację: może być używana wyłącznie jako klasa pochodna;
- wiersz 9: konstruktor klasy – zostanie wykonany przy każdym instancjonowaniu kontrolera pochodnego od klasy [I18NController];
- wiersz 12: pobierany jest kontekst żądania HTTP przetwarzanego obecnie przez kontroler;
- wiersz 15: zakłada się, że język jest określony przez parametr [lang], który można znaleźć w różnych miejscach. Szuka się w następującej kolejności:
- wiersz 15: w parametrach URL i [?lang=en-US],
- wiersz 19: w przesłanych parametrach [lang=de],
- wiersz 24: w sesji użytkownika,
- wiersz 29: w preferencjach językowych przesłanych przez klienta HTTP,
- wiersz 26: jeśli nic nie znaleziono, ustawia się kulturę na [fr-FR];
- wiersz 37: zapisujemy ustawienie kulturowe w sesji. To właśnie tam zostanie ono odzyskane podczas kolejnych zapytań. Użytkownik będzie mógł je zmienić, wprowadzając je w parametrach polecenia GET lub POST;
- wiersze 39–40: ustala się ustawienia regionalne widoku, który zostanie wyświetlony po przetworzeniu bieżącego zapytania.
Kontroler [SecondController] będzie wyglądał następująco:
using Exemple_03.Models;
using Exemples.Controllers;
using System.Web.Mvc;
namespace Exemple_03.Controllers
{
public class SecondController : I18NController
{
// Akcja14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Akcja14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
}
}
- wiersz 7: [SecondController] wywodzi się z [I18NController]. W ten sposób mamy pewność, że kultura widoku, który ma zostać wyświetlony, została zainicjowana;
- wiersz 13: wykorzystujemy szablon widoku [ViewModel14], który zamierzamy przedstawić;
- wiersze 13 i 20: widok [Action14Get.cshtml] zapewnia wyświetlenie formularza.
6.3. Internacjonalizacja szablonu widoku [ViewModel14]
Szablon widoku [ViewModel14] wygląda następująco:
![]() |
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; }
// walidacja
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// lista błędów
List<ValidationResult> résultats = new List<ValidationResult>();
// ten sam komunikat o błędzie dla wszystkich
string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
// Data 1
if (Date1.Date <= DateTime.Now.Date)
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
}
// E-mail 1
try
{
new MailAddress(Email1);
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
}
// Wyrażenie regularne 1
try
{
DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
}
// wyświetla się lista błędów
return résultats;
}
}
}
Ten model jest zinternacjonalizowaną wersją poprzedniego modelu [ViewModel11]. Opiszemy mechanizm internacjonalizacji dla pierwszego atrybutu pierwszej właściwości. Pozostałe atrybuty działają według tego samego mechanizmu.
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }
W poprzednim modelu [ViewModel11] linie te wyglądały następująco:
[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }
W wersji zinternacjonalizowanej, w wierszu 1, teksty do wyświetlenia umieszczono w pliku zasobów. W tym przypadku plik ten nosi nazwę [MyResources.resx] (typeof) i został umieszczony w katalogu głównym projektu. Nazywa się go plikiem zasobów.
![]() |
Utworzyliśmy tutaj trzy pliki zasobów:
- [MyResources]: zasób domyślny, gdy nie ma zasobu dla bieżącej lokalizacji;
- [MyResources.fr-FR]: zasób dla ustawień regionalnych [fr-FR];
- [MyResources.en-US]: zasób dla ustawień regionalnych [en-US];
Aby utworzyć plik zasobów, należy postępować w następujący sposób: [1, 2, 3]:
![]() |
![]() |
Spowoduje to utworzenie pliku zasobów o nazwie [MyResources2.resx]. Po dwukrotnym kliknięciu na ten plik wyświetli się następująca strona:
![]() |
Plik zasobów to słownik zawierający klucze i wartości powiązane z tymi kluczami. Klucz wpisujemy w pliku [1], wartość w pliku [2], a zakres zasobu w pliku [3]. Aby zasoby te były czytelne, muszą mieć zakres [Public]. Wróćmy do wiersza:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
- [ErrorMessageResourceType]: oznacza plik zasobów. Parametr [typeof] to nazwa pliku. Jest on przekształcany w klasę podczas kompilacji, a jego plik binarny jest dołączany do zestawu plików projektu. Ostatecznie więc [MyResources] to nazwa klasy zasobów;
- [ErrorMessageResourceName = "infoRequise"]: oznacza klucz w pliku zasobów. Ostatecznie wiersz ten oznacza, że komunikat o błędzie, który ma zostać wyświetlony, to wartość z pliku [MyResources] powiązana z kluczem [infoRequise].
Aby utworzyć klucz [infoRequise] i powiązaną z nim wartość w pliku [MyResources], należy postępować w następujący sposób:
![]() |
Wprowadza się klucz w polu [1], wartość w polu [2], a zakres zasobu w polu [3].
Pozostaje jeszcze jedna kwestia do wyjaśnienia: przestrzeń nazw klasy [MyResources]. Jest ona zdefiniowana we właściwościach pliku [MyResources.resx]:
![]() |
W pliku [1] definiujemy przestrzeń nazw klasy [MyResources], która zostanie utworzona na podstawie pliku zasobów [MyResources.resx]. Wróćmy do analizowanego wiersza z treścią zinternacjonalizowaną:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
Operator typeof oczekuje klasy, w tym przypadku klasy [MyResources]. Aby ta klasa została znaleziona, należy zaimportować jej przestrzeń nazw do klasy [ViewModel14]:
using Exemple_03.Resources;
Aby klasa [MyResources] była widoczna, projekt musi zostać wygenerowany co najmniej raz od momentu utworzenia pliku zasobów [MyResources]. Kod tej klasy jest widoczny w pliku [MyResources.Designer.cs]:
Po dwukrotnym kliknięciu tego pliku uzyskuje się dostęp do kodu klasy [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);
}
}
}
}
- wiersz 1: przestrzeń nazw klasy;
- wiersz 11: klucz [infoRequise] stał się statyczną właściwością klasy [MyResources]. Dostęp do niego uzyskuje się za pomocą notacji [MyResources.infoRequise]. Ponadto należy zauważyć, że ta właściwość ma zakres [public]. Bez tego nie byłaby dostępna. Warto o tym pamiętać, ponieważ niestety domyślnym zakresem jest [internal], a zapomnienie o zmianie tego zakresu powoduje trudne do zrozumienia błędy.
Dlaczego teraz są trzy pliki zasobów?
Utworzyliśmy plik [MyResources.resx]. Jest to zasób główny. Następnie tworzymy tyle plików zasobów [MyResources.locale.resx], ile jest lokalizacji (języków) do obsługi. W tym przypadku obsługujemy język francuski [fr-FR] oraz amerykański angielski [en-US]. Gdy bieżąca lokalizacja nie jest ani [fr-FR], ani [en-US], wykorzystywany jest zasób główny [MyResources.resx].
Ostateczna zawartość pliku [MyResources.resx] jest następująca:
![]() |
Komunikaty będą wyświetlane w języku francuskim, jeśli ustawienia regionalne nie zostaną rozpoznane. Ostateczna zawartość pliku [MyResources.fr-FR.resx] jest identyczna i uzyskano ją poprzez zwykłe skopiowanie pliku.
Ostateczna zawartość pliku [MyResources.en-US.resx] jest również uzyskiwana poprzez skopiowanie pliku, a następnie modyfikowana w następujący sposób:
![]() |
Wróćmy do widoku [ViewModel14] i jego metody [Validate]:
// weryfikacja
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// lista błędów
List<ValidationResult> résultats = new List<ValidationResult>();
// ten sam komunikat o błędzie dla wszystkich
string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
// Data 1
if (Date1.Date <= DateTime.Now.Date)
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
}
...
// wyświetla listę błędów
return résultats;
}
Wiersz 7 pokazuje, jak pobrać komunikat z pliku zasobów [MyResources]. W tym przypadku chcemy pobrać komunikat powiązany z kluczem [infoIncorrecte] w aktualnej kulturze:
- MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : pobiera obiekt powiązany z kluczem [infoIncorrecte] z pliku zasobów [MyResources.en-US.resx];
- widzieliśmy, że kontroler [I18NController] ustawia bieżącą kulturę w sesji powiązanej z kluczem [lang]. Bieżącą kulturę można zatem pobrać za pomocą System.Web.HttpContext.Current.Session["lang"] as string;
- zasób jest pobierany z typem [object]. Aby uzyskać komunikat o błędzie, stosuje się do niego metodę [ToString].
6.4. Internacjonalizacja widoku [Action14Get.cshtml]
Modyfikujemy widok formularza w następujący sposób:
![]() |
@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>
<!-- wybór języka -->
@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>
}
Uwaga: w wierszu 14 należy dostosować wersję jQuery do posiadanej wersji programu Visual Studio.
Zacznijmy od najprostszych linii, czyli 36–38. Wykorzystują one statyczne właściwości klasy [MyResources], którą właśnie opisaliśmy. Aby uzyskać dostęp do klasy [MyResources], należy zaimportować jej przestrzeń nazw (linia 2).
W przypadku komunikatów zinternacjonalizowanych należy uwzględnić również te, które są wyświetlane przez framework walidacji po stronie klienta. W tym celu należy skorzystać z bibliotek JQuery w wierszach 17–19. Pliki JQuery wykorzystujemy dla obu obsługiwanych przez nas kultur: [fr-FR] i [en-US]. Ponadto być może pamiętamy, że widok [Action13Get] wykorzystywał następujący skrypt JavaScript [myscripts.js]:
// podczas ładowania dokumentu
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Obecnie wartość nie jest już stała i wynosi [fr-FR], lecz ulega zmianom. Dlatego te wiersze są teraz generowane przez sam widok [Action14Get] w wierszach 21–26. Te sześć wierszy zostanie dołączonych do strony HTML wysyłanej do klienta.
- wiersz 23: zmienna JavaScript [culture] jest inicjowana z bieżącą kulturą wątku aktualnie przetwarzanego żądania. Być może pamiętamy, że została ona zainicjowana przez konstruktor klasy [I18NController]:
// ustawianie języka w sesji
httpContext.Session["lang"] = langue;
// zmiana ustawień kulturowych wątku
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
Jeśli bieżącą kulturą jest [en-US], skrypt JavaScript osadzony na stronie HTML przyjmuje następującą postać:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Jak już wspomniano, funkcja [$(document).ready] jest wykonywana po zakończeniu ładowania strony przez przeglądarkę. Jej wykonanie spowoduje ustalenie ustawień regionalnych frameworka walidacji po stronie klienta. Przy ustawieniu kultury [en-US] komunikaty o błędach frameworka będą wyświetlane w języku angielskim i będą pochodzić z pliku zasobów [MyResources.en-US.resx]. Zobaczymy, w jaki sposób.
Przyjrzyjmy się teraz wierszom 57–65:
<!-- wybór języka -->
@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>
}
Mamy tu drugi formularz, pierwszy znajduje się w wierszach 31–53. Formularz ten wyświetla na dole strony następujące linki:
![]() |
- wiersz 2: formularz jest wysyłany do akcji [Lang] kontrolera [Second]. Na razie nie widać żadnej wartości, która mogłaby zostać przesłana;
- wiersze 6 i 7: kliknięcie na linki powoduje wykonanie funkcji JavaScript [postForm]. Gdzie znajduje się ta funkcja? W skrypcie [myscripts2.js], do którego odwołuje się wiersz 20 widoku:
![]() |
Jej treść jest następująca:
function postForm(lang, url) {
// pobieramy drugi formularz z dokumentu
var form = document.forms[1];
// dodajemy do niego ukryty atrybut „lang”
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "lang");
hiddenField.setAttribute("value", lang);
// dodawanie ukrytego pola do formularza
form.appendChild(hiddenField);
// dodajemy do niego ukryty atrybut „url”
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "url");
hiddenField.setAttribute("value", url);
// dodajemy ukryte pole do formularza
form.appendChild(hiddenField);
// przesłanie
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) {
//Użyj wtyczki Globalization do analizy wartości
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
Wiersze 22–40 to te, które już występowały w skrypcie [myscripts.js] wykorzystanym w poprzednim przykładzie. Nie będziemy do nich wracać. Funkcja [postForm], uruchamiana po kliknięciu linków języków, znajduje się w wierszach 1–20:
- wiersz 1: funkcja przyjmuje dwa parametry: [lang], czyli kulturę wybraną przez użytkownika, oraz [url], czyli adres URL, pod który ma zostać przekierowana przeglądarka klienta po dokonaniu zmiany kultury. Oba te parametry są określane podczas wywołania:
<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>
- wiersz 3: pobieramy odwołanie do drugiego formularza w dokumencie;
- wiersze 5–8: programowo tworzy się tag
gdzie [xx-XX] to wartość parametru [lang] tej funkcji;
- wiersz 10: nadal programowo dodajemy ten tag do drugiego formularza. Ostatecznie wszystko przebiega tak, jakby ten tag był obecny w drugim formularzu od samego początku. Jego wartość zostanie zatem przesłana. Tego właśnie chcieliśmy;
- wiersze 11–17: powtarzamy ten sam mechanizm dla tagu
gdzie [url] jest wartością parametru [url] funkcji;
- wiersz 19: drugi formularz został właśnie wysłany. Do jakiego URL?
Należy powrócić do kodu drugiego formularza na stronie [Action14Get.cshtml]:
@using (Html.BeginForm("Lang", "Second"))
{
...
}
Formularz jest zatem wysyłany do URL [/Second/Lang]. Musimy zatem zdefiniować akcję [Lang] w kontrolerze [SecondController]. Będzie ona wyglądać następująco:
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);
}
// język
[HttpPost]
public RedirectResult Lang(string url)
{
// klient jest przekierowywany do adresu URL
return new RedirectResult(url);
}
}
- wiersz 18: akcja odpowiada wyłącznie na [POST];
- wiersz 19: pobiera wyłącznie parametr o nazwie [url];
- wiersz 22: odpowiada klientowi, aby przekierował się do tego URL.
Ale co stało się z parametrem o nazwie [lang]? Należy teraz pamiętać, że kontroler [SecondController] wywodzi się z klasy [I18NController] (wiersz 1 poniżej). To właśnie ten kontroler zarządza parametrem [lang]:
public abstract class I18NController : Controller
{
public I18NController()
{
// pobieramy kontekst bieżącego żądania
HttpContext httpContext = System.Web.HttpContext.Current;
// sprawdzamy żądanie pod kątem obecności parametru [lang]
// szuka się go wśród parametrów URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// szuka się go wśród przesłanych parametrów
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// szuka się go w sesji użytkownika
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// pierwszy parametr nagłówka HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// kultura fr-FR
langue = "fr-FR";
}
// ustawia się język w sesji
httpContext.Session["lang"] = langue;
// zmiana ustawień regionalnych wątku
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
W analizowanym przykładzie wysyłany jest parametr [lang]. Zostanie on zatem znaleziony w wierszu 13, wprowadzony do sesji w wierszu 31 i wykorzystany do aktualizacji kultury bieżącego wątku w wierszach 33–34.
Co stanie się dalej? Wróćmy do linków:
<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>
Przekierowanie URL to [/Second/Action14Get]. Wykonana zostaje zatem akcja [Action14Get]:
public class SecondController : I18NController
{
// Akcja14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
...
}
Wcześniej uruchamiany jest konstruktor klasy [I18NController]:
public abstract class I18NController : Controller
{
public I18NController()
{
// pobieramy kontekst bieżącego żądania
HttpContext httpContext = System.Web.HttpContext.Current;
// sprawdzamy żądanie pod kątem obecności parametru [lang]
// szukamy go wśród parametrów URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// szuka się go wśród przesłanych parametrów
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// szuka się go w sesji użytkownika
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// pierwszy parametr nagłówka HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// kultura fr-FR
langue = "fr-FR";
}
// ustawia się język w sesji
httpContext.Session["lang"] = langue;
// zmiana ustawień regionalnych wątku
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
Tym razem parametr [lang] zostanie znaleziony w sesji w wierszu 18. Załóżmy, że jego wartością jest [en-US]. Ta kultura staje się zatem kulturą wątku wykonującego zapytanie (wiersze 33–34). Wróćmy do akcji [Action14Get]:
// Akcja14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
W wierszu 5 zostanie utworzona instancja modelu widoku [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; }
....
Ponieważ kulturą bieżącego wątku jest [en-US], wykorzystany zostanie plik [MyResources.en-US.resx]. Komunikaty o błędach będą zatem w języku angielskim.
Po utworzeniu instancji szablonu [ViewModel14] wyświetlany jest widok [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>
Ponieważ kulturą bieżącego wątku jest [en-US], skrypt osadzony na stronie w wierszach 15–20 ma następującą postać:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Zapewnia to, że framework walidacji będzie działał z formatami amerykańskimi (data, waluta, liczby itp.). Z tego samego powodu komunikaty w wierszach 30–32 będą pobierane z pliku zasobów [MyResources.en-US.resx] i będą zatem w języku angielskim.
6.5. Przykłady działania
Oto kilka przykładów działania:
![]() |
- w pliku [1] znajduje się formularz w języku francuskim, a w pliku [2] – formularz w języku angielskim.
![]() |
- W [3], po stronie klienta, komunikaty o błędach są teraz wyświetlane w języku angielskim.
Jeśli przyjrzymy się kodowi źródłowemu strony, widać, że te komunikaty o błędach zostały wbudowane w stronę, a więc wygenerowane przez widok ASP.NET [Action14Get] i jego szablon [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. Internacjonalizacja dat
Internacjonalizacja to złożony problem. Przyjrzyjmy się zatem właściwości [Date1] i jej kalendarzowi:
![]() |
Można zauważyć, że kalendarz jest kalendarzem francuskim, podczas gdy kultura strony to [en-US]. W HTML5 istnieje atrybut [lang], który pozwala ustawić język strony lub jej elementu. W widoku [Action14Get.cshtml] można zatem wpisać następujący kod:
@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>
...
- wiersz 6: pobieramy ustawienia regionalne z sesji;
- wiersz 11: ustawia się atrybut [lang] strony na tę wartość.
Testy wykazują, że kalendarz pozostaje w języku francuskim, nawet gdy strona jest wyświetlana w języku angielskim. Występuje również problem z drugą datą w formularzu:
![]() |
W [1] data jest nadal wymagana w formacie francuskim dd/mm/rrrr (20/11/2013), podczas gdy format amerykański to mm/dd/rrrr (10/21/2013). Spróbujemy rozwiązać te dwa problemy za pomocą nowego widoku i nowego szablonu widoku.
JQuery UI to projekt wywodzący się z projektu JQuery, oferujący komponenty do formularzy, w tym kalendarz. Kalendarz ten można dostosować do różnych krajów. Właśnie to zamierzamy pokazać.
Na początek dodajmy [JQuery UI] do naszego projektu.
![]() |
![]() |
Po zainstalowaniu JQuery i UI w projekcie pojawiają się nowe elementy:
![]() |
- w [1] – biblioteka [JQuery UI] w wersji normalnej i zminimalizowanej;
- w [2] – arkusz stylów [JQuery UI];
Kalendarz JQuery UI jest domyślnie w języku angielskim. Aby go zinternacjonalizować, należy dodać skrypty, które można znaleźć w pliku URL [https://github.com/jquery/jquery-ui/tree/master/ui/i18n]:
![]() |
Aby uzyskać kalendarz JQuery UI w języku francuskim, należy skopiować zawartość powyższego pliku [jquery.ui.datepicker-fr.js] do folderu [Scripts] projektu.
![]() |
Kod nowego widoku [Action15.cshtml] uzyskano poprzez skopiowanie poprzedniego widoku [Action14.cshtml], a następnie jego modyfikację. Przedstawiamy jedynie zmiany:
![]() |
@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>
}
<!-- wybór języka -->
@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>
Uwaga: w wierszu 16 należy dostosować wersję jQuery-ui do tej, którą pobrałeś.
- wiersz 15: odwołujemy się do arkusza stylów z pliku JQuery UI;
- wiersz 16: odwołujemy się do pobranej wersji JQuery UI;
- wiersz 17: odwołujemy się do skryptu francuskiego kalendarza, który właśnie pobraliśmy;
- wiersz 34: metoda [Html.TextBox] wygeneruje tutaj tag o nazwie [input] typu [text], o identyfikatorze [Date1] i nazwie [Date1];
- wiersz 19: po zakończeniu ładowania strony funkcja JQuery UI [datepicker] zostanie zastosowana do elementu o identyfikatorze [Date1], czyli element z wiersza 34. Funkcja ta sprawia, że gdy użytkownik ustawi fokus na polu wprowadzania danych o identyfikatorze [Date1], pojawi się kalendarz umożliwiający wprowadzenie daty. Funkcja [datepicker] przyjmuje parametr określający język kalendarza. Zmienna [@Model.Regionale] musi mieć wartość:
- „fr” dla kalendarza francuskiego,
- '' dla kalendarza angielskiego;
Szablon poprzedniego widoku [Action15.cshtml] będzie następującym szablonem [ViewModel15]:
![]() |
Jego kod jest kodem nieznacznie zmodyfikowanego szablonu [ViewModel14]. Przedstawiamy jedynie zmiany:
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; }
// konstruktor
public ViewModel15()
{
// Aktualna kultura
Culture = HttpContext.Current.Session["lang"] as string;
cultureInfo=new CultureInfo(Culture);
// Region kalendarza JQuery
Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
// format daty
FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
}
// walidacja
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Lista błędów
List<ValidationResult> résultats = new List<ValidationResult>();
// ten sam komunikat o błędzie dla wszystkich
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" }));
}
// wyświetlanie listy błędów
return résultats;
}
// pola poza szablonem akcji
public string Culture { get; set; }
public string Regionale { get; set; }
public string StrDate1 { get; set; }
public string FormatDate { get; set; }
// dane lokalne
private CultureInfo cultureInfo;
}
}
W porównaniu z poprzednim modelem [ViewModel14] mamy cztery dodatkowe właściwości:
- wiersz 60: ustawienia regionalne widoku, „fr-FR” lub „en-US”. Ustawienia te są inicjowane w konstruktorze w wierszu 26;
- wiersz 61: ustawienia regionalne kalendarza JQuery, „fr” dla kalendarza francuskiego, „” dla kalendarza angielskiego. Pole to jest inicjowane w wierszu 29 konstruktora;
- wiersz 63: format daty z wiersza 15: „dd/MM/yyyy” dla daty francuskiej, „MM/dd/yyyy” dla daty angielskiej. Pole to jest inicjowane w wierszu 31 konstruktora;
- wiersz 62: ciąg znaków do wyświetlenia w polu wprowadzania danych [Date1]. Pole to zostanie zainicjowane przez akcję;
- wiersz 47: data [Regexp1] jest teraz sprawdzana pod kątem zgodności z formatem obowiązującym w bieżącej kulturze.
Wartości właściwości [Regionale] i [FormatDate] znajdują się w plikach zasobów [MyResources]. Francuskie pliki zasobów [MyResources], [MyResources.fr-FR], [1] oraz angielski plik zasobów [2] ulegają następującym zmianom:
![]() |
Jesteśmy już prawie gotowi. Dodajemy akcję [Action15] do kontrolera [SecondController]:
// Akcja15
public ViewResult Action15(FormCollection formData)
{
// metoda HTTP
string method = Request.HttpMethod.ToLower();
// szablon
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);
}
// widok
return View("Action15", modèle);
}
- wiersz 2: metoda [Action15] obsługuje zarówno [GET], jak i [POST]. W tym drugim przypadku wartości przesłane są pobierane z parametru [formData];
- wiersz 5: pobieramy metodę HTTP z zapytania;
- wiersz 7: tworzy się szablon widoku, który ma zostać wyświetlony (formularz);
- wiersze 8–11: w przypadku polecenia [GET] pole wprowadzania danych [Date1] jest inicjowane pustym ciągiem znaków;
- wiersze 12–16: w przypadku zamówienia [POST]:
- wiersz 14: szablon jest inicjowany wartościami zaksięgowanymi,
- wiersz 15: pole wprowadzania danych dla [Date1] jest inicjowane ciągiem znaków, który stanowi wartość [Date1] sformatowaną zgodnie z bieżącą kulturą – [dd/MM/yyyy] dla daty francuskiej, [MM/dd/yyyy] dla daty angielskiej;
- wiersz 18: widok [Action15.cshtml] jest wyświetlany wraz z szablonem.
Przeprowadźmy testy:
![]() |
- w formacie [1] – kalendarz francuski, gdy strona jest w języku francuskim;
- w formacie [2] – kalendarz angielski, gdy strona jest w języku angielskim;
- w formacie [3] – data w formacie francuskim, gdy strona jest w języku francuskim;
- w formacie [4] – tę samą datę w formacie angielskim, gdy strona jest w języku angielskim;
6.7. Conclusion
Jak widać, kwestia internacjonalizacji aplikacji jest złożona...
































