Skip to content

6. Internacionalização das vistas

Vamos abordar aqui a questão da internacionalização das vistas. Trata-se de uma questão complexa, sobre a qual se pode encontrar uma boa descrição no seguinte artigo de Scott Hanselman:

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

Comecemos por retomar a sua definição dos diferentes termos relacionados com a internacionalização das vistas:

Internacionalização (i18n)
fazer com que a aplicação suporte diferentes idiomas e configurações regionais
Localização (l10n)
fazer com que a aplicação suporte um par específico de idioma e configuração regional
Globalização
a combinação de Internationalisation e Localisation
Língua
língua falada – designada por um código ISO (fr: francês, es: espanhol, en: inglês, ...)
Configuração regional
uma variante da língua – 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 dos números reais

É possível observar uma anomalia no formulário de introdução de dados anterior:

 

Para o número real, digitámos [0,3] e isso não foi aceite. É necessário digitar [0.3]:

 

O formato esperado é, portanto, o formato anglo-saxónico e não o formato francês. Ao pesquisar na Internet, encontram-se 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 vista [Action13Get.cshtml] é idêntica à vista [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>

Nota: linha 5, adapte a versão de jQuery à da sua versão do Visual Studio.

  • Na linha 9, adicionámos um script [myscripts.js] . Este é 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) {
    //Utilize o plugin 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 este script foi encontrado. Não vou tentar explicá-lo, porque não o compreendo. O JavaScript tem, por vezes, aspetos difíceis de compreender. Nas linhas 4, 9 e 15, é utilizado um objeto [Globalize]. Este é 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 online;
  • em [3], digite o termo [globalization];
  • em [4], instale o pacote [Globalize] do projeto JQuery.

Assim que o pacote [Globalize] estiver instalado, surge um novo ramo 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;
  • no [3], os scripts específicos para a língua francesa com as configurações regionais (variantes) belgas (BE), canadianas (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 na 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-13 definem a cultura do lado do cliente como [fr-FR]:

  • linha 10: a função JQuery [ready] é executada quando o documento em que se encontra o script tiver sido totalmente carregado pelo navegador;
  • linhas 11-12: define-se a cultura do lado do cliente como [fr-FR]. Para tal, é necessário que o ficheiro [globalize.culture.fr-FR.js] esteja incluído na lista de scripts JavaScript associados ao documento.

Agora, podemos testar a nova aplicação:

 

Agora é possível introduzir [0,3] como valor real, o que antes não era possível. No entanto, surge outra anomalia:

 

No exemplo acima, a validação do lado do cliente permite-nos introduzir [11.2] com a notação anglo-saxónica. Este valor não é aceite do lado do servidor quando validamos o formulário:

 

É necessário introduzir [11,2] e, nesse caso, funciona tanto do lado do cliente como do lado do servidor. Do lado do cliente, a notação anglo-saxónica não deveria ser aceite. Deve ser possível...

Passemos agora à internacionalização das visualizações. Vamos continuar com o exemplo do formulário anterior, disponibilizando-o em duas línguas: francês e inglês.

6.2. Gerir uma cultura

O idioma das vistas é controlado pelo objeto [Thread.CurrentThread.CurrentUICulture]. Para apresentar as páginas na cultura [fr-FR], escreve-se:

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

A localização (datas, números, moedas, horas, etc.) é controlada pelo objeto [Thread.CurrentThread.CurrentCulture]. De forma semelhante ao que foi escrito anteriormente, escreve-se:

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

Estas duas instruções poderiam constar no construtor de cada controlador da aplicação. Mas também se pode querer extrair este código comum a todos os controladores. Seguiremos esta abordagem.

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 do pedido atual
      HttpContext httpContext = HttpContext.Current;
      // analisa-se a solicitação à procura do parâmetro [lang]
      // procura-se nos parâmetros do URL
      string langue = httpContext.Request.QueryString["lang"];
      if (langue == null)
      {
        // procura-se esse parâmetro nos parâmetros enviados
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // procura-se na sessão do utilizador
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1.º 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;
      // alteram-se 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 a sua instanciação direta: só pode ser utilizada se for derivada;
  • linha 9: o construtor da classe será executado sempre que for instanciado um controlador derivado de [I18NController];
  • linha 12: recupera-se o contexto da solicitação HTTP que está a ser processada pelo controlador;
  • linha 15: parte-se do princípio de que o idioma é definido por um parâmetro [lang], que pode ser encontrado em diferentes locais. Procura-se, por 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 utilizador,
    • linha 29: nas preferências de idioma enviadas pelo cliente HTTP,
    • linha 26: se não se tiver encontrado nada, define-se a cultura como [fr-FR];
  • linha 37: a cultura é guardada na sessão. É aí que será recuperada nas solicitações seguintes. O utilizador poderá alterá-la definindo-a nos parâmetros de um comando GET ou POST;
  • linhas 39-40: define-se a configuração regional da vista que será apresentada 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: o [SecondController] deriva do [I18NController]. Desta forma, garante-se que a configuração cultural da vista a apresentar terá sido inicializada;
  • linha 13: utiliza-se o modelo de vista [ViewModel14], que iremos apresentar;
  • linhas 13 e 20: a vista [Action14Get.cshtml] assegura a exibição do formulário.

6.3. Internacionalizar o modelo de vista [ViewModel14]

O modelo de vista [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" }));
      }
      // Expressão regular 1
      try
      {
        DateTime.ParseExact(Regexp1, "dd/MM/yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
      }
      catch
      {
        résultats.Add(new ValidationResult(errorMessage, new string[] { "Regexp1" }));
      }

      // apresenta a lista de erros
      return résultats;
    }
  }
}

Este modelo é a versão internacionalizada do modelo anterior [ViewModel11]. Vamos descrever o mecanismo de internacionalização para o primeiro atributo da primeira propriedade. Os restantes atributos seguem o mesmo mecanismo.


    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
public string Chaine1 { get; set; }

No modelo anterior, [ViewModel11], estas linhas eram as seguintes:


[Required(ErrorMessage = "Information requise")]
public string Chaine1 { get; set; }

Na versão internacionalizada, linha 1, os textos a apresentar são colocados num ficheiro de recursos. Aqui, este ficheiro chama-se [MyResources.resx] (typeof) e foi colocado na raiz do projeto. Chama-se-lhe um ficheiro de recursos.

Criámos aqui três ficheiros de recursos:

  • [MyResources]: recurso por predefinição quando não existe nenhum 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 ficheiro de recursos, proceda da seguinte forma: [1, 2, 3]:

Isto cria o ficheiro de recursos [MyResources2.resx]. Ao clicar duas vezes nele, surge a seguinte página:

Um ficheiro de recursos é um dicionário com chaves e valores associados a essas chaves. A chave é introduzida em [1], o valor em [2] e o âmbito do recurso em [3]. Para que estes recursos sejam legíveis, têm de ter o âmbito [Public]. Voltemos à linha:


    [Required(ErrorMessageResourceType = typeof(MyResources), ErrorMessageResourceName = "infoRequise")]
  • [ErrorMessageResourceType]: designa o ficheiro de recursos. O parâmetro [typeof] é o nome do ficheiro. Este é transformado numa classe pelo processo de compilação e o seu binário é incluído na montagem do projeto. Assim, no final, [MyResources] é o nome da classe dos recursos;
  • [ErrorMessageResourceName = "infoRequise"]: refere-se a uma chave no ficheiro de recursos. Em suma, a linha significa que a mensagem de erro a apresentar é o valor do ficheiro [MyResources] associado à chave [infoRequise].

Para criar a chave [infoRequise] e o valor associado no ficheiro [MyResources], deve-se proceder da seguinte forma:

Introduz-se a chave em [1], o valor em [2] e o âmbito do recurso em [3].

Resta esclarecer um último ponto: o espaço de nomes da classe [MyResources]. Este é definido nas propriedades do ficheiro [MyResources.resx]:

No ficheiro [1], definimos o espaço de nomes da classe [MyResources], que será criada a partir do ficheiro de recursos [MyResources.resx]. Voltemos à linha internacionalizada que estamos a analisar:


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

O operador typeof espera uma classe, neste caso a classe [MyResources]. Para que esta seja encontrada, é necessário importar o seu espaço de nomes 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 ficheiro de recursos [MyResources]. O código desta classe está visível no ficheiro [MyResources.Designer.cs]:

  

Ao clicar duas vezes neste ficheiro, acede-se ao 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 espaço de nomes da classe;
  • linha 11: a chave [infoRequise] tornou-se uma propriedade estática da classe [MyResources]. É acessível através da notação [MyResources.infoRequise]. Além disso, note-se que esta propriedade tem o âmbito [public]. Sem isso, não seria acessível. É importante ter isto em conta, pois, infelizmente, o âmbito por predefinição é [internal] e isso causa erros difíceis de compreender quando se esquece de alterar esse âmbito.

Por que razão existem agora três ficheiros de recursos?

  

Criámos o [MyResources.resx]. Este é o ficheiro de recursos raiz. Em seguida, criamos tantos ficheiros de recursos [MyResources.locale.resx] quantas forem as configurações regionais (idiomas) a gerir. Aqui, gerimos 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], é utilizada a recurso raiz [MyResources.resx].

