6. Internationalisation des vues
Nous allons aborder ici le problème de l'internationalisation des vues. C'est un problème complexe dont on trouvera une bonne description dans l'article suivant de Scott Hanselman :
Reprenons tout d'abord sa définition des différents termes liés à l'internationalisation des vues :
Internationalisation (i18n) | faire que l'application supporte différentes langues et locales |
Localisation (l10n) | faire que l'application supporte un couple langue / locale spécifique |
Globalisation | la combinaison de Internationalisation et Localisation |
Langue | langue parlée – désignée par un code ISO (fr : français, es : espagnol, en : anglais, ...) |
Locale | une variante de la langue – désignée également par un code ISO (en_GB : anglais de Grande-Bretagne, en_US : anglais des Etats-Unis, ...) |
Abordons le problème par un premier exemple.
6.1. Localisation des nombres réels
On peut remarquer une anomalie dans le formulaire de saisie précédent :
![]() |
Pour le nombre réel, nous avons tapé [0,3] et cela n'a pas été accepté. Il faut taper [0.3] :
![]() |
Le format attendu est donc le format anglo-saxon et non le format français. En recherchant sur internet, on trouve des solutions. En voici une.
Les actions [GET] et [POST] deviennent les suivantes :
// 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 vue [Action13Get.cshtml] est identique à la vue [Action12Get.cshtml] aux scripts Javascript près :
<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>
Note : ligne 5, adaptez la version de jQuery à celle de votre version de Visual Studio.
![]() |
// 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);
});
J'ai indiqué en ligne 1, où ce script avait été trouvé. Je n'essaierai pas de l'expliquer car je ne le comprends pas. Le Javascript a parfois des côtés hermétiques. Lignes 4, 9, 15, un objet [Globalize] est utilisé. Celui-ci est fourni par la bibliothèque JQuery Globalization qu'on peut obtenir avec [NuGet] :
![]() |
![]() |
- en [1], gérez les paquetages [NuGet] du projet [Exemple-03] ;
- en [2], consultez les paquetages en ligne ;
- en [3], tapez le terme [globalization] ;
- en [4], installez le paquetage [Globalize] du projet JQuery.
Une fois le paquetage [Globalize] installé, une nouvelle branche apparaît dans le dossier [Scripts] :
![]() |
- en [1], un dossier [globalize] a été créé avec le script principal [globalize.js] ;
- en [2], le script principal [globalize.js] est complété par des scripts spécifiques à une langue et une locale ;
- en [3], les scripts spécifiques à la langue française avec les locales (variantes) belges (BE), canadiennes (CA), françaises (FR), suisses (CH), luxembourgeoises (LU), Monégasque (MC).
Le script [globalize.js] et le script de notre culture [globalize.culture.fr-FR.js] doivent faire partie de la liste des scripts inclus dans notre page [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>
- ligne 5 : le script [globalize] ;
- ligne 6 : le script [globalize.culture.fr-FR.js] ;
- ligne 7 : le script [myscripts.js] ;
Revenons sur ce dernier 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);
});
Les lignes 10-13 fixent la culture côté client à [fr-FR] :
- ligne 10 : la fonction JQuery [ready] est exécutée lorsque le document dans lequel se trouve le script a été entièrement chargé par le navigateur ;
- lignes 11-12 : on fixe la culture côté client à [fr-FR]. Pour cela, il faut que le fichier [globalize.culture.fr-FR.js] soit inclus dans la liste des scripts Javascript associés au document.
Maintenant, nous pouvons tester la nouvelle application :
![]() |
On peut maintenant taper [0,3] pour le nombre réel, ce qu'on ne pouvait faire auparavant. On rencontre cependant une autre anomalie :
![]() |
Ci-dessus, la validation côté client nous laisse taper [11.2] avec la notation anglo-saxonne. Cette valeur n'est pas acceptée côté serveur lorsqu'on valide le formulaire :
![]() |
Il faut taper [11,2] et là ça fonctionne côtés client et serveur. Il faudrait que côté client, la notation anglo-saxonne ne soit pas acceptée. Ca doit être possible...
Abordons maintenant l'internationalisation des vues. Nous allons continuer avec l'exemple du formulaire précédent en l'offrant en deux langues : français et anglais.
6.2. Gérer une culture
La langue des vues est contrôlée par l'objet [Thread.CurrentThread.CurrentUICulture]. Pour afficher les pages dans la culture [fr-FR], on écrit :
La localisation (dates, nombres, monnaies, heures, ...) est contrôlée par l'objet [Thread.CurrentThread.CurrentCulture]. De façon similaire à ce qui a été écrit précédemment, on écrira :
Ces deux instructions pourraient être dans le constructeur de chaque contrôleur de l'application. Mais on pourrait vouloir également factoriser ce code commun à tous les contrôleurs. Nous suivons cette voie.
Nous créons deux nouveaux contrôleurs :
![]() |
- [I18NController] sera la classe mère de tous les contrôleurs utilisant l'internationalisation ;
- [SecondController] est un contrôleur exemple dérivé de [I18NController].
Le code du contrôleur [I18NController] est le suivant :
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace Exemples.Controllers
{
public abstract class I18NController : Controller
{
public I18NController()
{
// on récupère le contexte de la requête courante
HttpContext httpContext = 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 System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
}
}
- ligne 7 : [I18NController] dérive de la classe [Controller] ;
- ligne 7 : la classe est déclarée [abstract] pour empêcher son intanciation directe : elle ne peut qu'être dérivée pour être utilisée ;
- ligne 9 : le constructeur de la classe – sera exécuté à chaque instanciation d'un contrôleur dérivé de [I18NController] ;
- ligne 12 : on récupère le contexte de la requête HTTP en cours de traitement par le contrôleur ;
- ligne 15 : on fait l'hypothèse que la langue est fixée par un paramètre [lang] qu'on peut trouver à différents endroits. On cherche dans l'ordre :
- ligne 15 : dans les paramètres de l'URL [?lang=en-US],
- ligne 19 : dans les paramètres postés [lang=de],
- ligne 24 : dans la session de l'utilisateur,
- ligne 29 : dans les préférences de langue envoyées par le client HTTP,
- ligne 26 : si on n'a rien trouvé, on fixe la culture à [fr-FR] ;
- ligne 37 : on mémorise la culture dans la session. C'est là qu'elle sera retrouvée lors des requêtes suivantes. L'utilisateur pourra la modifier en la mettant dans les paramètres d'une commande GET ou POST ;
- lignes 39-40 : on fixe la culture de la vue qui sera affichée à l'issue du traitement de la requête courante.
Le contrôleur [SecondController] sera le suivant :
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);
}
}
}
- ligne 7 : [SecondController] dérive de [I18NController]. Ainsi est-on assuré que la culture de la vue à afficher aura été initialisée ;
- ligne 13 : on utilise le modèle de vue [ViewModel14] que nous allons présenter ;
- lignes 13 et 20 : la vue [Action14Get.cshtml] assure l'affichage du formulaire.
6.3. Internationaliser le modèle de vue [ViewModel14]
Le modèle de vue [ViewModel14] est le suivant :
![]() |
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)
{
// 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" }));
}
// 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" }));
}
// on rend la liste des erreurs
return résultats;
}
}
}
Ce modèle est le modèle précédent [ViewModel11] internationalisé. Nous allons décrire le mécanisme d'internationalisation pour le premier attribut de la première propriété. Les autres attributs suivent le même mécanisme.
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }
Dans le modèle précédent [ViewModel11], ces lignes étaient les suivantes :
[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }
Dans la version internationalisée, ligne 1, les textes à afficher sont placés dans un fichier de ressources. Ici ce fichier s'appelle [MyResources.resx] (typeof) et a été placé à la racine du projet. On l'appelle un fichier de ressources.
![]() |
Nous avons créé ici trois fichiers de ressources :
- [MyResources] : ressource par défaut lorsqu'il n'y a pas de ressource pour la locale courante ;
- [MyResources.fr-FR] : ressource pour la locale [fr-FR] ;
- [MyResources.en-US] : ressource pour la locale [en-US] ;
Pour créer un fichier de ressources on procède ainsi [1, 2, 3] :
![]() |
![]() |
Cela crée le fichier de ressources [MyResources2.resx]. Lorsqu'on double-clique dessus, on a la page suivante :
![]() |
Un fichier de ressources est un dictionnaire avec des clés et des valeurs associées à ces clés. On entre la clé en [1], la valeur en [2], la portée de la ressource en [3]. Pour que ces ressources soient lisibles, il faut qu'elles aient la portée [Public]. Revenons à la ligne :
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
- [ErrorMessageResourceType] : désigne le fichier des ressources. Le paramètre du [typeof] est le nom du fichier. Celui-ci est transformé en classe par le processus de compilation et son binaire inclus dans l'assembly du projet. Donc au final [MyResources] est le nom de la classe des ressources ;
- [ErrorMessageResourceName = "infoRequise"] : désigne une clé dans le fichier des ressources. Au final, la ligne signifie que le message d'erreur à afficher est la valeur du fichier [MyResources] associée à la clé [infoRequise].
Pour créer la clé [infoRequise] et la valeur associée dans le fichier [MyResources] on procède comme suit :
![]() |
On entre la clé en [1], la valeur en [2], la portée de la ressource en [3].
Il reste un dernier point à clarifier : l'espace de noms de la classe [MyResources]. Celui-ci est défini dans les propriétés du fichier [MyResources.resx] :
![]() |
En [1], nous définissons l'espace de noms de la classe [MyResources] qui va être créée à partir du fichier de ressources [MyResources.resx]. Revenons à la ligne internationalisée étudiée :
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
L'opérateur typeof attend une classe, ici la classe [MyResources]. Pour que celle-ci soit trouvée, il faut importer son espace de noms dans la classe [ViewModel14] :
using Exemple_03.Resources;
Pour que la classe [MyResources] soit visible, il faut qu'auparavant le projet ait été généré au moins une fois depuis la création du fichier de ressources [MyResources]. Le code de cette classe est visible dans le fichier [MyResources.Designer.cs] :
![]() |
Lorsqu'on double-clique sur ce fichier, on accède au code de la 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);
}
}
}
}
- ligne 1 : l'espace de noms de la classe ;
- ligne 11 : la clé [infoRequise] est devenue une propriété statique de la classe [MyResources]. Elle est accessible via la notation [MyResources.infoRequise]. Par ailleurs, on notera que cette propriété est de portée [public]. Sans cela, elle ne serait pas accessible. Il est bon de se le rappeler car malheureusement la portée par défaut est [internal] et cela est cause d'erreurs difficiles à comprendre lorsqu'on oublie de changer cette portée.
Pourquoi maintenant, trois fichiers de ressources ?
![]() |
Nous avons créé [MyResources.resx]. C'est la ressource racine. Ensuite nous créons autant de fichiers de ressources [MyResources.locale.resx] qu'il y a de locales (langues) à gérer. Ici nous gérons le français [fr-FR] et l'anglais américain [en-US]. Lorsque la locale courante n'est ni [fr-FR], ni [en-US], c'est la ressource racine [MyResources.resx] qui est utilisée.
Le contenu final de [MyResources.resx] est le suivant :
![]() |
Les messages seront en français lorsque la locale ne sera pas reconnue. Le contenu final de [MyResources.fr-FR.resx] est identique et obtenu par simple copie de fichier.
Le contenu final de [MyResources.en-US.resx] est obtenu lui aussi par copie de fichier puis modifié comme suit :
![]() |
Revenons sur la vue [ViewModel14] et sa méthode [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 ligne 7 montre comment récupérer un message du fichier de ressources [MyResources]. Ici on veut récupérer le message associé à la clé [infoIncorrecte] et ceci dans la culture du moment :
- MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : obtient l'objet associé à la clé [infoIncorrecte] dans le fichier de ressources [MyResources.en-US.resx] ;
- nous avons vu que le contrôleur [I18NController] mettait la culture courante en session associée à la clé [lang]. La culture courante peut donc être récupérée par System.Web.HttpContext.Current.Session["lang"] as string ;
- la ressource est récupérée avec le type [object]. Pour avoir le message d'erreur, on lui applique la méthode [ToString].
6.4. Internationaliser la vue [Action14Get.cshtml]
Nous faisons évoluer la vue d'affichage du formulaire de la façon suivante :
![]() |
@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>
<!-- choix d'une langue -->
@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>
}
Note : ligne 14, adaptez la version de jQuery à celle de votre version de Visual Studio.
Commençons par le plus simple, les lignes 36-38. Elles utilisent les propriétés statiques de la classe [MyResources] que nous venons de décrire. Pour avoir accès à la classe [MyResources], il faut importer son espace de noms (ligne 2).
Dans les messages internationalisés, il faut prévoir également ceux qui sont affichés par le framework de validation côté client. Pour cela, il faut utiliser les bibliothèques JQuery des lignes 17-19. Nous utilisons les fichiers JQuery pour les deux cultures que nous gérons [fr-FR] et [en-US]. Par ailleurs, on se souvient peut être que la vue [Action13Get] utilisait le script Javascript [myscripts.js] suivant :
// au chargement du document
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Maintenant, la culture n'est plus seulement [fr-FR], elle varie. Aussi ces lignes sont-elles désormais générées par la vue [Action14Get] elle-même aux lignes 21-26. Ces six lignes seront incluses dans la page HTML envoyée au client.
- ligne 23 : la variable Javascript [culture] est initialisée avec la culture courante du thread de la requête en cours de traitement. On se souvient peut être que celle-ci a été initialisée par le constructeur de la 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;
Si la culture courante est [en-US], le script Javascript incorporé dans la page HTML devient :
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
On a déjà dit que la fonction [$(document).ready] était exécutée à la fin du chargement de la page par le navigateur. Son exécution va avoir pour effet de fixer la culture du framework de validation côté client. Avec la culture [en-US] les messages d'erreur du framework seront en anglais et proviendront du fichier de ressources [MyResources.en-US.resx]. Nous verrons comment.
Maintenant examinons les lignes 57-65 :
<!-- choix d'une langue -->
@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>
}
On a là un second formulaire, le premier étant aux lignes 31-53. Ce formulaire affiche en bas de page les liens suivants :
![]() |
- ligne 2 : le formulaire est posté à l'action [Lang] du contrôleur [Second]. Pour l'instant on ne voit aucune valeur qui pourrait être postée ;
- lignes 6 et 7 : un clic sur les liens provoque l'exécution de la fonction Javascript [postForm]. Où se trouve cette fonction ? Dans le script [myscripts2.js] référencé ligne 20 de la vue :
![]() |
Son contenu est le suivant :
function postForm(lang, url) {
// on récupère le deuxième formulaire du document
var form = document.forms[1];
// on lui ajoute l'attribut caché 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'attribut caché 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]);
}
});
Les lignes 22-40 sont celles déjà présentes dans le script [myscripts.js] utilisé dans l'exemple précédent. Nous ne revenons pas dessus. La fonction [postForm] exécutée lors du clic sur les liens des langues est aux lignes 1-20 :
- ligne 1 : la fonction admet deux paramètres, [lang] qui est la culture choisie par l'utilisateur et [url] qui est l'URL vers laquelle doit être redirigé le navigateur client une fois le changement de culture opéré. Ces deux paramètres sont précisés à l'appel :
<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>
- ligne 3 : on récupère une référence sur le deuxième formulaire du document ;
- lignes 5-8 : on crée par programmation la balise
où [xx-XX] est la valeur du paramètre [lang] de la fonction ;
- ligne 10 : toujours par programmation, on ajoute cette balise au second formulaire. Au final, tout se passe comme si cette balise était présente depuis le début dans le second formulaire. Sa valeur sera donc postée. C'est ce qu'on voulait ;
- lignes 11-17 : on répète le même mécanisme pour une balise
où [url] est la valeur du paramètre [url] de la fonction ;
- ligne 19 : le second formulaire est maintenant posté. A quelle URL ?
Il faut revenir au code du second formulaire dans la page [Action14Get.cshtml] :
@using (Html.BeginForm("Lang", "Second"))
{
...
}
Le formulaire est donc posté à l'URL [/Second/Lang]. Il nous faut alors définir une action [Lang] dans le contrôleur [SecondController]. Ce sera la suivante :
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);
}
// langue
[HttpPost]
public RedirectResult Lang(string url)
{
// on redirige le client vers url
return new RedirectResult(url);
}
}
- ligne 18 : l'action ne répond qu'à un [POST] ;
- ligne 19 : elle ne récupère que le paramètre nommé [url] ;
- ligne 22 : elle répond au client de se rediriger vers cette URL.
Mais qu'est devenu le paramètre nommé [lang] ? Il faut maintenant se rappeler que le contrôleur [SecondController] dérive de la classe [I18NController] (ligne 1 ci-dessous). C'est ce contrôleur qui gère le paramètre [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;
}
Dans notre exemple étudié, le paramètre [lang] est posté. Il sera donc trouvé à la ligne 13, mis en session à la ligne 31 et utilisé pour mettre à jour la culture du thread courant lignes 33-34.
Que va-t-il se passer ensuite ? Revenons sur les liens :
<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 de redirection est [/Second/Action14Get]. L'action [Action14Get] est donc exécutée :
public class SecondController : I18NController
{
// Action14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
...
}
Auparavant, le constructeur de la classe [I18NController] est exécuté :
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;
}
Cette fois-ci, le paramètre [lang] sera trouvé dans la session par la ligne 18. Supposons que sa valeur soit [en-US]. Cette culture devient donc la culture du thread d'exécution de la requête (lignes 33-34). Revenons à l'action [Action14Get] :
// Action14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
Ligne 5, une instance du modèle de vue [ViewModel14] va être créée :
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; }
....
Parce que la culture du thread courant est [en-US], c'est le fichier [MyResources.en-US.resx] qui va être exploité. Les messages d'erreur seront donc en anglais.
Le modèle [ViewModel14] instancié, la vue [Action14Get.cshtml] est affichée :
@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>
Parce que la culture du thread courant est [en-US], le script embarqué dans la page aux lignes 15-20 est :
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Cela assure que le framework de validation va travailler avec les formats américains (date, monnaie, nombres, ...). Toujours pour la même raison, les messages des lignes 30-32 seront tirés du fichier de ressources [MyResources.en-US.resx] et seront donc en anglais.
6.5. Exemples d'exécution
Voici quelques exemple d'exécution :
![]() |
- en [1], le formulaire en français, en [2], le formulaire en anglais.
![]() |
- en [3], côté client, les messages d'erreur sont désormais en anglais.
Si on regarde le code source de la page, on voit que ces messages d'erreur ont été embarqués dans la page, donc générés par la vue ASP.NET [Action14Get] et son modèle [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. Internationalisation des dates
L'internationalisation est un problème complexe. Ainsi regardons la propriété [Date1] et son calendrier :
![]() |
On constate que le calendrier est un calendrier français alors que la culture de la page est [en-US]. En HTML5 existe un attribut [lang] permettant de fixer la langue de la page ou d'un composant de la page. On peut alors écrire dans la vue [Action14Get.cshtml] le code suivant :
@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>
...
- ligne 6 : on récupère la culture dans la session ;
- ligne 11 : on fixe l'attribut [lang] de la page avec cette valeur.
Les tests montrent que le calendrier reste en français même lorsque la page est par ailleurs affichée en anglais. Il y a également un problème avec l'autre date du formulaire :
![]() |
En [1], la date continue à être demandée au format français jj/mm/aaaa (20/11/2013) alors que le format américain est mm/dd/yyyy (10/21/2013). Nous allons essayer de résoudre ces deux problèmes avec une nouvelle vue et un nouveau modèle de vue.
JQuery UI est un projet dérivé du projet JQuery et offre des composants pour les formulaires dont un calendrier. Ce calendrier peut être internationalisé. C'est ce que nous allons montrer.
Pour commencer, ajoutons [JQuery UI] à notre projet.
![]() |
![]() |
Une fois JQuery UI installé, de nouveaux éléments apparaissent dans le projet :
![]() |
- en [1], la bibliothèque [JQuery UI] en versions normale et minifiée ;
- en [2], la feuille de style de [JQuery UI] ;
Le calendrier JQuery UI est par défaut en anglais. Pour être internationalisé, il faut ajouter des scripts qu'on peut trouver à l'URL [https://github.com/jquery/jquery-ui/tree/master/ui/i18n] :
![]() |
Pour avoir le calendrier JQuery UI en français, on copiera le contenu du fichier [jquery.ui.datepicker-fr.js] ci-dessus dans le dossier [Scripts] du projet.
![]() |
Le code de la nouvelle vue [Action15.cshtml] est obtenu par recopie de la vue précédente [Action14.cshtml] puis modifié. Nous ne présentons que les modifications :
![]() |
@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>
}
<!-- choix d'une langue -->
@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>
Note : ligne 16, adaptez la version de jQuery-ui à celle que vous avez téléchargée.
- ligne 15 : on référence la feuille de style de JQuery UI ;
- ligne 16 : on référence la version de JQuery UI téléchargée ;
- ligne 17 : on référence le script du calendrier français que nous venons de télécharger ;
- ligne 34 : la méthode [Html.TextBox] va générer ici une balise une balise [input] de type [text], d'id [Date1] et de name [Date1] ;
- ligne 19 : lorsque le chargement de la page sera terminé, la fonction JQuery UI [datepicker] sera appliquée à l'élément d'id [Date1], donc l'élément de la ligne 34. Cette fonction fait que lorsque l'utilisateur va mettre le focus sur le champ de saisie de [Date1], un calendrier va apparaître lui permettant de saisir une date. La fonction [datepicker] admet un paramètre qui lui indique la langue du calendrier. La variable [@Model.Regionale] doit valoir :
- 'fr' pour un calendrier français,
- '' pour un calendrier anglais ;
Le modèle de la vue précédente [Action15.cshtml] sera le modèle [ViewModel15] suivant :
![]() |
Son code est celui du modèle [ViewModel14] légèrement modifié. Nous ne présentons que les modifications :
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; }
// constructeur
public ViewModel15()
{
// Culture du moment
Culture = HttpContext.Current.Session["lang"] as string;
cultureInfo=new CultureInfo(Culture);
// Régionale du calendrier JQuery
Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
// format de date
FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
}
// 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", cultureInfo).ToString();
...
// Regexp1
try
{
DateTime.ParseExact(Regexp1, FormatDate, cultureInfo);
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
}
// on rend la liste des erreurs
return résultats;
}
// champs en-dehors du modèle de l'action
public string Culture { get; set; }
public string Regionale { get; set; }
public string StrDate1 { get; set; }
public string FormatDate { get; set; }
// données locales
private CultureInfo cultureInfo;
}
}
Par rapport au modèle précédent [ViewModel14], nous avons quatre propriétés supplémentaires :
- ligne 60 : la culture de la vue, 'fr-FR' ou 'en-US'. Cette culture est initialisée dans le constructeur ligne 26 ;
- ligne 61 : la culture régionale du calendrier JQuery, 'fr' pour un calendrier français, '' pour un calendrier anglais. Ce champ est initialisé par la ligne 29 du constructeur ;
- ligne 63 : le format de la date de la ligne 15 : 'dd/MM/yyyy' pour une date française, 'MM/dd/yyyy' pour une date anglaise. Ce champ est initialisé ligne 31 du constructeur ;
- ligne 62 : la chaîne de caractères à afficher dans le champ de saisie de [Date1]. Ce champ sera initialisé par l'action ;
- ligne 47 : la date [Regexp1] est maintenant vérifiée selon le format de la culture courante.
Les valeurs des propriétés [Regionale] et [FormatDate] sont trouvées dans les fichiers de ressources [MyResources]. Les fichiers de ressources français [MyResources] [MyResources.fr-FR] [1] et le fichier de ressources anglais [2] évoluent comme suit :
![]() |
Nous sommes presque prêts. Nous ajoutons une action [Action15] au contrôleur [SecondController] :
// Action15
public ViewResult Action15(FormCollection formData)
{
// méthode HTTP
string method = Request.HttpMethod.ToLower();
// modèle
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);
}
// affichage vue
return View("Action15", modèle);
}
- ligne 2 : la méthode [Action15] traite aussi bien les [GET] que les [POST]. Dans ce dernier cas, les valeurs postées sont récupérées dans le paramètre [formData] ;
- ligne 5 : on récupère la méthode HTTP de la requête ;
- ligne 7 : on crée le modèle de la vue qui va être affichée (le formulaire) ;
- lignes 8-11 : dans le cas d'une commande [GET], la zone de saisie de [Date1] est initialisée avec une chaîne vide ;
- lignes 12-16 : dans le cas d'une commande [POST] :
- ligne 14 : le modèle est initialisé avec les valeurs postées,
- ligne 15 : la zone de saisie de [Date1] est initialisée avec une chaîne de caractères qui est la valeur de [Date1] formatée selon la culture courante [dd/MM/yyyy] pour une date française, [MM/dd/yyyy] pour une date anglaise ;
- ligne 18 : la vue [Action15.cshtml] est affichée avec son modèle.
Faisons des tests :
![]() |
- en [1], un calendrier français lorsque la page est en français ;
- en [2], un calendrier anglais lorsque la page est en anglais ;
- en [3], une date au format français lorsque la page est en français ;
- en [4], la même date au format anglais lorsque la page est en anglais ;
6.7. Conclusion
On l'aura compris, le thème de l'internationalisation d'une application est un thème complexe...




































