Clique aqui para ler todos os artigos desta edição
Fundamentos básicos da internacionalização do .NET
por John Robbins
Aparentemente, a maioria dos desenvolvedores de software norte-americanos não pensa em tornar seus softwares amigáveis ao resto do mundo. A internacionalização tem sido sempre uma paixão. Recentemente, estive envolvido com o projeto Windows® Forms que tinha um firme propósito de ser totalmente internacionalizado.
À medida que comecei a pesquisar a internacionalização no Microsoft® .NET Framework, eu fiquei agradavelmente surpreso por encontrar suporte completo para vários idiomas. O único problema é que as informações sobre internacionalização estavam espalhadas pela documentação e em vários manuais, o que tornou a inicialização muito mais trabalhosa do que deveria.
Todos os pontos que abordarei sobre a internacionalização neste artigo se aplicarão tanto a aplicativos Windows Forms como ASP.NET. Como os arquivos ResX estão mais relacionados aos aplicativos Windows Forms, serei um pouco mais específico com relação a Windows Forms. Antes de entrar em detalhes, discutirei por que a internacionalização é tão importante, a fim de que você tenha uma boa justificativa para incorporá-la em seus projetos.
Por que se preocupar com a internacionalização?
Do ponto de vista comercial, uma única estatística deveria convencer seu empregador a fornecer um software internacionalizado: a Microsoft obtêm mais de 60 por cento de sua receita de fora dos Estados Unidos. Quanto mais suporte a outros idiomas, formatos numéricos, formatos de data etc. a Microsoft oferece, mais a empresa aumenta as vendas. Por exemplo, se você está trabalhando em .NET, não precisa escrever seu próprio IME (Input Method Editor), que é o que os idiomas orientais, como Japonês e Chinês, usam para digitar seus idiomas baseados em caracteres. Em vez disso, você simplesmente ajusta a configuração e está pronto.
Mesmo que você não pretenda fornecer um aplicativo totalmente internacionalizado, escrever um código internacionalizado significa escrever um código melhor. O primeiro benefício principal reside no fato de que o código é mais fácil de manter porque você não tem nenhuma string hardcoded, por isso alterar a redação em uma tela de UI significa que você pode apenas alterar o recurso, sem precisar forçar a recompilação de todo o aplicativo. Segundo, você obtém uma modularidade de UI muito melhor — o principal objetivo da maioria das pessoas quando se trata de internacionalização. Ao manter a UI e o código o mais separados possível, você facilita o teste de design de interface de usuário. Por esses motivos, você deve sempre trabalhar a internacionalização em todos os aplicativos.
Definições e regras
Se você nunca pensou em internacionalização antes, quero definir alguns conceitos-chave para que você possa compreender o restante do artigo. O conceito de internacionalização mais importante é o de localidade (locale), que descreve o país, a região e as convenções culturais relacionadas ao usuário. A localidade (locale) informa o idioma do usuário, o formato dos números, datas e horas, o formato da moeda e o sistema de unidade de medidas.
Você pode definir sua própria localidade (locale) a qualquer momento no Windows. Abra o ícone Regional and Language Options (Regional Options no Windows 2000) no Painel de Controle e, na guia regional Options (guia Geral, no Windows 2000), selecione uma localidade no primeiro dropdown no canto superior da caixa de diálogo. No Windows XP e nas versões posteriores, você verá exemplos de como os valores formatados serão exibidos. Não existe nenhuma função API pública para alterar a localidade por meio de programação; isso só pode ser feito pelo usuário.
Para determinar a localidade, o .NET segue as convenções especificadas pela RFC- 1766 da IETF (Internet Engineering Task Force), que especifica uma string com o seguinte formato: language code-country/region. Exceto por alguns casos especiais, as duas partes do formato são códigos de dois caracteres. Por exemplo, o espanhol falado no México é "es-MX", e o alemão falado em Liechtenstein é "de-LI". Como você pode ver, o idioma aparece antes do hífen, e a localidade aparece depois. Todos os idiomas e regiões suportados pelo .NET estão listados na documentação referente à classe CultureInfo.
Em alguns dos exemplos apresentados nesta coluna, você verá uma string de localidade composta apenas da primeira parte do idioma, como no caso do francês, "fr". Ao especificar apenas o idioma, você está declarando que o idioma está definido, mas não há nenhuma configuração cultural aplicada. Isto é muito importante ao reutilizar traduções comuns entre culturas diferentes. Analisarei também a cultura invariável, representada pela string vazia (""), que significa que nenhuma linguagem ou cultura foi aplicada. A cultura invariável pode ser usada para armazenar dados em formato neutro, bem como para fornecer a cultura básica para um assembly. Um último comentário sobre a string de localidade é que idiomas como azeri, que possuem variantes como latino e cirílico, serão expressos por meio do complemento "-Latn" ou "-Cyrl," respectivamente, na string de localidade (locale string). Assim, para definir a cultura como Azeri Cirílico (conforme falado no Azerbaijão), a string de localidade será "az-AZ-Cyrl".
Para armazenar os caracteres correntes, o Windows usa Unicode. No Unicode, é possível representar todos os caracteres dos alfabetos do mundo. Quem já passou pela via-crúcis de lidar com caracteres ASCII (8 bits), em que alguns idiomas (como o japonês) possuíam um, dois ou três bytes por caractere, vai adorar o Unicode. O Unicode significa que cada caractere é geralmente armazenado como um valor de 16 bits (UTF-16), embora também existam codificações em 8 bits (UTF-8) e 32 bits (UTF-32). A boa notícia para quem usa o .NET Framework é que o Unicode UTF-16 é o tipo nativo para caracteres e strings. Você só precisa assegurar que todos os seus dados estejam armazenados como o padrão, e o Unicode será fornecido gratuitamente.
Se você estiver compartilhando os dados de string com aplicativos que não oferecem suporte a Unicode 16 bits, as várias classes de codificação (encoding) no namespace System.Text entrarão em ação. Por exemplo, a maioria dos dados enviados usa UTF-8 (Unicode de 8 bits) como tipo de codificação. Para converter uma string .NET padrão em UTF-8, você usaria a classe UTF8Encoding.
Como existe uma classe ASCIIEncoding, você provavelmente pensa que o simples fato de usá-la irá converter automaticamente qualquer string Unicode em seus equivalentes ASCII exatos. Infelizmente, esse não é caso porque a classe ASCIIEncoding usa as páginas em código latino; assim se você estiver tentando converter para o idioma grego, a conversão não será correta. Uma maneira é pegar os bytes da string e a codificação baseada na página de código para a conversão em ASCII e, em seguida, passar a codificação para o método Encoding.Convert. A Listagem 1 mostra a maneira apropriada de converter do idioma grego ASCII para Unicode, e vice-versa.
Tabela 1 Convertendo entre ASCII e Unicode
using System ;
using System.Text ;
using System.Windows.Forms ;
namespace ASCII_To_Unicode
{
class StartUp
{
static void Main ( )
{
// Todos os caracteres gregos em maiúsculas.
Byte[] GreekBytes = {
0xB8 , // Epsilon with Tonos
0xB9 , // Eta with Tonos
0xBA , // Iota wiht Tonos
0xBC , // Omicron with Tonos
0xBE , // Upsilon with Tonos
0xBF , // Omega with Tonos
0x20 , // space
0xC1 , // Alfa
0xC2 , // Beta
0xC3 , // Gama
0xC4 , // Delta
0xC5 , // Epsilon
0xC6 , // Zeta
0xC7 , // Eta
0xC8 , // Teta
0xC9 , // Iota
0xCA , // Kapa
0xCB , // Lambda
0xCC , // Mi
0xCD , // Ni
0xCE , // Xi
0xCF , // Omicron
0xD0 , // Pi
0xD1 , // Ro
0xD3 , // Sigma
0xD4 , // Tau
0xD5 , // Upsilon
0xD6 , // Pi
0xD7 , // Qui
0xD8 , // Psi
0xD9 , // Omega
0x20 , // space
0xDA , // Iota com Dialytika
0xDB } ; // Upsilon com Dialytika
// Obtenha a página de código do idioma grego
Encoding EncodingGreek =
Encoding.GetEncoding(1253);
// Converta os caracteres de bytes do idioma
// grego para Unicode
// 1° parâmetro – Codificação atual dos bytes
// 2° parâmetro – A conversão para codificação
// 3° parâmetro – Os bytes a serem convertidos
Byte[] Cvted = Encoding.Convert (
EncodingGreek ,
Encoding.Unicode ,
GreekBytes ) ;
// Converta de bytes brutos para caracteres
// Unicode
String DoneString = Encoding.Unicode.GetString ( Cvted ) ;
// Como se trata de Unicode, o Windows irá
// exibi-lo corretamente
MessageBox.Show( DoneString , "Greek String");
// Converta a string Unicode para bytes Greek ASCII
Byte[] AsciiBytes =
Encoding.Convert ( Encoding.Unicode,
EncodingGreek,
Encoding.Unicode.GetBytes(DoneString));
// Verifique se a string convertida é igual
if ( GreekBytes.Length == AsciiBytes.Length )
{
for(int i=0 ; i < GreekBytes.Length ; i++)
{
if ( GreekBytes[i] != AsciiBytes[i] )
{
MessageBox.Show("Unicode to ASCII
conversion failed");
}
}
}
else
{
MessageBox.Show ("Failed to convert Unicode
back to ASCII" ) ;
}
}
}
}
A classe CultureInfo
No caso da internacionalização do .NET, a classe CultureInfo do namespace System.Globalization é um item indispensável para todas as suas necessidades culturais. Ela contém tudo, de formatação de números à criação de calendários e classificações. Cada thread possui uma instância CultureInfo padrão onde você pode obter a formatação de localidade apropriada com base na localidade do usuário. Curiosamente, no .NET não existe nenhuma maneira de você definir uma cultura global para todo um processo que possa anular as culturas de thread individuais. Após trabalhar com vários aplicativos em que um dos requisitos-chave consistia em usar uma cultura e idioma diferentes daqueles em uso pelo sistema operacional, eu sei que muitos de vocês irão se debater com esse problema. O programa InternationalizationDemo (incluído no download de código-fonte) mostra uma maneira de atacar esse problema.
Embora você possa pensar que só exista uma maneira de usar o valor CultureInfo por thread, existem na verdade duas, e você logo saberá como isso pode ser feito. Você obtém a primeira instância de CultureInfo através das propriedades estáticas (static) — CultureInfo.CurrentCulture e Thread.CurrentThread.CurrentCulture. A instância CultureInfo armazena as informações de localidade para executar todas as formatações de número, hora, data e calendário. A outra instância de CultureInfo é a cultura de exibição da UI (UI display culture) usada pela classe ResourceManager para carregar recursos. Você pode obter a cultura de exibição de interface de usuário lendo as propriedades estáticas (static properties), CultureInfo.CurrentUICulture e Thread.CurrentThread.CurrentUICulture.
Para mudar a localidade de um thread, defina as propriedades Thread.CurrentThread.CurrentCulture e Thread.CurrentThread.CurrentUICulture. Como Thread.CurrentThread.CurrentCulture é a instância de CultureInfo usada para exibir números, datas e outros itens afins, quando você constrói a nova classe CultureInfo, precisa especificar tanto o idioma como o valor de país/região no formato RFC-1766. Se tentar definir Thread.CurrentThread.CurrentCulture como uma instância de CultureInfo que possua apenas a parte de idioma definida, você receberá uma exceção. Conforme mencionei anteriormente, você pode definir Thread.CurrentThread.CurrentUICulture como uma classe CultureInfo que especifique apenas a parte do idioma em seu construtor. Desse modo, você pode compartilhar recursos de UI, tais como Windows Forms traduzidos, com diferentes culturas que falem o mesmo idioma, mas que não compartilhem outras formatações culturais.
A Listagem 2 mostra o código para uma aplicação console simples que gera a data e hora atuais usando três valores CultureInfo diferentes. O primeiro valor usa o padrão do sistema. No meu caso, ele corresponde ao idioma inglês falado nos Estados Unidos. Após enviar o CultureInfo do thread atual, o segundo valor será a data, exibida no idioma alemão falado na Alemanha. A última exibição é o resultado de se passar uma classe CultureInfo como o segundo parâmetro para o método DateTime.ToString. Como a classe CultureInfo é derivada de IFormatProvider, não será preciso fazer mais nada. A saída final do programa mostra a saída formatada apropriadamente para o idioma francês (falado na Suíça).
Listagem 2 Demo de internacionalização rápida
en-US = Monday, September 22, 2003 3:49:48 PM
de-DE = Montag, 22. September 2003 15:49:48
fr-CH = lundi, 22. septembre 2003 15:49:48
using System ;
using System.Threading ;
using System.Globalization ;
namespace QuickIntlDemo
{
class StartUp
{
static void Main(string[] args)
{
DateTime currTime = DateTime.Now ;
// Mostrar a data atual com base na localidade
// do usuário
Console.WriteLine ( "{0} = {1}" ,
CultureInfo.CurrentCulture.Name ,
currTime.ToString ( "F" )) ;
// Alterar a cultura para o idioma alemão
// falado na Alemanha
CultureInfo deDE = new CultureInfo ("de-DE");
Thread.CurrentThread.CurrentCulture = deDE ;
Thread.CurrentThread.CurrentUICulture = deDE ;
// Mostrar a data atual (agora em alemão)
Console.WriteLine ( "{0} = {1}" ,
CultureInfo.CurrentCulture.Name ,
currTime.ToString ( "F" )) ;
// Criar uma cultura para o francês falado na
// Suíça
CultureInfo frCH = new CultureInfo ("fr-CH");
// Passar a cultura para ToString para mostrar
// que isso pode ser feito
Console.WriteLine ( "{0} = {1}" ,
frCH.Name ,
currTime.ToString ( "F" , frCH ));
}
}
}
No início deste artigo, eu mencionei a cultura invariável. Com essa opção, não é aplicada nenhuma informação cultural, ou seja, ela não faz distinção entre culturas. Ela é importante em dois tipos de situação. A primeira é quando você precisa tomar uma decisão sobre segurança que depende do valor de retorno de comparações de string ou mudança de nomes. String.Compare usa a cultura CultureInfo.CurrentCulture para efetuar a comparação, e as vulnerabilidades de segurança podem atrapalhar o processo se você fizer a comparação com localidades ativas que jamais tiverem sido testadas pelo desenvolvedor. Ao lidar com segurança e strings, passe sempre a cultura invariável para qualquer método de casing ou de comparação, a fim de assegurar que não haja surpresas. Obter a cultura invariável é simples, e as duas linhas em C# a seguir são idênticas:
CultureInfo cultInvariant = new CultureInfo ( "" ) ;
CultureInfo cultInvariant = CultureInfo.InvariantCulture;
O outro uso valioso para a cultura invariável consiste em armazenar os dados em situações não serializadas. Em vez de armazenar os dados em um formato que possa mudar ou exigir uma nova análise (reparsing), você os armazena em um formato conhecido que não tenha nenhuma cultura associado a ele. Para ver uma ótima demonstração de como usar uma cultura invariável, consulte "Using the InvariantCulture Property" (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconusinginvariantcultureproperty.asp).
Por fim, desejo mencionar a ordenação de strings. Quando você cria uma classe CultureInfo ou acessa o padrão do usuário, a ordem de classificação das strings é definida como a classificação padrão para aquele idioma. No entanto, alguns idiomas, como o chinês falado na China (zh-CN), têm duas ordens de classificação. No caso de zh-CN, a ordem de classificação padrão é pela pronúncia, a qual é alternada pela contagem dos traços. Para usar a ordem de classificação alternativa, crie a classe CultureInfo usando o código inteiro (ID) da localidade (LCID) para a ordem de classificação. Você também pode criar uma classe CompareInfo usando o LCID alternativo, se é tudo que realmente precisa. Para obter a lista de idiomas com LCIDs e ordens de classificação alternativas, consulte Comparing and Sorting Data for a Specific Culture (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconsortingdataforspecificculture.asp).
A classificação de strings com Array.Sort usa a CultureInfo.CurrentCulture implicitamente por trás dos panos, como já era esperado. Infelizmente, se você quiser controlar a classificação de um array com base na cultura, nenhuma das sobrecargas do método Sort usará diretamente uma classe CultureInfo ou CompareInfo. Você precisa fornecer uma classe que implemente a interface IComparer. Felizmente, é bastante simples estimular uma para fazer o trabalho necessário. A Listagem 3 mostra a classe CulturalStringComparer que utilizo para garantir que minhas arrays de string permaneçam classificadas de forma apropriada. Ao construir o CulturalStringComparer, você pode obter a classe CompareInfo para o parâmetro a partir da propriedade CompareInfo de uma classe CultureInfo.
Listagem 3 CulturalStringComparer para Array.Sort
internal class CulturalStringComparer : IComparer
{
private CompareInfo m_compareInfo = null ;
public CulturalStringComparer ( CompareInfo
cultureCompareInfo )
{
if ( null == cultureCompareInfo )
{
m_compareInfo =
CultureInfo.CurrentCulture.CompareInfo ;
}
else
{
m_compareInfo = cultureCompareInfo ;
}
}
public int Compare ( Object a , Object b )
{
String sa = a as String ;
String sb = b as String ;
return ( m_compareInfo.Compare ( sa , sb ) ) ;
}
}
As dicas
Neste artigo analisei como lidar com a internacionalização do ponto de vista da programação em .NET. Se estiver usando ASP.NET, você provavelmente tem muitas páginas HTML com texto estático que precisarão ser internacionalizadas. Uma ótima ferramenta a conferir é o Enterprise Localization Toolkit (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/entloctoolkit.asp).
Se precisar formatar rapidamente uma seção selecionada do código com o Visual Studio® .NET, pressione Ctrl+K, Ctrl+F no teclado padrão e o comando Edit.FormatSelection fará o trabalho.
Conclusão
Agora que descrevi os fundamentos básicos da internacionalização no .NET, você deverá ser capaz de escrever um código mais flexível. Se quiser aprender mais, aconselho-o passar um tempo observando a classe CultureInfo para ver como manipular e formatar os vários itens em uma maneira que faça distinção entre as culturas. Considere escrever um programa que utilize os pares de country/region (país/região) na linha de comando e que exiba a data e hora atuais em vários formatos.
Todos os desenvolvedores devem ter o livro Developing International Software por Dr. International (Microsoft Press®, 2003). Esse excelente livro fornece detalhes avançados da internacionalização que você não encontrará em nenhum outro lugar.