Folgender Artikel beschreibt die technische Umsetzung  zweier Methoden um WPF Anwendungen mehrsprachig zu gestalten.

Mögliche Lösungsansätze:

  • Ressource-Files
  • Xml-Files

Ressource-Files (.resx) liegen sehr nahe, da der Zugriff auf diese bereits vom Framework bereitgestellt wird, und da Ressource-Files XML-Files sind, ist auch der Umgang mit ihnen kein Problem. Es gibt aber durchaus Situationen, wo „normale“ XML-Files die bessere Wahl wären – z.B. wenn »Plattform-Unabhängigkeit« , oder »Skalierbarkeit« eine Rolle spielen. Die beiden in Frage kommenden Möglichkeiten werden im Folgenden technisch analysiert und vorgestellt.

Lösungsansatz Resource-Files

Feste Elemente, können in einer Anwendung durch Resources in der jeweiligen Sprache definiert werden. Durch ein einfaches Duplizieren einer bestehenden Resource.SPRACHE.resx und der Anpassung dieser, wird automatisch eine neue Resource.SPRACHE.dll erstellt. Das garantiert ein schnelles Implementieren weiterer Sprachen durch den Entwickler¹. Die verschiedenen Sprachdateien haben aber nun den Nachteil das die entsprechenden Variable redundant vorliegt, was auch ein Erweitern der jeweiligen Sprache beim späteren Hinzufügen einer neuen Variable erschwert.

Technische Umsetzung

Benötigte Tools:

  • ResXFileCodeGeneratorEx – http://dmytro.kryvko.googlepages.com/
  • Klasse CultureResources.cs – http://www.codeproject.com/KB/WPF/WPFLocalize.aspx

Vorbereitung / Installation

  • Order Cultures in der Solution anlegen
  • Die Klasse CultureResources.cs² anlegen
  • Die Resources.resx aus Propertys nach Cultures verschieben, Achtung! Der Namensraum der CodeBehind muss hier aber weiter auf „Properties“ zeigen.
  • Die Settings.settings um „DefaultCulture“ vom Typ „System.Globalization.Culture“ erweitern (Die app.config wird dabei automatisch aktualisiert). Hier kann als Wert die Defaultsprache eingestellt werden, zB. en-US
  • Erstellen einer ResourceDictionary in Cultures mit den Namen ResourceDictionary.xaml mit folgendem Inhalt:

Die ResourceDictionary.xaml in der App.xaml bekannt machen () / ggfls. Mergen

In der View können nun die Inhalte welche übersetzt werden sollen um folgenden Syntax erweitert werden: {Binding Path=HIER.DER.RESOURCE.NAME, Source={StaticResource Resources}}

Beispiel View:

Es empfiehlt sich die Anwendung soweit vorzubereiten und innerhalb einer Resource die Sprachvariablen festzulegen und dann erst diese zu duplizieren und in der jeweiligen Sprache zu übersetzen. Ein späteres Hinzufügen von neuen Variablen ist recht aufwändig, da dieses in jeder erstellten Sprache ausgeführt werden muss. Die Möglichkeit dieses mit der Baml.exe, welche die DLL in *.csv Dateien parsen kann ist zwar gegeben, jedoch bleibt es nicht aus, alle Sprachdateien für sich anzupassen. Auch Bilder, Datei u.a. können über Resources gesteuert werden

——————–

¹Um eine Sprache hinzuzufügen, genügt es eine vorhandene Resources zu duplizieren und anzupassen. Die Resourcedatei muss in den Buildeigenschaften („Benutzerdefiniertes Tool“) um den Suffix „Ex“ (ResXFileCodeGeneratorEx) erweitert werden. Vorher muss vom Entwickler das Tool ResXFileCodeGeneratorEx heruntergeladen und installiert werden (siehe benötigte Tools – ResXFileCodeGeneratorEx)

²Die Klasse Culture beinhaltet Methoden, welche die vorhandenen Resources findet und in einer Liste bereitstellt.

Nachteile von Resources

Resources haben den entscheidenden Nachteil, dass hier während der Laufzeit keine neuen „Sprachvariablen“ hinzugefügt werden können, da dieses ein Neukompilieren voraussetzt, um die entsprechende *.dll zu erzeugen. Des Weiteren ist das Anpassen aller implementierten Resources bei neuen Sprachvariablen (notwendige Schritte: exportieren, anpassen, importieren, kompilieren) umständlich.

Resources sind Redundant zu den darunterliegenden Ebenen, das heißt, dass die Variablen in jeder Sprach.dll sich wiederholen, so entsteht auch die Gefahr von nicht vorhandenen oder „falsch“ geschriebenen Sprachenvariablen.

