Localização do Silverlight

Dicas e truques para carregar os recursos de localidade do Silverlight

Matthew Delisle

Baixar o código de exemplo

O Silverlight é uma excelente estrutura para criar aplicativos avançados de Internet (RIAs), mas ainda não oferece o suporte robusto para localização de que você usufrui em outros componentes do Microsoft .NET Framework. O Silverlight tem arquivos .resx, uma classe ResourceManager simples e um elemento no arquivo de projeto. Mas depois disso, você fica por conta própria. Não existem extensões de marcação personalizadas, e não há suporte para a classe DynamicResource.

Neste artigo, vou mostrar a você como remediar todos esses problemas. Vou apresentar uma solução que permitirá que um desenvolvedor carregue conjuntos de recursos em tempo de execução, usar qualquer formato para armazenar recursos, alterar recursos sem recompilar e demonstrar um carregamento lento de recursos.

Este artigo está dividido em três partes: Na primeira, desenvolverei um aplicativo simples usando o processo de localização detalhado pela Microsoft. Em seguida, apresentarei outra solução de localização que tem algumas vantagens em relação ao processo padrão. Por último, concluirei a solução com uma discussão sobre os componentes de back-end necessários para completá-la.

O processo de localização padrão

Começarei criando um aplicativo Silverlight que usa o processo de localização detalhado pela Microsoft. Uma descrição detalhada do processo está disponível em msdn.microsoft.com/library/cc838238(VS.95).

A interface de usuário contém um TextBlock e um Image, como ilustrado na Figura 1.

Figura 1 O aplicativo

O processo de localização descrito pela Microsoft usa arquivos .resx para armazenar os dados de recursos. Os arquivos .resx são incorporados no assembly principal ou em um assembly satélite e carregados uma única vez, durante a inicialização do aplicativo. É possível compilar aplicativos voltados para certos idiomas modificando o elemento SupportedCultures no arquivo de projeto. Este aplicativo de exemplo será localizado para dois idiomas: inglês e francês. Depois de adicionar os dois arquivos de recurso e as duas imagens representando sinalizadores, a estrutura do projeto fica parecida com a Figura 2.

Figura 2 Estrutura do projeto após a adição de arquivos .resx

Alterei a ação de compilação das imagens para conteúdo, assim posso referenciá-las usando uma sintaxe menos detalhada. Adicionarei duas entradas a cada arquivo. O TextBlock é referenciado por meio de uma propriedade chamada Welcome, e o controle Image por uma propriedade chamada FlagImage.

Quando arquivos de recurso são criados em um aplicativo Silverlight, o modificador padrão da classe de recursos gerada é interno. Infelizmente, o XAML não pode ler membros internos, ainda que estejam localizados no mesmo assembly. Para remediar essa situação, os modificadores das classes geradas devem ser mudados para públicos. Isso pode ser feito no modo de exibição de design do arquivo de recurso. O menu suspenso Modificador de Acesso permite designar o escopo da classe gerada.

Uma vez que os arquivos de recurso estejam prontos, você precisa associar os recursos no XAML. Para isso, crie uma classe wrapper com um campo estático fazendo referência a uma instância da classe de recurso. A classe é tão simples quanto esta:

public class StringResources {
  private static readonly strings strings = new strings();
  public strings Strings {  get { return strings; } }
}

Para torná-la acessível a partir do XAML, você precisa criar uma instância. Nesse caso, criarei a instância na classe App para que ela possa ser acessada durante todo o projeto:

<Application.Resources>
  <local:StringResources x:Key="LocalizedStrings"/>
</Application.Resources>

Agora a vinculação de dados no XAML é possível. O XAML de TextBlock e de Image é semelhante a este:

<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" 
  HorizontalAlignment="Center">
  <TextBlock Text="{Binding Strings.Welcome, Source={StaticResource LocalizedStrings}}" 
    FontSize="24"/>
</StackPanel>
<Image Grid.Row="1" Grid.ColumnSpan="2" 
  HorizontalAlignment="Center"
  Source="{Binding Strings.FlagImage, Source={StaticResource LocalizedStrings}}"/>

O caminho é a propriedade String seguida da chave da entrada de recurso. O código-fonte é a instância do wrapper StringResources da classe App.

Definindo a cultura

Existem três configurações de aplicativo que devem ser definidas para o aplicativo selecionar a configuração de cultura do navegador e exibir a cultura apropriada.

