Benutzerdefinierte Sprachen mit eingebetteten und externen Ressourcen in .NET Framework
Einführung
Bei der Arbeit sind wir auf ein normales Problem gestoßen, mit dem sich sicher viele Entwickler auseinandersetzen müssen, nämlich Sprachen und benutzerdefinierte Sprachen der Kunden, die ihren Kunden unsere Dienste anbieten.
Nehmen wir zum Beispiel an, dass die Basissprache eine informelle Sprache ist und ein Kunde eine formelle Sprache möchte, weil es sich bei seinen Kunden um ältere Menschen handelt.
Beachten Sie auch, dass diese Lösung für mehrere Sprachen gilt, wir werden Englisch und Spanisch verwenden.
Meine Muttersprache ist nicht Englisch, daher entschuldige ich mich für etwaige Fehler im Tutorial. Wenn Sie Fehler finden und diese beheben möchten, können Sie unter diesem Repo einen Pull-Request öffnen und ich werde ihn gerne genehmigen!
Ressourcen
Ressourcen sind XML-Dateien mit der Erweiterung .resx, die eine Schlüssel/Wert-Struktur haben und wie folgt aussehen:
<?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>
Wir werden Ressourcen für zwei verschiedene Sprachen erstellen: Englisch und Spanisch, daher lautet die Nomenklatur für die Dateien: resources.ISOLANGUAGECODE.resx, zum Beispiel: resource.es.resx für Spanisch und resource.resx für Englisch. Falls wir später Deutsch hinzufügen möchten, wird die Datei resource.de.resx heißen.
Eingebettete Ressource
Eingebettete Ressourcen werden beim Kompilieren des Projekts innerhalb von dll hinzugefügt.
Hier ist ein Bild mit einer eingebetteten Ressource. Wie Sie sehen können, ist die Datei Resource.resx in der Zusammenstellung nicht zu sehen.
Externe Ressource
Auf der anderen Seite handelt es sich bei den externen oder nicht eingebetteten Ressourcen um Ressourcen, die nach der Kompilierung dem Ordner hinzugefügt werden.
Die Ressourcendateien (.resx) befinden sich im Ordner Properties.
Das Projekt aufbauen
Beginnen wir mit einem Konsolenprojekt in .NET Framework.
Erstellen der eingebetteten Ressourcendatei
Fügen Sie eine Ressourcendatei mit einigen Schlüsseln hinzu und stellen Sie sicher, dass es sich um eine eingebettete Ressource handelt, die wir später zum Aktualisieren mit externen Ressourcen verwenden.
Erstellen der externen Ressourcendatei
Um die externen Ressourcen von den eingebetteten zu trennen, fügen wir die externen Ressourcen in einem Ordner mit einem Namen hinzu, damit wir später problemlos darauf zugreifen können.
Tipp: Wenn Sie einen Ordner im Ordner „Eigenschaften“ hinzufügen, ihn außerhalb erstellen und nach innen verschieben möchten, lässt Visual Studio die Erstellung nicht zu
Machen Sie dasselbe wie für die eingebettete Ressource, gehen Sie dann zu properties der Datei und innerhalb von Advanced, Build action und ändern Sie es in: Content und ändern Sie Copy to Output Dictionary in Copy if newer.
So sollte es aussehen:
Hinzufügen eines Schlüssels zu app.config
Da wir coole Entwickler sind und gerne alles gut gemacht haben und NICHT den Code für jeden Client ändern möchten, fügen wir einen Schlüssel zu „appconfig“ hinzu, der den Namen des Ordners enthält, in dem wir nach den Ressourcendateien suchen
<?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>
```Wenn wir also später nach den Ressourcen suchen, wird in `Properties.John` statt in `Properties.Doe` gesucht.
Dies erleichtert auch Änderungen, wenn die Anwendung bereits bereitgestellt ist, da Sie die app.config einfach ändern können.
## Zugriff auf eingebettete Ressourcendateien
Die Verwendung von .NET Framework ist ziemlich einfach. Um auf die eingebetteten Eigenschaften zuzugreifen, müssen wir lediglich das Objekt `Properties` verwenden, das die Ressourcendateien als Eigenschaft enthält, und in diesem Objekt befinden sich alle Schlüssel/Werte, die wir in der Datei `resx` haben.
```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();
}
Wenn wir das ausführen, sollte es uns etwa Folgendes zeigen:
Action_greeting value: Hello, Action_cancel value: Cancel
Zugriff auf externe Ressourcendateien
In diesem Teil ist es ziemlich dasselbe, Sie können über das Objekt Properties auf die Ressourcendateien zugreifen.
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;
Problem
Das Hauptproblem bei dieser Methode besteht darin, dass jeder Schlüssel eine Eigenschaft innerhalb des Objekts ist und wir ihn daher so aufrufen müssen, wie wir es zuvor gesehen haben. Wenn Sie den Schlüssel Action_greeting der Ressourcendatei von John aufrufen möchten, müssen wir Folgendes verwenden: Properties.John und dann Resource.Action_greeting.
Genau da liegt das Problem.
Denn wenn wir eine Anwendung für viele Kunden entwickeln, ist es keine gute Idee, die Art und Weise zu ändern, wie wir die Ressourcendateien für jeden von ihnen aufrufen.
Könnten Sie sich das vorstellen? Kompilieren Sie die Anwendung für jeden Kunden und ändern Sie John in Doe und dann in etwas anderes. Das ist verrückt!
Lösung
Unser Teamleiter hat sich eine ziemlich gute Methode ausgedacht, so etwas wie ein Fallback-System. Wir müssen ein Basismodell von Ressourcen haben und dann für jeden der Kunden eine Ressourcendatei haben, die die Datei mit ihren Ressourcen aktualisiert, und am Ende haben wir eine einzige Liste von Ressourcen.
Wenn der Kunde keine benutzerdefinierten Ressourcen möchte, verwenden wir die Basisressourcen, und wenn er sie möchte, verwenden wir seine.
Um dies in eine Checkliste aufzunehmen, müssen wir Folgendes tun:
- [] Finden Sie eine Möglichkeit, alle Schlüssel/Werte einem Wörterbuch für die eingebetteten Ressourcen zuzuordnen.
- [] Finden Sie eine Möglichkeit, alle Schlüssel/Werte einem Wörterbuch für die externen Ressourcen zuzuordnen
- Mischen Sie beide Dateien und erstellen Sie ein einziges Wörterbuch für jede Sprache
- [] Erstellen Sie eine Methode, die auf das Wörterbuch zugreift und den Wert zurückgibt
Diagramm
Lasst uns programmieren
Erstellen wir zunächst eine separate Klasse, in der wir unsere gesamte Logik haben, vom Abrufen der Ressourcendateien über deren Mischung bis hin zur Rückgabe des Werts. Diese Klasse wird CustomResources heißen.
So sieht es aus:
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)
{
...
}
}
Beachten Sie, dass wir Lazy Loading für Eigenschaften implementieren, was zur Steigerung der Leistung beiträgt und dafür sorgt, dass das Wörterbuch einmal geladen wird.
GetDictionaryFromEmbedded: gibt ein Wörterbuch aus eingebetteten Ressourcen zurück.GetDictionaryFromFile: gibt ein Wörterbuch von externen Ressourcen zurück.OverwriteDictionary: Mischen Sie zwei Wörterbücher und geben Sie ein einziges zurück.GetText: gibt einen Wert bei gegebenem Schlüssel zurück
Von der eingebetteten Ressource zum Wörterbuch
Wir müssen alle Eigenschaften aus der XML-Datei abrufen und ein Wörterbuch zurückgeben:
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;
}
Zwei Dinge:- Beachten Sie, dass ein Parameter namens embedded erforderlich ist. Parementers ist der Name der Datei, die Sie im Designer sehen können. In unserem Fall lautet er: resources-demo.Properties.Resource.
- Außerdem haben wir einen Parameter namens cultreInfoCode, der der Code für die auszuwählende Sprache ist. Zum Glück erledigt .NET Framework die Arbeit für uns und wir müssen nichts tun. Stellen Sie einfach ein, dass wir entweder Englisch oder Spanisch möchten, und es wählt zwischen
resource.es.resxoderresource.resx
Von der externen Ressource zum Wörterbuch
Das Herausfinden aus einer Datei ist etwas kompliziert, aber nicht schwierig. Wir müssen den aktuellen Speicherort der ausführbaren Datei ermitteln, den Speicherort der Ressourcendatei verknüpfen und sie dann in ein Wörterbuch analysieren.
Aber zuerst müssen Sie den Verweis auf System.Windows.Forms hinzufügen, um auf ResXResourceReader zugreifen zu können.
Nun zu unserer GetDictionaryFromFile-Methode:
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;
}
Die Dateiparameter müssen mit dem Speicherort von der ausführbaren Datei zur Ressourcendatei gefüllt werden, in unserem Fall: "\\Properties\\John\\Resource.resx".
Wörterbücher mischen
Wir sind so gut wie fertig. Denken Sie zunächst daran, System.Configuration zu den Referenzen hinzuzufügen, damit Sie auf app.settings zugreifen können.
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;
}
Text aus dem Wörterbuch abrufen
Erstellen Sie eine öffentliche Methode, die eine private Methode aufruft, die eine Sprache auswählt:
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}";
}
}
Beachten Sie, dass Sie den Schalter ändern müssen, wenn Sie weitere Sprachen hinzufügen.
Code zum Eigenschaften-Getter hinzufügen
Da wir jetzt über alle Methoden verfügen, können wir den Getter der öffentlichen Eigenschaft ändern, um die Werte abzurufen.
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;
}
}
- Zuerst erhalten wir den Indentifier aus den app.settings.
- Dann erhalten wir die Basisressourcen, die eingebetteten.
- Danach erhalten wir die benutzerdefinierten Ressourcen und dafür benötigen wir den Ordnernamen (der die Kennung darstellt).
- Dann mischen wir sie und geben den Wert zurück.
All dies wird einmal durchgeführt, daher das Lazy Loading.
Testen
Alles, was mit dem Code zu tun hat, ist fertig, also testen wir es jetzt. Um einen Wert aus dem Wörterbuch zu erhalten, müssen wir die Methode CustomResources.GetText(string key) aufrufen, die den Wert zurückgibt.
Komplette Ressourcendateien werden aktualisiert
Bei diesem Test möchten wir den gesamten Schlüssel/Wert der Ressourcendateien aktualisieren. Wie Sie auf den Bildern sehen können, haben wir dieselben Schlüssel, aber unterschiedliche Werte.
Wir werden John testen und um dies festzulegen, müssen wir die app.config auf <add key="CustomResources.Folder" value="John" /> setzen.
Schauen wir uns nun unsere Basisressourcendatei an (Properties/Resource.es.resx):
Und dann unsere externe Ressourcendatei (Properties/John/Resource.es.resx):
Okay, wenn alles eingestellt ist, führen wir die Konsolenanwendung aus, gehen zum get-Teil der Eigenschaften und überprüfen alles
folderIdentifier hat den Wert des Appsets:
baseResources hat den Wert der Basisressourcen, der eingebetteten:
customResources enthält die Werte der externen Ressourcen im Ordner John:
Und schließlich ist bei _ResourcesSpanish der Wert von den Basisressourcen zu den externen Ressourcen gemischt.
Es wird nur eines aktualisiert
Lassen Sie uns nun dasselbe testen, aber mit einem anderen Szenario. Aktualisieren Sie einfach einen Schlüssel und lassen Sie den anderen gleich.
Wie Sie sehen können, haben die Dateien die gleiche Wertbedeutung für Action_greeting, aber einen anderen Wert für Action_cancel, sodass nur Action_cancel aktualisiert werden sollte.
Die Ressourcendatei fehlt
Wenn Sie die externe Ressourcendatei nicht bereitstellen, spielt das überhaupt keine Rolle, denn da wir eine Datei erwarten, wird bei einem Fehlschlag ein leeres Wörterbuch zurückgegeben, und beim Mischen beider Wörterbücher wird am Ende das Basiswörterbuch angezeigt.
Fehlendes Paar in eingebetteter Ressource
Wenn Sie ein Paar in der externen Ressourcendatei haben, wird es standardmäßig nicht zum endgültigen Wörterbuch hinzugefügt. Sie können dies ändern, indem Sie beim Mischen beider Wörterbücher die Methode OverwriteDictionary() aufrufen und den Parameter addIfDoesntExist auf true setzen.
Verschiedene Sprachen
Wie Sie sehen, haben wir keine Sprache angegeben, da dies alles von der Funktion GetText(string key) erledigt wird, die GetText(string key, string language) aufruft, und der Parameter language von TwoLetterISOLanguageName gefüllt wird, was die aktuelle Sprache unseres Threads zurückgibt.
In meinem Fall habe ich Spanisch als Standardsprache und deshalb wird es immer auf Spanisch angezeigt, wir können aber auch versuchen, Englisch zu verwenden.
Lassen Sie uns ein wenig programmieren und etwas erstellen, um sowohl Spanisch als auch Englisch zu überprüfen.
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)}");
}
Wenn Sie dies ausführen, sollte die Ausgabe etwa so aussehen:
[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
Die ersten beiden Werte stammen aus resources.resx, die auf Englisch sind, und die letzten beiden sind auf Spanisch und die Werte werden aus resources.es.resx abgerufen.
Das ist es
In diesem Tutorial haben wir eine Möglichkeit gefunden, sowohl eingebettete als auch externe Ressourcen für Sprachen zu mischen. Dies war eine Lösung für ein Problem, das wir im Team hatten, und es läuft seitdem ohne Probleme.
Sie können den Quellcode hier überprüfen.
Wenn Sie Fragen haben, twittern Sie mich gerne unter @emimontesdeocaa und ich melde mich bei Ihnen, wenn ich Zeit habe.














