Skip to content

6. Internazionalizzazione delle viste

Qui affronteremo il tema dell'internazionalizzazione delle viste. Si tratta di un argomento complesso, di cui è possibile trovare una buona descrizione nel seguente articolo di Scott Hanselman:

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

Per prima cosa, esaminiamo le sue definizioni dei vari 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/impostazioni locali
Globalizzazione
la combinazione di internazionalizzazione e localizzazione
Lingua
lingua parlata – designata da un codice ISO (fr: francese, es: spagnolo, en: inglese, ...)
Impostazioni locali
una variante della lingua – anch'essa designata da un codice ISO (en_GB: inglese britannico, en_US: inglese americano, ...)

Affrontiamo il problema con un primo esempio.

6.1. Localizzazione dei numeri reali

Potresti notare un'anomalia nel modulo di input precedente:

Image

Per il numero reale, abbiamo digitato [0,3] e non è stato accettato. È necessario digitare [0.3]:

Image

Il formato previsto è quindi quello anglosassone, non quello francese. Una rapida ricerca online rivela alcune soluzioni. Eccone una.

Le azioni [GET] e [POST] diventano le seguenti:


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


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

La vista [Action13Get.cshtml] è identica alla vista [Action12Get.cshtml] ad eccezione degli 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, modifica la versione di jQuery in modo che corrisponda alla tua versione di Visual Studio.

  • alla riga 9, abbiamo aggiunto uno script [ myscripts.js ]. È 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) {
    //Use the Globalization plugin to parse the value        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});
 
// au chargement du document
$(document).ready(function () {
  var culture = 'fr-FR';
  Globalize.culture(culture);
});

Ho indicato alla riga 1 dove si trova questo script. Non cercherò di spiegarlo perché non lo capisco. Il JavaScript a volte può essere un po' criptico. Alle righe 4, 9 e 15 viene utilizzato un oggetto [Globalize]. Questo è fornito dalla libreria jQuery Globalization, che può essere ottenuta tramite [NuGet]:

  • in [1], gestisci i pacchetti [NuGet] per il progetto [Example-03];
  • in [2], sfoglia i pacchetti online;
  • in [3], digitare il termine [globalizzazione];
  • in [4], installare il pacchetto [Globalize] per il progetto JQuery.

Una volta installato il pacchetto [Globalize], nella cartella [Scripts] compare una nuova cartella:

  • in [1], è stata creata una cartella [globalize] contenente lo script principale [globalize.js];
  • in [2], lo script principale [globalize.js] è integrato da script specifici per una lingua e un'impostazione locale;
  • in [3], gli script specifici per la lingua francese con le impostazioni locali belga (BE), canadese (CA), francese (FR), svizzera (CH), lussemburghese (LU) e monegasca (MC).

Lo script [globalize.js] e il nostro script di cultura [globalize.culture.fr-FR.js] devono essere inclusi nell'elenco degli script 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];

Diamo un'occhiata più da vicino 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));
}
 
...
 