O conteúdo final de [MyResources.resx] é o seguinte:

 

As mensagens serão apresentadas 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 ficheiro.

O conteúdo final de [MyResources.en-US.resx] é também obtido por cópia do ficheiro e, em seguida, modificado da seguinte forma:

 

Voltemos à vista [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" }));
      }
...
      // apresenta a lista de erros
      return résultats;
}

A linha 7 mostra como recuperar uma mensagem do ficheiro de recursos [MyResources]. Aqui, pretendemos 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 ficheiro 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-lhe o método [ToString].

6.4. Internacionalizar a vista [Action14Get.cshtml]

Alteramos a vista de exibição do formulário da seguinte forma:

  

@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>
}

Nota: linha 14, adapte a versão de jQuery à da sua versão do Visual Studio.

Comecemos pelo mais simples: as linhas 36-38. Estas utilizam as propriedades estáticas da classe [MyResources] que acabámos de descrever. Para aceder à classe [MyResources], é necessário importar o seu espaço de nomes (linha 2).

Nas mensagens internacionalizadas, é necessário incluir também as que são apresentadas pelo framework de validação do lado do cliente. Para tal, é necessário utilizar as bibliotecas JQuery das linhas 17 a 19. Utilizamos os ficheiros JQuery para as duas culturas que gerimos: [fr-FR] e [en-US]. Além disso, talvez se recorde que a vista [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 já não é apenas [fr-FR], mas varia. Assim, estas linhas são agora geradas pela própria vista [Action14Get] nas linhas 21-26. Estas 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á a ser processada. Talvez nos lembremos de que esta foi inicializada pelo construtor da classe [I18NController]:

      // definir o idioma na sessão
      httpContext.Session["lang"] = langue;
      // alteraçã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 referido que a função [$(document).ready] é executada no final do carregamento da página pelo navegador. A sua execução terá como efeito definir a configuração regional do framework de validação do lado do cliente. Com a configuração [en-US], as mensagens de erro do framework serão em inglês e provirão do ficheiro de recursos [MyResources.en-US.resx]. Veremos como.

Agora, analisemos as linhas 57-65:


<!-- 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>
}

