Linguagens personalizadas usando recursos incorporados e externos no .NET Framework
Introdução
No trabalho, encontramos um problema normal com o qual tenho certeza que muitos desenvolvedores têm que lidar, que são linguagens e linguagens personalizadas dos clientes que estão prestando nossos serviços aos seus clientes.
Por exemplo, digamos que a linguagem base seja uma linguagem informal e um cliente queira uma linguagem formal porque seus clientes são pessoas mais velhas.
Observe também que esta solução é para vários idiomas, usaremos inglês e espanhol.
Minha língua materna não é o inglês. Peço desculpas por quaisquer erros no tutorial. Se você encontrar erros e quiser corrigi-los, você pode abrir uma solicitação pull em este repositório e terei prazer em aprová-lo!
Recursos
Recursos são arquivos XML com extensão .resx que possuem uma estrutura chave/valor e se parecem com o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Action_cancel" xml:space="preserve">
<value>Finish</value>
</data>
<data name="Action_greeting" xml:space="preserve">
<value>Hello</value>
</data>
</root>
Estaremos criando recursos para dois idiomas diferentes: inglês e espanhol, então a nomenclatura dos arquivos será: resources.ISOLANGUAGECODE.resx, por exemplo: resource.es.resx para espanhol e resource.resx para inglês, caso queiramos adicionar posteriormente o alemão, o arquivo será nomeado resource.de.resx.
Recurso incorporado
Recursos incorporados, quando o projeto for compilado, serão adicionados dentro do dll.
Aqui está uma imagem com um recurso incorporado, como você pode ver, o arquivo Resource.resx não pode ser visto na compilação.
Recurso externo
Por outro lado os recursos externos ou não incorporados são recursos que serão adicionados à pasta após a compilação.
Os arquivos de recursos (.resx) estarão dentro da pasta Properties
Construindo o projeto
Vamos começar com um projeto de console no .NET Framework.
Criando o arquivo de recurso incorporado
Adicione um arquivo de recurso com algumas chaves e certifique-se de que seja um recurso incorporado, que posteriormente usaremos para atualizar com recursos externos.
Criando o arquivo de recurso externo
Para separar os recursos externos dos incorporados, iremos adicionar os recursos externos dentro de uma pasta com um nome para que possamos acessá-los facilmente posteriormente.
Dica: para adicionar uma pasta dentro da pasta Propriedades, criá-la fora e movê-la para dentro, o Visual Studio não permite criá-la
Faça o mesmo que fizemos para o recurso incorporado, vá para properties do arquivo e dentro de Advanced, Build action e altere para: Content e altere Copy to Output Dictionary para Copy if newer.
É assim que deve ser:
Adicionando uma chave ao app.config
Como somos desenvolvedores legais e gostamos de ter tudo bem feito e NÃO alterar o código de cada cliente, vamos adicionar uma chave ao appconfig que terá o nome da pasta onde estaremos procurando os arquivos de recursos
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<appSettings>
<add key="CustomResources.Folder" value="John" />
</appSettings>
</configuration>
```Então, mais tarde, quando procurarmos os recursos, ele procurará em `Properties.John` em vez de `Properties.Doe`.
Além disso, isso facilita a alteração quando o aplicativo já está implantado, pois você pode alterar facilmente o app.config.
## Acessando arquivos de recursos incorporados
Usar o .NET Framework é bastante fácil, então para acessar as propriedades do Embedded basta usar o objeto `Properties`, que terá como propriedade os arquivos de recursos, e dentro desse objeto estarão toda a chave/valor que temos no arquivo `resx`.
```csharp
static void Main(string[] args)
{
var hello = Properties.Resource.Action_greeting;
var bye = Properties.Resource.Action_cancel;
Console.WriteLine($"Action_greeting value: {hello}, Action_cancel value: {bye}");
Console.Read();
}
Executando isso deve nos mostrar algo assim:
Action_greeting value: Hello, Action_cancel value: Cancel
Acessando arquivos de recursos externos
Esta parte é praticamente a mesma, você pode acessar os arquivos de recursos pelo objeto Properties.
var hello = Properties.Resource.Action_greeting;
var bye = Properties.Resource.Action_cancel;
var johnhello = Properties.John.Resource.Action_greeting;
var johnbye = Properties.John.Resource.Action_cancel;
var doehello = Properties.Doe.Resource.Action_greeting;
var doebye = Properties.Doe.Resource.Action_cancel;
Problema
O principal problema ao usar este método é que cada chave é uma propriedade dentro do objeto, então temos que chamá-la como vimos antes. Se você quiser chamar a chave Action_greeting do arquivo de recursos de John temos que usar o seguinte Properties.John e depois Resource.Action_greeting.
Aí está o problema.
Isso porque se estamos desenvolvendo uma aplicação para muitos clientes, é uma má ideia mudar a forma como chamamos os arquivos de recursos de cada um deles.
Você consegue imaginar isso? Compilar a aplicação para cada cliente e mudar John para Doe e depois para outra coisa. Isso é loucura!
Solução
Nosso líder de equipe pensou em um método muito bom, algo como um sistema substituto, devemos ter um modelo base de recursos, e então para cada um dos clientes temos um arquivo de recursos que irá atualizar o arquivo com seus recursos, e terminamos com uma única lista de recursos.
Se o cliente não quiser recursos customizados usamos os recursos base, e se ele quiser, usamos os deles.
Para colocar isso em uma lista de verificação, temos que fazer:
- [] Encontre uma maneira de mapear todas as chaves/valores para um dicionário para os recursos incorporados.
- [] Encontre uma maneira de mapear todas as chaves/valores para um dicionário para os recursos externos
- [] Misture os dois arquivos e tenha um único dicionário para cada idioma
- Crie um método que acesse o dicionário e retorne o valor
Diagrama
Vamos codificar
Primeiramente, vamos criar uma classe separada onde teremos toda a nossa lógica, desde a obtenção dos arquivos de recursos, até a mistura deles e o retorno do valor. Essa classe será chamada CustomResources.
Isto é o que parece:
class CustomResources
{
private static Dictionary<string, string> _ResourcesEnglish;
private static Dictionary<string, string> ResourcesEnglish;
private static Dictionary<string, string> _ResourcesSpanish;
private static Dictionary<string, string> ResourcesSpanish;
private static Dictionary<string, string> OverwriteDictionary(Dictionary<string, string> currentDictionary, Dictionary<string, string> newDictionary, bool addIfDoesntExist = false)
{
...
}
private static Dictionary<string, string> GetDictionaryFromEmbedded(string embedded, string cultureInfoCode)
{
...
}
private static Dictionary<string, string> GetDictionaryFromFile(string file)
{
...
}
public static string GetText(string key)
{
...
}
public static string GetText(string key, string language)
{
...
}
}
Observe que estamos implementando o carregamento lento para propriedades, o que ajuda a aumentar o desempenho e faz com que o dicionário seja carregado uma vez.
GetDictionaryFromEmbedded: retorna um dicionário de recursos incorporados.GetDictionaryFromFile: retorna um dicionáriou de recursos externos.OverwriteDictionary: mistura dois dicionários e retorna um único.GetText: retorna um valor dada uma chave
Do recurso incorporado ao dicionário
Temos que pegar todas as propriedades do arquivo xml e retornar um dicionário:
private static Dictionary<string, string> GetDictionaryFromEmbedded(string embedded, string cultureInfoCode)
{
Dictionary<string, string> res = new Dictionary<string, string>();
try
{
ResourceManager rm = new ResourceManager(embedded, Assembly.GetExecutingAssembly());
var resourceSet = rm.GetResourceSet(new CultureInfo(cultureInfoCode), true, true);
var resourceDictionary = resourceSet.Cast<DictionaryEntry>()
.ToDictionary(r => r.Key.ToString(),
r => r.Value.ToString());
res = resourceDictionary;
}
catch (Exception e)
{
string a = e.Message;
// Error getting resource file
}
return res;
}
Duas coisas:- Observe que ele precisa de um parâmetro chamado embedded, que parementers é o nome do arquivo que você pode ver no designer, no nosso caso é: resources-demo.Properties.Resource.
- Também temos um parâmetro chamado cultreInfoCode, que é o código do idioma a ser selecionado. Felizmente para nós, o .NET Framework faz o trabalho para nós e não precisamos fazer nada, apenas definir que queremos inglês ou espanhol, e ele selecionará entre
resource.es.resxouresource.resx
Do recurso externo ao dicionário
Sair do arquivo é um pouco hackeado, mas não difícil, temos que obter a localização atual do executável, concatenar a localização do arquivo de recurso e então analisá-lo em um dicionário.
Mas primeiro você deve adicionar a referência a System.Windows.Forms, para poder acessar ResXResourceReader.
Agora vamos ao nosso método GetDictionaryFromFile:
private static Dictionary<string, string> GetDictionaryFromFile(string file)
{
Dictionary<string, string> res = new Dictionary<string, string>();
string currentPath = (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) + file).Replace("file:\\", "");
try
{
using (ResXResourceReader resxReader = new ResXResourceReader(currentPath))
{
foreach (DictionaryEntry entry in resxReader)
{
res.Add((string)entry.Key, (string)entry.Value);
}
}
}
catch (Exception e)
{
string a = e.Message;
}
return res;
}
Os parâmetros do arquivo precisam ser preenchidos com a localização do executável até o arquivo de recurso, no nosso caso é: "\\Properties\\John\\Resource.resx".
Misturando dicionários
Já terminamos, primeiro lembre-se de adicionar System.Configuration às referências para que você possa acessar app.settings.
private static Dictionary<string, string> OverwriteDictionary(Dictionary<string, string> currentDictionary, Dictionary<string, string> newDictionary, bool addIfDoesntExist = false)
{
var identifier = ConfigurationManager.AppSettings["CustomResources.Folder"];
if (String.IsNullOrEmpty(identifier))
return currentDictionary;
foreach (var item in newDictionary)
{
try
{
currentDictionary[item.Key] = item.Value;
}
catch (Exception)
{
if(addIfDoesntExist)
currentDictionary.Add(item.Key, item.Value);
}
}
return currentDictionary;
}
Obtendo texto do dicionário
Crie um método público que chame um método privado que selecione um idioma:
private static string GetText(string key, string language)
{
try
{
switch (language)
{
case "es":
return ResourceSpanish[key];
case "en":
default:
return ResourceEnglish[key];
}
}
catch (Exception)
{
return $"No value with key: {key} and language: {language}";
}
}
public static string GetText(string key)
{
try
{
return GetText(key, Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName);
}
catch (Exception)
{
return $"No value with key: {key}";
}
}
Observe que você terá que modificar a opção se estiver adicionando mais idiomas.
Adicionando código ao getter de propriedades
Como temos todos os métodos agora, podemos modificar o getter da propriedade pública para obter os valores.
private static Dictionary<string, string> _ResourcesEnglish;
private static Dictionary<string, string> ResourcesEnglish
{
get
{
if (_ResourcesEnglish == null)
{
var folderIndentifier = ConfigurationManager.AppSettings["CustomResources.Folder"]; ;
var baseResources = GetDictionaryFromEmbedded("resources-demo.Properties.Resource");
var customResources = GetDictionaryFromFile($"\\Properties\\{folderIndentifier}\\Resource.resx", "en");
_ResourcesEnglish = OverwriteDictionary(baseResources, customResources);
}
return _ResourcesEnglish;
}
}
private static Dictionary<string, string> _ResourcesSpanish;
private static Dictionary<string, string> ResourcesSpanish
{
get
{
if (_ResourcesSpanish == null)
{
var folderIndentifier = ConfigurationManager.AppSettings["CustomResources.Folder"]; ;
var baseResources = GetDictionaryFromEmbedded("resources-demo.Properties.Resources");
var customResources = GetDictionaryFromFile($"\\Properties\\{folderIndentifier}\\Resources.es.resx", "es");
_ResourcesSpanish = OverwriteDictionary(baseResources, customResources);
}
return _ResourcesSpanish;
}
}
- Primeiro obtemos o identificador de app.settings.
- Depois obtemos os recursos básicos, os incorporados.
- Depois disso obtemos os recursos customizados e para isso precisamos do nome da pasta (que é o identificador).
- Em seguida, misturamos e retornamos o valor.
Tudo isso será feito uma vez, após o carregamento lento.
Teste
Tudo relacionado ao código está finalizado, então agora vamos testar, para obter um valor do dicionário temos que chamar o método CustomResources.GetText(string key) que retorna o valor.
Atualizando arquivos de recursos inteiros
Este teste é um caso em que queremos atualizar toda a chave/valor dos arquivos de recursos, como você pode ver nas imagens temos as mesmas chaves, mas valores diferentes.
Vamos testar John, e para definir isso teremos o app.config definido como <add key="CustomResources.Folder" value="John" />.
Agora vamos verificar nosso arquivo de recurso base (Properties/Resource.es.resx):
E então nosso arquivo de recurso externo (Properties/John/Resource.es.resx):
Ok com tudo definido, rodamos a aplicação console, vamos parar na parte get das propriedades e verificar tudo
folderIdentifier tem o valor do appseting:
baseResources tem o valor dos recursos base, os incorporados:
customResources possui os valores dos recursos externos dentro da pasta John:
E por fim, _ResourcesSpanish tem o valor misturado dos recursos base aos recursos externos.
Atualizando apenas um
Agora vamos testar o mesmo mas com cenário diferente, basta atualizar uma chave e deixar a outra igual.
Como você pode ver, os arquivos têm o mesmo significado de valor para Action_greeting, mas valores diferentes para Action_cancel, portanto, devem ser atualizados apenas Action_cancel.
Faltando o arquivo de recurso
Se você não fornecer o arquivo de recurso externo, não importa nada porque como esperamos ter um arquivo, se falhar retornará um dicionário vazio e ao misturar os dois dicionários terminará com o base.
Par ausente no recurso incorporado
Se você tiver um par no arquivo de recurso externo, por padrão ele não o adicionará ao dicionário final, você pode alterar isso chamando o método OverwriteDictionary() ao misturar os dois dicionários e definir o parâmetro addIfDoesntExist como true.
Diferentes idiomas
Como você pode ver, não especificamos nenhum idioma, porque tudo isso está sendo feito pela função GetText(string key) que está chamando GetText(string key, string language), e o parâmetro language é preenchido por TwoLetterISOLanguageName que retorna o idioma atual do nosso thread.
No meu caso tenho o espanhol como idioma padrão e por isso sempre mostra em espanhol, mas podemos tentar usar o inglês também.
Vamos codificar um pouco e construir algo para verificar espanhol e inglês.
static void Main(string[] args)
{
// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
WriteText("Action_greeting");
WriteText("Action_cancel");
// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
WriteText("Action_greeting");
WriteText("Action_cancel");
Console.Read();
}
private static void WriteText(string key) {
Console.WriteLine($"[Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}] Key: {key} -> value: {CustomResources.GetText(key)}");
}
Executando isso, a saída deve ser algo assim:
[Culture: en-US] Key: Action_greeting -> value: Hello
[Culture: en-US] Key: Action_cancel -> value: Finish
[Culture: es-ES] Key: Action_greeting -> value: Hola
[Culture: es-ES] Key: Action_cancel -> value: Terminar
Os dois primeiros valores são de resources.resx que estão em inglês e os últimos ambos estão em espanhol e os valores são recuperados de resources.es.resx.
#É isso
Neste tutorial encontramos uma forma de mesclar recursos embarcados e externos para idiomas, essa foi uma solução para um problema que tivemos na equipe e está rodando desde então sem problemas.
Você pode verificar o código fonte aqui.
Se você tiver alguma dúvida, sinta-se à vontade para me enviar um tweet para @emimontesdeocaa e entrarei em contato com você quando tiver tempo.














