Lugha.WinUI 0.3.0

dotnet add package Lugha.WinUI --version 0.3.0
                    
NuGet\Install-Package Lugha.WinUI -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Lugha.WinUI" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lugha.WinUI" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Lugha.WinUI" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Lugha.WinUI --version 0.3.0
                    
#r "nuget: Lugha.WinUI, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Lugha.WinUI@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Lugha.WinUI&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Lugha.WinUI&version=0.3.0
                    
Install as a Cake Tool

Lugha.WinUI

WinUI 3 integration for the Lugha typed localisation ecosystem. Provides a reactive locale host, an immutable locale registry with BCP 47 fallback, system language synchronisation, and RTL flow direction support - enabling runtime language switching with full compile-time safety.

dotnet add package Lugha.WinUI

Requires .NET 10 and Windows App SDK 1.8 or later.

Quick Start

1. Create a locale registry

LocaleRegistry<TLocale> is an immutable, thread-safe dictionary mapping BCP 47 language tags to pre-constructed locale instances. It uses FrozenDictionary with case-insensitive lookup. Create it once at application startup.

if (LocaleRegistry<IAppLocale>.Create(
        new EnGbLocale(), new ArSaLocale(), new EsEsLocale())
    is not Result<LocaleRegistry<IAppLocale>, DuplicateLanguageTag>.Ok(var registry))
{
    return; // duplicate language tag
}

Each locale's Culture.Name is used as the lookup key. Create returns a ResultOk with the registry, or Err with the first duplicate tag found.

2. Create a locale host

WinUILocaleHost<TLocale> extends the core LocaleHost<TLocale> with a reactive FlowDirection property. It implements INotifyPropertyChanged so that x:Bind with Mode=OneWay re-evaluates all text bindings and layout direction when the active locale changes.

var host = LocaleHostFactory.Create(registry.Default, MainWindow.DispatcherQueue);

LocaleHostFactory.Create returns a WinUILocaleHost that wraps the DispatcherQueue dispatch logic. The host is the single point of mutable state in the Lugha ecosystem — all text resolution remains pure. Only the selection of which locale is active is mutable.

3. Store as a singleton

Both LocaleRegistry and LocaleHost should be created once and shared across the application. In App.xaml.cs:

public sealed partial class App : Application
{
    public static LocaleRegistry<IAppLocale>? Registry { get; private set; }
    public static WinUILocaleHost<IAppLocale>? Host { get; private set; }
    public static Window? MainWindow { get; private set; }

    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        EnGbLocale defaultLocale = new();

        if (LocaleRegistry<IAppLocale>.Create(
                defaultLocale, new ArSaLocale(), new EsEsLocale())
            is not Result<LocaleRegistry<IAppLocale>, DuplicateLanguageTag>.Ok(var registry))
        {
            return;
        }

        Registry = registry;
        MainWindow = new MainWindow();
        Host = LocaleHostFactory.Create(registry.Default, MainWindow.DispatcherQueue);
        MainWindow.Activate();
    }
}

4. Bind in XAML

Invariant text (labels, titles) binds directly to the locale host's Current property. RTL layout binds to Host.FlowDirection — a reactive property on WinUILocaleHost that updates automatically when the locale changes:

<Page x:Class="MyApp.Views.MainPage">
    <Grid FlowDirection="{x:Bind Host.FlowDirection, Mode=OneWay}">
        
        <TextBlock Text="{x:Bind Host.Current.Navigation.Dashboard, Mode=OneWay}" />
        <TextBlock Text="{x:Bind Host.Current.Navigation.Settings, Mode=OneWay}" />
    </Grid>
</Page>

Parameterised text (methods with arguments) can use x:Bind path-to-function syntax. x:Bind re-evaluates the entire path when Host.PropertyChanged fires for Current:

<TextBlock Text="{x:Bind Host.Current.Connection.Connected(ViewModel.ServerName), Mode=OneWay}" />

This eliminates the need for code-behind bridge properties for parameterised strings. The x:Bind compiler generates the method call and re-evaluates it whenever either Host.Current or ViewModel.ServerName changes.

5. Switch locale at runtime

Use the registry to resolve a locale by tag, then set it on the host. This can be called from any thread - the property change notification is dispatched to the UI thread via DispatcherQueue.

private void OnLanguageSelected(string languageTag)
{
    if (App.Registry is not { } registry) return;
    IAppLocale locale = registry.Resolve(languageTag);
    App.Host?.SetLocale(locale);
    SystemLanguageSync.TryApply(locale);
}

BCP 47 Subtag Fallback

Resolve(string) strips subtags right-to-left until a match is found. This enables registering a base locale (e.g. es) and matching regional variants (e.g. es-419, es-MX) without registering each explicitly:

// Assuming registry created with EsLocale (es) and EsEsLocale (es-ES):
registry.Resolve("es-ES");  // exact match -> EsEsLocale
registry.Resolve("es-419"); // strips to es -> EsLocale
registry.Resolve("es-MX");  // strips to es -> EsLocale
registry.Resolve("fr-FR");  // no match -> returns Default

Resolve is a total function — it always returns a locale, falling back to Default when no match is found. Use TryResolve if you need null for unregistered tags.

Exact matches use the original string with no allocation. The subtag fallback path uses FrozenDictionary.GetAlternateLookup<ReadOnlySpan<char>>() for zero-allocation lookup — no intermediate strings are allocated regardless of how many subtags are stripped.

