c
| Por Leandro Macedo lmacedo@lmacedo.eti.br Leandro Macedo coordena projetos .Net na Sofis Informática no RJ. É um Microsoft Certified Professional, graduado em Tecnologia da Informação e Pós Graduado em Tecnologia e Segurança de Banco de Dados. É membro ativo da comunidade DevAspNet, e pode ser facilmente encontrado na lista de discussão (br.groups.yahoo.com/group/devasPNET) e nas reuniões do grupo. |
|
|
|
|
| Aplicações web multi-idioma com a classe GlobalPage | |
|
|
|
No mundo globalizado de hoje, nada mais comum do que a necessidade de se oferecer
suporte a múltiplos idiomas nos sites corporativos, visando o aumento
das vendas, internacionalizando não só os portais como também
os softwares comercializados por essas empresas.
Este artigo irá abordar a globalização de aplicações
Asp.Net, passando por uma breve introdução, e chegando na implementação
e uso da classe GlobalPage, com o objetivo de facilitar a internacionalização
de aplicações web, aproximando-se das facilidades presentes nos
projetos Windows Forms.
RFC 1766
Os idiomas (culturas) possuem uma padronização quanto a sua identificação,
que deve ser seguida quando trabalhamos com internacionalização.
O documento que rege essa especificação é a RFC 1766 (Tags
for the Identification of Languages), que é uma junção
da ISO 639 (Language Codes) com a ISO 3166 (Country Codes).
A cultura se refere a língua do usuário, opcionalmente combinada
com a localização (país ou região). A língua
deve ser representada por 2 letras minúsculas e a localização
por duas letras maiúsculas, concatenadas com um "-" entre elas.
Alguns exemplos de culturas : pt (língua portuguesa), pt-BR (português
do Brasil), pt-PT (português de Portugal), en-US (inglês dos Estados
Unidos), es-AR (espanhol da Argentina), etc.
Nós podemos obter o nome de todas as culturas específicas utilizando
a classe CultureInfo do namespace System.Globalization, como no exemplo abaixo:
Dim Culturas() As System.Globalization.CultureInfo
Culturas = System.Globalization.CultureInfo.GetCultures(Globalization.CultureTypes.SpecificCultures)
For Each Cultura As System.Globalization.CultureInfo In Culturas
Console.WriteLine(Cultura.Name)
Next
Arquivos de Recurso
Recurso é qualquer dado não executável, que é distribuído
com uma aplicação. Armazenar os dados em arquivos de recurso,
nos permite mudar esses dados sem a necessidade de alterar a aplicação.
Arquivos de recurso são importantes peças no trabalho de globalização,
já que se encaixam perfeitamente bem em mensagens de erro e críticas,
texto de objetos da interface, imagens, etc.
No .Net esses arquivos estão presentes em todo lado, basta pedir para
exibir os arquivos ocultos em nossos projetos windows ou web, que veremos diversos
arquivos com a extensão ".resx" dando flexibilidade a diversos
mecanismos do .Net.
Um ponto importante a destacar, é que esses arquivos ".resx"
são nada mais do que arquivos xml. Pronto; falou-se em flexibilidade,
chegamos no xml, nada mais usual que ele para esse tipo de coisa !
Globalização em Windows Forms
Quem já teve a oportunidade de trabalhar com globalização
em aplicações Windows Forms, percebeu o quanto essa tarefa é
fácil de ser realizada, basta trocar a propriedade "Localizable"
do form para "True", preencher o "Text" dos objetos com
os valores default, e depois ir trocando a propriedade "Language"
e ir preenchendo novamente o "Text" dos controles. Por último
basta mudar a cultura da Thread atual antes de exibir o form, que lá
estarão os controles traduzidos perfeitamente, como em um passe de mágica
!
Como isso está funcionando por "debaixo dos panos" ? Simples,
1 arquivo ".resx" é criado para cada "Language" utilizada
no form, e lá ficam armazenados os textos dos controles em cada cultura
a ser trabalhada, e automaticamente o framework se encarrega de ler o arquivo
respectivo a cultura da thread corrente em tempo de execução,
e preencher os controles com os valores presentes no xml.
Infelizmente não existe esse mecanismo facilitador nos Web Forms, o que
nos leva a pegar uma "inspiração" na forma de trabalho
dos Windows Forms, e a tentar criar algo próximo na Web, que nos leve
a menos trabalho para internacionalizar nossas aplicações Web
!
Iniciando a aplicação de exemplo
Vamos criar uma nova aplicação Web, e chamar de GlobalDemo, nela
vamos criar uma página de login (login.aspx) e um formulário de
cadastro de usuários (cadastro.aspx); criaremos também um user
control chamado menu.ascx que será o menu do nosso site.
Vejam abaixo o código inicial (layout) desses 3 arquivos, não
esquecendo de dar bastante importância ao nome(id) dos controles pois
eles serão utilizados posteriormente para referenciá-los nos arquivos
de recurso. É importante salientar também, que a propriedade "Text"
deverá permanecer em branco.
<%@ Control Language="vb"
AutoEventWireup="false" Codebehind="menu.ascx.vb" Inherits="GlobalDemo.menu"
TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %>
<asp:HyperLink
id="lnk_login"
runat="server"
NavigateUrl="login.aspx"></asp:HyperLink>
-
<asp:HyperLink
id="lnk_cadastro"
runat="server"
NavigateUrl="cadastro.aspx"></asp:HyperLink>
<asp:LinkButton id="cmd_portugues" runat="server" CausesValidation="False">Português</asp:LinkButton>
<asp:LinkButton id="cmd_ingles" runat="server" CausesValidation="False">English</asp:LinkButton>
<asp:LinkButton id="cmd_espanhol" runat="server" CausesValidation="False">Español</asp:LinkButton>
<%@ Page Language="vb" AutoEventWireup="false" Codebehind="login.aspx.vb" Inherits="GlobalDemo.login"%>
<%@ Register TagPrefix="uc1" TagName="menu" Src="menu.ascx" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<title>login</title>
<meta name="GENERATOR" content="Microsoft Visual Studio .NET 7.1">
<meta name="CODE_LANGUAGE" content="Visual Basic .NET 7.1">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<uc1:menu id="Menu1" runat="server"></uc1:menu><BR>
<BR>
<BR>
<BR>
<BR>
<asp:Label id="lbl_login" runat="server"></asp:Label><BR>
<asp:TextBox id="txt_email" runat="server"></asp:TextBox>
<asp:RegularExpressionValidator id="valid_login" runat="server" ControlToValidate="txt_email" ValidationExpression="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*">*</asp:RegularExpressionValidator><BR>
<BR>
<asp:Label id="lbl_senha" runat="server"></asp:Label><BR>
<asp:TextBox id="txt_senha" runat="server" TextMode="Password"></asp:TextBox>
<asp:RequiredFieldValidator id="valid_senha" runat="server" ControlToValidate="txt_senha">*</asp:RequiredFieldValidator><BR>
<BR>
<asp:Button id="cmd_ok" runat="server"></asp:Button>
<asp:ValidationSummary id="ValidationSummary1" runat="server" ShowMessageBox="True" ShowSummary="False"></asp:ValidationSummary>
</form>
</body>
</HTML>
<%@ Page Language="vb" AutoEventWireup="false" Codebehind="cadastro.aspx.vb" Inherits="GlobalDemo.cadastro"%>
<%@ Register TagPrefix="uc1" TagName="menu" Src="menu.ascx" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<title>cadastro</title>
<meta name="GENERATOR" content="Microsoft Visual Studio .NET 7.1">
<meta name="CODE_LANGUAGE" content="Visual Basic .NET 7.1">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<uc1:menu id="Menu1" runat="server"></uc1:menu><BR>
<BR>
<BR>
<asp:Label id="lbl_primeiro_nome" runat="server"></asp:Label>
<asp:TextBox id="txt_nome" runat="server"></asp:TextBox><BR>
<BR>
<asp:Label id="lbl_ultimo_nome" runat="server"></asp:Label>
<asp:TextBox id="txt_sobrenome" runat="server"></asp:TextBox><BR>
<BR>
<asp:Label id="lbl_email" runat="server"></asp:Label>
<asp:TextBox id="txt_email" runat="server"></asp:TextBox><BR>
<BR>
<asp:Label id="lbl_senha" runat="server"></asp:Label>
<asp:TextBox id="txt_senha" runat="server"></asp:TextBox><BR>
<BR>
<asp:Label id="lbl_confirmar" runat="server"></asp:Label>
<asp:TextBox id="txt_confirmar" runat="server"></asp:TextBox><BR>
<BR>
<asp:Button id="cmd_ok" runat="server"></asp:Button>
</form>
</body>
</HTML>
Criando os arquivos de recurso
Por que quando criamos as páginas não preenchemos a propriedade
"Text" dos controles ? Simplesmente porque elas serão multi-língua,
ou seja, seu conteúdo não estará fixo nos controles, mas
sim dentro dos arquivos de recurso de cada cultura.
Nesse exemplo iremos trabalhar com 3 linguagens neutras, sem se prender a localização
específica, serão elas o Português (pt), Inglês (en)
e o Espanhol (es).
Para criar os arquivos ".resx" basta clicar com o botão direito
do mouse sobre o projeto web, e pedir para adicionar um novo item, selecionando
"assembly resource file". Daremos a eles os nomes de textos.pt.resx,
textos.en.resx e textos.es.resx respectivamente.
Para preencher os arquivos basta clicar 2x sobre eles, e em "name"
digitar o "nome da página ou user control" + "."
+ "id do controle", e em "value" digitar o valor que queremos
que apareça no "Text" dos controles traduzido para cada idioma.
A visualização em modo XML dos 3 arquivos ficaria assim:
<root>
<data name="menu.lnk_login">
<value>Entrar</value>
</data>
<data name="menu.lnk_cadastro">
<value>Cadastrar-se</value>
</data>
<data name="login.lbl_login">
<value>Endereço eletrônico</value>
</data>
<data name="login.lbl_senha">
<value>Senha</value>
</data>
<data name="login.cmd_ok">
<value>Entrar</value>
</data>
<data name="login.valid_login">
<value>Email inválido</value>
</data>
<data name="login.valid_senha">
<value>Informe a senha</value>
</data>
<data name="cadastro.lbl_primeiro_nome">
<value>Nome</value>
</data>
<data name="cadastro.lbl_ultimo_nome">
<value>Sobrenome</value>
</data>
<data name="cadastro.lbl_email">
<value>Endereço eletrônico</value>
</data>
<data name="cadastro.lbl_senha">
<value>Senha</value>
</data>
<data name="cadastro.lbl_confirmar">
<value>Confirmar senha</value>
</data>
<data name="cadastro.cmd_ok">
<value>Enviar</value>
</data>
</root>
<root>
<data name="menu.lnk_login">
<value>Sign In</value>
</data>
<data name="menu.lnk_cadastro">
<value>Register</value>
</data>
<data name="login.lbl_login">
<value>Email</value>
</data>
<data name="login.lbl_senha">
<value>Password</value>
</data>
<data name="login.cmd_ok">
<value>Login</value>
</data>
<data name="login.valid_login">
<value>Invalid Email</value>
</data>
<data name="login.valid_senha">
<value>Password is required</value>
</data>
<data name="cadastro.lbl_primeiro_nome">
<value>First name</value>
</data>
<data name="cadastro.lbl_ultimo_nome">
<value>Last name</value>
</data>
<data name="cadastro.lbl_email">
<value>Email</value>
</data>
<data name="cadastro.lbl_senha">
<value>Password</value>
</data>
<data name="cadastro.lbl_confirmar">
<value>Repeat Password</value>
</data>
<data name="cadastro.cmd_ok">
<value>Send</value>
</data>
</root>
<root>
<data name="menu.lnk_login">
<value>Iniciar sesión</value>
</data>
<data name="menu.lnk_cadastro">
<value>Registrar</value>
</data>
<data name="login.lbl_login">
<value>Mail</value>
</data>
<data name="login.lbl_senha">
<value>Contraseña</value>
</data>
<data name="login.cmd_ok">
<value>Entrar</value>
</data>
<data name="login.valid_login">
<value>Informar mail valido</value>
</data>
<data name="login.valid_senha">
<value>Informar contraseña</value>
</data>
<data name="cadastro.lbl_primeiro_nome">
<value>Nombre</value>
</data>
<data name="cadastro.lbl_ultimo_nome">
<value>Último nombre</value>
</data>
<data name="cadastro.lbl_email">
<value>Mail</value>
</data>
<data name="cadastro.lbl_senha">
<value>Contraseña</value>
</data>
<data name="cadastro.lbl_confirmar">
<value>Confirmar Contraseña</value>
</data>
<data name="cadastro.cmd_ok">
<value>Envia</value>
</data>
</root>
Caso eu precisasse incluir imagens (ícones, bitmaps...) no meu arquivo de recursos, eu poderia utilizar o aplicativo ResEditor, que pode ser encontrado (com código fonte) dentro da pasta "Tutorials" do Framework SDK, e que facilita bastante o trabalho.
Criando a classe GlobalPage
O objetivo agora é criar uma classe que leia os arquivos de recurso e
preencha o "Text" dos controles automaticamente, bastando apenas trocar
a herança de nossas páginas de System.Web.UI.Page para GlobalPage.
A classe também deverá manter os arquivos de recurso em Cache,
invalidando-o caso ocorram mudanças nos arquivos ".resx" de
origem.
Vamos então ao código da classe !
Public Class GlobalPage
Inherits System.Web.UI.Page
'propriedade que indica se devemos ou não globalizar a aplicação (cadastrada no web.config)
Private Localizable As String = ConfigurationSettings.AppSettings("Localizable")
'coleção com os objetos ao qual a classe oferece suporte a globalização
Private aObjetos As System.Collections.Specialized.StringCollection
'nomes dos recursos e valores, extraidos do arquivo ".resx" e mantido em cache
Private Recurso As NameObject
'nome da pagina atual para localizar nos recursos
Private ScriptName As String
Public Function PageName() As String
Dim script As String = System.IO.Path.GetFileName(Request.FilePath)
script = script.Substring(0, script.Length - 5).Trim.ToLower
Return script
End Function
No OnLoad, chamamos método que verifica necessidade de globalizar, e inicializa a sessão.
Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
init_global()
MyBase.OnLoad(e)
End Sub
Private Sub init_global()
'se não cadastrado no web.config entende-se que não trabalharemos com globalização
If IsNothing(Localizable) Then
Localizable = "false"
End If
'se estamos trabalhando com globalização
If Localizable = "true" Then
'cria a variavel de sessão que indica a cultura atual
'lendo do web.config ou do Client
If IsNothing(Session("language")) Then
Dim landefa As String = ConfigurationSettings.AppSettings("DefaultLanguage")
If IsNothing(landefa) Then
landefa = DefaultLanguage()
Else
landefa = IIf(landefa.Trim.Length > 1, landefa.Trim, DefaultLanguage())
End If
Dim lan2 As String = landefa.Substring(0, 2).Trim.ToLower
If (Not lan2 = "pt") AndAlso (Not lan2 = "en") AndAlso (Not lan2 = "es") Then
Session("language") = "pt-BR"
Else
Session("language") = landefa
End If
End If
'chama metodos que realizam a globalização
muda_thread()
muda_objetos()
End If
End Sub
'função que obtem a linguagem do Client
Private Function DefaultLanguage() As String
Dim sLan As String = ""
Try
Dim aLan As String() = Request.UserLanguages()
sLan = aLan(0)
If sLan.Trim.Length > 2 Then
sLan = sLan.Substring(0, 3) + sLan.Substring(3, 2).Trim.ToUpper
End If
Catch ex As Exception
End Try
Return sLan
End Function
Private Sub muda_thread()
Dim Lingua As String = Session("language")
Dim cultura As New CultureInfo(Lingua)
System.Threading.Thread.CurrentThread.CurrentUICulture = cultura
System.Threading.Thread.CurrentThread.CurrentCulture = cultura
End Sub
Private Sub muda_objetos()
init_resource()
ScriptName = PageName()
If Page.HasControls Then
For Each oControl As Control In Page.Controls
If oControl.GetType.Name = "HtmlForm" Then
Globaliza(oControl.Controls)
End If
Next
End If
End Sub
Método que carrega o arquivo de recursos para o Cache, caso necessário.
Private Sub init_resource()
'nome do cache que irá armazenar o recurso para essa cultura
Dim cache_name As String = "recurso_" & Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName
'caso essa cultura ainda não esteja no cache
If IsNothing(Cache.Item(cache_name)) Then
'obtem nome base do arquivo de recursos do web.config
Dim file_name As String = ConfigurationSettings.AppSettings("ResFile")
If IsNothing(file_name) Then
file_name = "recursos"
End If
'obtem nome e caminho fisico do arquivo
file_name = AppDomain.CurrentDomain.BaseDirectory.Replace("/", "\").Trim & file_name.Trim & "." & Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName & ".resx"
'instancia o ResourceReader responsavel por carregar o ".resx"
Dim oRM As New ResXResourceReader(file_name.Trim)
Dim oID As IDictionaryEnumerator = oRM.GetEnumerator
Dim oList As New Collections.Specialized.ListDictionary
'preenche um dicionário que será usado de base
'para a coleção de nomes e objetos
For Each dd As DictionaryEntry In oRM
oList.Add(dd.Key, dd.Value)
Next
'instancia a nova coleção com os recursos dessa cultura
Recurso = New NameObject(oList, True)
'insere coleção no Cache com dependência para o arquivo
Cache.Insert(cache_name, Recurso, New Caching.CacheDependency(file_name))
Else
'o arquivo dessa cultura ja está carregado no cache
'então basta ler ele para a propriedade Recurso
Recurso = DirectCast(Cache.Item(cache_name), NameObject)
End If
End Sub
Private Function objeto_valido(ByVal objname As String) As Boolean
If IsNothing(aObjetos) Then
aObjetos = New System.Collections.Specialized.StringCollection
aObjetos.Add("Label")
aObjetos.Add("Button")
aObjetos.Add("LinkButton")
aObjetos.Add("ImageButton")
aObjetos.Add("HyperLink")
aObjetos.Add("CheckBox")
aObjetos.Add("RadioButton")
aObjetos.Add("RequiredFieldValidator")
aObjetos.Add("CompareValidator")
aObjetos.Add("RegularExpressionValidator")
aObjetos.Add("CustomValidator")
aObjetos.Add("HtmlGenericControl")
aObjetos.Add("HtmlInputButton")
End If
Return aObjetos.Contains(objname)
End Function
Protected Function MsText(ByVal str As String) As String
muda_thread()
init_resource()
Return FindValue(str)
End Function
Protected Function MsObject(ByVal str As String) As Object
muda_thread()
init_resource()
Return FindObject(str)
End Function
Private Function FindValue(ByVal KeyText As String) As String
Return Recurso(KeyText)
End Function
Private Function FindObject(ByVal KeyText As String) As Object
Return Recurso(KeyText)
End Function
Private Sub Globaliza(ByVal oControls As ControlCollection, Optional ByVal Uc As String = Nothing)
For Each oFormControl As Control In oControls
If IsChild(oFormControl, "System.Web.UI.UserControl") Then
If Not IsNothing(oFormControl.Controls) Then
Globaliza(oFormControl.Controls, oFormControl.GetType.BaseType.Name.Trim)
End If
Else
Dim str_text As String = Nothing
Dim str_find As String = ""
If Not IsNothing(oFormControl.ID) Then
If IsNothing(Uc) Then
str_find = ScriptName & "." & oFormControl.ID.Trim.ToLower
Else
str_find = Uc & "." & oFormControl.ID.Trim.ToLower
End If
End If
If objeto_valido(oFormControl.GetType.Name) Then
str_text = FindValue(str_find)
End If
If (Not IsNothing(str_text)) AndAlso (Not str_text = "") Then
If oFormControl.GetType.Name = "Label" Then
CType(oFormControl, Label).Text = str_text
ElseIf oFormControl.GetType.Name = "Button" Then
CType(oFormControl, Button).Text = str_text
ElseIf oFormControl.GetType.Name = "LinkButton" Then
CType(oFormControl, LinkButton).Text = str_text
ElseIf oFormControl.GetType.Name = "ImageButton" Then
CType(oFormControl, ImageButton).AlternateText = str_text
ElseIf oFormControl.GetType.Name = "HyperLink" Then
CType(oFormControl, HyperLink).Text = str_text
ElseIf oFormControl.GetType.Name = "CheckBox" Then
CType(oFormControl, CheckBox).Text = str_text
ElseIf oFormControl.GetType.Name = "RadioButton" Then
CType(oFormControl, RadioButton).Text = str_text
ElseIf oFormControl.GetType.Name = "RequiredFieldValidator" Then
CType(oFormControl, RequiredFieldValidator).ErrorMessage = str_text
ElseIf oFormControl.GetType.Name = "CompareValidator" Then
CType(oFormControl, CompareValidator).ErrorMessage = str_text
ElseIf oFormControl.GetType.Name = "RangeValidator" Then
CType(oFormControl, RangeValidator).ErrorMessage = str_text
ElseIf oFormControl.GetType.Name = "RegularExpressionValidator" Then
CType(oFormControl, RegularExpressionValidator).ErrorMessage = str_text
ElseIf oFormControl.GetType.Name = "CustomValidator" Then
CType(oFormControl, CustomValidator).ErrorMessage = str_text
ElseIf oFormControl.GetType.Name = "HtmlGenericControl" Then
CType(oFormControl, HtmlGenericControl).InnerText = str_text
ElseIf oFormControl.GetType.Name = "HtmlInputButton" Then
CType(oFormControl, HtmlInputButton).Value = str_text
End If
End If
End If
Next
End Sub
Usando a classe GlobalPage
Primeiro devemos cadastrar no web.config as nossas 3 chaves de configuração
já citadas acima.
<appSettings>
<add key="Localizable" value="true" />
<add key="ResFile" value="textos" />
<add key="DefaultLanguage" value="pt-BR" />
</appSettings>
Private Sub cmd_portugues_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_portugues.Click
CType(Me.Page, GlobalWeb.GlobalPage).muda_lingua("pt-BR")
End Sub
Private Sub cmd_ingles_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_ingles.Click
CType(Me.Page, GlobalWeb.GlobalPage).muda_lingua("en-US")
End Sub
Private Sub cmd_espanhol_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_espanhol.Click
CType(Me.Page, GlobalWeb.GlobalPage).muda_lingua("es-AR")
End Sub
Por último, basta mudar a herança de nossos web forms para GlobalPage, e pronto ! É só rodar, que a globalização estará implementada !
Caso fosse necessário ler um texto qualquer ou até mesmo um objeto
serializado dentro do recurso, bastaria usar as funções agora
presentes na página MsText(nome_recurso) ou MsObject(nome_recurso).
Conclusão
Neste artigo podemos ver um pouco sobre a necessidade de internacionalizar nossos
produtos, e como o .net framework pode nos ajudar nesta tarefa, tornando-a bastante
simples com o uso de classes personalizadas como a GlobalPage, e o uso de arquivos
de recursos.
Mantenha-se antenado, pois a nova versão do ASP.Net (Whidbey) promete
muitas novidades !
Links
- Aplicação de Exemplo + Código fonte
da classe GlobalPage
- Meu blog: www.lmacedo.eti.br
- RFC 1766 : www.faqs.org/rfcs/rfc1766.html
- ISO 639 : www.w3.org/WAI/ER/IG/ert/iso639.htm
- ISO 3166 : www.w3.org/International/O-misc-iso3166.html
- MSDN : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cptutorials/html/Resources_and_Localization_using_the__NET_Framework_SDK.asp
- Globalização no ASP.net 2.0 : http://msdn.microsoft.com/asp.net/whidbey/default.aspx?pull=/library/en-us/dnvs05/html/ASP2local.asp