Temos aqui um segundo formulário, sendo que o primeiro se encontra nas linhas 31 a 53. Este formulário apresenta, na parte inferior da página, as seguintes ligações:

  • linha 2: o formulário é enviado para a ação [Lang] do controlador [Second]. Por enquanto, não se vê nenhum valor que possa ser enviado;
  • linhas 6 e 7: clicar nos links faz com que a função JavaScript [postForm] seja executada. Onde se encontra esta função? No script [myscripts2.js] referenciado na linha 20 da vista:

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-lhe 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-lhe 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) {
    //Utilizar o plugin 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-40 são as que já se encontram no script [myscripts.js] utilizado no exemplo anterior. Não voltaremos a abordá-las. A função [postForm], executada ao clicar nos links dos idiomas, encontra-se nas linhas 1-20:

  • linha 1: a função aceita dois parâmetros, [lang], que é a cultura escolhida pelo utilizador, e [url], que é a URL para a qual o navegador do cliente deve ser redirecionado após a alteração da cultura. Estes 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 baliza
<input type="hidden" value="xx-XX"/>

onde [xx-XX] é o valor do parâmetro [lang] da função;

  • linha 10: ainda por programação, adiciona-se esta baliza ao segundo formulário. No final, tudo acontece como se esta baliza estivesse presente desde o início no segundo formulário. O seu valor será, portanto, enviado. Era isso que se pretendia;
  • linhas 11-17: repetimos o mesmo mecanismo para uma baliza
<input type="hidden" value="url"/>

onde [url] é o valor do parâmetro [url] da função;

  • linha 19: o segundo formulário é agora enviado. Para qual URL?

É necessário 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]. Temos, então, de 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: recupera apenas o parâmetro denominado [url];
  • linha 22: responde ao cliente para que este seja redirecionado para este URL.

