Internationalization Options for WinUI 3

WinUI 3 fully supports internationalization. That said, the most reliable approach right now requires an app restart after switching languages for flawless results. While dynamic UI language refresh without restart is technically possible, the implementation gets overly complicated. I’ve spent ages troubleshooting this myself — running into broken UI text or crashes after language switches countless times. After lots of trial and error, I’ve landed on a solid internationalization workflow for WinUI 3, which I’ll walk you through below.

First add language initialization logic inside the code-behind of App.xaml.cs. If users haven’t picked a custom language, the app pulls the OS’s default language; it falls back to English if the system language isn’t on our supported list.

protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    const string LangSettingKey = "AppLanguageCode";
    // All supported language tags of project
    List<string> supportLangs = new() { "zh-CN", "en-US", "th-TH" };
    string finalLang = "en-US"; // Default language

    try
    {
        var localSetting = ApplicationData.Current.LocalSettings;

        // ① Use saved language if local configuration exists
        if (localSetting.Values.TryGetValue(LangSettingKey, out var savedVal))
        {
            finalLang = NormalizeLang(savedVal?.ToString() ?? "");
        }
        else
        {
            // ② No saved config: read system primary preferred language
            string sysLang = ApplicationLanguages.Languages.FirstOrDefault() ?? "";
            sysLang = NormalizeLang(sysLang);

            // ③ Use system language if supported, otherwise fall back to English
            finalLang = supportLangs.Contains(sysLang) ? sysLang : "en-US";
        }
    }
    catch
    {
        // Ignore exception when running unpackaged, ApplicationData unavailable
    }

    ApplicationLanguages.PrimaryLanguageOverride = finalLang;

    _window = new MainWindow();
    _window.Activate();
}

/// <summary>
/// Standardize culture code: map system returned zh-Hans-CN to zh-CN
/// </summary>
private static string NormalizeLang(string lang)
{
    if (string.IsNullOrEmpty(lang)) return lang;

    return lang switch
    {
        "zh-Hans-CN" => "zh-CN",
        "zh-Hant-TW" => "zh-TW", // For future Traditional Chinese support
        _ => lang
    };
}

This setup can still be refined. For cleaner code, don’t hardcode supported language lists here. Instead, wrap them inside a static helper class so you can reference the same set anywhere you need to adjust language settings, like so:

    public class Languages
    {
        public static List<string> supportLangs = new() { "zh-CN", "en-US", "th-TH" };
    }

The core purpose of the above code block is setting up app language on startup: load user’s saved preference first, fall back to system default when no custom setting exists, then default to English as final backup.

Build a dedicated page for language switching

We need a dedicated interface for users to change display language, so I created an independent Page specifically for language configuration.

<?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="ThaiTong.Pages.LanguageSettingPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ThaiTong.Pages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">
    <StackPanel Padding="20" Spacing="24">
        <TextBlock x:Uid="Txt_LangTip" FontSize="22"/>

        <ComboBox x:Name="cbLangSelect" Width="320" Height="40" CornerRadius="6">
            <ComboBoxItem Tag="zh-CN" Content="简体中文"/>
            <ComboBoxItem Tag="en-US" Content="English"/>
            <ComboBoxItem Tag="th-TH" Content="ภาษาไทย"/>
        </ComboBox>

        <StackPanel Orientation="Horizontal" Spacing="12">
            <Button x:Name="btnSave" x:Uid="Btn_Save" Content="Save" Click="BtnSave_Click" Width="120" Height="38" CornerRadius="5"/>
        </StackPanel>

        <TextBlock Foreground="Gray" Text="Note: After saving, page will refresh to apply language."/>
    </StackPanel>
</Page>

Corresponding backend C# code:

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.Windows.ApplicationModel.Resources;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using ThaiTong.Common;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Globalization;
using Windows.Storage;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace ThaiTong.Pages
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class LanguageSettingPage : Page
    {

        private const string LangSettingKey = "AppLanguageCode";
        private string _currentLang;

        private readonly ResourceLoader _resLoader;

        public LanguageSettingPage()
        {
            InitializeComponent();
            _resLoader = new ResourceLoader();

            var setting = ApplicationData.Current.LocalSettings;
            if (setting.Values.TryGetValue(LangSettingKey, out var saveLang))
            {
                _currentLang = saveLang.ToString();
            }
            else
            {
                string sysLang = ApplicationLanguages.Languages.FirstOrDefault() ?? "";
                _currentLang = Languages.supportLangs.Contains(sysLang) ? sysLang : "en-US";
            }

            // Pre-select matched language in dropdown list
            foreach (var item in cbLangSelect.Items)
            {
                if (item is ComboBoxItem cbi && cbi.Tag.ToString() == _currentLang)
                {
                    cbLangSelect.SelectedItem = item;
                    break;
                }
            }
        }

        // Save selected language setting
        private async void BtnSave_Click(object sender, RoutedEventArgs e)
        {
            if (cbLangSelect.SelectedItem is not ComboBoxItem selectItem) return;
            string newLang = selectItem.Tag.ToString();
            if (_currentLang == newLang) return;

            ApplicationData.Current.LocalSettings.Values[LangSettingKey] = newLang;
            _currentLang = newLang;
            ApplicationLanguages.PrimaryLanguageOverride = newLang;

            // Load localized text from resw resource file
            ContentDialog tipDialog = new ContentDialog()
            {
                XamlRoot = this.XamlRoot,
                Title = _resLoader.GetString("Dlg_LangChange_Title"),
                Content = _resLoader.GetString("Dlg_LangChange_Content"),
                CloseButtonText = _resLoader.GetString("Dlg_LangChange_BtnOk")
            };
            await tipDialog.ShowAsync();
        }


    }
}

Every time users save a new language preference, we prompt them to restart the application so all interface text updates fully.

WinUI 3 splits localization implementation into two separate parts: markup inside XAML files and text lookup within C# backend code.

For XAML elements, use the x:Uid attribute like this example:

<TextBlock x:Uid="Txt_LangTip" FontSize="22"/>

Next set up your localization resource folder structure: create a top-level Strings folder in your project, then add individual subfolders named after each culture code (such as en-US for US English). Inside every language folder add a Resources.resw file filled with key-value translation pairs.

  <data name="Btn_Save.Content" xml:space="preserve">
      <value>Save</value>
  </data>

The name field acts as your lookup key. The appended .Content points to the target control property; the Txt_LangTip from earlier would be defined as shown below:

<data name="Txt_LangTip.Text" xml:space="preserve"><value>Select Application Language</value></data>

We tag on .Text here because we’re targeting the Text property of the TextBlock control specifically.

For dynamic text inside C# code-behind, initialize an instance of ResourceLoader first:

 private readonly ResourceLoader _resLoader;

 public LanguageSettingPage()
 {
     InitializeComponent();
     _resLoader = new ResourceLoader();

After initialization, fetch localized strings anywhere via the GetString method:

Title = _resLoader.GetString("Dlg_LangChange_Title"),

Leave a Reply

Your email address will not be published. Required fields are marked *