A primeira configuração é o elemento SupportedCultures do arquivo .csproj. No momento, não há uma caixa de diálogo no Visual Studio para editar a configuração, por isso o arquivo de projeto deve ser editado manualmente. É possível editar um arquivo de projeto abrindo-o fora do Visual Studio ou descarregando o projeto e selecionando a opção de editar no menu de contexto dentro do Visual Studio.

Para habilitar os idiomas inglês e francês para o aplicativo, o valor do elemento SupportedCultures é parecido com este:

<SupportedCultures>fr</SupportedCultures>

Os valores de culturas são separados por vírgulas. Não é necessário especificar a cultura neutra; ela é compilada na DLL principal.

As próximas etapas são necessárias para escolher a configuração de idioma do navegador. Um parâmetro deve ser adicionado ao objeto incorporado do Silverlight na página da Web. O valor dos parâmetros é a cultura da interface de usuário atual, obtida do servidor. Isso requer que a página da Web seja um arquivo .aspx. A sintaxe do parâmetro é:

<param name="uiculture" 
  value="<%=Thread.CurrentThread.CurrentCulture.Name %>" />

A última etapa obrigatória desse processo é editar o arquivo web.config e adicionar um elemento de globalização dentro do elemento system.web, definindo os valores de seus atributos como automáticos:

<globalization culture="auto" uiCulture="auto"/>

Como mencionado anteriormente, um aplicativo Silverlight tem uma configuração de idioma neutro. Para definir a configuração, vá até a guia Silverlight das propriedades do projeto e clique em Informações do Assembly. A propriedade de idioma neutro está localizada na parte inferior da caixa de diálogo, como mostrado na Figura 3.

Figura 3 Definindo o idioma neutro

Recomendo definir o idioma neutro como um idioma sem região. Essa configuração é uma contingência e é mais útil se abrange uma gama de localidades em potencial. Definir um idioma neutro adiciona um atributo de assembly ao arquivo assemblyinfo.cs, assim:

[assembly: NeutralResourcesLanguageAttribute("en")]

Depois de tudo isso, você tem um aplicativo localizado que lê a configuração de idioma do navegador na inicialização e carrega os recursos apropriados.

Um processo de localização personalizado

As limitações do processo de localização padrão são provenientes do uso de ResourceManager e de arquivos .resx. A classe ResourceManager não altera conjuntos de recursos em tempo de execução com base em alterações de cultura no ambiente. O uso de arquivos .resx prende o desenvolvedor a um conjunto de recursos por idioma e não permite flexibilidade na manutenção dos recursos.

Em resposta a essas limitações, vejamos uma solução alternativa que emprega recursos dinâmicos.

Para tornar os recursos dinâmicos, o gerenciador de recursos precisa enviar uma notificação quando o conjunto de recursos ativo é alterado. Para enviar notificações no Silverlight, você deve implementar a interface INotifyPropertyChanged. Internamente, cada conjunto de recursos será um dicionário com uma chave e um tipo de valor de cadeia de caracteres.

A estrutura Prism e o Managed Extensibility Framework (MEF) são populares para desenvolvimento em Silverlight e dividem o aplicativo em vários arquivos .xap. Para localização, cada arquivo .xap precisa ter sua própria instância do gerenciador de recursos. Para enviar notificações a cada arquivo .xap (cada instância do gerenciador de recursos), preciso acompanhar cada instância que é criada e repetir essa lista quando é necessário enviar notificações. A Figura 4 mostra o código dessa funcionalidade SmartResourceManager.

Figura 4 SmartResourceManager

public class SmartResourceManager : INotifyPropertyChanged {
  private static readonly List<SmartResourceManager> Instances = 
    new List<SmartResourceManager>();
  private static Dictionary<string, string> resourceSet;
  private static readonly Dictionary<string, 
    Dictionary<string, string>> ResourceSets = 
    new Dictionary<string, Dictionary<string, string>>();

  public Dictionary<string, string> ResourceSet {
    get { return resourceSet; }
    set { resourceSet = value;
    // Notify all instances
    foreach (var obj in Instances) {
      obj.NotifyPropertyChanged("ResourceSet");
    }
  }
}

public SmartResourceManager() {
  Instances.Add(this);
}

public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property) {
  var evt = PropertyChanged;

  if (evt != null) {
    evt(this, new PropertyChangedEventArgs(property));
  }
}