// au chargement du document
$(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 contenente lo script è stato completamente caricato dal browser;
  • righe 11–12: la cultura lato client viene impostata su [fr-FR]. Affinché ciò funzioni, il file [globalize.culture.fr-FR.js] deve essere incluso nell'elenco degli script JavaScript associati al documento.

Ora possiamo testare la nuova applicazione:

Image

Ora possiamo inserire [0.3] come numero reale, cosa che prima non potevamo fare. Tuttavia, incontriamo un altro problema:

Image

Come visto in precedenza, la convalida lato client ci permette di inserire [11.2] utilizzando la notazione anglosassone. Questo valore non viene accettato sul lato server quando inviamo il modulo:

Image

Dobbiamo digitare [11,2], e in questo modo funziona sia sul lato client che sul lato server. Sul lato client, la notazione anglosassone non dovrebbe essere accettata. Deve essere possibile...

Affrontiamo ora l'internazionalizzazione delle viste. Continueremo con l'esempio del modulo precedente, proponendolo in due lingue: francese e inglese.

6.2. Gestione di una cultura

La lingua delle viste è controllata dall'oggetto [Thread.CurrentThread.CurrentUICulture]. Per visualizzare le pagine nella cultura [fr-FR], scriviamo:

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

La localizzazione (date, numeri, valute, orari, ecc.) è controllata dall'oggetto [Thread.CurrentThread.CurrentCulture]. Analogamente a quanto scritto in precedenza, scriviamo:

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

Queste due istruzioni potrebbero essere inserite nel costruttore di ciascun controller dell'applicazione. Tuttavia, potremmo anche voler estrapolare questo codice comune a tutti i controller. Adotteremo questo approccio.

Creiamo due nuovi controller:

Image

  • [I18NController] sarà la classe base per tutti i controller che utilizzano l'internazionalizzazione;
  • [SecondController] è un controller di esempio derivato da [I18NController].

Il codice per il controller [I18NController] è il seguente:


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

  • riga 7: [I18NController] deriva dalla classe [Controller];
  • riga 7: la classe è dichiarata [abstract] per impedire l'istanziazione diretta: può essere utilizzata solo se derivata;
  • riga 9: il costruttore della classe verrà eseguito ogni volta che viene istanziato un controller derivato da [I18NController];
  • riga 12: recuperiamo il contesto della richiesta HTTP attualmente in elaborazione da parte del controller;
  • riga 15: supponiamo che la lingua sia impostata da un parametro [lang] che può trovarsi in varie posizioni. Effettuiamo la ricerca nel seguente ordine:
    • riga 15: nei parametri dell'URL [?lang=en-US],
    • riga 19: nei parametri inviati [lang=de],
    • riga 24: nella sessione dell'utente,
    • riga 29: nelle preferenze di lingua inviate dal client HTTP,
    • riga 26: se non viene trovato nulla, la lingua viene impostata su [fr-FR];
  • riga 37: memorizziamo la lingua nella sessione. È da qui che verrà recuperata per le richieste successive. L'utente può modificarla includendola nei parametri di una richiesta GET o POST;
  • righe 39–40: impostiamo la lingua per la vista che verrà visualizzata dopo l'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
    {
      // 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);
      }
    }
}

  • Riga 7: [SecondController] estende [I18NController]. Questo garantisce che le impostazioni locali per la vista da visualizzare siano state inizializzate;
  • Riga 13: Utilizziamo il modello di vista [ViewModel14], che presenteremo tra poco;
  • Righe 13 e 20: La vista [Action14Get.cshtml] visualizza il 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; }
 
    // validation
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // error list
      List<ValidationResult> résultats = new List<ValidationResult>();
      // the same error msg for all
      string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
 
      // Date 1
      if (Date1.Date <= DateTime.Now.Date)
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
      }
      // Email1
      try
      {
        new MailAddress(Email1);
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
      }
      // Regexp1
      try
      {
        DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }
 
      // return the list of errors
      return résultats;
    }
  }
}

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, il testo da visualizzare è inserito in un file di risorse. In questo caso, il file si chiama [MyResources.resx] (typeof) ed è stato inserito 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, segui questi passaggi [1, 2, 3]:

In questo modo viene creato il file di risorse [MyResources2.resx]. Facendo doppio clic su di esso, viene visualizzata la pagina seguente:

Un file di risorse è un dizionario contenente chiavi e valori associati a tali chiavi. Inserisci la chiave in [1], il valore in [2] e l'ambito della risorsa in [3]. Affinché queste risorse siano leggibili, devono avere l'ambito [Public]. Torniamo alla riga:


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

  • [ErrorMessageResourceType]: si riferisce al file di risorse. Il parametro [typeof] è il nome del file. Questo viene convertito in una classe durante il processo di compilazione e il suo binario viene incluso nell'assembly del progetto. Quindi, in definitiva, [MyResources] è il nome della classe di risorse;
  • [ErrorMessageResourceName = "infoRequise"]: si riferisce a una chiave nel file delle risorse. In definitiva, la riga significa che il messaggio di errore da visualizzare è il valore nel file [MyResources] associato alla chiave [infoRequise].

