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