Como você pode ver, é criada uma lista estática para conter todas as instâncias do gerenciador de recursos. O conjunto de recursos ativo é armazenado no campo resourceSet e cada recurso que foi carregado é armazenado na lista ResourceSets. No construtor, a instância atual é armazenada na lista Instances. A classe implementa INotifyPropertyChanged da maneira padrão. Quando o conjunto de recursos ativo é alterado, repito a lista de instâncias e disparo o evento PropertyChanged de cada uma.

A classe SmartResourceManager precisa de uma forma de alterar a cultura em tempo de execução e é tão simples quanto um método que usa um objeto CultureInfo:

public void ChangeCulture(CultureInfo culture) {
  if (!ResourceSets.ContainsKey(culture.Name)) {
    // Load the resource set
  }
  else {
    ResourceSet = ResourceSets[culture.Name];
    Thread.CurrentThread.CurrentCulture = 
      Thread.CurrentThread.CurrentUICulture = 
      culture;
  }
}

Esse método verifica se a cultura solicitada já foi carregada. Se não foi, ele carrega a cultura e a define como ativa. Se a cultura já foi carregada, o método simplesmente define o conjunto de recursos correspondente como ativo. O código para carregar um recurso é omitido por enquanto.

Para ser completo, também mostrarei os dois métodos para carregar um recurso por programação (veja a Figura 5). O primeiro método usa apenas uma chave de recurso e retorna o recurso da cultura ativa. O segundo método pega um recurso e um nome de cultura e retorna o recurso dessa cultura específica. 

Figura 5 Carregando recursos

public string GetString(string key) {
  if (string.IsNullOrEmpty(key)) return string.Empty;

  if (resourceSet.ContainsKey(key)) {
    return resourceSet[key];
  }
  else {
    return string.Empty;
  }
}

public string GetString(string key, string culture) {
  if (ResourceSets.ContainsKey(culture)) {
    if (ResourceSets[culture].ContainsKey(key)) {
      return ResourceSets[culture][key];
    }
    else {
      return string.Empty;
    }
  }
  else {
    return string.Empty;
  }
}

Se você executasse o aplicativo agora, todas as cadeias de caracteres localizadas estariam vazias porque nenhum conjunto de recursos foi baixado. Para carregar o conjunto de recursos inicial, vou criar um método chamado Initialize que usa o arquivo de idioma neutro e o identificador de cultura. Esse método deveria ser chamado apenas uma vez durante a vida útil do aplicativo (veja a Figura 6).

Figura 6 Inicializando o idioma neutro

public SmartResourceManager() {
  if (Instances.Count == 0) {
    ChangeCulture(Thread.CurrentThread.CurrentUICulture);    
  }
  Instances.Add(this);
}

public void Initialize(string neutralLanguageFile, 
  string neutralLanguage) {
  lock (lockObject) {
    if (isInitialized) return;
    isInitialized = true;
  }

  if (string.IsNullOrWhiteSpace(neutralLanguageFile)) {
    // No neutral resources
    ChangeCulture(Thread.CurrentThread.CurrentUICulture);
  }
  else {
    LoadNeutralResources(neutralLanguageFile, neutralLanguage);
  } 
}

Ligação a XAML

Uma extensão de marcação personalizada ofereceria a sintaxe de associação mais fluente para os recursos localizados. Infelizmente, não há extensões de marcação personalizadas disponíveis no Silverlight. A vinculação a um dicionário está disponível no Silverlight 3 e nas versões posteriores, e a sintaxe é parecida com o seguinte:

<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" 
  HorizontalAlignment="Center">
  <TextBlock Text="{Binding Path=ResourceSet[Welcome], Source={StaticResource 
SmartRM}}" FontSize="24"/>
</StackPanel>
<Image Grid.Row="1" Grid.ColumnSpan="2" 
  HorizontalAlignment="Center" 
  Source="{Binding ResourceSet[FlagImage], Source={StaticResource SmartRM}}"/>

O caminho contém o nome da propriedade de dicionário com a chave entre colchetes. Se você estiver usando o Silverlight 2, terá duas opções: criar uma classe ValueConverter ou emitir um objeto fortemente tipado usando reflexão. A criação de um objeto fortemente tipado usando reflexão está além do escopo deste artigo. O código de uma ValueConverter seria semelhante ao exibido na Figura 7.

Figura 7 Classe ValueConverter personalizada