Per creare la chiave [infoRequise] e il valore ad essa associato nel file [MyResources], procedere come segue:

Inserire la chiave in [1], il valore in [2] e l'ambito della risorsa in [3].

C'è 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 dal file di risorse [MyResources.resx]. Torniamo alla riga internazionalizzata che abbiamo esaminato:


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

L'operatore typeof richiede una classe, in questo caso la classe [MyResources]. Affinché questa classe possa essere trovata, il suo namespace deve essere importato nella classe [ViewModel14]:


using Exemple_03.Resources;

Affinché la classe [MyResources] sia visibile, il progetto deve essere stato compilato almeno una volta da quando è stato creato il file di risorse [MyResources]. Il codice di questa classe è visibile nel file [MyResources.Designer.cs]:

Image

Quando si fa 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 un ambito [public]. Senza questo, non sarebbe accessibile. È importante ricordarlo perché, purtroppo, l'ambito predefinito è [internal] e questo può causare errori difficili da comprendere se si dimentica di modificare l'ambito.

Perché ora ci sono tre file di risorse?

Image

Abbiamo creato [MyResources.resx]. Questa è la risorsa radice. Successivamente, creiamo tanti file di risorse [MyResources.locale.resx] quanti sono i locale (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 principale [MyResources.resx].

Il contenuto finale di [MyResources.resx] è il seguente:

Image

I messaggi saranno in francese quando la lingua non viene 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] si ottiene copiando il file e modificandolo come segue:

Image

Torniamo alla vista [ViewModel14] e al suo metodo [Validate]:


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

La riga 7 mostra come recuperare un messaggio dal file di risorse [MyResources]. In questo caso, vogliamo recuperare il messaggio associato alla chiave [infoIncorrecte] nella cultura corrente:

  • MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : recupera l'oggetto associato alla chiave [infoIncorrecte] dal file di risorse [MyResources.en-US.resx];
  • abbiamo visto che il controller [I18NController] imposta la cultura corrente nella sessione associata alla chiave [lang]. La cultura corrente può quindi essere recuperata utilizzando System.Web.HttpContext.Current.Session["lang"] as string;
  • La risorsa viene recuperata come [object]. Per ottenere il messaggio di errore, applichiamo il metodo [ToString].

6.4. Internazionalizzazione della vista [Action14Get.cshtml]

Aggiorniamo la vista di visualizzazione del modulo come segue:

Image


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

Nota: riga 14 — assicurati che la versione di jQuery corrisponda alla tua versione di Visual Studio.

Cominciamo dalla parte più semplice, le righe 36–38. 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, è necessario includere anche quelli visualizzati dal framework di convalida lato client. A tal fine, utilizzare le librerie jQuery alle righe 17–19. Utilizziamo i file jQuery per le due impostazioni locali che supportiamo: [fr- FR] e [en-US]. Inoltre, come forse ricorderete, la vista [Action13Get] utilizzava il seguente script JavaScript [myscripts.js]:


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

Ora, la cultura non è più solo [fr-FR]; 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 client.

  • Riga 23: La variabile JavaScript [culture] viene inizializzata con la cultura corrente del thread che gestisce la richiesta. Ricorderete che questa è stata inizializzata dal costruttore della classe [I18NController]:


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

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>

Abbiamo già detto che la funzione [$(document).ready] viene eseguita una volta che il browser ha finito di caricare la pagina. La sua esecuzione imposterà la cultura del framework di validazione lato client. Con la cultura [en-US], i messaggi di errore del framework saranno in inglese e proverranno dal file di risorse [MyResources.en-US.resx]. Vediamo come.