System Language and RTL

SystemLanguageSync

SystemLanguageSync synchronises the Windows App SDK language setting with the active locale. It is deliberately separate from LocaleHost because ApplicationLanguages.PrimaryLanguageOverride is a global, persistent side effect that survives application restarts.

Packaged apps only. ApplicationLanguages.PrimaryLanguageOverride requires a packaged (MSIX) application identity. In unpackaged apps (WindowsPackageType=None), TryApply returns false and the override is silently skipped. Use FlowDirection() directly for RTL support in unpackaged apps.

// Set the platform language override only (returns false in unpackaged apps)
SystemLanguageSync.TryApply(locale);

// Set the platform language override and update RTL flow direction
SystemLanguageSync.TryApply(locale, rootElement);

FlowDirection() extension

The FlowDirection() extension method on ILocale returns the WinUI FlowDirection enum value based on CultureInfo.TextInfo.IsRightToLeft:

FlowDirection direction = locale.FlowDirection();
// en-GB -> LeftToRight
// ar-SA -> RightToLeft

Use this to update layout when switching locales:

rootElement.FlowDirection = locale.FlowDirection();

Full locale switch

Combine registry resolution, host update, and system sync for a complete locale switch:

private void OnLanguageSelected(string languageTag)
{
    if (App.Registry is not { } registry) return;
    IAppLocale locale = registry.Resolve(languageTag);
    App.Host?.SetLocale(locale);
    SystemLanguageSync.TryApply(locale);
}

Multi-Window Locale

Each LocaleHost<TLocale> is bound to the DispatcherQueue of the thread that owns its UI elements. For multi-window applications, create a separate LocaleHost per window, sharing the same LocaleRegistry:

// Main window
var mainHost = LocaleHostFactory.Create(locale, mainWindow.DispatcherQueue);

// Secondary window (different thread)
var secondaryHost = LocaleHostFactory.Create(locale, secondaryWindow.DispatcherQueue);

Both hosts share the immutable registry and locale instances. Switching locale on one host does not affect the other - coordinate explicitly if desired.

API Reference

LocaleRegistry<TLocale>

Member Description
static Result<..., DuplicateLanguageTag> Create(TLocale default, params ReadOnlySpan<TLocale>) Factory. Returns Ok with the registry or Err with the duplicate tag.
TLocale Default The default locale, guaranteed registered.
int Count Number of registered locales.
IEnumerable<string> Languages The set of registered language tags.
IEnumerable<TLocale> Locales All registered locale instances.
TLocale Resolve(string language) BCP 47 lookup with subtag fallback. Returns Default if no match.
TLocale? TryResolve(string language) BCP 47 lookup with subtag fallback. Returns null if no match.
bool Contains(string language) Whether a locale with the exact tag is registered. Does not perform subtag fallback.

LocaleHost<TLocale> (core Lugha)

Member Description
LocaleHost(TLocale initial, Action<Action> dispatch) Creates a host with the initial locale and a dispatch delegate.
TLocale Current The active locale. Bind to this in XAML.
void SetLocale(TLocale locale) Switches the active locale. Thread-safe.
event PropertyChangedEventHandler? PropertyChanged Raised when Current changes. Dispatched via the delegate.
protected virtual void OnCurrentChanged() Override in subclasses to raise PropertyChanged for derived properties.
protected void OnPropertyChanged(PropertyChangedEventArgs) Raises PropertyChanged.

WinUILocaleHost<TLocale> (Lugha.WinUI)

Member Description
FlowDirection FlowDirection Reactive WinUI FlowDirection derived from the active locale. Re-evaluated when Current changes.

LocaleHostFactory (Lugha.WinUI)

Member Description
static WinUILocaleHost<TLocale> Create(TLocale initial, DispatcherQueue dispatcher) Creates a WinUILocaleHost that dispatches via DispatcherQueue.

SystemLanguageSync

Member Description
static bool TryApply(ILocale locale) Sets PrimaryLanguageOverride. Returns false in unpackaged apps.
static bool TryApply(ILocale locale, FrameworkElement rootElement) Sets PrimaryLanguageOverride and updates FlowDirection. Returns false if override was not set.

LocaleExtensions

Member Description
static FlowDirection FlowDirection(this ILocale locale) Returns LeftToRight or RightToLeft based on the locale's text info.

Thread Safety

  • LocaleRegistry<TLocale> is immutable after construction. Safe to read from any thread.
  • LocaleHost<TLocale>.SetLocale may be called from any thread. If called off the UI thread, the property update is dispatched via DispatcherQueue.TryEnqueue. The PropertyChanged event always fires on the UI thread.
  • Locale instances themselves are immutable and pure - sharing them across threads is safe.
  • SystemLanguageSync.TryApply must be called from the UI thread (it accesses FrameworkElement properties).

Sample Application

See Lugha.Samples.WinUI for a complete packaged WinUI 3 app demonstrating registry setup, locale switching, RTL layout, x:Bind bindings, and Gettext source generation with four locales.

Dependencies

  • Lugha - core runtime library.
  • Microsoft.WindowsAppSDK 1.8 or later.

Licence

Apache License 2.0

Product Compatible and additional computed target framework versions.
.NET net10.0-windows10.0.19041 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 110 3/21/2026
0.2.2 114 3/6/2026
0.2.1 107 3/6/2026
0.2.0 108 3/6/2026
0.1.0 107 3/5/2026