Lokalisierung in WinUI 3: Deutsch und Englisch mit .resw-Dateien (Teil 16)
Wenn eine App mehrere Sprachen unterstützen soll, braucht man eine Infrastruktur, die UI-Texte von der Programmlogik trennt. In WinUI 3 geschieht das über .resw-Ressourcendateien und das x:Uid-Attribut in XAML. Dieses Konzept heißt i18n — kurz für „Internationalization“, weil zwischen dem ersten „I“ und dem letzten „n“ genau 18 Buchstaben liegen.
Ressourcendateien: Wo leben die Texte?
WinUI 3 erwartet Ressourcendateien in einer festen Ordnerstruktur innerhalb des App-Projekts. Der Ordnername ist der BCP-47-Sprachcode — ein standardisiertes Format für Sprachkennzeichnungen. de steht für Deutsch, en-US für amerikanisches Englisch. Windows erwartet en-US als Default-Fallback; würde man nur en schreiben, käme zur Build-Zeit eine Warnung, weil Windows keinen passenden Fallback für seine eigene Systemsprache en-US findet.
Eine .resw-Datei ist technisch eine XML-Datei. Jeder Eintrag hat einen Namen (den Schlüssel) und einen Textwert:
<data name="NavStartseite.Content" xml:space="preserve">
<value>Startseite</value>
</data>
<data name="SaveButton.Content" xml:space="preserve">
<value>Speichern</value>
</data>
Das Build-System wandelt diese Dateien beim Kompilieren in eine binäre .pri-Datei um (Package Resource Index). Diese enthält alle Sprachen gebündelt, und Windows wählt zur Laufzeit die passende Sprache automatisch aus.
x:Uid: Das Bindeglied zwischen XAML und Ressource
In XAML verbindet das Attribut x:Uid ein Element mit den passenden Ressourceneinträgen. Der Schlüssel in der .resw-Datei folgt dem Schema Uid.Eigenschaft:
<!-- Vorher: Text hardkodiert -->
<Button Content="Speichern" Click="OnSaveClick"/>
<!-- Nachher: Text kommt aus der Ressourcendatei -->
<Button x:Uid="SaveButton" Click="OnSaveClick"/>
In Resources.resw muss dann der Schlüssel SaveButton.Content existieren. Das .Content am Ende entspricht exakt dem Eigenschaftsnamen in WinUI 3. Das Gleiche funktioniert für Text, Header, PlaceholderText und andere Eigenschaften. Wichtig: Das Content="..."-Attribut im XAML wird weggelassen. Der Wert aus der Ressourcendatei ersetzt ihn vollständig. Würde man beides schreiben, würde der Ressourcenwert zwar trotzdem geladen, aber es wäre irreführender Code.
Attached Properties: Tooltips lokalisieren
Für ToolTipService.ToolTip — eine sogenannte Attached Property — funktioniert x:Uid ebenfalls. Der Ressourcenschlüssel enthält dann den vollständigen Eigenschaftspfad: SaveButton.ToolTipService.ToolTip. Die Punkte im Schlüsselnamen sind kein Problem — Windows trennt intern am letzten Punkt, um Eigenschaftsname und Elementname zu unterscheiden.
ILocalizationService: Texte aus dem Code abrufen
x:Uid löst alle Fälle, bei denen Texte direkt in XAML stehen. Aber manchmal braucht man lokalisierte Strings auch im Code — zum Beispiel in einer Fehlermeldung, die ein ViewModel aufbaut. Dafür gibt es in EchoPlay den ILocalizationService:
public interface ILocalizationService
{
string Get(string key);
}
Die Implementierung nutzt den ResourceLoader aus dem Windows App SDK:
public sealed class LocalizationService : ILocalizationService
{
private readonly ResourceLoader _loader = ResourceLoader.GetForViewIndependentUse();
public string Get(string key) => _loader.GetString(key);
}
GetForViewIndependentUse() ist wichtig: Die alternative Methode GetForCurrentView() funktioniert nur im UI-Thread und würde in einem Service-Kontext zu Laufzeitfehlern führen. Die GetForViewIndependentUse-Variante ist thread-sicher und kann von überall aufgerufen werden. Der Service wird als Singleton im DI-Container registriert, weil eine ResourceLoader-Instanz teuer zu erzeugen ist und thread-sicher bleibt.
Sprachwechsel: Warum braucht die App einen Neustart?
WinUI 3 lädt die Ressourcendateien einmalig beim App-Start — basierend auf ApplicationLanguages.PrimaryLanguageOverride oder, falls nicht gesetzt, auf der Windows-Systemsprache. Ein Wechsel der Sprache zur Laufzeit hat keinen Effekt auf bereits geladene XAML-Seiten, weil deren x:Uid-Werte beim Parsen des XAML aufgelöst wurden. Der einzige Weg, die neue Sprache anzuzeigen, ist ein App-Neustart:
public async Task ChangeLanguageAsync(string languageCode)
{
// Sprache in der DB speichern
_loadedSettings.ActiveLanguage = languageCode;
await settingsService.SaveAsync(_loadedSettings);
// Sprachpräferenz für den nächsten Start setzen
Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = languageCode;
// App als MSIX-Paket neu starten
Microsoft.Windows.AppLifecycle.AppInstance.Restart(string.Empty);
}
Schritt 1 ist entscheidend: Die gewählte Sprache muss in der Datenbank gespeichert sein, bevor die App neu startet. Sonst würde beim Neustart wieder die vorherige Sprache geladen. ApplicationLanguages.PrimaryLanguageOverride ist eine Windows-Einstellung, die pro App gespeichert wird und auch nach einem Neustart erhalten bleibt. AppInstance.Restart ist die Windows App SDK-Methode für gepackte MSIX-Apps — sie startet die App-Instanz sauber neu, als würde der Nutzer die App frisch öffnen.
AppSettings: Sprache dauerhaft speichern
In der Datenbank wird die Sprache als einfacher String gespeichert:
public class AppSettings : BaseEntity
{
public string ActiveLanguage { get; set; } = "de";
}
Der Kreislauf sieht so aus: Die App liest die Sprache aus der Datenbank, setzt PrimaryLanguageOverride beim Start, WinUI 3 lädt die richtige .resw-Datei, und alle x:Uid-Texte erscheinen in der gewählten Sprache.
Was ist mit programmatisch gesetzten Texten?
Status-Texte wie „Sync läuft…“ oder Fehlermeldungen in ViewModels sind in EchoPlay vorerst noch auf Deutsch hardkodiert. Das ist eine bewusste Entscheidung: Erst wenn die x:Uid-Infrastruktur steht und funktioniert, macht es Sinn, auch ViewModel-Strings über ILocalizationService.Get(...) zu laden. XAML-Texte und ViewModel-Texte haben unterschiedliche Lebenszyklen — XAML-Texte werden einmal beim Seitenladen gesetzt und ändern sich nicht, während ViewModel-Texte sich mehrfach zur Laufzeit ändern können. Für den zweiten Fall würde ILocalizationService.Get(key) einen lokalisierten String-Template zurückgeben, den das ViewModel dann befüllt.
Die gezeigten Code-Beispiele dienen zur Veranschaulichung. Nutzung auf eigene Verantwortung. Mehr dazu