Diese Implementierung ist in dieser Weise Plattformabhängig und so auch proprietär an Windowssysteme bzw. an .NET Plattformen gebunden.


Lösungsansatz XML Files

Wenn Ressource-Files wegfallen, liegen „normale“ XML-Files nahe. Alle Strings werden in ein zentrales XML-File geladen.
Der Zugriff auf die Strings funktioniert über eine zentrale Klasse, welche die ZugriffsLogik kapseln soll.

Anforderungen:

  • 1-n Sprachen werden unterstützt (muss soweit skalierbar sein, dass neue Sprachen jederzeit hinzugefügt werden können,  ohne dass die Applikation angepasst werden, oder neu kompiliert muss).
  • Die Strings müssen in verschiedene Bereiche (Regions) unterteilt werden können – sonst verliert man schnell den Überblick über die zu verwalteten Strings.
  • Eine StringVariable soll einmalig sein
  • Das Source-File sollte „Notepad-kompatibel“ sein
  • Modularität muss unterstützt werden
  • Muss von allen Systemen unterstützt werden
  • Möglichkeit einfacher Übersetzung durch Outsourcing

Exemplarischer Aufbau einer XML Struktur:


	
		Bearbeiten
		Edit
		Bewerken
		Modifier
	


	
		Arbeitsplatz
		Workplace
		Werkplek
		Poste de Travail
	

Die Unterteilung in die Region-Nodes ermöglicht es, eine gewisse Struktur in das XML-File zu bringen. So behält man auch bei hunderten von Texten den Überblick.

Die Sprachen können frei ergänzt werden, die Bezeichnung der Sprach-Nodes (hier de-DE, en-US, nl-NL, fr-FR) ist dabei egal, da dessen Bezeichnung dann einfach als Parameter mitgegeben wird.
In einer dynamischen Anwendungslandschaft, in deren der Anwender eigene Elemente oder Controls anlegen kann, ist dieses System einzusetzen. Hier kann durch eine Serializierung neuer „Vokabeln“ sichergestellt werden, das die Übersetzung des jeweiligen Elements zur Laufzeit geschehen kann.

Umsetzung in GUI:

Als Datenspeicher für die Daten bieten sich durch das Key / Value – Verfahren Dictionary-Objekte an.
Jede Hierarchie-Stufe (Key, Value) wird in ein eigenes Dictionary-Objekt gespeichert – und diese werden dann entsprechend verschachtelt.

Zwei einfache Methoden für die Realisierung:

  • LoadSourceFile(„language.xml“) Wird nur dazu verwendet, das XML-File in eine XmlDocument-Instanz zu laden, die dann anschliessend in der GetTranslatedDictionaryFromXML verarbeitet wird.
  • GetTranslatedDictionaryFromXML() wird aufgerufen wenn die Standard-Spracheinstellungen genutzt werden. Diese ruft die im folgenden beschriebene Methode mit der default-Spracheinstellung auf und gibt das Dictionary zurück.
  • Die eigentliche Methode GetTranslatedDictionaryFromXML(String language) iteriert die XML und speichert im Key/Value Verfahren die Werte im Dictionary und gibt dieses zurück.
#region Method: GetTranslatedDictionaryFromXML
    public static Dictionary GetTranslatedDictionaryFromXML(String language)
    {
      var xmlDoc = LoadSourceFile(SourceFile);
      var translatedDictionary = new Dictionary();

      try
      {
        foreach (XmlNode regionNode in xmlDoc.SelectNodes(XML_ROOTNODE))
        {
          foreach (XmlNode keyNode in regionNode.ChildNodes)
          {
            foreach (XmlNode value in keyNode.ChildNodes)
            {
              if (language == value.Name)
              {
                Value = value.InnerText;
              }
            }
            Key = keyNode.Attributes["key"].Value;
            translatedDictionary.Add(Key, Value);
          }
        }
      }
      catch (Exception ex)
      {
        throw new Exception("Error - " + SourceFile + ": " + ex.Message);
      }
      return translatedDictionary;
    }
    public static Dictionary GetTranslatedDictionaryFromXML()
    {
      return GetTranslatedDictionaryFromXML(DefaultLanguage);
    }
    #endregion

Hierbei werden beide Methoden zur Verfügung stehen, somit kann optional die gewünschte Sprache mit übergeben werden, anders verwendet die Methode die definierte Default-Sprache.

[Die Besonderheit in diesem Verfahren ist, dass es möglich ist die angezeigten Sprachen zu „mischen“, so könnten Kundenwünsche in der Art „Ich hätte gerne die Inhalte in Englisch, aber die Menüstruktur auf Holländisch“, umgesetzt werden.]

