6. Internacionalización de las vistas
Aquí abordaremos el problema de la internacionalización de las vistas. Se trata de un problema complejo del que se puede encontrar una buena descripción en el siguiente artículo de Scott Hanselman:
[http://www.hanselman.com/blog/GlobalizationInternationalizationAndLocalizationInASPNETMVC3JavaScriptAndJQueryPart1.aspx]
Comencemos por repasar su definición de los distintos términos relacionados con la internacionalización de las vistas:
Internacionalización (i18n) | hacer que la aplicación sea compatible con diferentes idiomas y configuraciones regionales |
Localización (l10n) | hacer que la aplicación admita una combinación específica de idioma y configuración regional |
Globalización | la combinación de Internationalisation y Localisation |
Idioma | Idioma hablado —designado por un código ISO (fr: francés, es: español, en: inglés, ...) |
Configuración regional | una variante del idioma – también designada por un código ISO (en_GB: inglés británico, en_US: inglés estadounidense, ...) |
Abordemos el problema con un primer ejemplo.
6.1. Localización de los números reales
Se puede observar una anomalía en el formulario de introducción de datos anterior:
![]() |
Para el número real, hemos escrito [0,3] y no ha sido aceptado. Hay que escribir [0.3]:
![]() |
Por lo tanto, el formato esperado es el anglosajón y no el francés. Buscando en Internet, se encuentran soluciones. Aquí hay una.
Las acciones [GET] y [POST] quedan así:
// Acción13-GET
[HttpGet]
public ViewResult Action13Get()
{
return View("Action13Get", new ViewModel11());
}
// Acción13-POST
[HttpPost]
public ViewResult Action13Post(ViewModel11 modèle)
{
return View("Action13Get", modèle);
}
La vista [Action13Get.cshtml] es idéntica a la vista [Action12Get.cshtml], salvo por los scripts de 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: en la línea 5, adapta la versión de jQuery a la de tu versión de Visual Studio.
- En la línea 9, hemos añadido un script [myscripts.js] . Este es el siguiente:
![]() |
// 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) {
//Utiliza el complemento de globalización para analizar el valor
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
// al cargar el documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
He indicado en la línea 1 dónde se encontró este script. No voy a intentar explicarlo porque no lo entiendo. El JavaScript a veces tiene aspectos un tanto crípticos. En las líneas 4, 9 y 15 se utiliza un objeto [Globalize]. Este lo proporciona la biblioteca JQuery Globalization, que se puede obtener con [NuGet]:
![]() |
![]() |
- en [1], gestione los paquetes [NuGet] del proyecto [Exemple-03];
- en [2], consulta los paquetes en línea;
- en [3], escribe el término [globalization];
- en [4], instale el paquete [Globalize] del proyecto JQuery.
Una vez instalado el paquete [Globalize], aparecerá una nueva rama en la carpeta [Scripts]:
![]() |
- en [1], se ha creado una carpeta [globalize] con el script principal [globalize.js];
- en [2], el script principal [globalize.js] se complementa con scripts específicos para un idioma y una configuración regional;
- en [3], se añaden los scripts específicos para el francés con las variantes belgas (BE), canadienses (CA), francesas (FR), suizas (CH), luxemburguesas (LU) y monegasca (MC).
El script [globalize.js] y el script de nuestra cultura [globalize.culture.fr-FR.js] deben formar parte de la lista de scripts incluidos en nuestra página [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>
- línea 5: el script [globalize];
- línea 6: el script [globalize.culture.fr-FR.js];
- línea 7: el script [myscripts.js];
Volvamos a este último script:
// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
return this.optional(element) ||
!isNaN(Globalize.parseFloat(value));
}
...
// al cargar el documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Las líneas 10-13 establecen la configuración de idioma del lado del cliente en [fr-FR]:
- línea 10: la función JQuery [ready] se ejecuta cuando el navegador ha cargado por completo el documento en el que se encuentra el script;
- líneas 11-12: se establece la configuración regional del lado del cliente en [fr-FR]. Para ello, es necesario que el archivo [globalize.culture.fr-FR.js] esté incluido en la lista de scripts de JavaScript asociados al documento.
Ahora podemos probar la nueva aplicación:
![]() |
Ahora podemos introducir [0,3] como número real, algo que antes no podíamos hacer. Sin embargo, se produce otra anomalía:
En el ejemplo anterior, la validación del lado del cliente nos permite introducir [11.2] con la notación anglosajona. Este valor no se acepta en el lado del servidor al validar el formulario:
Hay que escribir [11,2] y así funciona tanto en el lado del cliente como en el del servidor. Sería necesario que, en el lado del cliente, no se aceptara la notación anglosajona. Debe de ser posible...
Pasemos ahora a la internacionalización de las vistas. Seguiremos con el ejemplo del formulario anterior, ofreciéndolo en dos idiomas: francés e inglés.
6.2. Gestionar una cultura
El idioma de las vistas se controla mediante el objeto [Thread.CurrentThread.CurrentUICulture]. Para mostrar las páginas en la cultura [fr-FR], se escribe:
La localización (fechas, números, monedas, horas, etc.) se controla mediante el objeto [Thread.CurrentThread.CurrentCulture]. De forma similar a lo escrito anteriormente, se escribirá:
Estas dos instrucciones podrían estar en el constructor de cada controlador de la aplicación. Pero quizá también queramos factorizar este código común a todos los controladores. Seguiremos este camino.
Creamos dos nuevos controladores:
![]() |
- [I18NController] será la clase padre de todos los controladores que utilicen la internacionalización;
- [SecondController] es un controlador de ejemplo derivado de [I18NController].
El código del controlador [I18NController] es el siguiente:
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace Exemples.Controllers
{
public abstract class I18NController : Controller
{
public I18NController()
{
// se recupera el contexto de la solicitud actual
HttpContext httpContext = HttpContext.Current;
// se examina la solicitud en busca del parámetro [lang]
// se busca en los parámetros de URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// se busca en los parámetros enviados
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
//: se busca en la sesión del usuario
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primer parámetro del encabezado HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// cultura fr-FR
langue = "fr-FR";
}
// se establece el idioma en la sesión
httpContext.Session["lang"] = langue;
// Se modifican las configuraciones de idioma del hilo
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
}
}
- línea 7: [I18NController] deriva de la clase [Controller];
- línea 7: la clase se declara como [abstract] para impedir su instanciación directa: solo puede utilizarse si se deriva de ella;
- línea 9: el constructor de la clase se ejecutará cada vez que se instancie un controlador derivado de [I18NController];
- línea 12: se recupera el contexto de la solicitud HTTP que está procesando el controlador;
- línea 15: se parte de la hipótesis de que el idioma viene determinado por un parámetro [lang] que puede encontrarse en distintos lugares. Se busca en el siguiente orden:
- línea 15: en los parámetros de URL y [?lang=en-US],
- línea 19: en los parámetros enviados [lang=de],
- línea 24: en la sesión del usuario,
- línea 29: en las preferencias de idioma enviadas por el cliente HTTP,
- línea 26: si no se ha encontrado nada, se establece la configuración regional en [fr-FR];
- línea 37: se almacena la configuración regional en la sesión. Ahí es donde se recuperará en las siguientes consultas. El usuario podrá modificarla introduciéndola en los parámetros de un comando GET o POST;
- líneas 39-40: se establece la configuración regional de la vista que se mostrará una vez procesada la consulta actual.
El controlador [SecondController] será el siguiente:
using Exemple_03.Models;
using Exemples.Controllers;
using System.Web.Mvc;
namespace Exemple_03.Controllers
{
public class SecondController : I18NController
{
// Acción14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Acción14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
}
}
- línea 7: [SecondController] deriva de [I18NController]. De este modo, se garantiza que se habrá inicializado la configuración regional de la vista que se va a mostrar;
- línea 13: se utiliza la plantilla de vista [ViewModel14] que vamos a presentar;
- líneas 13 y 20: la vista [Action14Get.cshtml] se encarga de mostrar el formulario.
6.3. Internacionalizar la plantilla de vista [ViewModel14]
La plantilla de vista [ViewModel14] es la siguiente:
![]() |
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; }
// validación
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// lista de errores
List<ValidationResult> résultats = new List<ValidationResult>();
// el mismo mensaje de error para todos
string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
// Fecha 1
if (Date1.Date <= DateTime.Now.Date)
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
}
// Correo electrónico 1
try
{
new MailAddress(Email1);
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
}
// Expresión regular 1
try
{
DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
}
// se muestra la lista de errores
return résultats;
}
}
}
Este modelo es la versión internacionalizada del modelo anterior [ViewModel11]. A continuación describiremos el mecanismo de internacionalización para el primer atributo de la primera propiedad. Los demás atributos siguen el mismo mecanismo.
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }
En el modelo anterior, [ViewModel11], estas líneas eran las siguientes:
[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }
En la versión internacionalizada, línea 1, los textos que se van a mostrar se colocan en un archivo de recursos. En este caso, este archivo se llama [MyResources.resx] (typeof) y se ha colocado en la raíz del proyecto. Se denomina archivo de recursos.
![]() |
Aquí hemos creado tres archivos de recursos:
- [MyResources]: recurso por defecto cuando no hay ningún recurso para la configuración regional actual;
- [MyResources.fr-FR]: recurso para la configuración regional [fr-FR];
- [MyResources.en-US]: recurso para la configuración regional [en-US];
Para crear un archivo de recursos, se procede de la siguiente manera: [1, 2, 3]:
![]() |
![]() |
Esto crea el archivo de recursos [MyResources2.resx]. Al hacer doble clic en él, aparece la siguiente página:
![]() |
Un archivo de recursos es un diccionario con claves y valores asociados a dichas claves. La clave se introduce en [1], el valor en [2] y el ámbito del recurso en [3]. Para que estos recursos sean legibles, deben tener el ámbito [Public]. Volvamos a la línea:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
- [ErrorMessageResourceType]: designa el archivo de recursos. El parámetro de [typeof] es el nombre del archivo. Este se transforma en una clase mediante el proceso de compilación y su binario se incluye en el ensamblado del proyecto. Por lo tanto, al final, [MyResources] es el nombre de la clase de recursos;
- [ErrorMessageResourceName = "infoRequise"]: hace referencia a una clave del archivo de recursos. En definitiva, la línea significa que el mensaje de error que se debe mostrar es el valor del archivo [MyResources] asociado a la clave [infoRequise].
Para crear la clave [infoRequise] y el valor asociado en el archivo [MyResources], se procede de la siguiente manera:
![]() |
Se introduce la clave en [1], el valor en [2] y el ámbito del recurso en [3].
Queda un último punto por aclarar: el espacio de nombres de la clase [MyResources]. Este se define en las propiedades del archivo [MyResources.resx]:
![]() |
En [1], definimos el espacio de nombres de la clase [MyResources], que se creará a partir del archivo de recursos [MyResources.resx]. Volvamos a la línea internacionalizada que estamos analizando:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
El operador typeof espera una clase, en este caso la clase [MyResources]. Para que se pueda encontrar, es necesario importar su espacio de nombres en la clase [ViewModel14]:
using Exemple_03.Resources;
Para que la clase [MyResources] sea visible, es necesario que el proyecto se haya generado al menos una vez desde la creación del archivo de recursos [MyResources]. El código de esta clase se puede ver en el archivo [MyResources.Designer.cs]:
Al hacer doble clic en este archivo, se accede al código de la clase [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);
}
}
}
}
- línea 1: el espacio de nombres de la clase;
- línea 11: la clave [infoRequise] se ha convertido en una propiedad estática de la clase [MyResources]. Se puede acceder a ella mediante la notación [MyResources.infoRequise]. Por otra parte, cabe señalar que esta propiedad tiene un ámbito [public]. Sin ello, no sería accesible. Es conveniente recordarlo, ya que, lamentablemente, el ámbito por defecto es [internal] y esto provoca errores difíciles de comprender cuando se olvida cambiar dicho ámbito.
¿Por qué ahora hay tres archivos de recursos?
Hemos creado [MyResources.resx]. Este es el recurso raíz. A continuación, creamos tantos archivos de recursos [MyResources.locale.resx] como idiomas haya que gestionar. En este caso, gestionamos el francés ([fr-FR]) y el inglés americano ([en-US]). Cuando la configuración regional actual no es ni [fr-FR] ni [en-US], se utiliza el recurso raíz [MyResources.resx].
El contenido final de [MyResources.resx] es el siguiente:
![]() |
Los mensajes aparecerán en francés cuando no se reconozca la configuración regional. El contenido final de [MyResources.fr-FR.resx] es idéntico y se obtiene simplemente copiando el archivo.
El contenido final de [MyResources.en-US.resx] también se obtiene copiando el archivo y, a continuación, se modifica de la siguiente manera:
![]() |
Volvamos a la vista [ViewModel14] y a su método [Validate]:
// validación
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// lista de errores
List<ValidationResult> résultats = new List<ValidationResult>();
// el mismo mensaje de error para todos
string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
// Fecha 1
if (Date1.Date <= DateTime.Now.Date)
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
}
...
// se muestra la lista de errores
return résultats;
}
La línea 7 muestra cómo recuperar un mensaje del archivo de recursos [MyResources]. En este caso, queremos recuperar el mensaje asociado a la clave [infoIncorrecte] en la configuración regional actual:
- MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : obtiene el objeto asociado a la clave [infoIncorrecte] en el archivo de recursos [MyResources.en-US.resx];
- ya hemos visto que el controlador [I18NController] establece la cultura actual en la sesión asociada a la clave [lang]. Por lo tanto, la cultura actual se puede recuperar mediante System.Web.HttpContext.Current.Session["lang"] as string;
- el recurso se recupera con el tipo [object]. Para obtener el mensaje de error, se le aplica el método [ToString].
6.4. Internacionalizar la vista [Action14Get.cshtml]
Modificamos la vista de visualización del formulario de la siguiente manera:
![]() |
@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>
<!-- selección de idioma -->
@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: en la línea 14, adapta la versión de jQuery a la de tu versión de Visual Studio.
Empecemos por lo más sencillo, las líneas 36-38. Estas utilizan las propiedades estáticas de la clase [MyResources] que acabamos de describir. Para acceder a la clase [MyResources], hay que importar su espacio de nombres (línea 2).
En los mensajes internacionalizados, también hay que tener en cuenta los que muestra el marco de validación del lado del cliente. Para ello, hay que utilizar las bibliotecas JQuery de las líneas 17-19. Utilizamos los archivos JQuery para las dos configuraciones regionales que gestionamos: [fr-FR] y [en-US]. Por otra parte, quizá recuerdes que la vista [Action13Get] utilizaba el siguiente script de JavaScript [myscripts.js]:
// al cargar el documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Ahora, el formato ya no es solo [fr-FR], sino que varía. Por lo tanto, estas líneas las genera ahora la propia vista [Action14Get] en las líneas 21-26. Estas seis líneas se incluirán en la página HTML enviada al cliente.
- línea 23: la variable de JavaScript [culture] se inicializa con la configuración regional actual del hilo de la solicitud que se está procesando. Quizá recordemos que esta fue inicializada por el constructor de la clase [I18NController]:
// Se establece el idioma en la sesión
httpContext.Session["lang"] = langue;
// Se modifican las configuraciones culturales del hilo
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
Si la configuración regional actual es [en-US], el script de JavaScript incorporado en la página HTML queda así:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Ya se ha mencionado que la función [$(document).ready] se ejecuta al finalizar la carga de la página por parte del navegador. Su ejecución tendrá como efecto establecer la configuración regional del marco de validación del lado del cliente. Con la configuración de idioma [en-US], los mensajes de error del marco de trabajo estarán en inglés y procederán del archivo de recursos [MyResources.en-US.resx]. Veremos cómo.
Ahora examinemos las líneas 57-65:
<!-- selección de un idioma -->
@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>
}
Aquí tenemos un segundo formulario; el primero se encuentra en las líneas 31-53. Este formulario muestra al pie de página los siguientes enlaces:
![]() |
- línea 2: el formulario se envía a la acción [Lang] del controlador [Second]. Por el momento no vemos ningún valor que pudiera enviarse;
- líneas 6 y 7: al hacer clic en los enlaces se ejecuta la función JavaScript [postForm]. ¿Dónde se encuentra esta función? En el script [myscripts2.js] al que se hace referencia en la línea 20 de la vista:
![]() |
Su contenido es el siguiente:
function postForm(lang, url) {
// se recupera el segundo formulario del documento
var form = document.forms[1];
// se le añade el atributo oculto «lang»
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "lang");
hiddenField.setAttribute("value", lang);
// Se añade el campo oculto al formulario
form.appendChild(hiddenField);
// se le añade el atributo oculto «url»
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "url");
hiddenField.setAttribute("value", url);
// se añade el campo oculto al formulario
form.appendChild(hiddenField);
// envío
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) {
//Utiliza el complemento de globalización para analizar el valor
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
Las líneas 22-40 son las que ya figuran en el script [myscripts.js] utilizado en el ejemplo anterior. No volveremos sobre ellas. La función [postForm], que se ejecuta al hacer clic en los enlaces de idiomas, se encuentra en las líneas 1-20:
- línea 1: la función admite dos parámetros, [lang], que es la configuración regional elegida por el usuario, y [url], que es la URL a la que debe redirigirse el navegador del cliente una vez realizado el cambio de configuración regional. Estos dos parámetros se especifican en la llamada:
<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ínea 3: se obtiene una referencia al segundo formulario del documento;
- líneas 5-8: se crea mediante programación la etiqueta
donde [xx-XX] es el valor del parámetro [lang] de la función;
- línea 10: también mediante programación, se añade esta etiqueta al segundo formulario. Al final, todo ocurre como si esta etiqueta hubiera estado presente desde el principio en el segundo formulario. Por lo tanto, su valor se enviará. Eso es lo que queríamos;
- líneas 11-17: se repite el mismo mecanismo para una etiqueta
donde [url] es el valor del parámetro [url] de la función;
- línea 19: ahora se envía el segundo formulario. ¿A qué URL?
Hay que volver al código del segundo formulario en la página [Action14Get.cshtml]:
@using (Html.BeginForm("Lang", "Second"))
{
...
}
Por lo tanto, el formulario se envía a URL [/Second/Lang]. A continuación, debemos definir una acción [Lang] en el controlador [SecondController]. Será la siguiente:
public class SecondController : I18NController
{
// Acción14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Acción14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
// idioma
[HttpPost]
public RedirectResult Lang(string url)
{
// Se redirige al cliente a la URL
return new RedirectResult(url);
}
}
- línea 18: la acción solo responde a un [POST];
- línea 19: solo recupera el parámetro denominado [url];
- línea 22: responde al cliente para que se redirija a este URL.
Pero, ¿qué ha sido del parámetro denominado [lang]? Ahora hay que recordar que el controlador [SecondController] deriva de la clase [I18NController] (línea 1 más abajo). Es este controlador el que gestiona el parámetro [lang]:
public abstract class I18NController : Controller
{
public I18NController()
{
// se recupera el contexto de la solicitud actual
HttpContext httpContext = System.Web.HttpContext.Current;
// se examina la solicitud en busca del parámetro [lang]
// se busca en los parámetros de URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// se busca en los parámetros enviados
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
//: se busca en la sesión del usuario
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primer parámetro del encabezado HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// cultura fr-FR
langue = "fr-FR";
}
// se establece el idioma en la sesión
httpContext.Session["lang"] = langue;
// Se modifican las configuraciones de idioma del hilo
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
En el ejemplo que estamos analizando, se envía el parámetro [lang]. Por lo tanto, se encontrará en la línea 13, se activará en la línea 31 y se utilizará para actualizar el contexto del hilo actual en las líneas 33-34.
¿Qué va a pasar a continuación? Volvamos a los enlaces:
<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>
El URL de redirección es [/Second/Action14Get]. Por lo tanto, se ejecuta la acción [Action14Get]:
public class SecondController : I18NController
{
// Acción14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
...
}
Previamente, se ejecuta el constructor de la clase [I18NController]:
public abstract class I18NController : Controller
{
public I18NController()
{
// se recupera el contexto de la solicitud actual
HttpContext httpContext = System.Web.HttpContext.Current;
// se examina la solicitud en busca del parámetro [lang]
// se busca en los parámetros de la URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// se busca en los parámetros enviados
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
//: se busca en la sesión del usuario
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primer parámetro del encabezado HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// cultura fr-FR
langue = "fr-FR";
}
// se establece el idioma en la sesión
httpContext.Session["lang"] = langue;
// Se modifican las configuraciones de idioma del hilo
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
En esta ocasión, el parámetro [lang] se encontrará en la sesión en la línea 18. Supongamos que su valor es [en-US]. Por lo tanto, esta configuración regional se convierte en la configuración regional del hilo de ejecución de la consulta (líneas 33-34). Volvamos a la acción [Action14Get]:
// Acción14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
En la línea 5, se creará una instancia del modelo de 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; }
....
Dado que la configuración regional del hilo actual es [en-US], se utilizará el archivo [MyResources.en-US.resx]. Por lo tanto, los mensajes de error aparecerán en inglés.
Una vez instanciada la plantilla [ViewModel14], se muestra 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>
Dado que la cultura del hilo actual es [en-US], el script incrustado en la página en las líneas 15-20 es:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Esto garantiza que el marco de validación funcione con los formatos estadounidenses (fecha, moneda, números, etc.). Por la misma razón, los mensajes de las líneas 30-32 se extraerán del archivo de recursos [MyResources.en-US.resx] y, por lo tanto, estarán en inglés.
6.5. Ejemplos de ejecución
A continuación se muestran algunos ejemplos de ejecución:
![]() |
- en [1], el formulario en francés; en [2], el formulario en inglés.
![]() |
- En [3], en el lado del cliente, los mensajes de error ahora aparecen en inglés.
Si observamos el código fuente de la página, vemos que estos mensajes de error se han integrado en la página, por lo que han sido generados por la vista ASP.NET [Action14Get] y su plantilla [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. Internacionalización de las fechas
La internacionalización es un tema complejo. Veamos, pues, la propiedad [Date1] y su calendario:
![]() |
Se observa que el calendario es francés, mientras que la configuración regional de la página es [en-US]. En HTML5 existe un atributo [lang] que permite establecer el idioma de la página o de un componente de la misma. Por lo tanto, podemos escribir en la vista [Action14Get.cshtml] el siguiente código:
@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>
...
- línea 6: se recupera la configuración regional de la sesión;
- línea 11: se establece el atributo [lang] de la página con este valor.
Las pruebas muestran que el calendario sigue apareciendo en francés incluso cuando el resto de la página se muestra en inglés. También hay un problema con la otra fecha del formulario:
![]() |
En [1], la fecha sigue solicitándose en formato francés dd/mm/aaaa (20/11/2013), mientras que el formato estadounidense es mm/dd/aaaa (10/21/2013). Vamos a intentar resolver estos dos problemas con una nueva vista y un nuevo modelo de vista.
JQuery UI es un proyecto derivado del proyecto JQuery y ofrece componentes para formularios, entre los que se incluye un calendario. Este calendario se puede internacionalizar. Eso es lo que vamos a mostrar.
Para empezar, añadamos [JQuery UI] a nuestro proyecto.
![]() |
![]() |
Una vez instalados JQuery y UI, aparecen nuevos elementos en el proyecto:
![]() |
- en [1], la biblioteca [JQuery UI] en sus versiones normal y minificada;
- en [2], la hoja de estilo de [JQuery UI];
El calendario JQuery UI está, por defecto, en inglés. Para internacionalizarlo, hay que añadir scripts que se pueden encontrar en URL [https://github.com/jquery/jquery-ui/tree/master/ui/i18n]:
![]() |
Para tener el calendario JQuery UI en francés, hay que copiar el contenido del archivo [jquery.ui.datepicker-fr.js] anterior en la carpeta [Scripts] del proyecto.
![]() |
El código de la nueva vista [Action15.cshtml] se obtiene copiando la vista anterior [Action14.cshtml] y modificándola posteriormente. Solo mostramos las modificaciones:
![]() |
@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>
}
<!-- selección de un idioma -->
@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: en la línea 16, adapta la versión de jQuery-ui a la que hayas descargado.
- línea 15: se hace referencia a la hoja de estilo de JQuery UI;
- línea 16: se hace referencia a la versión descargada de JQuery UI;
- línea 17: se hace referencia al script del calendario francés que acabamos de descargar;
- línea 34: el método [Html.TextBox] generará aquí una etiqueta [input] de tipo [text], con el ID [Date1] y el nombre [Date1];
- línea 19: cuando haya finalizado la carga de la página, la función JQuery UI [datepicker] se aplicará al elemento con id [Date1], es decir, el elemento de la línea 34. Esta función hace que, cuando el usuario coloque el foco en el campo de entrada de [Date1], aparezca un calendario que le permita introducir una fecha. La función [datepicker] admite un parámetro que le indica el idioma del calendario. La variable [@Model.Regionale] debe tener el valor:
- «fr» para un calendario en francés,
- '' para un calendario en inglés;
La plantilla de la vista anterior, [Action15.cshtml], será la siguiente plantilla [ViewModel15]:
![]() |
Su código es el de la plantilla [ViewModel14], ligeramente modificado. Solo indicamos los cambios:
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; }
// constructor
public ViewModel15()
{
// Configuración regional actual
Culture = HttpContext.Current.Session["lang"] as string;
cultureInfo=new CultureInfo(Culture);
// Región del calendario JQuery
Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
// formato de fecha
FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
}
// Validación
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Lista de errores
List<ValidationResult> résultats = new List<ValidationResult>();
// el mismo mensaje de error para todos
string errorMessage = MyResources.ResourceManager.GetObject("infoIncorrecte", cultureInfo).ToString();
...
// Expresión regular 1
try
{
DateTime.ParseExact(Regexp1, FormatDate, cultureInfo);
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
}
// se muestra la lista de errores
return résultats;
}
// campos fuera del modelo de la acción
public string Culture { get; set; }
public string Regionale { get; set; }
public string StrDate1 { get; set; }
public string FormatDate { get; set; }
// Datos locales
private CultureInfo cultureInfo;
}
}
En comparación con el modelo anterior [ViewModel14], tenemos cuatro propiedades adicionales:
- línea 60: la configuración regional de la vista, «fr-FR» o «en-US». Esta configuración regional se inicializa en el constructor de la línea 26;
- línea 61: la configuración regional del calendario JQuery, «fr» para un calendario francés, «» para un calendario inglés. Este campo se inicializa en la línea 29 del constructor;
- línea 63: el formato de la fecha de la línea 15: «dd/MM/yyyy» para una fecha francesa, «MM/dd/yyyy» para una fecha inglesa. Este campo se inicializa en la línea 31 del constructor;
- línea 62: la cadena de caracteres que se mostrará en el campo de entrada de [Date1]. Este campo se inicializará mediante la acción;
- línea 47: la fecha [Regexp1] se comprueba ahora según el formato de la cultura actual.
Los valores de las propiedades [Regionale] y [FormatDate] se encuentran en los archivos de recursos [MyResources]. Los archivos de recursos en francés [MyResources], [MyResources.fr-FR] y [1], así como el archivo de recursos en inglés [2], se modifican de la siguiente manera:
![]() |
Ya casi estamos listos. Añadimos una acción [Action15] al controlador [SecondController]:
// Acción15
public ViewResult Action15(FormCollection formData)
{
// método HTTP
string method = Request.HttpMethod.ToLower();
// plantilla
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);
}
// visualización de vista
return View("Action15", modèle);
}
- línea 2: el método [Action15] procesa tanto los [GET] como los [POST]. En este último caso, los valores enviados se recuperan en el parámetro [formData];
- línea 5: se recupera el método HTTP de la consulta;
- línea 7: se crea la plantilla de la vista que se va a mostrar (el formulario);
- líneas 8-11: en el caso de un comando [GET], el campo de entrada de [Date1] se inicializa con una cadena vacía;
- líneas 12-16: en el caso de un pedido [POST]:
- línea 14: la plantilla se inicializa con los valores contabilizados;
- línea 15: el campo de entrada de [Date1] se inicializa con una cadena de caracteres que es el valor de [Date1] formateado según la configuración regional actual: [dd/MM/yyyy] para una fecha francesa, [MM/dd/yyyy] para una fecha inglesa;
- línea 18: se muestra la vista [Action15.cshtml] con su plantilla.
Hagamos algunas pruebas:
![]() |
- en [1], un calendario francés cuando la página está en francés;
- en [2], un calendario inglés cuando la página está en inglés;
- en [3], una fecha en formato francés cuando la página está en francés;
- en [4], la misma fecha en formato inglés cuando la página está en inglés;
6.7. Conclusion
Como habremos comprendido, el tema de la internacionalización de una aplicación es un tema complejo...
