public class LocalizeConverter : IValueConverter {
  public object Convert(object value, 
    Type targetType, object parameter, 
    System.Globalization.CultureInfo culture) {
    if (value == null) return string.Empty;

    Dictionary<string, string> resources = 
      value as Dictionary<string, string>;
    if (resources == null) return string.Empty;
    string param = parameter.ToString();

    if (!resources.ContainsKey(param)) return string.Empty;

    return resources[param];
  }
}

A classe LocalizeConverter usa o dicionário e o parâmetro passado e retorna o valor dessa chave no dicionário. Depois de criar uma instância do conversor, a sintaxe de associação seria parecida com esta:

<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" 
  HorizontalAlignment="Center">
  <TextBlock Text="{Binding Path=ResourceSet, Source={StaticResource SmartRM}, Converter={StaticResource LocalizeConverter}, Convert-erParameter=Welcome}" FontSize="24"/>
</StackPanel>
<Image Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Center" 
  Source="{Binding ResourceSet, Source={StaticResource SmartRM}, Converter={StaticResource LocalizeConverter}, ConverterParameter=FlagImage}"/>

A sintaxe é mais detalhada usando o conversor, embora você tenha mais flexibilidade. Todavia, no restante do artigo, continuarei sem o conversor e, no lugar dele, usarei a sintaxe de associação do dicionário.

Configurações de localidade, o retorno

Existem duas configurações de aplicativo que devem ser definidas para que a cultura seja selecionada pelo aplicativo Silverlight. São as mesmas configurações discutidas com o processo de localização padrão. O elemento de globalização no arquivo web.config precisa ter valores culture e uiCulture automáticos:

<globalization culture="auto" uiCulture="auto"></globalization>

Além disso, o objeto Silverlight no arquivo .aspx precisa ser passado como um parâmetro no valor culture da interface de usuário atual do thread:

<param name="uiculture" 
  value="<%=Thread.CurrentThread.CurrentCulture.Name %>" />

Para mostrar a localização dinâmica do aplicativo, vou adicionar alguns botões que facilitam a alteração de cultura, como ilustrado na Figura 8. O evento de clique do botão English é semelhante a este:

(App.Current.Resources["SmartRM"] as SmartResourceManager).ChangeCulture(
  new CultureInfo("en"));

Figura 8 Botões de alteração de cultura

Com alguns dados simulados, o aplicativo executaria e exibiria o idioma apropriado. A solução aqui permite a localização dinâmica em tempo de execução e é extensível o suficiente para carregar os recursos usando uma lógica personalizada.

A próxima seção aborda como preencher a lacuna que restou: onde os recursos são armazenados e como recuperá-los.

Componentes de servidor

Agora vamos criar um banco de dados para armazenar recursos e um serviço Windows Communication Foundation (WCF) para recuperar esses recursos. Em aplicativos maiores, você pode criar camadas de dados e de negócios, mas para este exemplo não usarei abstrações.

O motivo pelo qual escolhi um serviço WCF é a facilidade de criação e a robustez oferecida pelo WCF. O motivo pelo qual optei por armazenar os recursos em um banco de dados relacional é a facilidade de manutenção e administração. Poderia ser criado um aplicativo de administração que permitisse aos tradutores modificar os recursos facilmente.

Estou usando o SQL Server 2008 Express para esse aplicativo. A Figura 9 mostra o esquema de dados.

Figura 9 Esquema para tabelas de localização no SQL Server 2008 Express

Tag é um grupo de recursos nomeado. StringResource é a entidade que representa um recurso. A coluna LocaleId representa o nome da cultura em que está o recurso. A coluna Comment é adicionada para fins de compatibilidade com o formato .resx. As colunas CreatedDate e ModifiedDate são adicionadas para fins de auditoria.

É possível associar uma StringResource a vários Tags. A vantagem disso é que você pode criar grupos específicos (por exemplo, os recursos para uma única tela) e baixar somente esses recursos. A desvantagem é que você pode atribuir vários recursos com os mesmos LocaleId, Key e Tag. Nesse caso, convém criar um gatilho para gerenciar a criação ou a atualização de recursos ou usar a coluna ModifiedDate ao recuperar conjuntos de recursos para determinar qual é o recurso mais recente.

Vou recuperar os dados usando LINQ to SQL. A primeira operação do serviço usará um nome de cultura e retornará todos os recursos associados a ela. Esta é a interface:

[ServiceContract]
public interface ILocaleService {
  [OperationContract]
  Dictionary<string, string> GetResources(string cultureName);
}

Esta é a implementação:

public class LocaleService : ILocaleService {
  private acmeDataContext dataContext = new acmeDataContext();

  public Dictionary<string, string> GetResources(string cultureName) {
  return (from r in dataContext.StringResources
          where r.LocaleId == cultureName
          select r).ToDictionary(x => x.Key, x => x.Value);
  }
}

A operação simplesmente localiza todos os recursos cuja LocaleId é igual ao parâmetro cultureName. O campo dataContext é uma instância da classe LINQ to SQL vinculada ao banco de dados SQL Server. E pronto! LINQ e WCF tornam as coisas tão simples.

Agora é o momento de vincular o serviço WCF à classe SmartResourceManager. Depois de adicionar uma referência de serviço ao aplicativo Silverlight, me registro para receber o evento completo da operação GetResources no construtor:

public SmartResourceManager() {
  Instances.Add(this);
  localeClient.GetResourcesCompleted += 
    localeClient_GetResourcesCompleted;
  if (Instances.Count == 0) {
    ChangeCulture(Thread.CurrentThread.CurrentUICulture);
  } 
}

O método de retorno de chamada deveria adicionar o conjunto de recursos à lista de conjunto de recursos e torná-lo o conjunto ativo. O código é mostrado na Figura 10.

Figura 10 Adicionando recursos

private void localeClient_GetResourcesCompleted(object sender, 
  LocaleService.GetResourcesCompletedEventArgs e) {
  if (e.Error != null) {
    var evt = CultureChangeError;

    if (evt != null)
      evt(this, new CultureChangeErrorEventArgs(
        e.UserState as CultureInfo, e.Error));
  }
  else {
    if (e.Result == null) return;

    CultureInfo culture = e.UserState as CultureInfo;

    if (culture == null) return;

    ResourceSets.Add(culture.Name, e.Result);
    ResourceSet = e.Result;
    Thread.CurrentThread.CurrentCulture = 
      Thread.CurrentThread.CurrentUICulture = culture;
  }
}

O método ChangeCulture precisa ser modificado para fazer uma chamada à operação do WCF:

public void ChangeCulture(CultureInfo culture) {
  if (!ResourceSets.ContainsKey(culture.Name)) {
    localeClient.GetResourceSetsAsync(culture.Name, culture);
  }
  else {
    ResourceSet = ResourceSets[culture.Name];
    Thread.CurrentThread.CurrentCulture = 
      Thread.CurrentThread.CurrentUICulture = culture;
  }
}

Carregando a localidade neutra

Esse aplicativo precisa de uma forma de recuperação se os serviços Web não puderem ser contatados ou se o tempo limite estiver se esgotando. Um arquivo de recurso contendo os recursos de idioma neutro deve ser armazenado fora dos serviços Web e carregado na inicialização. Ele funcionará como contingência e um aprimoramento de desempenho na chamada de serviço.

Vou criar outro construtor SmartResourceManager com dois parâmetros: uma URL apontando para o arquivo de recursos de idioma neutro e um código de cultura identificando a cultura do arquivo de recurso (veja a Figura 11).

Figura 11 Carregando a localidade neutra

public SmartResourceManager(string neutralLanguageFile,   string neutralLanguage) {
  Instances.Add(this);
  localeClient.GetResourcesCompleted += 
    localeClient_GetResourcesCompleted;

  if (Instances.Count == 1) {
    if (string.IsNullOrWhiteSpace(neutralLanguageFile)) {
      // No neutral resources
      ChangeCulture(Thread.CurrentThread.CurrentUICulture);
    }
    else {
      LoadNeutralResources(neutralLanguageFile, neutralLanguage);
    }
  }
}

Se não houver um arquivo de recurso neutro, será executado o processo normal de chamar o WCF. O método LoadNeutralResources usa um WebClient para recuperar o arquivo de recurso do servidor. Em seguida, ele analisa o arquivo e converte a cadeia de caracteres XML em um objeto Dictionary. Não vou mostrar o código aqui porque ele é um pouco grande e um tanto trivial, mas você pode encontrá-lo no código para download deste artigo caso esteja interessado. 