Ora esaminiamo le righe 57–65:


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

Ecco un secondo modulo; il primo si trova alle righe 31–53. Questo modulo mostra i seguenti link in fondo alla pagina:

  • riga 2: il modulo viene inviato all'azione [Lang] del controller [Second]. Per ora, non vediamo alcun valore che possa essere inviato;
  • righe 6 e 7: cliccando sui link si attiva 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) {
  // on récupère le deuxième formulaire du document
  var form = document.forms[1];
  // on lui ajoute l'hidden attribute lang
  var hiddenField = document.createElement("input");
  hiddenField.setAttribute("type", "hidden");
  hiddenField.setAttribute("name", "lang");
  hiddenField.setAttribute("value", lang);
  // ajout du champ caché dans le formulaire
  form.appendChild(hiddenField);
  // on lui ajoute l'hidden attribute url
  var hiddenField = document.createElement("input");
  hiddenField.setAttribute("type", "hidden");
  hiddenField.setAttribute("name", "url");
  hiddenField.setAttribute("value", url);
  // ajout du champ caché dans le formulaire
  form.appendChild(hiddenField);
  // soumission
  form.submit();
}
 
// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseFloat(value));
}
 
$.validator.methods.date = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseDate(value));
}
 
jQuery.extend(jQuery.validator.methods, {
  range: function (value, element, param) {
    //Use the Globalization plugin to parse the value        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});

Le righe 22–40 sono le stesse già presenti nello script [myscripts.js] utilizzato nell'esempio precedente. Non le ripeteremo qui. La funzione [postForm], che viene eseguita quando si fa clic sui link delle lingue, si trova alle righe 1–20:

  • riga 1: la funzione accetta due parametri, [lang], che è la cultura selezionata dall'utente, e [url], che è l'URL a cui il browser client deve essere reindirizzato una volta effettuato il cambio di cultura. Questi due parametri sono specificati nella 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: recuperiamo un riferimento al secondo modulo nel documento;
  • righe 5-8: creiamo il tag a livello di programmazione
<input type="hidden" value="xx-XX"/>

dove [xx-XX] è il valore del parametro [lang] della funzione;

  • Riga 10: Ancora una volta, utilizzando il codice, aggiungiamo questo campo al secondo modulo. In definitiva, si comporta come se questo campo fosse stato presente nel secondo modulo fin dall'inizio. Il suo valore verrà quindi inviato. È esattamente ciò che volevamo;
  • Righe 11–17: Ripetiamo lo stesso processo per un tag
<input type="hidden" value="url"/>

dove [url] è il valore del parametro [url] della funzione;

  • riga 19: il secondo modulo viene ora inviato. A quale URL?

Dobbiamo tornare al codice del secondo modulo nella pagina [Action14Get.cshtml]:


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

Il modulo viene quindi inviato all'URL [/Second/Lang]. Dobbiamo quindi definire un'azione [Lang] nel controller [SecondController]. Sarà la seguente:


public class SecondController : I18NController
    {
      // Action14-GET
      [HttpGet]
      public ViewResult Action14Get()
      {
        return View("Action14Get", new ViewModel14());
      }
 
      // Action14-POST
      [HttpPost]
      public ViewResult Action14Post(ViewModel14 modèle)
      {
        return View("Action14Get", modèle);
      }
 
      // language
      [HttpPost]
      public RedirectResult Lang(string url)
      {
        // we redirect the client to url
        return new RedirectResult(url);
      }
 
    }
  • riga 18: l'azione risponde solo a un [POST];
  • riga 19: recupera solo il parametro denominato [url];
  • riga 22: indica al client di reindirizzarsi a questo URL.

Ma che fine ha fatto il parametro denominato [lang]? Dobbiamo ora 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()
    {
      // on récupère le contexte de la requête courante
      HttpContext httpContext = System.Web.HttpContext.Current;
      // on examine la requête à la recherche du paramètre [lang]
      // on le cherche dans les paramètres de l'URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // on le cherche dans les paramètres postés
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // on le cherche dans la session de l'utilisateur
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1er paramètre de l'entête HTTP AcceptLanguages
        langue = httpContext.Request.UserLanguages[0];
      }
      if (langue == null)
      {
        // culture fr-FR
        langue = "fr-FR";
      }
      // on met la langue en session
      httpContext.Session["lang"] = langue;
      // on modifie les cultures du thread            
      Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}

Nel nostro esempio, il parametro [lang] viene passato per riferimento. Verrà quindi trovato alla riga 13, memorizzato nella sessione alla riga 31 e utilizzato per aggiornare la cultura del thread corrente alle righe 33–34.

Cosa succede dopo? Rivediamo i 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>

L'URL di reindirizzamento è [/Second/Action14Get]. Viene quindi eseguita l'azione [Action14Get]:


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

Questa volta, il parametro [lang] verrà trovato nella sessione alla riga 18. Supponiamo che il suo valore sia [en-US]. Questa cultura diventa quindi la cultura del thread che esegue la richiesta (righe 33–34). Torniamo all'azione [Action14Get]:


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

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é l'impostazione locale corrente del thread è [en-US], lo script incorporato nella pagina alle righe 15–20 è:


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

Questo garantisce che il framework di validazione utilizzi i formati statunitensi (data, valuta, numeri, ecc.). Per lo stesso motivo, i messaggi nelle righe 30–32 verranno recuperati dal file di 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 osserviamo il codice sorgente della pagina, possiamo notare che questi messaggi di errore sono stati incorporati nella pagina, il che significa che sono generati dalla vista ASP.NET [Action14Get] e dal suo modello di vista [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 della data

L'internazionalizzazione è una questione complessa. Esaminiamo la proprietà [Date1] e il relativo calendario:

Image

Possiamo notare che il calendario è francese, anche se la lingua della pagina è [en-US]. In HTML5, esiste un attributo [lang] che consente di impostare la lingua della pagina o di un componente della pagina. Possiamo quindi scrivere il seguente codice nella vista [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>
...

  • riga 6: recupera le impostazioni locali dalla sessione;
  • riga 11: impostiamo l'attributo [lang] della pagina su questo valore.

I test dimostrano che il calendario rimane in francese anche quando la pagina viene visualizzata in inglese. C'è anche un problema con l'altro campo data nel modulo:

In [1], la data viene ancora richiesta nel formato francese gg/mm/aaaa (20/11/2013), mentre il formato americano è mm/gg/aaaa (21/10/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 localizzato. È proprio ciò che dimostreremo.

Per iniziare, aggiungiamo [jQuery UI] al nostro progetto.

Una volta installato jQuery UI, nel progetto compaiono nuovi elementi:

  • in [1], la libreria [jQuery UI] sia nella versione normale che in quella minificata;
  • in [2], il foglio di stile [JQuery UI];

Il calendario jQuery UI è in inglese per impostazione predefinita. Per internazionalizzarlo, è necessario aggiungere gli script disponibili all'URL [https://github.com/jquery/jquery-ui/tree/master/ui/i18n]:

Per avere 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 per la nuova vista [Action15.cshtml] si ottiene copiando la vista precedente [Action14.cshtml] e modificandola. Mostreremo 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>
  }
  <!-- language selection -->
  @using (Html.BeginForm("Lang", "Second"))
  {
    <table>
      <tr>
        <td><a href="javascript:postForm('fr-FR','/Second/Action15')">Français</a></td>
        <td><a href="javascript:postForm('en-US','/Second/Action15')">English</a></td>
      </tr>
    </table>
  }
</body>
</html>

Nota: alla riga 16, modifica la versione di jQuery UI in modo che corrisponda a quella che hai scaricato.

  • Riga 15: fare riferimento al foglio di stile di jQuery UI;
  • riga 16: fare riferimento alla versione scaricata di jQuery UI;
  • riga 17: fare riferimento allo script del calendario francese appena scaricato;
  • riga 34: il metodo [Html.TextBox] genererà qui un tag [input], di tipo [text], con id [Date1] e nome [Date1];
  • riga 19: quando il caricamento della pagina sarà terminato, la funzione jQuery UI [datepicker] verrà applicata all'elemento con id [Date1], ovvero l'elemento alla riga 34. Questa funzione garantisce che, quando l'utente seleziona il campo di immissione [Date1], venga visualizzato un calendario che gli consenta di selezionare una data. La funzione [datepicker] accetta un parametro che specifica la lingua del calendario. La variabile [@Model.Regionale] deve essere impostata su:
  • 'fr' per un calendario francese,
  • '' per un calendario inglese;

Il modello per la vista precedente [Action15.cshtml] sarà il seguente modello [ViewModel15]:

Il suo codice è quello del modello [ViewModel14], leggermente modificato. Mostriamo 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; }
 
    // manufacturer
    public ViewModel15()
    {
      // Culture of the moment
      Culture = HttpContext.Current.Session["lang"] as string;
      cultureInfo=new CultureInfo(Culture);
      // Regional calendar JQuery
      Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
      // date format
      FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
    }
 
 
 
    // validation
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      // error list
      List<ValidationResult> résultats = new List<ValidationResult>();
      // the same error msg for all
      string errorMessage = MyResources.ResourceManager.GetObject("infoIncorrecte", cultureInfo).ToString();
...
      // Regexp1
      try
      {
        DateTime.ParseExact(Regexp1, FormatDate, cultureInfo);
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }
 
      // return the list of errors
      return résultats;
 
    }
 
    // fields outside the action model
    public string Culture { get; set; }
    public string Regionale { get; set; }
    public string StrDate1 { get; set; }
    public string FormatDate { get; set; }
 
    // local data
    private CultureInfo cultureInfo;
  }
}

