Dies ist der zweite Teil einer Artikelserie zu Lokalisierung von WPF Anwendungen. Nach einem allgemeinen Überblick über die Lokalisierung in Teil 1 erkläre ich in diesem Artikel, wie man WPF Anwendungen mit der WPF Lokalisierungsapi lokalisieren kann.
Satellitenassemblies
Soll eine Anwendung in mehrere Sprachen übersetzt werden, erzeugt man für jede Sprache eine Satellitenassembly, die alle zu übersetzenden Resourcen (Texte, Bilder usw.) in der jeweiligen Zielsprache enthält.
In WPF wird anders als in Windows Forms oder ASP.NET nicht mehr .resx als Resourcenformat für die Lokalisierung verwendet. Stattdessen werden komplette Formulare bzw. Steuerelemente in übersetzter Form in den Satellitenassemblies der jeweiligen Sprachen gespeichert.
Lokalisierung über .baml Dateien
WPF Formulare und Steuerelemente werden über .xaml Dateien beschrieben. Diese werden beim Übersetzen der Anwendung in ein binäres Format (.baml) übersetzt. Die .baml Dateien werden als Resourcen in den Assemblies der Anwendung gespeichert. So wird z.B. aus einer MainWindow.xaml eine MainWindow.baml, die dann z.B. in der Assembly Anwendung.exe als Resource gespeichert wird.
Wenn nun ein Formular bzw. Steuerelement in einer weiteren Sprache angeboten werden soll, muss die dazugehörige .baml Datei übersetzt als Resource in einer Satellitenassembly der Zielsprache vorliegen.
Um eine WPF Anwendung zu übersetzen müssen wir also die .baml Dateien der Anwendung übersetzen und in Satellitenassemblies speichern.
Außerdem benötigen alle Elemente in unseren .xaml Dateien, die übersetzt werden sollen, eine (pro Datei) eindeutige Bezeichnung in Form eines x:Uid-Attributs (mehr dazu in einem späteren Artikel).
Leider gibt es dafür keine Unterstützung in Visual Studio oder Blend. Auch in Visual Studio 2010 wird sich daran nichts ändern.
Die WPF Lokalisierungs-API
Stattdessen gibt es eine API, über die lokalisierte BAML Dateien erzeugt werden können. Diese API besteht im Kern aus einer Klasse, der Klasse BamlLocalizer.
BamlLocalizer
Die Klasse BamlLocalizer kann aus einer .baml Datei Resourcen extrahieren (ExtractResources-Methode) und später mit übersetzten Resourcen eine lokalisierte .baml Datei erzeugen (UpdateBaml-Methode).
Zunächst benötigen wir Zugriff auf die .baml Datei. Wir haben ja bisher nur die .xaml Dateien, für die BamlLocalizer-Klasse benötigen wir aber die binäre Form der .xaml Dateien im BAML Format.
Für den Zugriff auf die .baml Dateien bieten sich mindestens zwei Wege an: wir könnten die .baml Dateien aus dem fertig übersetzten Assemblies laden, oder die Dateien verwenden, die als “Zwischenprodukt” bei der Übersetzung unserer Anwendung anfallen.
Die erste Variante wird von Microsoft empfohlen und in Beispielen für die Lokalisierungs-API verwendet. Persönlich finde ich diesen Weg jedoch umständlicher, in meinen eigenen Lokalisierungstools verwende ich deshalb die .baml Dateien, die sowieso als “Abfallprodukt” bei der Übersetzung unseres Projekts erzeugt werden (diese Dateien befinden sich normalerweise “irgendwo” im Unterverzeichnis “obj” des WPF-Projekts).
Um eine .baml Datei übersetzen zu können, benötigen wir Zugriff auf die in der .baml Datei enthaltenen Resourcen (also Texte und sonstige Eigenschaften von Objekten, die wir übersetzen bzw. anpassen möchten). Diese Resourcen erhalten wir, indem wir die .baml Datei als Stream an den BamlLocalizer übergeben und die ExtractResources-Methode aufrufen.
BAML Resourcen – Das BamlLocalizationDictionary
Von der ExtractResources-Methode erhalten wir ein BamlLocalizationDictionary-Objekt, das alle Resourcen der .baml Datei enthält.
Jede Resource besteht aus einem Schlüssel und dazugehörigen Werten.
Der Schlüssel ist vom Typ BamlLocalizableResourceKey. Er besteht aus dem Namen der Eigenschaft, die in dem Eintrag gespeichert ist (PropertyName, z.B. “Text”), der Klasse, zu der diese Eigenschaft gehört (ClassName, z.B. “TextBlock”), dem Namen der Assembly, in der diese Klasse gespeichert ist (AssemblyName) und einer eindeutigen Bezeichnung (Uid). Die Eigenschaften des Schlüssels sind nicht veränderbar.
Zu jedem Schlüssel sind verschiedene Werte in Form eines BamlLocalizableResource-Objekts gespeichert. Die Wichtigste Eigenschaft dieses Objekts ist die Eigenschaft Content, über die der Wert der Eigenschaft gespeichert wird (z.B. “Hello World” als Wert für die Text-Eigenschaft des TextBlock-Objekts). Das ist der Wert, den wir übersetzen möchten. In den weiteren Eigenschaften können wir Informationen für den Übersetzer hinterlegen, der unsere Resourcen für uns übersetzen soll. In der Eigenschaft Comments können wir z.B. Kommentare und Hinweise für den Übersetzer speichern. Modifiable legt fest, ob der Übersetzer überhaupt diese Eigenschaft übersetzen darf. Mit Readable können wir festlegen, ob eine Eigenschaft für den Übersetzer sichtbar ist oder nicht. Über LocalizationCategory werden Resourcen in verschiedene Kategorien einteilen, z.B. “Text”, “Label”, “Button” usw..
Um die Einträge im BamlLocalizationDictionary übersetzen zu können, müssen wir die Inhalte des Dictionaries in irgendeiner Form speichern, z.B. in eine Datei oder in eine Datenbank. Dafür gibt es keine Klassen in der WPF Lokalisierungs-API, wir müssen also eigenen Code schreiben. Es gibt lediglich ein Beispiel dafür in der MSDN (locbaml), zu dem ich später noch etwas schreiben werde.
Diese Datei können wir nun selbst übersetzen oder an einen Übersetzer weitergeben, der die Datei für uns übersetzt. Problematisch ist dabei allerdings, dass so sehr leicht ungültige Dateien erstellt werden können (z.B. wenn bei einer Eigenschaft wie “Width” ein Text statt einer Zahl eingegeben wird, ein Teil des Schlüssels geändert wird, oder einfach nur ein Komma falsch gesetzt wrid).
Aus der übersetzten Datei müssen wir nun wieder ein BamlLocalizationDictionary erzeugen. Dafür ist wieder eigener Code erforderlich.
Wir haben nun ein BamlLocalizationDictionary mit den übersetzten Resourcen. Dieses können wir an die UpdateBaml-Methode der BamlLocalizer-Klasse übergeben, um eine lokalisierte .baml Datei zu erzeugen.
Lokalisierte Satellitenassemblies erzeugen
Als Ergebnis erhalten wir eine lokalisierte .baml Datei. Diese müssen wir nun noch in einer Satellitenassembly speichern. Dafür gibt es wieder mindestens zwei Alternativen.
Microsofts locbaml-Beispiel erzeugt die Satellitenassembly selbst komplett neu. Das Beispiel berücksichtigt allerdings keine Resourcen aus anderen Quellen (z.B. .resx), so dass eine so erzeugte Satellitenassembly die Satellitenassembly mit den .resx-Resourcen einfach überschreibt. Außerdem kann locbaml keine signierten Satellitenassemblies erzeugen.
In meinen eigenen Lokalisierungstools nutze ich stattdessen die MSBuild-Skripte aus, die Visual Studio zur Kompilierung von WPF-Projekten verwendet. Die lokaliiserten .baml Dateien werden so von MSBuild genauso behandelt wie evtl. vorahndene andere Resourcen im Projekt (.resx usw.), und von den WPF-MSBuild-Skripten selbst in die Satellitenassemblies geschrieben. Dadurch gibt es keine Probleme mit anderen Resourcen, und die Satellitenassemblies werden “ganz normal” signiert, falls das so in den Projekteigenschaften konfiguriert wurde. Eventuell schreibe ich später dazu einen eigenen Artikel, da ich diesen Ansatz bisher nirgendwo sonst gesehen habe.
Fazit
Lokalisierung über die WPF Lokalisierungs-API ist umständlich. Fertige Tools gibt es nicht, es gibt lediglich ein Beispielprogramm für die API (dazu später mehr in einem eigenen Artikel).
Vorteil dieses Ansatzes ist, dass am Xaml-Code selbst keine Änderungen erforderlich sind, mal abgesehen von zusätzlichen x:Uid Attributen. Somit gibt es keine Probleme bei der Darstellung mit anderen Tools (Blend oder Visual Studio).
In den nächsten Teilen dieser Serie werde ich eventuell noch Alternativen zur WPF Lokalisierungs-API vorstellen. Außerdem möchte ich die Verwendung des locbaml-Beispiels erklären und zeigen, wie sich das locbaml-Beispiel anpassen und erweitern lässt.
Technorati Tags:
WPF,
Lokalisierung