Para chamar o construtor SmartResourceManager parametrizado, preciso mover a instanciação de SmartResourceManager para o code-behind da classe App (porque o Silverlight não dá suporte ao XAML 2009). Não quero codificar o arquivo de recurso nem o código de cultura, então terei de criar uma classe ConfigurationManager personalizada, que você pode ver no código para download.

Após a integração de ConfigurationManager à classe App, o método de retorno de chamada do evento Startup é semelhante a este:

private void Application_Startup(object sender, StartupEventArgs e) {
  ConfigurationManager.Error += ConfigurationManager_Error;
  ConfigurationManager.Loaded += ConfigurationManager_Loaded;
  ConfigurationManager.LoadSettings();
}

O método de retorno de chamada da inicialização agora serve para carregar as configurações de aplicativo e se registrar para retornos de chamada. Se você optar por tornar o carregamento dos parâmetros de configuração uma chamada em segundo plano, fique atento às condições de corrida que poderá encontrar. Aqui estão os métodos de retorno de chamada para os eventos de ConfigurationManager:

private void ConfigurationManager_Error(object sender, EventArgs e) {
  Resources.Add("SmartRM", new SmartResourceManager());
  this.RootVisual = new MainPage();
}

private void ConfigurationManager_Loaded(object sender, EventArgs e) {
  Resources.Add("SmartRM", new SmartResourceManager(
    ConfigurationManager.GetSetting("neutralLanguageFile"), 
    ConfigurationManager.GetSetting("neutralLanguage")));
  this.RootVisual = new MainPage();
}

O método de retorno de chamada do evento Error carrega SmartResourceManager sem um idioma neutro, e o método de retorno de chamada do evento Loaded é carregado com um idioma neutro.

Preciso colocar o arquivo de recurso em um lugar onde eu não tenha de recompilar nada caso o altere. Vou colocá-lo no diretório ClientBin do projeto Web e, depois de criar o arquivo de recurso, vou alterar sua extensão para .xml de modo que fique publicamente acessível e a classe WebClient possa acessá-lo do aplicativo Silverlight. Como o arquivo está publicamente acessível, não coloque dados confidenciais nele.

ConfigurationManager também lê no diretório ClientBin. Ela procura um arquivo chamado appSettings.xml, que é parecido com o seguinte:

<AppSettings>
  <Add Key="neutralLanguageFile" Value="strings.xml"/>
  <Add Key="neutralLanguage" Value="en-US"/>
</AppSettings>

Uma vez que appSettings.xml e strings.xml estejam em vigor, ConfigurationManager e SmartResourceManager podem trabalhar juntas para carregar o idioma neutro. Há espaço para aprimoramentos nesse processo porque, se a cultura ativa do thread for diferente do idioma neutro e o serviço Web estiver inativo, a cultura ativa do thread será diferente do conjunto de recursos ativo. Deixarei isso como um exercício para você.

Conclusão

Um assunto que não abordei neste artigo foi a normalização dos recursos no servidor. Digamos que faltam duas chaves no recurso fr-FR que existem no recurso fr. Ao solicitar os recursos fr-FR, o serviço Web deveria inserir as chaves que estão faltando do recurso fr mais geral.

Outro aspecto dessa solução que não abordei é como carregar recursos por cultura e conjunto de recursos em vez de somente cultura. Isso é útil para carregar recursos por tela ou por arquivo .xap.

No entanto, a solução que apresentei aqui permite que você faça alguns procedimentos úteis, inclusive carregar conjuntos de recursos em tempo de execução, usar qualquer formato para armazenar recursos, alterar recursos sem recompilar e demonstrar um carregamento lento de recursos.

A solução aqui apresentada é de finalidade geral, e você pode se conectar a ela em vários pontos e alterar a implementação consideravelmente. Espero que isso ajude a reduzir sua carga de programação diária.

Para leituras mais detalhadas sobre internacionalização, consulte o livro “.NET Internationalization: The Developer’s Guide to Building Global Windows and Web Applications” (Addison-Wesley, 2006), de Guy Smith-Ferrier. Smith-Ferrier também tem um vídeo excelente sobre internacionalização em seu site; o título do vídeo é “Internationalizing Silverlight at SLUGUK” (bit.ly/gJGptU).

Matthew Delisle gosta de estudar os aspectos de software e de hardware dos computadores. Sua primeira filha nasceu em 2010e ele acha que ela está quase pronta para começar a carreira de programação. Acompanhe o trabalho de Delisle em seu blog, mattdelisle.net.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: John Brodeur