Rispetto al modello precedente [ViewModel14], abbiamo quattro proprietà aggiuntive:

  • riga 60: le impostazioni locali della vista, 'fr-FR' o 'en-US'. Queste impostazioni locali vengono inizializzate 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 alla riga 29 del costruttore;
  • riga 63: il formato della data dalla 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 [Date1]. Questo campo verrà inizializzato dall'azione;
  • riga 47: la data [Regexp1] viene ora convalidata in base al formato delle impostazioni locali correnti.

I valori delle proprietà [Regionale] e [FormatDate] si trovano nei file di risorse [MyResources]. I file di risorse francesi [MyResources] [MyResources.fr-FR] [1] e il file di risorse inglese [2] cambiano come segue:

Siamo quasi pronti. Aggiungiamo un'azione [Action15] al controller [SecondController]:


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

  • Riga 2: il metodo [Action15] gestisce sia le richieste [GET] che [POST]. In quest'ultimo caso, i valori inviati vengono recuperati nel parametro [formData];
  • riga 5: viene recuperato il metodo HTTP della richiesta;
  • riga 7: viene creato il modello di visualizzazione da visualizzare (il modulo);
  • righe 8–11: nel caso di una richiesta [GET], il campo di immissione [Date1] viene inizializzato con una stringa vuota;
  • righe 12–16: nel caso di una richiesta [POST]:
    • riga 14: il modello viene inizializzato con i valori inviati,
    • riga 15: il campo di immissione [Date1] viene inizializzato con una stringa che è il valore di [Date1] formattato secondo le impostazioni locali correnti [gg/MM/aaaa] per una data in francese, [MM/gg/aaaa] per una data in inglese;
  • riga 18: la vista [Action15.cshtml] viene visualizzata con il suo modello.

Eseguiamo alcuni test:

  • in [1], un calendario in 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. Conclusione

Come possiamo vedere, il tema dell'internazionalizzazione delle applicazioni è complesso...