6. Internazionalizzazione delle viste
Affronteremo qui il problema dell’internazionalizzazione delle viste. Si tratta di un problema complesso di cui si trova una buona descrizione nel seguente articolo di Scott Hanselman:
[http://www.hanselman.com/blog/GlobalizationInternationalizationAndLocalizationInASPNETMVC3JavaScriptAndJQueryPart1.aspx]
Riprendiamo innanzitutto la sua definizione dei diversi termini relativi all’internazionalizzazione delle viste:
Internazionalizzazione (i18n) | rendere l’applicazione compatibile con diverse lingue e impostazioni locali |
Localizzazione (l10n) | rendere l’applicazione compatibile con una specifica coppia lingua/impostazione locale |
Globalizzazione | la combinazione di Internationalisation e Localisation |
Lingua | lingua parlata – indicata da un codice ISO (fr: francese, es: spagnolo, en: inglese, ...) |
Impostazioni locali | una variante della lingua – indicata anch’essa da un codice ISO (en_GB: inglese della Gran Bretagna, en_US: inglese degli Stati Uniti, ...) |
Affrontiamo il problema con un primo esempio.
6.1. Localizzazione dei numeri reali
Si può notare un’anomalia nel modulo di inserimento precedente:
![]() |
Per il numero reale, abbiamo digitato [0,3] e non è stato accettato. È necessario digitare [0.3]:
![]() |
Il formato richiesto è quindi quello anglosassone e non quello francese. Cercando su Internet, si trovano delle soluzioni. Eccone una.
Le azioni [GET] e [POST] diventano le seguenti:
// Azione13-GET
[HttpGet]
public ViewResult Action13Get()
{
return View("Action13Get", new ViewModel11());
}
// Azione13-POST
[HttpPost]
public ViewResult Action13Post(ViewModel11 modèle)
{
return View("Action13Get", modèle);
}
La vista [Action13Get.cshtml] è identica alla vista [Action12Get.cshtml], a parte gli script 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>
Nota: alla riga 5, adattare la versione di jQuery a quella della propria versione di Visual Studio.
- alla riga 9, abbiamo aggiunto uno script [myscripts.js] . Lo script è il seguente:
![]() |
// 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) {
//Utilizzare il plugin Globalization per analizzare il valore
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
// al momento del caricamento del documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Ho indicato alla riga 1 dove è stato trovato questo script. Non cercherò di spiegarlo perché non lo capisco. Il JavaScript a talvolta aspetti poco chiari. Nelle righe 4, 9 e 15 viene utilizzato un oggetto [Globalize]. Questo è fornito dalla libreria JQuery Globalization, che si può ottenere con [NuGet]:
![]() |
![]() |
- in [1], gestite i pacchetti [NuGet] del progetto [Exemple-03];
- in [2], consultare i pacchetti online;
- in [3], digitare il termine [globalization];
- in [4], installare il pacchetto [Globalize] del progetto JQuery.
Una volta installato il pacchetto [Globalize], nella cartella [Scripts] compare un nuovo ramo:
![]() |
- in [1], è stata creata una cartella [globalize] con lo script principale [globalize.js];
- in [2], lo script principale [globalize.js] è stato integrato con script specifici per una lingua e un’impostazione locale;
- in [3], gli script specifici per la lingua francese con le impostazioni locali (varianti) belghe (BE), canadesi (CA), francesi (FR), svizzere (CH), lussemburghesi (LU), monegasche (MC).
Lo script [globalize.js] e lo script relativo alla nostra cultura [globalize.culture.fr-FR.js] devono essere inclusi nell'elenco degli script presenti nella nostra pagina [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>
- riga 5: lo script [globalize];
- riga 6: lo script [globalize.culture.fr-FR.js];
- riga 7: lo script [myscripts.js];
Torniamo a quest’ultimo script:
// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
return this.optional(element) ||
!isNaN(Globalize.parseFloat(value));
}
...
// al caricamento del documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Le righe 10-13 impostano la cultura lato client su [fr-FR]:
- riga 10: la funzione JQuery [ready] viene eseguita quando il documento in cui si trova lo script è stato caricato completamente dal browser;
- righe 11-12: si imposta la lingua lato client su [fr-FR]. A tal fine, è necessario che il file [globalize.culture.fr-FR.js] sia incluso nell'elenco degli script JavaScript associati al documento.
Ora possiamo testare la nuova applicazione:
![]() |
Ora è possibile digitare [0,3] come numero reale, cosa che prima non era possibile. Si riscontra tuttavia un’altra anomalia:
Come si vede sopra, la convalida lato client ci permette di digitare [11.2] con la notazione anglosassone. Questo valore non viene accettato lato server quando si invia il modulo:
Bisogna digitare [11,2] e in questo caso funziona sia sul lato client che sul lato server. Sul lato client, la notazione anglosassone non dovrebbe essere accettata. Dovrebbe essere possibile...
Passiamo ora all’internazionalizzazione delle viste. Continueremo con l’esempio del modulo precedente, rendendolo disponibile in due lingue: francese e inglese.
6.2. Gestire una cultura
La lingua delle viste è controllata dall’oggetto [Thread.CurrentThread.CurrentUICulture]. Per visualizzare le pagine nella cultura [fr-FR], si scrive:
La localizzazione (date, numeri, valute, ore, ...) è controllata dall’oggetto [Thread.CurrentThread.CurrentCulture]. Analogamente a quanto scritto in precedenza, si scriverà:
Queste due istruzioni potrebbero trovarsi nel costruttore di ciascun controller dell'applicazione. Tuttavia, si potrebbe anche voler estrapolare questo codice comune a tutti i controller. Seguiremo questa strada.
Creiamo due nuovi controller:
![]() |
- [I18NController] sarà la classe padre di tutti i controller che utilizzano l’internazionalizzazione;
- [SecondController] è un controller di esempio derivato da [I18NController].
Il codice del controller [I18NController] è il seguente:
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace Exemples.Controllers
{
public abstract class I18NController : Controller
{
public I18NController()
{
// si recupera il contesto della richiesta corrente
HttpContext httpContext = HttpContext.Current;
// si esamina la richiesta alla ricerca del parametro [lang]
// lo si cerca tra i parametri di URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// lo si cerca tra i parametri inviati
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// si cerca nella sessione dell'utente
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primo parametro dell'intestazione HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// cultura fr-FR
langue = "fr-FR";
}
// si imposta la lingua nella sessione
httpContext.Session["lang"] = langue;
// si modificano le impostazioni di lingua del thread
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
}
}
- riga 7: [I18NController] deriva dalla classe [Controller];
- riga 7: la classe [abstract] è dichiarata per impedire la sua istanziazione diretta: può essere utilizzata solo se derivata;
- riga 9: il costruttore della classe verrà eseguito ad ogni istanziazione di un controller derivato da [I18NController];
- riga 12: si recupera il contesto della richiesta HTTP attualmente in elaborazione da parte del controller;
- riga 15: si ipotizza che la lingua sia impostata da un parametro [lang] che può trovarsi in diverse posizioni. Si cerca nell’ordine:
- riga 15: nei parametri di URL [?lang=en-US],
- riga 19: nei parametri inviati [lang=de],
- riga 24: nella sessione dell'utente,
- riga 29: nelle preferenze linguistiche inviate dal client HTTP,
- riga 26: se non è stato trovato nulla, si imposta la cultura su [fr-FR];
- riga 37: si memorizza la cultura nella sessione. È lì che verrà recuperata nelle richieste successive. L'utente potrà modificarla inserendola nei parametri di un comando GET o POST;
- righe 39-40: si imposta la lingua della vista che verrà visualizzata al termine dell'elaborazione della richiesta corrente.
Il controller [SecondController] sarà il seguente:
using Exemple_03.Models;
using Exemples.Controllers;
using System.Web.Mvc;
namespace Exemple_03.Controllers
{
public class SecondController : I18NController
{
// Azione14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Azione14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
}
}
- riga 7: [SecondController] deriva da [I18NController]. In questo modo si garantisce che la cultura della vista da visualizzare sia stata inizializzata;
- riga 13: si utilizza il modello di vista [ViewModel14] che presenteremo;
- righe 13 e 20: la vista [Action14Get.cshtml] garantisce la visualizzazione del modulo.
6.3. Internazionalizzazione del modello di vista [ViewModel14]
Il modello di vista [ViewModel14] è il seguente:
![]() |
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; }
// convalida
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// elenco degli errori
List<ValidationResult> résultats = new List<ValidationResult>();
// lo stesso messaggio di errore per tutti
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" }));
}
// Email1
try
{
new MailAddress(Email1);
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
}
// Espressione regolare 1
try
{
DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
}
// viene visualizzato l'elenco degli errori
return résultats;
}
}
}
Questo modello è la versione internazionalizzata del modello precedente [ViewModel11]. Descriveremo il meccanismo di internazionalizzazione per il primo attributo della prima proprietà. Gli altri attributi seguono lo stesso meccanismo.
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }
Nel modello precedente [ViewModel11], queste righe erano le seguenti:
[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }
Nella versione internazionalizzata, alla riga 1, i testi da visualizzare sono inseriti in un file di risorse. In questo caso il file si chiama [MyResources.resx] (typeof) ed è stato collocato nella directory principale del progetto. Si tratta di un file di risorse.
![]() |
Qui abbiamo creato tre file di risorse:
- [MyResources]: risorsa predefinita quando non è presente alcuna risorsa per la lingua corrente;
- [MyResources.fr-FR]: risorsa per la lingua [fr-FR];
- [MyResources.en-US]: risorsa per la lingua [en-US];
Per creare un file di risorse si procede come segue [1, 2, 3]:
![]() |
![]() |
In questo modo viene creato il file di risorse [MyResources2.resx]. Facendo doppio clic su di esso, si apre la seguente pagina:
![]() |
Un file di risorse è un dizionario con chiavi e valori associati a tali chiavi. La chiave è [1], il valore è [2], l'ambito della risorsa è [3]. Affinché queste risorse siano leggibili, devono avere l’ambito [Public]. Torniamo alla riga:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
- [ErrorMessageResourceType]: indica il file delle risorse. Il parametro di [typeof] è il nome del file. Questo viene trasformato in una classe dal processo di compilazione e il suo binario viene incluso nell’assembly del progetto. Quindi, in definitiva, [MyResources] è il nome della classe delle risorse;
- [ErrorMessageResourceName = "infoRequise"]: indica una chiave nel file delle risorse. In definitiva, la riga significa che il messaggio di errore da visualizzare è il valore del file [MyResources] associato alla chiave [infoRequise].
Per creare la chiave [infoRequise] e il valore associato nel file [MyResources], procedere come segue:
![]() |
Si inserisce la chiave in [1], il valore in [2], l’ambito della risorsa in [3].
Rimane un ultimo punto da chiarire: lo spazio dei nomi della classe [MyResources]. Questo è definito nelle proprietà del file [MyResources.resx]:
![]() |
In [1] definiamo lo spazio dei nomi della classe [MyResources] che verrà creata a partire dal file di risorse [MyResources.resx]. Torniamo alla riga internazionalizzata che stiamo esaminando:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
L’operatore typeof richiede una classe, in questo caso la classe [MyResources]. Affinché questa venga individuata, è necessario importarne lo spazio dei nomi nella classe [ViewModel14]:
using Exemple_03.Resources;
Affinché la classe [MyResources] sia visibile, è necessario che il progetto sia stato generato almeno una volta dopo la creazione del file di risorse [MyResources]. Il codice di questa classe è visibile nel file [MyResources.Designer.cs]:
Facendo doppio clic su questo file, si accede al codice della classe [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);
}
}
}
}
- riga 1: lo spazio dei nomi della classe;
- riga 11: la chiave [infoRequise] è diventata una proprietà statica della classe [MyResources]. È accessibile tramite la notazione [MyResources.infoRequise]. Inoltre, si noti che questa proprietà ha come ambito [public]. Senza di esso, non sarebbe accessibile. È bene tenerlo a mente perché, purtroppo, l'ambito predefinito è [internal] e ciò causa errori difficili da comprendere quando si dimentica di modificare tale ambito.
Perché ora ci sono tre file di risorse?
Abbiamo creato [MyResources.resx]. Questa è la risorsa radice. Successivamente creiamo tanti file di risorse [MyResources.locale.resx] quanti sono i set di caratteri (lingue) da gestire. In questo caso gestiamo il francese [fr-FR] e l’inglese americano [en-US]. Quando la lingua corrente non è né [fr-FR] né [en-US], viene utilizzata la risorsa radice [MyResources.resx].
Il contenuto finale di [MyResources.resx] è il seguente:
![]() |
I messaggi saranno in francese quando la lingua non verrà riconosciuta. Il contenuto finale di [MyResources.fr-FR.resx] è identico e si ottiene semplicemente copiando il file.
Anche il contenuto finale di [MyResources.en-US.resx] viene ottenuto tramite copia del file e successivamente modificato come segue:
![]() |
Torniamo alla vista [ViewModel14] e al suo metodo [Validate]:
// convalida
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// elenco degli errori
List<ValidationResult> résultats = new List<ValidationResult>();
// stesso messaggio di errore per tutti
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" }));
}
...
// viene visualizzato l'elenco degli errori
return résultats;
}
La riga 7 mostra come recuperare un messaggio dal file delle risorse [MyResources]. In questo caso si desidera recuperare il messaggio associato alla chiave [infoIncorrecte] nella cultura corrente:
- MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : ottiene l'oggetto associato alla chiave [infoIncorrecte] nel file di risorse [MyResources.en-US.resx];
- abbiamo visto che il controller [I18NController] impostava la cultura corrente nella sessione associata alla chiave [lang]. La cultura corrente può quindi essere recuperata tramite System.Web.HttpContext.Current.Session["lang"] as string;
- la risorsa viene recuperata con il tipo [object]. Per ottenere il messaggio di errore, si applica ad essa il metodo [ToString].
6.4. Internazionalizzazione della vista [Action14Get.cshtml]
Modifichiamo la vista di visualizzazione del modulo nel modo seguente:
![]() |
@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>
<!-- scelta della lingua -->
@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>
}
Nota: riga 14, adattare la versione di jQuery a quella della propria versione di Visual Studio.
Cominciamo dalla parte più semplice, le righe 36-38. Queste utilizzano le proprietà statiche della classe [MyResources] che abbiamo appena descritto. Per accedere alla classe [MyResources], è necessario importarne lo spazio dei nomi (riga 2).
Nei messaggi internazionalizzati, occorre includere anche quelli visualizzati dal framework di validazione lato client. A tal fine, occorre utilizzare le librerie JQuery delle righe 17-19. Utilizziamo i file JQuery per le due culture che gestiamo: [fr-FR] e [en-US]. Inoltre, forse ricorderete che la vista [Action13Get] utilizzava il seguente script JavaScript [myscripts.js]:
// durante il caricamento del documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Ora, il campo «culture» non è più solo [fr-FR], ma varia. Pertanto, queste righe vengono ora generate dalla vista [Action14Get] stessa alle righe 21-26. Queste sei righe saranno incluse nella pagina HTML inviata al cliente.
- riga 23: la variabile JavaScript [culture] viene inizializzata con la cultura corrente del thread della richiesta in fase di elaborazione. Si ricorderà forse che questa è stata inizializzata dal costruttore della classe [I18NController]:
// impostazione della lingua nella sessione
httpContext.Session["lang"] = langue;
// modifica delle impostazioni culturali del thread
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
Se la cultura corrente è [en-US], lo script JavaScript incorporato nella pagina HTML diventa:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Come già detto, la funzione [$(document).ready] viene eseguita al termine del caricamento della pagina da parte del browser. La sua esecuzione avrà l’effetto di impostare la lingua del framework di validazione lato client. Con l’impostazione [en-US], i messaggi di errore del framework saranno in inglese e proverranno dal file di risorse [MyResources.en-US.resx]. Vedremo come.
Ora esaminiamo le righe 57-65:
<!-- scelta della lingua -->
@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>
}
Qui abbiamo un secondo modulo, il primo si trova alle righe 31-53. Questo modulo visualizza in fondo alla pagina i seguenti link:
![]() |
- riga 2: il modulo viene inviato all’azione [Lang] del controller [Second]. Per ora non si vede alcun valore che possa essere inviato;
- righe 6 e 7: cliccando sui link si avvia l’esecuzione della funzione JavaScript [postForm]. Dove si trova questa funzione? Nello script [myscripts2.js], a cui si fa riferimento alla riga 20 della vista:
![]() |
Il suo contenuto è il seguente:
function postForm(lang, url) {
// si recupera il secondo modulo del documento
var form = document.forms[1];
// si aggiunge l'attributo nascosto "lang"
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "lang");
hiddenField.setAttribute("value", lang);
// si aggiunge il campo nascosto al modulo
form.appendChild(hiddenField);
// si aggiunge l'attributo nascosto "url"
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "url");
hiddenField.setAttribute("value", url);
// si aggiunge il campo nascosto al modulo
form.appendChild(hiddenField);
// invio
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) {
//Utilizzare il plugin Globalization per analizzare il valore
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
Le righe 22-40 sono quelle già presenti nello script [myscripts.js] utilizzato nell’esempio precedente. Non ci soffermeremo su di esse. La funzione [postForm], eseguita quando si fa clic sui link delle lingue, si trova alle righe 1-20:
- riga 1: la funzione accetta due parametri, [lang], che rappresenta la cultura selezionata dall’utente, e [url], che indica l’URL URL verso cui deve essere reindirizzato il browser del cliente una volta effettuata la modifica della cultura. Questi due parametri vengono specificati al momento della chiamata:
<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>
- riga 3: si recupera un riferimento al secondo modulo del documento;
- righe 5-8: si crea programmaticamente il tag
dove [xx-XX] è il valore del parametro [lang] della funzione;
- riga 10: sempre tramite programmazione, si aggiunge questo tag al secondo modulo. Alla fine, è come se questo tag fosse presente fin dall’inizio nel secondo modulo. Il suo valore verrà quindi inviato. È proprio quello che volevamo;
- righe 11-17: si ripete lo stesso meccanismo per un tag
dove [url] è il valore del parametro [url] della funzione;
- riga 19: il secondo modulo viene ora inviato via POST. A quale URL?
È necessario tornare al codice del secondo modulo nella pagina [Action14Get.cshtml]:
@using (Html.BeginForm("Lang", "Second"))
{
...
}
Il modulo viene quindi inviato a URL [/Second/Lang]. Dobbiamo quindi definire un'azione [Lang] nel controller [SecondController]. Sarà la seguente:
public class SecondController : I18NController
{
// Azione14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Azione14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
// lingua
[HttpPost]
public RedirectResult Lang(string url)
{
// si reindirizza il client all'URL
return new RedirectResult(url);
}
}
- riga 18: l’azione risponde solo a un [POST];
- riga 19: recupera solo il parametro denominato [url];
- riga 22: risponde al client di reindirizzarsi verso questo URL.
Ma che fine ha fatto il parametro denominato [lang]? A questo punto occorre ricordare che il controller [SecondController] deriva dalla classe [I18NController] (riga 1 qui sotto). È questo controller che gestisce il parametro [lang]:
public abstract class I18NController : Controller
{
public I18NController()
{
// si recupera il contesto della richiesta corrente
HttpContext httpContext = System.Web.HttpContext.Current;
// si esamina la richiesta alla ricerca del parametro [lang]
// lo si cerca tra i parametri di URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// lo si cerca tra i parametri inviati
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// si cerca nella sessione dell'utente
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primo parametro dell'intestazione HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// cultura fr-FR
langue = "fr-FR";
}
// si imposta la lingua nella sessione
httpContext.Session["lang"] = langue;
// si modificano le impostazioni di lingua del thread
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
Nel nostro esempio, il parametro [lang] viene inviato. Verrà quindi individuato alla riga 13, inserito nella sessione alla riga 31 e utilizzato per aggiornare la cultura del thread corrente alle righe 33-34.
Cosa succederà dopo? Torniamo ai collegamenti:
<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>
Il URL di reindirizzamento è [/Second/Action14Get]. Viene quindi eseguita l’azione [Action14Get]:
public class SecondController : I18NController
{
// Azione14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
...
}
In precedenza, viene eseguito il costruttore della classe [I18NController]:
public abstract class I18NController : Controller
{
public I18NController()
{
// si recupera il contesto della richiesta corrente
HttpContext httpContext = System.Web.HttpContext.Current;
// si esamina la richiesta alla ricerca del parametro [lang]
// lo si cerca tra i parametri di URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// lo si cerca tra i parametri inviati
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// si cerca nella sessione dell'utente
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primo parametro dell'intestazione HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// cultura fr-FR
langue = "fr-FR";
}
// si imposta la lingua nella sessione
httpContext.Session["lang"] = langue;
// si modificano le impostazioni locali del thread
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
In questo caso, il parametro [lang] verrà individuato nella sessione alla riga 18. Supponiamo che il suo valore sia [en-US]. Questa cultura diventa quindi la cultura del thread di esecuzione della richiesta (righe 33-34). Torniamo all’azione [Action14Get]:
// Azione14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
Alla riga 5 verrà creata un’istanza del modello di vista [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; }
....
Poiché la cultura del thread corrente è [en-US], verrà utilizzato il file [MyResources.en-US.resx]. I messaggi di errore saranno quindi in inglese.
Una volta istanziato il modello [ViewModel14], viene visualizzata la vista [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>
Poiché la cultura del thread corrente è [en-US], lo script incorporato nella pagina alle righe 15-20 è:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Ciò garantisce che il framework di validazione funzioni con i formati statunitensi (data, valuta, numeri, ecc.). Sempre per lo stesso motivo, i messaggi delle righe 30-32 saranno ricavati dal file delle risorse [MyResources.en-US.resx] e saranno quindi in inglese.
6.5. Esempi di esecuzione
Ecco alcuni esempi di esecuzione:
![]() |
- in [1], il modulo in francese; in [2], il modulo in inglese.
![]() |
- In [3], sul lato client, i messaggi di errore sono ora in inglese.
Se si esamina il codice sorgente della pagina, si nota che questi messaggi di errore sono stati incorporati nella pagina, quindi generati dalla vista ASP.NET [Action14Get] e dal relativo modello [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. Internazionalizzazione delle date
L’internazionalizzazione è un problema complesso. Diamo quindi un’occhiata alla proprietà [Date1] e al suo calendario:
![]() |
Si nota che il calendario è francese, mentre la cultura della pagina è [en-US]. In HTML5 esiste un attributo [lang] che consente di impostare la lingua della pagina o di un componente della pagina. È quindi possibile scrivere nella vista [Action14Get.cshtml] il seguente codice:
@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>
...
- riga 6: si recupera la cultura dalla sessione;
- riga 11: si imposta l'attributo [lang] della pagina con questo valore.
I test dimostrano che il calendario rimane in francese anche quando la pagina viene visualizzata in inglese. C’è inoltre un problema con l’altra data del modulo:
![]() |
In [1], la data continua a essere richiesta nel formato francese gg/mm/aaaa (20/11/2013), mentre il formato americano è mm/dd/yyyy (10/21/2013). Cercheremo di risolvere questi due problemi con una nuova vista e un nuovo modello di vista.
JQuery UI è un progetto derivato dal progetto JQuery e offre componenti per i moduli, tra cui un calendario. Questo calendario può essere internazionalizzato. È proprio ciò che mostreremo.
Per iniziare, aggiungiamo [JQuery UI] al nostro progetto.
![]() |
![]() |
Una volta installati JQuery e UI, nel progetto compaiono nuovi elementi:
![]() |
- in [1], la libreria [JQuery UI] nelle versioni normale e minificata;
- in [2], il foglio di stile di [JQuery UI];
Il calendario JQuery UI è impostato di default in inglese. Per renderlo internazionalizzato, è necessario aggiungere gli script che si trovano in URL [https://github.com/jquery/jquery-ui/tree/master/ui/i18n]:
![]() |
Per ottenere il calendario JQuery UI in francese, copiare il contenuto del file [jquery.ui.datepicker-fr.js] sopra indicato nella cartella [Scripts] del progetto.
![]() |
Il codice della nuova vista [Action15.cshtml] si ottiene copiando la vista precedente [Action14.cshtml] e modificandola. Riportiamo solo le modifiche:
![]() |
@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>
}
<!-- scelta della lingua -->
@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>
Nota: riga 16, adattare la versione di jQuery-ui a quella scaricata.
- riga 15: si fa riferimento al foglio di stile di JQuery UI;
- riga 16: si fa riferimento alla versione scaricata di JQuery UI;
- riga 17: si fa riferimento allo script del calendario francese che abbiamo appena scaricato;
- riga 34: il metodo [Html.TextBox] genererà qui un tag [input] di tipo [text], con id [Date1] e name [Date1];
- riga 19: una volta completato il caricamento della pagina, la funzione JQuery UI [datepicker] verrà applicata all’elemento con id [Date1], ovvero l'elemento della riga 34. Questa funzione fa sì che, quando l'utente posiziona il focus sul campo di immissione di [Date1], compaia un calendario che gli consenta di inserire una data. La funzione [datepicker] accetta un parametro che le indica la lingua del calendario. La variabile [@Model.Regionale] deve assumere il valore:
- 'fr' per un calendario in francese,
- '' per un calendario inglese;
Il modello della vista precedente [Action15.cshtml] sarà il seguente modello [ViewModel15]:
![]() |
Il suo codice è quello del modello [ViewModel14] leggermente modificato. Riportiamo solo le modifiche:
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; }
// costruttore
public ViewModel15()
{
// Impostazione corrente
Culture = HttpContext.Current.Session["lang"] as string;
cultureInfo=new CultureInfo(Culture);
// Impostazioni regionali del calendario JQuery
Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
// formato della data
FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
}
// Convalida
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Elenco degli errori
List<ValidationResult> résultats = new List<ValidationResult>();
// stesso messaggio di errore per tutti
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" }));
}
// viene restituito l'elenco degli errori
return résultats;
}
// campi al di fuori del modello dell'azione
public string Culture { get; set; }
public string Regionale { get; set; }
public string StrDate1 { get; set; }
public string FormatDate { get; set; }
// dati locali
private CultureInfo cultureInfo;
}
}
Rispetto al modello precedente [ViewModel14], abbiamo quattro proprietà aggiuntive:
- riga 60: la cultura della vista, 'fr-FR' o 'en-US'. Questa cultura viene inizializzata nel costruttore alla riga 26;
- riga 61: la cultura regionale del calendario JQuery, 'fr' per un calendario francese, '' per un calendario inglese. Questo campo viene inizializzato dalla riga 29 del costruttore;
- riga 63: il formato della data della riga 15: 'dd/MM/yyyy' per una data francese, 'MM/dd/yyyy' per una data inglese. Questo campo viene inizializzato alla riga 31 del costruttore;
- riga 62: la stringa da visualizzare nel campo di immissione di [Date1]. Questo campo verrà inizializzato dall'azione;
- riga 47: la data [Regexp1] viene ora verificata in base al formato della cultura corrente.
I valori delle proprietà [Regionale] e [FormatDate] si trovano nei file di risorse [MyResources]. I file di risorse in francese [MyResources], [MyResources.fr-FR], [1] e il file di risorse in inglese [2] subiscono le seguenti modifiche:
![]() |
Siamo quasi pronti. Aggiungiamo un’azione [Action15] al controller [SecondController]:
// Azione15
public ViewResult Action15(FormCollection formData)
{
// metodo HTTP
string method = Request.HttpMethod.ToLower();
// modello
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);
}
// visualizzazione vista
return View("Action15", modèle);
}
- riga 2: il metodo [Action15] gestisce sia i [GET] che i [POST]. In quest’ultimo caso, i valori inviati vengono recuperati dal parametro [formData];
- riga 5: si recupera il metodo HTTP dalla richiesta;
- riga 7: si crea il modello della vista che verrà visualizzata (il modulo);
- righe 8-11: nel caso di un comando [GET], il campo di immissione di [Date1] viene inizializzato con una stringa vuota;
- righe 12-16: nel caso di un ordine [POST]:
- riga 14: il modello viene inizializzato con i valori registrati,
- riga 15: il campo di immissione di [Date1] viene inizializzato con una stringa di caratteri che corrisponde al valore di [Date1] formattato secondo le impostazioni locali correnti: [dd/MM/yyyy] per una data francese, [MM/dd/yyyy] per una data inglese;
- riga 18: la vista [Action15.cshtml] viene visualizzata con il relativo modello.
Facciamo alcuni test:
![]() |
- in [1], un calendario francese quando la pagina è in francese;
- in [2], un calendario inglese quando la pagina è in inglese;
- in [3], una data in formato francese quando la pagina è in francese;
- in [4], la stessa data in formato inglese quando la pagina è in inglese;
6.7. Conclusion
Come si può intuire, il tema dell’internazionalizzazione di un’applicazione è un argomento complesso...
































