6. Internacionalização das visualizações
Abordaremos aqui a questão da internacionalização das visualizações. Trata-se de um problema complexo, sobre o qual há uma boa descrição no seguinte artigo de Scott Hanselman:
[http://www.hanselman.com/blog/GlobalizationInternationalizationAndLocalizationInASPNETMVC3JavaScriptAndJQueryPart1.aspx]
Vamos, antes de mais nada, retomar sua definição dos diferentes termos relacionados à internacionalização das visualizações:
Internacionalização (i18n) | fazer com que o aplicativo suporte diferentes idiomas e configurações regionais |
Localização (l10n) | fazer com que o aplicativo suporte um par específico de idioma e configuração regional |
Globalização | a combinação de Internationalisation e Localisation |
Idioma | língua falada – designada por um código ISO (fr: francês, es: espanhol, en: inglês, ...) |
Configuração regional | uma variante do idioma – também designada por um código ISO (en_GB: inglês da Grã-Bretanha, en_US: inglês dos Estados Unidos, ...) |
Vamos abordar o problema com um primeiro exemplo.
6.1. Localização de números reais
É possível notar uma anomalia no formulário de preenchimento anterior:
![]() |
Para o número real, digitamos [0,3] e isso não foi aceito. É preciso digitar [0.3]:
![]() |
O formato esperado é, portanto, o formato anglo-saxão e não o formato francês. Pesquisando na internet, encontramos soluções. Aqui está uma delas.
As ações [GET] e [POST] passam a ser as seguintes:
// Ação13-GET
[HttpGet]
public ViewResult Action13Get()
{
return View("Action13Get", new ViewModel11());
}
// Ação13-POST
[HttpPost]
public ViewResult Action13Post(ViewModel11 modèle)
{
return View("Action13Get", modèle);
}
A visualização [Action13Get.cshtml] é idêntica à visualização [Action12Get.cshtml], com exceção dos scripts 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>
Observação: na linha 5, adapte a versão de jQuery à sua versão do Visual Studio.
- Na linha 9, adicionamos um script [myscripts.js] . Ele é o seguinte:
![]() |
// 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 o plug-in de globalização para analisar o valor
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
// ao carregar o documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Indiquei na linha 1 onde esse script foi encontrado. Não vou tentar explicá-lo, pois não o entendo. O JavaScript às vezes tem aspectos um tanto obscuros. Nas linhas 4, 9 e 15, é utilizado um objeto [Globalize]. Esse objeto é fornecido pela biblioteca JQuery Globalization, que pode ser obtida com [NuGet]:
![]() |
![]() |
- no [1], gerencie os pacotes [NuGet] do projeto [Exemple-03];
- em [2], consulte os pacotes on-line;
- em [3], digite o termo [globalization];
- em [4], instale o pacote [Globalize] do projeto JQuery.
Após a instalação do pacote [Globalize], uma nova ramificação aparece na pasta [Scripts]:
![]() |
- no [1], foi criada uma pasta [globalize] com o script principal [globalize.js];
- em [2], o script principal [globalize.js] é complementado por scripts específicos para um idioma e uma configuração regional;
- em [3], os scripts específicos para o idioma francês com as configurações regionais (variantes) belgas (BE), canadenses (CA), francesas (FR), suíças (CH), luxemburguesas (LU), monegasco (MC).
O script [globalize.js] e o script da nossa cultura [globalize.culture.fr-FR.js] devem fazer parte da lista de scripts incluídos em nossa 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>
- linha 5: o script [globalize];
- linha 6: o script [globalize.culture.fr-FR.js];
- linha 7: o script [myscripts.js];
Voltemos 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));
}
...
// ao carregar o documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
As linhas 10 a 13 definem a cultura do lado do cliente como [fr-FR]:
- linha 10: a função JQuery [ready] é executada quando o documento no qual o script está localizado for totalmente carregado pelo navegador;
- linhas 11-12: define-se a cultura do lado do cliente como [fr-FR]. Para isso, é necessário que o arquivo [globalize.culture.fr-FR.js] esteja incluído na lista de scripts JavaScript associados ao documento.
Agora, podemos testar o novo aplicativo:
![]() |
Agora é possível digitar [0,3] como valor real, o que não era possível anteriormente. No entanto, encontramos outra anomalia:
No exemplo acima, a validação do lado do cliente permite que digitemos [11.2] com a notação anglo-saxônica. Esse valor não é aceito do lado do servidor ao validarmos o formulário:
É preciso digitar [11,2] e, nesse caso, funciona tanto no lado do cliente quanto no lado do servidor. Seria necessário que, no lado do cliente, a notação anglo-saxônica não fosse aceita. Isso deve ser possível...
Vamos abordar agora a internacionalização das visualizações. Continuaremos com o exemplo do formulário anterior, disponibilizando-o em dois idiomas: francês e inglês.
6.2. Gerenciando uma cultura
O idioma das visualizações é controlado pelo objeto [Thread.CurrentThread.CurrentUICulture]. Para exibir as páginas na cultura [fr-FR], escreve-se:
A localização (datas, números, moedas, horas, etc.) é controlada pelo objeto [Thread.CurrentThread.CurrentCulture]. De forma semelhante ao que foi escrito anteriormente, escrever-se-á:
Essas duas instruções poderiam estar no construtor de cada controlador do aplicativo. Mas também pode ser interessante extrair esse código comum a todos os controladores. Seguiremos esse caminho.
Criamos dois novos controladores:
![]() |
- [I18NController] será a classe pai de todos os controladores que utilizam a internacionalização;
- [SecondController] é um controlador de exemplo derivado de [I18NController].
O código do controlador [I18NController] é o seguinte:
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace Exemples.Controllers
{
public abstract class I18NController : Controller
{
public I18NController()
{
// recupera-se o contexto da solicitação atual
HttpContext httpContext = HttpContext.Current;
// examina-se a solicitação em busca do parâmetro [lang]
// procura-se nos parâmetros do URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// procura-se nos parâmetros enviados
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// procura-se na sessão do usuário
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primeiro parâmetro do cabeçalho HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// idioma fr-FR
langue = "fr-FR";
}
// define-se o idioma na sessão
httpContext.Session["lang"] = langue;
// modificamos as configurações de idioma do thread
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
}
}
- linha 7: [I18NController] deriva da classe [Controller];
- linha 7: a classe é declarada como [abstract] para impedir sua instanciação direta: ela só pode ser derivada para ser utilizada;
- linha 9: o construtor da classe será executado a cada instanciação de um controlador derivado de [I18NController];
- linha 12: recupera-se o contexto da solicitação HTTP que está sendo processada pelo controlador;
- linha 15: parte-se do pressuposto de que o idioma é definido por um parâmetro [lang], que pode ser encontrado em diferentes locais. A busca é feita na seguinte ordem:
- linha 15: nos parâmetros de URL e [?lang=en-US],
- linha 19: nos parâmetros enviados [lang=de],
- linha 24: na sessão do usuário,
- linha 29: nas preferências de idioma enviadas pelo cliente HTTP,
- linha 26: se nada for encontrado, define-se a cultura como [fr-FR];
- linha 37: a cultura é armazenada na sessão. É lá que ela será recuperada nas consultas seguintes. O usuário poderá alterá-la definindo-a nos parâmetros de um comando GET ou POST;
- linhas 39-40: define-se a configuração regional da visualização que será exibida após o processamento da consulta atual.
O controlador [SecondController] será o seguinte:
using Exemple_03.Models;
using Exemples.Controllers;
using System.Web.Mvc;
namespace Exemple_03.Controllers
{
public class SecondController : I18NController
{
// Ação14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Ação14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
}
}
- linha 7: [SecondController] deriva de [I18NController]. Assim, garante-se que a configuração cultural da visualização a ser exibida tenha sido inicializada;
- linha 13: utiliza-se o modelo de visualização [ViewModel14], que apresentaremos a seguir;
- linhas 13 e 20: a visualização [Action14Get.cshtml] garante a exibição do formulário.
6.3. Internacionalizar o modelo de visualização [ViewModel14]
O modelo de visualização [ViewModel14] é o seguinte:
![]() |
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; }
// validação
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// lista de erros
List<ValidationResult> résultats = new List<ValidationResult>();
// a mesma mensagem de erro para todos
string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
// Data 1
if (Date1.Date <= DateTime.Now.Date)
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
}
// E-mail 1
try
{
new MailAddress(Email1);
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Email1" }));
}
// Regexp1
try
{
DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
}
catch
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
}
// exibimos a lista de erros
return résultats;
}
}
}
Este modelo é a versão internacionalizada do modelo anterior [ViewModel11]. Descreveremos o mecanismo de internacionalização para o primeiro atributo da primeira propriedade. Os demais atributos seguem o mesmo mecanismo.
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }
No modelo anterior, [ViewModel11], essas linhas eram as seguintes:
[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }
Na versão internacionalizada, linha 1, os textos a serem exibidos são colocados em um arquivo de recursos. Aqui, esse arquivo se chama [MyResources.resx] (typeof) e foi colocado na raiz do projeto. Ele é chamado de arquivo de recursos.
![]() |
Criamos aqui três arquivos de recursos:
- [MyResources]: recurso padrão quando não há recurso para a localização atual;
- [MyResources.fr-FR]: recurso para a localização [fr-FR];
- [MyResources.en-US]: recurso para a localização [en-US];
Para criar um arquivo de recursos, proceda da seguinte forma: [1, 2, 3]:
![]() |
![]() |
Isso cria o arquivo de recursos [MyResources2.resx]. Ao clicar duas vezes nele, é exibida a seguinte página:
![]() |
Um arquivo de recursos é um dicionário com chaves e valores associados a essas chaves. A chave é inserida em [1], o valor em [2] e o escopo do recurso em [3]. Para que esses recursos sejam legíveis, é necessário que tenham o escopo [Public]. Voltemos à linha:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
- [ErrorMessageResourceType]: indica o arquivo de recursos. O parâmetro [typeof] é o nome do arquivo. Esse nome é transformado em classe pelo processo de compilação e seu binário é incluído no assembly do projeto. Portanto, no final, [MyResources] é o nome da classe de recursos;
- [ErrorMessageResourceName = "infoRequise"]: refere-se a uma chave no arquivo de recursos. No final das contas, a linha significa que a mensagem de erro a ser exibida é o valor do arquivo [MyResources] associado à chave [infoRequise].
Para criar a chave [infoRequise] e o valor associado no arquivo [MyResources], proceda da seguinte forma:
![]() |
Insira a chave em [1], o valor em [2] e o escopo do recurso em [3].
Resta um último ponto a esclarecer: o espaço de nomes da classe [MyResources]. Este é definido nas propriedades do arquivo [MyResources.resx]:
![]() |
No [1], definimos o espaço de nomes da classe [MyResources], que será criada a partir do arquivo de recursos [MyResources.resx]. Voltemos à linha internacionalizada analisada:
[Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
O operador typeof espera uma classe, neste caso a classe [MyResources]. Para que ela seja encontrada, é necessário importar seu namespace para a classe [ViewModel14]:
using Exemple_03.Resources;
Para que a classe [MyResources] fique visível, é necessário que o projeto tenha sido gerado pelo menos uma vez desde a criação do arquivo de recursos [MyResources]. O código dessa classe está visível no arquivo [MyResources.Designer.cs]:
Ao clicar duas vezes nesse arquivo, acessa-se o código da 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);
}
}
}
}
- linha 1: o namespace da classe;
- linha 11: a chave [infoRequise] tornou-se uma propriedade estática da classe [MyResources]. Ela pode ser acessada por meio da notação [MyResources.infoRequise]. Além disso, observe-se que essa propriedade tem escopo [public]. Sem isso, ela não seria acessível. É importante lembrar disso, pois, infelizmente, o escopo padrão é [internal], o que causa erros difíceis de entender quando se esquece de alterar esse escopo.
Por que agora são três arquivos de recursos?
Criamos o [MyResources.resx]. Esse é o recurso raiz. Em seguida, criamos tantos arquivos de recursos [MyResources.locale.resx] quantas forem as localizações (idiomas) a serem gerenciadas. Aqui, gerenciamos o francês [fr-FR] e o inglês americano [en-US]. Quando a localização atual não é nem [fr-FR] nem [en-US], é utilizado o recurso raiz [MyResources.resx].
O conteúdo final de [MyResources.resx] é o seguinte:
![]() |
As mensagens serão exibidas em francês quando a configuração regional não for reconhecida. O conteúdo final de [MyResources.fr-FR.resx] é idêntico e obtido por simples cópia do arquivo.
O conteúdo final de [MyResources.en-US.resx] também é obtido por cópia de arquivo e, em seguida, modificado da seguinte forma:
![]() |
Voltemos à visualização [ViewModel14] e ao seu método [Validate]:
// validação
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// lista de erros
List<ValidationResult> résultats = new List<ValidationResult>();
// a mesma mensagem de erro para todos
string errorMessage=MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo(System.Web.HttpContext.Current.Session["lang"] as string)).ToString();
// Data 1
if (Date1.Date <= DateTime.Now.Date)
{
résultats.Add(new ValidationResult(errorMessage, new string[] { "Date1" }));
}
...
// exibe a lista de erros
return résultats;
}
A linha 7 mostra como recuperar uma mensagem do arquivo de recursos [MyResources]. Aqui, queremos recuperar a mensagem associada à chave [infoIncorrecte], na cultura atual:
- MyResources.ResourceManager.GetObject("infoIncorrecte", new CultureInfo("en-US")) : obtém o objeto associado à chave [infoIncorrecte] no arquivo de recursos [MyResources.en-US.resx];
- vimos que o controlador [I18NController] definia a cultura atual na sessão associada à chave [lang]. A cultura atual pode, portanto, ser recuperada por System.Web.HttpContext.Current.Session["lang"] as string;
- o recurso é recuperado com o tipo [object]. Para obter a mensagem de erro, aplica-se a ele o método [ToString].
6.4. Internacionalizar a visualização [Action14Get.cshtml]
Alteramos a exibição do formulário da seguinte maneira:
![]() |
@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>
<!-- escolha de um 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>
}
Observação: linha 14, adapte a versão do jQuery à sua versão do Visual Studio.
Vamos começar pelo mais simples: as linhas 36 a 38. Elas utilizam as propriedades estáticas da classe [MyResources] que acabamos de descrever. Para acessar a classe [MyResources], é necessário importar seu namespace (linha 2).
Nas mensagens internacionalizadas, é preciso incluir também aquelas exibidas pelo framework de validação do lado do cliente. Para isso, é preciso utilizar as bibliotecas JQuery nas linhas 17 a 19. Utilizamos os arquivos JQuery para as duas culturas que gerenciamos: [fr-FR] e [en-US]. Além disso, talvez se lembre que a visualização [Action13Get] utilizava o seguinte script JavaScript [myscripts.js]:
// ao carregar o documento
$(document).ready(function () {
var culture = 'fr-FR';
Globalize.culture(culture);
});
Agora, o valor não é mais apenas [fr-FR], ele varia. Portanto, essas linhas agora são geradas pela própria visualização [Action14Get] nas linhas 21 a 26. Essas seis linhas serão incluídas na página HTML enviada ao cliente.
- linha 23: a variável JavaScript [culture] é inicializada com a cultura atual do thread da solicitação que está sendo processada. Talvez você se lembre de que ela foi inicializada pelo construtor da classe [I18NController]:
// definimos o idioma na sessão
httpContext.Session["lang"] = langue;
// modificação das configurações culturais do thread
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
Se a cultura atual for [en-US], o script JavaScript incorporado na página HTML passa a ser:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Já foi mencionado que a função [$(document).ready] é executada ao final do carregamento da página pelo navegador. Sua execução terá como efeito definir a configuração regional do framework de validação do lado do cliente. Com a configuração de idioma [en-US], as mensagens de erro do framework serão em inglês e provirão do arquivo de recursos [MyResources.en-US.resx]. Veremos como isso funciona.
Agora, vamos examinar as linhas 57 a 65:
<!-- seleção de um 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>
}
Temos aqui um segundo formulário; o primeiro está nas linhas 31 a 53. Esse formulário exibe, na parte inferior da página, os seguintes links:
![]() |
- linha 2: o formulário é enviado para a ação [Lang] do controlador [Second]. Por enquanto, não vemos nenhum valor que possa ser enviado;
- linhas 6 e 7: clicar nos links faz com que a função JavaScript [postForm] seja executada. Onde fica essa função? No script [myscripts2.js], referenciado na linha 20 da visualização:
![]() |
Seu conteúdo é o seguinte:
function postForm(lang, url) {
// recupera-se o segundo formulário do documento
var form = document.forms[1];
// adiciona-se a ele o atributo oculto “lang”
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "lang");
hiddenField.setAttribute("value", lang);
// adição do campo oculto ao formulário
form.appendChild(hiddenField);
// adiciona-se a ele o atributo oculto url
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "url");
hiddenField.setAttribute("value", url);
// adiciona-se o campo oculto ao formulário
form.appendChild(hiddenField);
// envio
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 o plug-in de globalização para analisar o valor
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
As linhas 22 a 40 são as que já constam no script [myscripts.js] utilizado no exemplo anterior. Não voltaremos a abordá-las. A função [postForm], executada ao clicar nos links de idiomas, está nas linhas 1 a 20:
- linha 1: a função aceita dois parâmetros, [lang], que é a cultura escolhida pelo usuário, e [url], que é a URL para a qual o navegador do cliente deve ser redirecionado após a alteração da cultura. Esses dois parâmetros são especificados na chamada:
<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>
- linha 3: obtém-se uma referência ao segundo formulário do documento;
- linhas 5-8: cria-se programaticamente a tag
onde [xx-XX] é o valor do parâmetro [lang] da função;
- linha 10: ainda por meio de programação, adiciona-se essa tag ao segundo formulário. No final, tudo ocorre como se essa tag estivesse presente desde o início no segundo formulário. Seu valor será, portanto, enviado. Era isso que se pretendia;
- linhas 11-17: repetimos o mesmo mecanismo para uma tag
onde [url] é o valor do parâmetro [url] da função;
- linha 19: o segundo formulário agora é enviado. Para qual URL?
É preciso voltar ao código do segundo formulário na página [Action14Get.cshtml]:
@using (Html.BeginForm("Lang", "Second"))
{
...
}
O formulário é, portanto, enviado para o URL [/Second/Lang]. Precisamos, então, definir uma ação [Lang] no controlador [SecondController]. Será a seguinte:
public class SecondController : I18NController
{
// Ação14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
// Ação14-POST
[HttpPost]
public ViewResult Action14Post(ViewModel14 modèle)
{
return View("Action14Get", modèle);
}
// idioma
[HttpPost]
public RedirectResult Lang(string url)
{
// redireciona o cliente para a URL
return new RedirectResult(url);
}
}
- linha 18: a ação responde apenas a um [POST];
- linha 19: ela recupera apenas o parâmetro denominado [url];
- linha 22: ela responde ao cliente para que ele seja redirecionado para esse URL.
Mas o que aconteceu com o parâmetro chamado [lang]? É preciso lembrar agora que o controlador [SecondController] deriva da classe [I18NController] (linha 1 abaixo). É esse controlador que gerencia o parâmetro [lang]:
public abstract class I18NController : Controller
{
public I18NController()
{
// recupera-se o contexto da solicitação atual
HttpContext httpContext = System.Web.HttpContext.Current;
// examina-se a solicitação em busca do parâmetro [lang]
// procura-se esse parâmetro nos parâmetros de URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// procura-se nos parâmetros enviados
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// procura-se na sessão do usuário
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primeiro parâmetro do cabeçalho HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// idioma fr-FR
langue = "fr-FR";
}
// define-se o idioma na sessão
httpContext.Session["lang"] = langue;
// modifica-se as configurações de idioma do thread
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
No exemplo que estamos analisando, o parâmetro [lang] é enviado. Ele será, portanto, encontrado na linha 13, colocado na sessão na linha 31 e utilizado para atualizar o escopo do thread atual nas linhas 33-34.
O que acontecerá a seguir? Voltemos aos links:
<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>
O URL de redirecionamento é o [/Second/Action14Get]. A ação [Action14Get] é, portanto, executada:
public class SecondController : I18NController
{
// Ação14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
...
}
Antes disso, o construtor da classe [I18NController] é executado:
public abstract class I18NController : Controller
{
public I18NController()
{
// recuperando o contexto da solicitação atual
HttpContext httpContext = System.Web.HttpContext.Current;
// examina-se a solicitação em busca do parâmetro [lang]
// procura-se nos parâmetros do URL
string langue = httpContext.Request.QueryString["lang"];
if (langue == null)
{
// procura-se nos parâmetros enviados
langue = httpContext.Request.Form["lang"];
}
if (langue == null)
{
// procura-se na sessão do usuário
langue = httpContext.Session["lang"] as string;
}
if (langue == null)
{
// primeiro parâmetro do cabeçalho HTTP AcceptLanguages
langue = httpContext.Request.UserLanguages[0];
}
if (langue == null)
{
// idioma fr-FR
langue = "fr-FR";
}
// define-se o idioma na sessão
httpContext.Session["lang"] = langue;
// modificamos as configurações de idioma do thread
Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}
Desta vez, o parâmetro [lang] será encontrado na sessão na linha 18. Suponhamos que seu valor seja [en-US]. Essa cultura passa, portanto, a ser a cultura do thread de execução da consulta (linhas 33-34). Voltemos à ação [Action14Get]:
// Ação14-GET
[HttpGet]
public ViewResult Action14Get()
{
return View("Action14Get", new ViewModel14());
}
Na linha 5, será criada uma instância do modelo de visualização [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; }
....
Como a cultura do thread atual é [en-US], o arquivo [MyResources.en-US.resx] será utilizado. As mensagens de erro serão, portanto, em inglês.
Com o modelo [ViewModel14] instanciado, a visualização [Action14Get.cshtml] é exibida:
@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>
Como a cultura do thread atual é [en-US], o script incorporado na página nas linhas 15 a 20 é:
<script>
$(document).ready(function () {
var culture = 'en-US';
Globalize.culture(culture);
});
</script>
Isso garante que o framework de validação funcione com os formatos americanos (data, moeda, números, etc.). Pelo mesmo motivo, as mensagens das linhas 30 a 32 serão extraídas do arquivo de recursos [MyResources.en-US.resx] e, portanto, estarão em inglês.
6.5. Exemplos de execução
Aqui estão alguns exemplos de execução:
![]() |
- em [1], o formulário em francês; em [2], o formulário em inglês.
![]() |
- No [3], no lado do cliente, as mensagens de erro agora estão em inglês.
Se analisarmos o código-fonte da página, vemos que essas mensagens de erro foram incorporadas à página, portanto, geradas pela visualização ASP.NET [Action14Get] e seu modelo [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. Internacionalização de datas
A internacionalização é um problema complexo. Portanto, vamos examinar a propriedade [Date1] e seu calendário:
![]() |
Percebe-se que o calendário é francês, enquanto a cultura da página é [en-US]. Em HTML5, existe um atributo [lang] que permite definir o idioma da página ou de um componente da página. Assim, é possível escrever na visualização [Action14Get.cshtml] o seguinte 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>
...
- linha 6: recupera-se a cultura da sessão;
- linha 11: define-se o atributo [lang] da página com esse valor.
Os testes mostram que o calendário permanece em francês mesmo quando a página é exibida em inglês. Há também um problema com a outra data do formulário:
![]() |
No [1], a data continua sendo solicitada no formato francês dd/mm/aaaa (20/11/2013), enquanto o formato americano é mm/dd/aaaa (10/21/2013). Vamos tentar resolver esses dois problemas com uma nova visualização e um novo modelo de visualização.
JQuery UI é um projeto derivado do projeto JQuery e oferece componentes para formulários, incluindo um calendário. Esse calendário pode ser internacionalizado. É isso que vamos mostrar.
Para começar, vamos adicionar o [JQuery UI] ao nosso projeto.
![]() |
![]() |
Após a instalação de JQuery e UI, novos elementos aparecem no projeto:
![]() |
- no [1], a biblioteca [JQuery UI] nas versões normal e minificada;
- em [2], a folha de estilo de [JQuery UI];
O calendário JQuery UI vem por padrão em inglês. Para internacionalizá-lo, é preciso adicionar scripts que podem ser encontrados no URL [https://github.com/jquery/jquery-ui/tree/master/ui/i18n]:
![]() |
Para ter o calendário JQuery UI em francês, copie o conteúdo do arquivo [jquery.ui.datepicker-fr.js] acima para a pasta [Scripts] do projeto.
![]() |
O código da nova visualização [Action15.cshtml] é obtido por meio da cópia da visualização anterior [Action14.cshtml] e, em seguida, modificado. Apresentamos apenas as alterações:
![]() |
@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>
}
<!-- escolha de um 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>
Observação: linha 16, adapte a versão do jQuery-ui àquela que você baixou.
- linha 15: faz-se referência à folha de estilo de JQuery UI;
- linha 16: faz-se referência à versão baixada de JQuery UI;
- linha 17: faz-se referência ao script do calendário francês que acabamos de baixar;
- linha 34: o método [Html.TextBox] irá gerar aqui uma tag [input] do tipo [text], com id [Date1] e name [Date1];
- linha 19: quando o carregamento da página estiver concluído, a função JQuery UI [datepicker] será aplicada ao elemento com id [Date1], ou seja, o elemento da linha 34. Essa função faz com que, quando o usuário colocar o foco no campo de entrada de [Date1], apareça um calendário que lhe permita inserir uma data. A função [datepicker] aceita um parâmetro que indica o idioma do calendário. A variável [@Model.Regionale] deve ter o valor:
- 'fr' para um calendário em francês,
- '' para um calendário em inglês;
O modelo da visualização anterior, [Action15.cshtml], será o seguinte modelo [ViewModel15]:
![]() |
Seu código é o do modelo [ViewModel14], ligeiramente modificado. Apresentamos apenas as alterações:
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; }
// construtor
public ViewModel15()
{
// Cultura atual
Culture = HttpContext.Current.Session["lang"] as string;
cultureInfo=new CultureInfo(Culture);
// Região do calendário JQuery
Regionale = MyResources.ResourceManager.GetObject("regionale", cultureInfo).ToString();
// formato de data
FormatDate = MyResources.ResourceManager.GetObject("formatDate", cultureInfo).ToString();
}
// validação
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Lista de erros
List<ValidationResult> résultats = new List<ValidationResult>();
// a mesma mensagem de erro para todos
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" }));
}
// exibimos a lista de erros
return résultats;
}
// campos fora do modelo da ação
public string Culture { get; set; }
public string Regionale { get; set; }
public string StrDate1 { get; set; }
public string FormatDate { get; set; }
// dados locais
private CultureInfo cultureInfo;
}
}
Em relação ao modelo anterior, [ViewModel14], temos quatro propriedades adicionais:
- linha 60: a cultura da visualização, 'fr-FR' ou 'en-US'. Essa cultura é inicializada no construtor da linha 26;
- linha 61: a cultura regional do calendário JQuery, 'fr' para um calendário francês, '' para um calendário inglês. Esse campo é inicializado pela linha 29 do construtor;
- linha 63: o formato da data da linha 15: 'dd/MM/yyyy' para uma data francesa, 'MM/dd/yyyy' para uma data inglesa. Esse campo é inicializado na linha 31 do construtor;
- linha 62: a sequência de caracteres a ser exibida no campo de entrada de [Date1]. Este campo será inicializado pela ação;
- linha 47: a data [Regexp1] é agora verificada de acordo com o formato da cultura atual.
Os valores das propriedades [Regionale] e [FormatDate] são encontrados nos arquivos de recursos [MyResources]. Os arquivos de recursos em francês [MyResources], [MyResources.fr-FR], [1] e o arquivo de recursos em inglês [2] sofrem as seguintes alterações:
![]() |
Estamos quase prontos. Adicionamos uma ação [Action15] ao controlador [SecondController]:
// Ação15
public ViewResult Action15(FormCollection formData)
{
// método HTTP
string method = Request.HttpMethod.ToLower();
// modelo
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);
}
// exibição da visualização
return View("Action15", modèle);
}
- linha 2: o método [Action15] processa tanto o [GET] quanto o [POST]. Neste último caso, os valores postados são recuperados no parâmetro [formData];
- linha 5: recupera-se o método HTTP da consulta;
- linha 7: cria-se o modelo da visualização que será exibida (o formulário);
- linhas 8-11: no caso de um comando [GET], o campo de entrada de [Date1] é inicializado com uma string vazia;
- linhas 12-16: no caso de um pedido [POST]:
- linha 14: o modelo é inicializado com os valores lançados;
- linha 15: o campo de entrada de [Date1] é inicializado com uma sequência de caracteres que corresponde ao valor de [Date1], formatado de acordo com a cultura atual — [dd/MM/yyyy] para uma data francesa, [MM/dd/yyyy] para uma data em inglês;
- linha 18: a visualização [Action15.cshtml] é exibida com seu modelo.
Vamos fazer alguns testes:
![]() |
- em [1], um calendário francês quando a página está em francês;
- em [2], um calendário inglês quando a página estiver em inglês;
- em [3], uma data no formato francês quando a página estiver em francês;
- em [4], a mesma data no formato inglês quando a página estiver em inglês;
6.7. Conclusion
Como se pode perceber, o tema da internacionalização de um aplicativo é complexo...
