Mas o que aconteceu ao parâmetro denominado [lang]? É preciso agora lembrar que o controlador [SecondController] deriva da classe [I18NController] (linha 1 abaixo). É este controlador que gere o parâmetro [lang]:


  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // recupera-se o contexto do pedido atual
      HttpContext httpContext = System.Web.HttpContext.Current;
      // analisa-se a solicitação à procura 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 esse parâmetro nos parâmetros enviados
        langue = httpContext.Request.Form["lang"];
      }
      if (langue == null)
      {
        // procura-se na sessão do utilizador
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1.º 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;
      // alteram-se as configurações de idioma do thread            
      Thread.CurrentThread.CurrentCulture = new CultureInfo(langue);
      Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
}

No exemplo que estamos a analisar, o parâmetro [lang] é enviado. Será, portanto, encontrado na linha 13, colocado na sessão na linha 31 e utilizado para atualizar a cultura do thread atual nas linhas 33-34.

O que irá acontecer a seguir? Voltemos às ligações:


<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());
      }
...
}

Anteriormente, o construtor da classe [I18NController] é executado:


  public abstract class I18NController : Controller
  {
    public I18NController()
    {
      // recuperar o contexto do pedido atual
      HttpContext httpContext = System.Web.HttpContext.Current;
      // analisa-se a solicitação à procura do parâmetro [lang]
      // procura-se esse parâmetro 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 utilizador
        langue = httpContext.Session["lang"] as string;
      }
      if (langue == null)
      {
        // 1.º 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;
      // alteram-se 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 o seu valor seja [en-US]. Esta cultura torna-se, assim, a cultura do segmento 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 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; }
....

Como a cultura do thread atual é [en-US], será utilizado o ficheiro [MyResources.en-US.resx]. As mensagens de erro serão, portanto, em inglês.

Com o modelo [ViewModel14] instanciado, é apresentada a 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>

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>

Isto garante que o framework de validação irá funcionar com os formatos americanos (data, moeda, números, etc.). Pela mesma razão, as mensagens das linhas 30-32 serão extraídas do ficheiro de recursos [MyResources.en-US.resx] e, por isso, estarão em inglês.

6.5. Exemplos de execução

Eis alguns exemplos de execução:

  • em [1], o formulário em francês; em [2], o formulário em inglês.
  • No [3], do lado do cliente, as mensagens de erro estão agora em inglês.

Se analisarmos o código-fonte da página, verificamos que estas mensagens de erro foram incorporadas na página, ou seja, geradas pela vista ASP.NET [Action14Get] e pelo 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 das datas

A internacionalização é um problema complexo. Assim, analisemos a propriedade [Date1] e o seu calendário:

  

Verifica-se que o calendário é um 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. Podemos, então, escrever na vista [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 este valor.

Os testes mostram que o calendário permanece em francês, mesmo quando a página é apresentada em inglês. Existe também um problema com a outra data do formulário:

No [1], a data continua a ser solicitada no formato francês dd/mm/aaaa (20/11/2013), enquanto o formato americano é mm/dd/aaaa (10/21/2013). Vamos tentar resolver estes dois problemas com uma nova vista e um novo modelo de vista.

O JQuery UI é um projeto derivado do projeto JQuery e oferece componentes para formulários, incluindo um calendário. Este calendário pode ser internacionalizado. É isso que vamos mostrar.

Para começar, vamos adicionar o [JQuery UI] ao nosso projeto.

Depois de instalar o JQuery e o UI, surgem novos elementos no projeto:

  • em [1], a biblioteca [JQuery UI] nas versões normal e minimizada;
  • em [2], a folha de estilo de [JQuery UI];

O calendário JQuery UI está, por predefinição, em inglês. Para ser internacionalizado, é necessário 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, deve-se copiar o conteúdo do ficheiro [jquery.ui.datepicker-fr.js] acima para a pasta [Scripts] do projeto.

O código da nova vista [Action15.cshtml] é obtido através da cópia da vista 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>

Nota: linha 16, adapte a versão do jQuery-ui à que descarregou.

  • linha 15: faz-se referência à folha de estilo de JQuery UI;
  • linha 16: faz-se referência à versão descarregada de JQuery UI;
  • linha 17: faz-se referência ao script do calendário francês que acabámos de descarregar;
  • linha 34: o método [Html.TextBox] irá gerar aqui uma baliza [input] do tipo [text], com o id [Date1] e o nome [Date1];
  • linha 19: quando o carregamento da página estiver concluído, a função JQuery UI [datepicker] será aplicada ao elemento com o id [Date1], ou seja, o elemento da linha 34. Esta função faz com que, quando o utilizador colocar o foco no campo de introdução de dados de [Date1], apareça um calendário que lhe permita introduzir uma data. A função [datepicker] aceita um parâmetro que lhe 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 vista anterior [Action15.cshtml] será o seguinte modelo [ViewModel15]:

O seu código é o do modelo [ViewModel14], ligeiramente alterado. 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" }));
      }

      // apresenta 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 comparação com o modelo anterior [ViewModel14], temos quatro propriedades adicionais:

  • linha 60: a cultura da vista, «fr-FR» ou «en-US». Esta 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. Este 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. Este campo é inicializado na linha 31 do construtor;
  • linha 62: a cadeia de caracteres a apresentar no campo de introdução de dados 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] encontram-se nos ficheiros de recursos [MyResources]. Os ficheiros de recursos em francês [MyResources], [MyResources.fr-FR], [1] e o ficheiro 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);
        }
        // visualização da vista
        return View("Action15", modèle);
}
  • linha 2: o método [Action15] processa tanto o [GET] como o [POST]. Neste último caso, os valores enviados são recuperados no parâmetro [formData];
  • linha 5: recupera-se o método HTTP da consulta;
  • linha 7: cria-se o modelo da vista que será apresentada (o formulário);
  • linhas 8-11: no caso de um comando [GET], o campo de introdução de dados de [Date1] é inicializado com uma cadeia vazia;
  • linhas 12-16: no caso de uma encomenda [POST]:
    • linha 14: o modelo é inicializado com os valores lançados,
    • linha 15: o campo de introdução de [Date1] é inicializado com uma cadeia 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 inglesa;
  • linha 18: a vista [Action15.cshtml] é apresentada com o 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 ver, o tema da internacionalização de uma aplicação é um tema complexo...