Langages personnalisés utilisant des ressources intégrées et externes dans .NET Framework
#Présentation
Au travail, nous avons rencontré un problème normal auquel je suis presque sûr que de nombreux développeurs doivent faire face, à savoir les langages et les langages personnalisés des clients qui offrent nos services à leurs clients.
Par exemple, disons que la langue de base utilise un langage informel et qu’un client souhaite un langage formel parce que ses clients sont des personnes âgées.
Notez également que cette solution est disponible en plusieurs langues, nous utiliserons l’anglais et l’espagnol.
Ma langue maternelle n’est pas l’anglais. Je m’excuse pour toute erreur dans le tutoriel. Si vous trouvez des erreurs et souhaitez les corriger, vous pouvez ouvrir une pull request sur ce dépôt et je l’approuverai avec plaisir !
Ressources
Les ressources sont des fichiers XML avec l’extension .resx qui ont une structure clé/valeur et ressemblent à ce qui suit :
<?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>
Nous allons créer des ressources pour deux langues différentes : anglais et espagnol, donc la nomenclature des fichiers sera : resources.ISOLANGUAGECODE.resx, par exemple : resource.es.resx pour l’espagnol et resource.resx pour l’anglais, au cas où nous voudrions ajouter plus tard l’allemand, le fichier sera nommé resource.de.resx.
Ressource intégrée
Ressources embarquées, lorsque le projet sera compilé, elles seront ajoutées à l’intérieur du dll.
Voici une image avec une ressource intégrée, comme vous pouvez le voir, le fichier Resource.resx n’est pas visible dans la compilation.
Ressource externe
De l’autre côté les ressources externes ou non embarquées sont des ressources qui seront ajoutées au dossier après compilation.
Les fichiers de ressources (.resx) se trouveront dans le dossier Properties
Construire le projet
Commençons par un projet de console dans .NET Framework.
Création du fichier de ressources intégré
Ajoutez un fichier de ressources avec quelques clés et assurez-vous qu’il s’agit d’une ressource intégrée, que nous utiliserons plus tard pour mettre à jour avec des ressources externes.
Création du fichier de ressources externe
Afin de séparer les ressources externes de celles intégrées, nous ajouterons les ressources externes dans un dossier avec un nom afin de pouvoir y accéder facilement plus tard.
Astuce : pour ajouter un dossier dans le dossier Propriétés, créez-le à l’extérieur et déplacez-le à l’intérieur, Visual Studio ne vous permet pas de le créer
Faites la même chose que pour la ressource intégrée, puis accédez à properties du fichier et à l’intérieur de Advanced, Build action et remplacez-le par : Content et remplacez Copy to Output Dictionary par Copy if newer.
Voici à quoi cela devrait ressembler :
Ajout d’une clé à app.config
Parce que nous sommes des développeurs sympas et que nous aimons que tout soit bien fait et NE PAS changer le code pour chaque client, ajoutons une clé au appconfig qui portera le nom du dossier dans lequel nous rechercherons les fichiers de ressources
<?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>
```Ainsi, plus tard, lorsque nous rechercherons les ressources, il cherchera dans `Properties.John` au lieu de `Properties.Doe`.
Cela facilite également la modification lorsque l'application est déjà déployée puisque vous pouvez facilement modifier le fichier app.config.
## Accès aux fichiers de ressources intégrés
Utiliser .NET Framework est assez simple, donc pour accéder aux propriétés intégrées, il suffit d'utiliser l'objet `Properties`, qui aura les fichiers de ressources comme propriété, et à l'intérieur de cet objet il y aura toute la clé/valeur que nous avons dans le fichier `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();
}
En cours d’exécution, cela devrait nous montrer quelque chose comme ceci :
Action_greeting value: Hello, Action_cancel value: Cancel
Accès aux fichiers de ressources externes
Cette partie est à peu près la même, vous pouvez accéder aux fichiers de ressources par l’objet 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;
Problème
Le principal problème de cette méthode est que chaque clé est une propriété à l’intérieur de l’objet, nous devons donc l’appeler comme nous l’avons vu précédemment. Si vous souhaitez appeler la clé Action_greeting du fichier de ressources de John nous devons utiliser le Properties.John suivant puis Resource.Action_greeting.
C’est là que réside le problème.
En effet, si nous développons une application pour un grand nombre de clients, ce n’est pas une bonne idée de changer la façon dont nous appelons les fichiers de ressources pour chacun d’eux.
Pourriez-vous imaginer cela ? Compiler l’application pour chaque client et remplacer John par Doe puis par autre chose. C’est fou !
#Solution
Notre chef d’équipe a pensé à une très bonne méthode, quelque chose comme un système de secours, nous devons avoir un modèle de base de ressources, puis pour chacun des clients avoir un fichier de ressources qui mettra à jour le fichier avec leurs ressources, et nous nous retrouvons avec une seule liste de ressources.
Si le client ne veut pas de ressources personnalisées, nous utilisons les ressources de base, et s’il les souhaite, nous utilisons les siennes.
Pour mettre cela dans une liste de contrôle, nous devons faire :
- Trouvez un moyen de mapper toutes les clés/valeurs à un dictionnaire pour les ressources intégrées.
- Trouver un moyen de mapper toutes les clés/valeurs à un dictionnaire pour les ressources externes
- Mélangez les deux fichiers et disposez d’un seul dictionnaire pour chaque langue
- Crée une méthode qui accède au dictionnaire et renvoie la valeur
Diagramme
Codons
Tout d’abord, créons une classe séparée où nous aurons toute notre logique, depuis l’obtention des fichiers de ressources jusqu’à leur mélange et le renvoi de la valeur. Cette classe s’appellera CustomResources.
Voilà à quoi ça ressemble :
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)
{
...
}
}
Notez que nous implémentons le chargement paresseux pour les propriétés, ce qui contribue à augmenter les performances et permet de charger le dictionnaire une fois.
GetDictionaryFromEmbedded: renvoie un dictionnaire à partir des ressources embarquées.GetDictionaryFromFile: renvoie un dictionnaire provenant de ressources externes.OverwriteDictionary: mélange deux dictionnaires et renvoie un seul.GetText: renvoie une valeur étant donné une clé
De la ressource intégrée au dictionnaire
Nous devons récupérer toutes les propriétés du fichier XML et renvoyer un dictionnaire :
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;
}
Deux choses :- Notez qu’il a besoin d’un paramètre appelé embedded, que parementers est le nom du fichier que vous pouvez voir dans le concepteur, dans notre cas c’est : resources-demo.Properties.Resource.
- Nous avons également un paramètre appelé cultureInfoCode, qui est le code de la langue à sélectionner. Heureusement pour nous, .NET Framework fait le travail à notre place et nous n’avons rien à faire, il suffit de définir que nous voulons l’anglais ou l’espagnol, et il choisira entre
resource.es.resxouresource.resx
De la ressource externe au dictionnaire
Obtenir du fichier un peu hacky mais pas difficile, nous devons obtenir l’emplacement actuel de l’exécutable, concaténer l’emplacement du fichier de ressources, puis l’analyser dans un dictionnaire.
Mais vous devez d’abord ajouter la référence à System.Windows.Forms, afin d’accéder à ResXResourceReader.
Passons maintenant à notre méthode 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;
}
Les paramètres du fichier doivent être renseignés avec l’emplacement de l’exécutable vers le fichier de ressources, dans notre cas : "\\Properties\\John\\Resource.resx".
Mélanger des dictionnaires
Nous avons presque terminé, tout d’abord, n’oubliez pas d’ajouter System.Configuration aux références pour pouvoir accéder à 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;
}
Récupérer du texte à partir du dictionnaire
Créez une méthode publique qui appelle une méthode privée qui sélectionne une langue :
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}";
}
}
Notez que vous devez modifier le commutateur si vous ajoutez plus de langues.
Ajout de code au getter de propriétés
Puisque nous disposons actuellement de toutes les méthodes, nous pouvons modifier le getter de la propriété publique pour obtenir les valeurs.
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;
}
}
- Nous obtenons d’abord l’identifiant de app.settings.
- Ensuite, nous obtenons les ressources de base, celles intégrées.
- Après cela, nous obtenons les ressources personnalisées et pour cela nous avons besoin du nom du dossier (qui est l’identifiant).
- Ensuite, nous les mélangeons et renvoyons la valeur.
Tout cela sera fait une seule fois, d’où le chargement paresseux.
Tests
Tout ce qui concerne le code est terminé, alors testons-le maintenant, pour obtenir une valeur du dictionnaire, nous devons appeler la méthode CustomResources.GetText(string key) qui renvoie la valeur.
Mise à jour de fichiers de ressources entiers
Ce test est un cas où nous souhaitons mettre à jour l’intégralité de la clé/valeur des fichiers de ressources, comme vous pouvez le voir sur les images, nous avons les mêmes clés mais des valeurs différentes.
Nous allons tester John, et pour définir cela, nous aurons le fichier app.config défini sur <add key="CustomResources.Folder" value="John" />.
Vérifions maintenant notre fichier de ressources de base (Properties/Resource.es.resx) :
Et puis notre fichier de ressources externe (Properties/John/Resource.es.resx) :
Ok, avec tout réglé, nous lançons l’application console, arrêtons-nous à la partie get des propriétés et vérifions tout
folderIdentifier a la valeur de l’appseting :
baseResources a la valeur des ressources de base, celles embarquées :
customResources a les valeurs des ressources externes à l’intérieur du dossier John :
Et enfin, _ResourcesSpanish a la valeur mélangée des ressources de base aux ressources externes.
Mise à jour d’un seul
Testons maintenant la même chose mais avec un scénario différent, mettons simplement à jour une clé et laissons l’autre être la même.
Comme vous pouvez le voir, les fichiers ont la même valeur pour Action_greeting mais une valeur différente pour Action_cancel, ils ne doivent donc mettre à jour que Action_cancel.
Il manque le fichier de ressources
Si vous ne fournissez pas le fichier de ressources externe, cela n’a aucune importance car comme nous nous attendons à avoir un fichier, s’il échoue, il renverra un dictionnaire vide et lors du mélange des deux dictionnaires, il se retrouvera avec celui de base.
Paire manquante dans la ressource intégrée
Si vous avez une paire dans le fichier de ressources externe, par défaut, il ne l’ajoutera pas au dictionnaire final, vous pouvez changer cela en appelant la méthode OverwriteDictionary() lors du mélange des deux dictionnaires et en définissant le paramètre addIfDoesntExist sur true.
Différentes langues
Comme vous pouvez le voir, nous n’avons spécifié aucune langue, car tout cela est fait par la fonction GetText(string key) qui appelle GetText(string key, string language), et le paramètre language est rempli par TwoLetterISOLanguageName qui renvoie la langue actuelle de notre fil.
Dans mon cas, j’ai l’espagnol comme langue par défaut et c’est pourquoi il l’affiche toujours en espagnol, mais nous pouvons aussi essayer d’utiliser l’anglais.
Codons un peu et construisons quelque chose pour vérifier l’espagnol et l’anglais.
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)}");
}
En exécutant ceci, le résultat devrait ressembler à ceci :
[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
Les deux premières valeurs proviennent de resources.resx qui sont en anglais et les dernières sont toutes deux en espagnol et les valeurs sont récupérées de resources.es.resx.
C’est ça
Dans ce tutoriel, nous avons trouvé un moyen de mélanger des ressources intégrées et externes pour les langues, c’était une solution à un problème que nous avions dans l’équipe et cela fonctionne depuis lors sans aucun problème.
Vous pouvez vérifier le code source ici.
Si vous avez des questions, n’hésitez pas à me tweeter à @emimontesdeocaa et je vous répondrai quand j’aurai le temps.














