Localizing a XAML app with plain .resx usually means string-typed keys that break silently at
runtime, and a language change that only takes effect after a restart or window reload.
ResXLocalization fixes both: you keep the .resx files and editors you already use, and get
compile-checked keys (a renamed or deleted resource becomes a build error, not a runtime
surprise) and instant, in-place language switching - in Avalonia and WPF alike.
<!-- Strongly-typed key, generated from your .resx, updates live on every culture switch. -->
<TextBlock Text="{l:Localize {x:Static res:AppStringsKeys.Greeting}}" />// Switch the whole UI to German - every bound string re-resolves in place, no reload.
Localizer.Current.CurrentCulture = new CultureInfo("de");
The included sample apps (Avalonia · WPF) - pick a language and every binding updates live, no reload.
- ⚡ Live, no-reload language switching - set one property and every bound string updates in place. No window reload, no view rebuild, no restart.
- 🔒 Type-safe, compile-checked keys - a source generator turns every
.resxinto strongly-typed keys with full IntelliSense. - 📦 Zero configuration - install one NuGet package and build; typed keys are generated automatically, nothing else to set up.
- 🚀 Native AOT & trim clean (Avalonia) - no reflection over your resources; publishes with
PublishAot=trueout of the box. (WPF is Windows-only and does not support Native AOT.) - 🗂️ First-class multiple
.resx- look up by typed key, scope to one file, or search across all registered files in a defined order. - 🔤 Enum localization built in - localize enum members by naming convention, in item templates and as bound values.
- 🧮 Format arguments -
Get(key, args…)formats the translation in the active culture; in XAML, bind theLocalizeArgs.Arg0…Arg8attached properties. - 🩺 Missing-translation diagnostics - a visible
!key!sentinel (configurable) plus aTranslationNotFoundevent for logging and coverage reports. - 🌐 Language-picker ready -
GetAvailableCultures()discovers the cultures your app actually ships. - 🧩 MVVM-friendly - an injectable
ILocalizerservice withINotifyPropertyChanged, markup extensions for XAML, and a clean code-behind API. - 🪶 Leak-safe - discarded controls stay collectable (Avalonia uses weak events; WPF binds to the singleton through WPF's own weak binding-target references).
- An app targeting .NET 8 or later, built with the .NET 8 SDK or later.
- For Avalonia: Avalonia 12. For WPF: Windows.
.resxresource files with the standard sibling*.Designer.csaccessor, as generated by Visual Studio's or Rider's classic resx tooling. SDK-onlyGenerateResxSourceaccessors are not eligible - see Generated keys.
Install the package for your UI framework:
# Avalonia
dotnet add package ResXLocalization.Avalonia
# WPF
dotnet add package ResXLocalization.WPFResXLocalization.Avalonia |
ResXLocalization.WPF |
|
|---|---|---|
| Targets | net8.0 · net10.0 |
net8.0-windows · net10.0-windows |
| Platforms | cross-platform (Avalonia 12) | Windows only |
| Native AOT | ✅ Fully supported | ❌ Not supported (WPF limitation) |
That's it - the source generator and the build wiring for your .resx files are included; there is
nothing else to configure.
A few project properties are recommended. For Avalonia (required if you publish with Native AOT - see Native AOT and trimming):
<PropertyGroup>
<!-- The cultures you ship. Each one builds a satellite assembly; required for AOT. -->
<SatelliteResourceLanguages>en;de</SatelliteResourceLanguages>
<!-- Lets the analyzers verify AOT-safety on a normal build. -->
<IsAotCompatible>true</IsAotCompatible>
<!-- Recommended for Avalonia apps in general. -->
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>For WPF, only the cultures you ship are worth declaring (WPF does not support AOT):
<PropertyGroup>
<SatelliteResourceLanguages>en;de</SatelliteResourceLanguages>
</PropertyGroup>Add a .resx file the normal way - for example Resources/AppStrings.resx (your neutral/default language) - and a satellite file per culture, e.g. Resources/AppStrings.de.resx:
| Key | AppStrings.resx (English) |
AppStrings.de.resx (German) |
|---|---|---|
WindowTitle |
My Application |
Meine Anwendung |
Greeting |
Hello and welcome! |
Hallo und willkommen! |
On every build, the source generator inspects each eligible .resx and emits a typed key class named <FileName>Keys in the same namespace as your resource file. For AppStrings.resx you get:
// <auto-generated/>
namespace YourApp.Resources;
public static partial class AppStringsKeys
{
public static readonly ResourceKey Greeting = new("Greeting", AppStrings.ResourceManager);
public static readonly ResourceKey WindowTitle = new("WindowTitle", AppStrings.ResourceManager);
// …one ResourceKey per string entry, sorted, with sanitized member names.
}Each ResourceKey carries both the key name and the ResourceManager it belongs to - that is what makes typed lookups direct and collision-free. Which files qualify, what becomes a key, and the two generator diagnostics are in Generated keys.
Register each resource file once, then choose the starting culture.
Avalonia (Program.cs):
using System.Globalization;
using RentADeveloper.ResXLocalization;
using YourApp.Resources;
Localizer.Current.RegisterResourceManager(AppStrings.ResourceManager);
Localizer.Current.CurrentCulture = new CultureInfo("en");WPF (App.xaml.cs):
using System.Globalization;
using System.Windows;
using RentADeveloper.ResXLocalization;
using YourApp.Resources;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Localizer.Current.RegisterResourceManager(AppStrings.ResourceManager);
Localizer.Current.CurrentCulture = new CultureInfo("en");
}
}Registration is only needed for search-all lookups; typed and scoped lookups need no registration.
Tip
In Avalonia, put the registration (and the initial CurrentCulture) inside BuildAvaloniaApp() rather than Main. BuildAvaloniaApp runs at runtime and under the XAML previewer, so search-all lookups like {l:Localize Greeting} resolve at design time too instead of showing the !Greeting! sentinel.
Add the namespaces and bind with the {l:Localize} markup extension.
Avalonia:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:RentADeveloper.ResXLocalization.Avalonia;assembly=ResXLocalization.Avalonia"
xmlns:res="clr-namespace:YourApp.Resources"
Title="{l:Localize {x:Static res:AppStringsKeys.WindowTitle}}">
<TextBlock Text="{l:Localize {x:Static res:AppStringsKeys.Greeting}}" />
</Window>WPF:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:RentADeveloper.ResXLocalization.WPF;assembly=ResXLocalization.WPF"
xmlns:res="clr-namespace:YourApp.Resources"
Title="{l:Localize {x:Static res:AppStringsKeys.WindowTitle}}">
<TextBlock Text="{l:Localize {x:Static res:AppStringsKeys.Greeting}}" />
</Window>xmlns:lpoints at the framework package (Avalonia:assembly=ResXLocalization.Avalonia; WPF:assembly=ResXLocalization.WPF).xmlns:respoints at the namespace of your generated keys / resource accessors.
Localizer.Current.CurrentCulture = new CultureInfo("de");Every {l:Localize} binding re-resolves immediately. No reload, no flicker.
Three lookup modes, one fallback chain:
Localizer.Current.Get(AppStringsKeys.Greeting); // typed - compile-checked, one file
Localizer.Current.Get("Greeting", AppStrings.ResourceManager); // scoped - this key, in this file
Localizer.Current.Get("Greeting"); // search-all - registered files, in orderFallback is .NET's own: a missing de-DE entry falls back to de, then to the neutral resources. A
key that cannot be resolved anywhere renders the configurable !key! sentinel and raises
TranslationNotFound. Because fallback runs first, a key that resolves from a parent or neutral
value is not reported as missing.
Enum members are localized by naming convention - one .resx entry per member, named
Enum_<EnumTypeName>_<MemberName> - and composite-format resources take their arguments from
Get(key, args…) in code or from the LocalizeArgs.Arg0…Arg8 attached properties in XAML.
| Page | What it covers |
|---|---|
| Lookup modes and culture fallback | Typed, scoped and search-all lookups, registration order, the fallback chain, missing-translation diagnostics, dependency injection, subscription lifetime |
| Localizing enum values | The naming convention, item templates, LocalizeEnumConverter, custom prefixes and scoping |
| Dynamic format arguments | Get(key, args…) and the LocalizeArgs.Arg0…Arg8 attached properties |
| Native AOT and trimming | What to declare, how to publish, and what a trimming failure looks like |
| Generated keys | Which .resx files qualify, what becomes a key, the generator diagnostics, the Roslyn floor |
| API reference | Every public type and member |
A string shows up as !key!. The key could not be resolved in the current culture's complete
fallback chain. Check that the key exists in the neutral .resx, and - for search-all lookups like
{l:Localize Greeting} or {l:LocalizeEnum} - that the resource manager was registered with
RegisterResourceManager. Subscribe to TranslationNotFound to log every miss. See
Lookup modes and culture fallback.
No typed …Keys class is generated. The neutral file's name must be dot-free and a classic
.Designer.cs accessor must sit in the same folder; SDK GenerateResxSource output does not
qualify. RXLGEN001 identifies malformed .resx XML, RXLGEN002 a missing accessor. See
Generated keys.
A translation is right when you run it and wrong after publishing. List the cultures you ship in
SatelliteResourceLanguages, or the satellite assemblies are not published. See
Native AOT and trimming.
Avalonia version resolution. The Avalonia package declares 12.0.5 as its minimum so it still builds under the .NET 8 SDK; applications building with a current SDK resolve Avalonia 12.1 or later normally.
Two complete, runnable showcases exercise every feature and combination - a scrolling window with a live language ComboBox:
dotnet run --project samples/ResXLocalization.Avalonia.Sample
dotnet run --project samples/ResXLocalization.WPF.Sample # Windows onlyYou need the .NET 10 SDK (see global.json); the produced packages target .NET 8 and .NET 10.
# Build everything (must be 0 warnings / 0 errors - warnings are promoted to errors).
dotnet build ResXLocalization.slnx -c Release # Windows (includes WPF)
dotnet build ResXLocalization.NonWindows.slnf -c Release # Linux/macOS (skips WPF)See CONTRIBUTING.md for the gate scripts, the test suites, packing, and the Native AOT check.
This project follows Semantic Versioning. See the CHANGELOG for the history of changes.
Contributions are welcome! Please read CONTRIBUTING.md first. In short: open an issue to discuss larger changes, keep the build warning-free, add a CHANGELOG.md entry under Unreleased, and make sure the tests pass.
Released under the MIT License. © 2026 David Liebeherr.
Thank you Mike James from AvaloniaUI OÜ for the written permission to use Avalonia in the name of the ResXLocalization.Avalonia NuGet package.
Avalonia is a registered trademark of AvaloniaUI OÜ. This project is not affiliated with or endorsed by AvaloniaUI OÜ.
David Liebeherr - rent-a-developer 📧 info@rent-a-developer.de
If this library saves you time, a ⭐ on GitHub is appreciated!