MVVM

In der View wird das Element wie folgt gebunden, diese Methode kann auf alle Elemente abgebildet weredn:


Im ViewModel, wird in das jeweilige ViewModelProperty das Dictionary gespeichert:

Translation = TranslateXml.GetTranslatedDictionaryFromXML();
TranslationFR = TranslateXml.GetTranslatedDictionaryFromXML(“fr-FR“);
TranslationNL = TranslateXml.GetTranslatedDictionaryFromXML(“nl-NL“);

Fazit

Der 2. Lösungsansatz ist dem Ersten vorzuziehen, da hier die Vorteile klar auf der Hand liegen. Es wird gewährleistet, das betriebsystemunabhängige Systeme, gleichermaßen eine einzige LanguageSource bereitsteht, die von allen Systemarchitekturen gelesen werden kann.
Dieses Konzept kann ohne Probleme auch auf andere Datenquellen angewendet werden.

Sprachvariablen liegen in einer XML einmalig vor, so wird verhindert das man sich in einer anderen Sprache verschreibt, oder vergisst diese hinzuzufügen. Weiterhin können XML Dateien als Services bereitgestellt und in Workflows mit eingebunden und ggfl. angepasst oder verändert werden.
Eine Erweiterbarkeit der Sprachdateien während der Laufzeit wird ebenfalls durch diese Lösung gewährleistet. Vorteile dieses Ansatzes liegen sicherlich auch in der einfachen Handhabung des XML-Files.

Es werden keine speziellen Programme oder User-Interfaces benötigt, um die Strings anzupassen, z.B. kann die XML nach Excel exportiert werden und an einem Übersetzer „outgesourced“ werden.

Im Endeffekt muss verglichen werden, wie weit man seine Entwicklung modular aufbauen möchte und wie weit man eine Skalierung des System gewährleisten will. Die Entwicklung zeigt aber, dass es wichtig ist, eine Anwendungslandschaft zu entwickeln, die in klaren Schichten voneinander getrennt ist um so Business und GUI vollkommen voneinander zu trennen. Wenn es später heißt, lasst uns die Anwendung auch auf dem IPhone, Smartphone oder mittels Website in PHP/ASP/Silverlight abbilden, spielen solche Entscheidungen eine gravierende Rolle.

Mehrsprachigkeit / Lokalisierung in WPF
Markiert in:                 

15 Kommentare zu „Mehrsprachigkeit / Lokalisierung in WPF

  • Pingback:WPF Lokalisierung | Biggle's Blog

  • Januar 4, 2010 um 11:52
    Permalink

    Hallo,

    gibts für die Lösung mit XML auch irgendwo ein vollständiges Beispiel?

    Danke!

    MCT

  • Januar 4, 2010 um 22:08
    Permalink

    Hmm ich verstehe deine Frage nicht, ist doch eigtl alles beschrieben?! Die XML liegt vor und wie die GUI diese konsumiert. Welche Art von Beispiel hättest du denn gern : )

  • Januar 5, 2010 um 07:20
    Permalink

    Es ist mir z.B. nicht ersichtlich was die LoadSourceFile-Methode macht. Dann verstehe ich nicht ganz wie ich z.B. eine Übersetzung in meinem Code aus dem Dictionary auslesen kann (z.B. für eine Messagebox).

    Liebe Grüße,

    MCT

  • Januar 5, 2010 um 08:51
    Permalink

    Ok, vergiss mal meine Fragen von gerade. Die haben sich soeben geklärt ;-). Nun habe ich das Dictionary ausgelesen und als Variable im Code zur Verfügung. Was ich nun nicht hinbekomme ist wie ich einen Antrag an ein Element binde. Also wie das mit dem Binding und dem Property funktioniert.

    Dann ist mir aufgefallen du berücksichtigst garnicht die Regionen, oder? Du hast immer alle Übersetzungen im Dictionary?!

    Liebe Grüße,

    MCT

  • Januar 5, 2010 um 11:12
    Permalink

    entweder du legst einen Namen für den Button fest btnMyButton und im CodeBehind dann btnMyButton.Content = Translate.GetString(„CustomerModule“, „SaveCustomer“);

    oder wenn du das mvvm pattern verwendest, dann definierst du ein ViewModelProperty und bindest den Content vom Button an dieses. < Button Content="{Binding MyViewModelProperty}">

  • Januar 5, 2010 um 10:21
    Permalink

    Ja, das stimmt, ich benutze hier in diesem Fall die Regionen nicht, ist aber angedacht gewesen um verschiedene Module damit zu übersetzen.

    Mittlerweile hat sich die Klasse auch etwas verändert und es wird via Linq auf die xml zugegriffen:

    Die XML sieht jetzt wie folgt aus:
    < ?xml version="1.0" encoding="utf-8" standalone="yes"? >

    < Module id="CustomerModule">
    < CultureCode>de-DE< /CultureCode>
    < Key>Customers< /Key>
    < Value>Kunden< /Value>
    < /Module >
    < /Localization >

    public static class Translate
    {
    private const string defaultLang = „de-DE“;
    private static XElement localization = null;

    public static string GetString(string module, string key)
    {
    return GetString(module, key, defaultLang);
    }

    public static string GetString(string module, string key, string lang)
    {
    if(localization == null)
    localization = XElement.Load(„Services/LocalizationService.xml“);

    string result = (from l in localization.Elements()
    where (string)l.Attribute(„id“) == module &&
    l.Element(„Key“).Value == key &&
    l.Element(„CultureCode“).Value == lang
    select l.Element(„Value“).Value).SingleOrDefault();

    //ist der Wert nicht vorhanden, wird der uebergebene Key zurückgegeben.
    if (result == „“)
    result = key;

    return result;
    }
    }

    und das Property initialisierst Du dann so:
    Label = Translate.GetString(„CustomerModule“, „LastName“);

    Zum Binding, hier musst du den DataContext auf this festlegen.

    Ich hoffe ich konnte helfen ; )
    Grüsse Mario

  • Januar 5, 2010 um 10:46
    Permalink

    Erstmal danke bis hier hin, das hat mir schon viel geholfen ;-)

    Eine Frage bleibt aber noch. Wie beschrifte ich nun beispielsweise einen Button mit einem Text aus dem Dictionary?

    In meinem Code habe ich folgendes:

    Dictionary Translation = new Dictionary();

    public Window1()
    {
    InitializeComponent();
    Translation = Translator.GetTranslatedDictionaryFromXML();
    }

    In meiner XAML-Datei ist dann der Button so deklariert:

    Ich weiß, das ist superleichtes Binding, aber ich stehe noch am Anfang und bekomme nun die Schaltfläche nicht beschriftet. Das Dictionary ist bereits gefüllt, das klappt.

    Liebe Grüße und nochmal danke,

    MCT

  • Januar 5, 2010 um 11:28
    Permalink

    Hast du evtl. mal einen Beispielcode für die ViewModelProperty-Lösung? Die hast du ja auch oben in deinem Tutorial verwendet. Ich versteh aber nicht ganz wie das funktioniert :-/

  • Januar 5, 2010 um 15:49
    Permalink

    Dieses Thema ist etwas komplexer, schau dir dazu mal das MVVM Pattern an.

  • April 20, 2010 um 14:04
    Permalink

    Hallo,

    mir würde ein Demo mit funktionsfähigem Beispielcode auch sehr helfen dies zu verstehen. Könntest du -falls es nicht zu viel Umstände macht-
    diesen anbieten?

  • Juni 7, 2010 um 11:36
    Permalink

    Hallo Mario,

    Es würde mich auch freuen, wenn du so nett wärest und ein kleines Beispiel hinzufügen würdest. Ich bin neu mit WPF und manches sitz noch nicht so fest, dass ich es nachvollziehen kann. Danke für deinen Blog.

    Noch eine Frage. Verwendest Du Prism (Composite WPF) und wenn ja wie war deine Erfahrung in Real Projects?

    Gruß,

    Yovanny Rod.

  • Juni 8, 2010 um 14:54
    Permalink

    Hi,

    ich bin atm noch gut beschäftigt, ich werde danach aber mal ein Beispiel fertig stellen.

    Also, stay tuned : )

  • Juni 16, 2010 um 17:09
    Permalink

    Hallo,

    die MVVM-Lösung gefällt mir sehr gut. Aber: Wenn man z.B. 10 Sprachen unterstützt, dann würden bei Deinem Code auch alle 10 Sprachen in den Speicher geladen werden. Gibt es einen ressourcenschonenderen Weg?

    LG
    Sascha

  • Juni 17, 2010 um 08:08
    Permalink

    Hallo Sascha,

    wenn man die gesamte XML in den Speicher lädt, dann ist es so wie du sagst ja, man kann aber auch die XML via LINQ pro Modul/Pages/Klasse o.ä. auslesen und verarbeiten.

    Die MVVM Lösung lässt sich auch mit der Resourcesvariante umsetzen.

Kommentare sind geschlossen.