Globalization & Localization
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Globalization is designing an app so that it can work for users in any region: it handles Unicode text, formats
dates and numbers the way the user expects, lays out right-to-left languages, and keeps every user-visible string out
of the code. Localization is the next step: translating those strings (and other culture-specific resources such as
images) for each language the app supports. This page follows both for Bookshelf Desktop, which ships in English
and Spanish. It covers the WPF toolchain (.resx files, satellite assemblies, strongly typed resource classes,
xml:lang, FlowDirection), the ways to bind XAML to resources, switching language while the app runs, what became
of x:Uid and LocBaml on modern .NET, and the different resource system of WinUI 3 (.resw files and MRT Core).
The page was written against these versions, current in October 2026:
-
.NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026). The resource and culture APIs used here (
ResourceManager,CultureInfo,FrameworkElement.Language,FlowDirection) have not changed in a way that affects this page between .NET Framework 4.8 and .NET 10; the exception is the LocBaml tool, discussed below. -
CommunityToolkit.Mvvm 8.4.2 for the view models.
-
Visual Studio 2026 18.11.1, whose Resource Explorer edits
.resxfiles side by side per locale. -
Windows App SDK 2.5.1 for the WinUI 3 section.
Vocabulary: Culture, UI Culture, Language and Flow Direction
Four separate settings influence a localized app. Mixing them up is the most common source of localization bugs.
| Setting | What it controls | Where it is set |
|---|---|---|
|
Formatting and parsing: number and date formats, currency symbol, sort order, casing rules. Per thread. |
Defaults to the Windows regional format; assign it in code |
|
Which resources are loaded: the |
Defaults to the Windows display language; assign it in code |
|
The language WPF uses inside the element tree: binding and |
XAML attribute |
|
Whether the layout and text flow left-to-right or right-to-left. Inherited down the tree. |
XAML or code. Default |
The .NET guidance is to use CurrentCulture for formatting tasks and CurrentUICulture for retrieving resources,
and notes that both can be set per thread. WPF adds a third setting, Language, whose default is en-US and
does not follow the Windows culture. That surprises many developers, and a dedicated section below explains how to
align it with the thread culture.
The following diagram shows how a string reaches the screen when the UI culture is es-ES:
The runtime tries the specific culture first (es-ES), then its parent (es), then the neutral fallback. The .NET
documentation describes this as the resource fallback process, and warns that without a default (neutral) resource an
exception is thrown when the lookup finds nothing, so the neutral resource set must be complete.
Source: Resources in .NET apps
Globalization Rules Before Any Translation
The design rules that make translation cheap are mostly about not building the UI in a way that assumes English. Microsoft’s WPF guidance and the .NET world-ready checklist agree on these:
| Rule | Reason |
|---|---|
Write the UI in XAML |
XAML is exposed to the localization APIs; UI built in code is not |
Avoid fixed widths and absolute positions |
Translated text is often longer; the .NET guidance suggests allowing 50 to 75 percent more room in some languages |
Use |
Buttons keep equal widths when "Browse…" becomes a longer word |
Avoid |
Fixed coordinates clip translated text |
Set |
Wrapping avoids clipping instead of truncating a long translation |
Do not concatenate phrases at run time |
Word order differs per language; use one composite format string per sentence |
Do not hard-code strings |
Every user-visible string belongs in a resource |
Avoid images that contain text |
They are expensive to localize |
Set |
It drives spell checking, hyphenation and font fallback |
Never store passwords or private data in resource files |
Resources ship with the app |
The layout part of this is covered by the Layout & Panels page of this section (Grid, shared size groups,
Auto sizing): see Layout & Panels.
Resource Files (.resx)
A .resx file is an XML file of name/value pairs. Each string is a data element with a name and a nested value.
Microsoft recommends editing .resx files through Visual Studio rather than by hand, since the format must be
well-formed XML in a predefined schema.
<data name="BooksTitle" xml:space="preserve">
<value>My books</value>
</data>
Source: Create resource files
The localization convention is a culture suffix in the file name: the neutral file keeps the root name, and each language adds the culture before the extension. Visual Studio’s guidance is to give localized files the same root name as the main file and to designate the application’s default (neutral) culture in the project properties.
| File | Culture | Content |
|---|---|---|
|
Neutral (the default culture of the project) |
English text, the complete set of keys |
|
Spanish (any region) |
Spanish text |
|
Spanish (Spain) |
Only the strings that differ from |
At compile time the .resx files are converted to binary .resources files. The neutral resources are embedded in
the main assembly; each localized culture is embedded in its own satellite assembly in a culture-named directory
next to the main assembly, for example es\Bookshelf.Desktop.resources.dll. A satellite assembly holds the
resources of a single culture and no code, so you can update translations without replacing the main assembly.
Source: Create resource files
Source: Resources in .NET apps
The Bookshelf Desktop Resource Set
The page uses these keys (neutral English in Strings.resx, Spanish in Strings.es.resx). Keys avoid dots so they
can be used in a XAML indexer path later.
| Key | English | Spanish |
|---|---|---|
|
Bookshelf Desktop |
Bookshelf Desktop |
|
My books |
Mis libros |
|
Add book |
Añadir libro |
|
Remove |
Quitar |
|
Search by title or author |
Buscar por título o autor |
|
Remove "{0}" from your shelf? |
¿Quitar "{0}" de tu estantería? |
|
1 book |
1 libro |
|
{0} books |
{0} libros |
|
Price |
Precio |
|
Last opened |
Última apertura |
|
Language |
Idioma |
Because the table contains format placeholders, Visual Studio’s Resource Explorer (17.11 and later) is useful: it
shows all locales side by side, validates that placeholders such as {0} match across translations, and flags
missing translations.
Project File
In an SDK-style project the .resx files are picked up by default. The project only needs to say what the neutral
language is, and the designer-generated class should be public so XAML can reference it.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<RootNamespace>Bookshelf.Desktop</RootNamespace>
<!-- The language of Strings.resx; becomes the NeutralResourcesLanguage attribute. -->
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<!-- Generates a public strongly typed class named Strings. -->
<EmbeddedResource Update="Properties\Strings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
Source: Create resource files
The NeutralResourcesLanguage assembly attribute (set by NeutralLanguage in the project, or written by hand) tells
the ResourceManager which language the main assembly’s resources are in, so it can skip the probe for a satellite
assembly of the same language. The WPF overview shows both locations: UltimateResourceFallbackLocation.MainAssembly
when the source language is embedded in the main assembly, and Satellite when it is moved to a satellite.
using System.Resources;
// Equivalent to <NeutralLanguage>en</NeutralLanguage> in the project file.
[assembly: NeutralResourcesLanguage("en", UltimateResourceFallbackLocation.MainAssembly)]
Strongly Typed Resources
The Visual Studio resource designer (the Generator metadata above) produces a class with one static property per
key, backed by a ResourceManager and a static Culture property. The generated code looks like this (simplified):
namespace Bookshelf.Desktop.Properties;
using System.Globalization;
using System.Resources;
public class Strings
{
private static ResourceManager? resourceMan;
private static CultureInfo? resourceCulture;
public static ResourceManager ResourceManager =>
resourceMan ??= new ResourceManager("Bookshelf.Desktop.Properties.Strings", typeof(Strings).Assembly);
// When set, overrides CurrentUICulture for lookups made through this class.
public static CultureInfo? Culture
{
get => resourceCulture;
set => resourceCulture = value;
}
public static string BooksTitle => ResourceManager.GetString("BooksTitle", resourceCulture) ?? "";
public static string AddBook => ResourceManager.GetString("AddBook", resourceCulture) ?? "";
public static string ConfirmRemove => ResourceManager.GetString("ConfirmRemove", resourceCulture) ?? "";
}
Source: Resources in .NET apps
The class shown is a hand-simplified illustration of the generator output, not a verbatim copy of what Visual Studio
writes. The key points are the compile-time check (a mistyped key is a compile error) and the Culture override.
Using Resources From View Models and Services
View models must not reach for the UI to get a message; they use the same resource class. Composite messages use
string.Format with the current culture and a single resource per sentence.
namespace Bookshelf.Desktop;
using System.Globalization;
using Bookshelf.Core;
using Bookshelf.Desktop.Properties;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class BooksViewModel : ObservableObject
{
private readonly IBookRepository repository;
private readonly IDialogService dialogs;
public BooksViewModel(IBookRepository repository, IDialogService dialogs)
{
this.repository = repository;
this.dialogs = dialogs;
}
public ObservableCollection<BookViewModel> Books { get; } = new();
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
public partial BookViewModel? SelectedBook { get; set; }
// Whole sentences, never concatenated fragments. Singular and plural are separate resources.
public string BookCountText => Books.Count == 1
? Strings.BookCountOne
: string.Format(CultureInfo.CurrentCulture, Strings.BookCountOther, Books.Count);
[RelayCommand(CanExecute = nameof(CanRemove))]
private async Task RemoveAsync()
{
if (SelectedBook is null)
{
return;
}
var message = string.Format(CultureInfo.CurrentCulture, Strings.ConfirmRemove, SelectedBook.Title);
if (await dialogs.ConfirmAsync("Bookshelf Desktop", message))
{
Books.Remove(SelectedBook);
OnPropertyChanged(nameof(BookCountText));
}
}
private bool CanRemove() => SelectedBook is not null;
}
(ObservableCollection<T> needs using System.Collections.ObjectModel;, omitted for brevity as on the other pages of
this section.) Two choices here are deliberate. The singular and plural forms are different keys because the plural
rules differ by language (English has two forms; Russian and Arabic have more), so a single {0} book(s) string is not
translatable. And ConfirmRemove holds the whole question with a placeholder, so the Spanish translation can place
the title wherever Spanish grammar needs it.
The ResourceManager is also a plain class, so it can be used with an explicit culture without touching any thread
setting, which is how tests and the language switcher below look up the Spanish text:
using System.Globalization;
using Bookshelf.Desktop.Properties;
var es = CultureInfo.GetCultureInfo("es");
// Uses the satellite assembly for "es" regardless of the current thread culture.
string title = Strings.ResourceManager.GetString("BooksTitle", es) ?? "";
Source: Globalization (WPF)
Binding XAML to Resources
There are three ways to put a resource string in XAML. They differ in whether the text can change while the window is open.
| Technique | How | Updates when the language changes at run time? |
|---|---|---|
|
|
No: evaluated once when the XAML loads |
Indexer binding to a localization source |
|
Yes, when the source raises |
|
|
Yes, when the dictionary is replaced |
x:Static
The simplest approach needs the generated class to be public, because XAML must be able to see the member.
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:p="clr-namespace:Bookshelf.Desktop.Properties"
Title="{x:Static p:Strings.AppTitle}"
SizeToContent="WidthAndHeight"
MinWidth="480">
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Buttons" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Buttons" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Grid.Column="0" Content="{x:Static p:Strings.AddBook}" Padding="12,4" />
<TextBlock Grid.Column="1" Text="{x:Static p:Strings.BooksTitle}"
FontSize="18" Margin="12,0" TextWrapping="Wrap" VerticalAlignment="Center" />
<Button Grid.Column="2" Content="{x:Static p:Strings.RemoveBook}" Padding="12,4" />
</Grid>
</Window>
The layout follows the guidance on this page: SizeToContent, Auto columns that share a size group so both buttons
stay the same width when a translation is longer, and TextWrapping on the title. The x:Static markup extension is
described on Markup Extensions & Namescopes.
A Localization Source for Live Language Switching
x:Static cannot refresh. To change language without restarting, bind to an object that raises PropertyChanged
for all its indexer values when the culture changes. An indexer binding ([Key]) re-reads the indexer when the
source raises PropertyChanged with an empty property name, which the data-binding page describes as telling every
binding on the object to refresh.
namespace Bookshelf.Desktop;
using System.ComponentModel;
using System.Globalization;
using Bookshelf.Desktop.Properties;
public sealed class LocalizationSource : INotifyPropertyChanged
{
public static LocalizationSource Instance { get; } = new();
private LocalizationSource()
{
}
public event PropertyChangedEventHandler? PropertyChanged;
// XAML: {Binding [BooksTitle], Source={x:Static local:LocalizationSource.Instance}}
public string this[string key] =>
Strings.ResourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? $"!{key}!";
public void Refresh() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
}
Source: Data binding overview (WPF)
<TextBlock xmlns:local="clr-namespace:Bookshelf.Desktop"
Text="{Binding [BooksTitle], Source={x:Static local:LocalizationSource.Instance}, Mode=OneWay}" />
<Button Content="{Binding [AddBook], Source={x:Static local:LocalizationSource.Instance}, Mode=OneWay}" />
Source: Data binding overview (WPF)
The !key! fallback makes a missing resource visible on screen instead of showing nothing. The indexer binding does
not give design-time text in the XAML designer and has no compile-time check on the key, which is why many teams keep
x:Static for text that never changes at run time and use the source only for text that must switch live. The
binding mechanics (indexer paths, Source, Mode) are on Data Binding.
Switching Culture
Changing the thread’s culture does not by itself redraw anything. The full sequence for a language switcher is: set
the culture, tell the resource sources to refresh, and update Language and FlowDirection.
Choosing the Startup Culture
Set the culture as early as possible, before the first window is created. Microsoft’s guidance for the default
property is that you should always set the culture of the main thread explicitly and not rely on
DefaultThreadCurrentUICulture alone for it, because setting the default also changes existing threads that have not
been assigned a culture explicitly. Set both: the main thread for the UI, and the defaults so that worker threads and
thread-pool threads created later inherit the same culture.
namespace Bookshelf.Desktop;
using System.Globalization;
using System.Windows;
using System.Windows.Markup;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
// Saved user preference, or null to follow Windows.
var saved = Environment.GetEnvironmentVariable("BOOKSHELF_LANGUAGE");
var culture = string.IsNullOrWhiteSpace(saved)
? CultureInfo.CurrentUICulture
: CultureInfo.GetCultureInfo(saved);
Apply(culture);
// Make the WPF element tree use the same culture for bindings and StringFormat.
// OverrideMetadata may be called only once per type, so do it here and nowhere else.
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(culture.IetfLanguageTag)));
base.OnStartup(e);
}
internal static void Apply(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
}
The OverrideMetadata call is the widely used idiom for making FrameworkElement.Language follow the culture; it is
not shown on the pages cited above, which only state that the default is en-US and that the property inherits. The
environment variable stands in for the settings store described on the Local Data, Settings & Files page of this
section; a real app would read a saved setting there.
A Language Service for the Running App
namespace Bookshelf.Desktop;
using System.Globalization;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public sealed record LanguageOption(string Name, string Tag);
public partial class LanguageViewModel : ObservableObject
{
public IReadOnlyList<LanguageOption> Languages { get; } =
[
new("English", "en"),
new("Español", "es"),
];
[ObservableProperty]
public partial LanguageOption Selected { get; set; }
public LanguageViewModel()
{
var current = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
Selected = Languages.FirstOrDefault(l => l.Tag == current) ?? Languages[0];
}
partial void OnSelectedChanged(LanguageOption value)
{
var culture = CultureInfo.GetCultureInfo(value.Tag);
App.Apply(culture);
LocalizationSource.Instance.Refresh();
// Flow direction follows the writing system of the chosen culture.
var flow = culture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
foreach (Window window in Application.Current.Windows)
{
window.FlowDirection = flow;
}
}
}
Source: Globalization (WPF)
<StackPanel Orientation="Horizontal" xmlns:local="clr-namespace:Bookshelf.Desktop">
<TextBlock Text="{Binding [LanguageLabel], Source={x:Static local:LocalizationSource.Instance}, Mode=OneWay}"
VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox ItemsSource="{Binding Languages}"
SelectedItem="{Binding Selected}"
DisplayMemberPath="Name"
MinWidth="120" />
</StackPanel>
Source: Data binding overview (WPF)
This switches everything bound through the localization source and the culture used by code. It does not change
FrameworkElement.Language for the already created elements, because the metadata override in OnStartup is read
when each element initializes. Controls that need the new language immediately (for example for
StringFormat date text) can set Language explicitly on the window:
window.Language = XmlLanguage.GetLanguage(culture.IetfLanguageTag). Texts the XAML loaded through x:Static stay in
the old language until the window is recreated. For that reason the safest product behaviour is often "the language
changes after restart" for apps that use x:Static widely, and a live switch only for apps built on the localization
source or dynamic dictionaries.
Culture-Aware Formatting of Dates and Numbers
Resource strings handle words. Numbers and dates are handled by .NET culture data, and WPF applies it in two places:
code (ToString, string.Format, which use CurrentCulture unless told otherwise) and bindings (StringFormat,
value converters), which receive a culture argument from the binding engine.
| Value and format | English (en-US) |
Spanish (es-ES) |
|---|---|---|
|
1,234.50 |
1.234,50 |
|
$1,234.50 |
1.234,50 € (currency sign after the number) |
|
10/23/2026 |
23/10/2026 |
The same date with |
Friday, October 23, 2026 |
viernes, 23 de octubre de 2026 |
The exact patterns come from the operating system’s culture data, and the .NET guidance says explicitly that globalization data is not stable: it is updated several times a year, so tests should not hard-code exact formatted output. The table is indicative, not normative.
Formatting in Code
Pass the culture explicitly when the output is meant for display, and use the invariant culture with an explicit round-trip format when the value is stored or exchanged. Never persist a localized date or number.
using System.Globalization;
decimal price = 1234.5m;
var lastOpened = new DateTime(2026, 10, 23, 18, 30, 0);
// Display: follows the user's culture.
string shownPrice = price.ToString("C2", CultureInfo.CurrentCulture);
string shownDate = lastOpened.ToString("d", CultureInfo.CurrentCulture);
// Storage and interchange: invariant culture, explicit round-trip format.
string storedPrice = price.ToString(CultureInfo.InvariantCulture);
string storedDate = lastOpened.ToString("O", CultureInfo.InvariantCulture);
Formatting in Bindings and the Language Property
StringFormat in a binding is formatted by the binding engine with a culture taken from the binding. The
FrameworkElement.Language default is en-US, so, unless you override it, a StringFormat like {}{0:C2} shows
dollar-style numbers even on a Spanish Windows. This is the problem the OnStartup override above solves. The
other fix is per binding, with ConverterCulture:
<StackPanel xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<!-- Follows FrameworkElement.Language (en-US by default, es after the override in App.OnStartup). -->
<TextBlock Text="{Binding Price, StringFormat={}{0:C2}}" />
<!-- Fixed to a culture, regardless of Language. -->
<TextBlock Text="{Binding Price, StringFormat={}{0:C2}, ConverterCulture=es-ES}" />
<TextBlock Text="{Binding LastOpened, StringFormat={}{0:d}}" />
</StackPanel>
Source: Data binding overview (WPF)
The {} prefix is the escape that stops the XAML parser from treating {0:C2} as a markup extension; the
Data Binding page explains it. When ConverterCulture is not set, the
binding engine uses the Language property of the binding target, which defaults to en-US unless an element above
it sets it (see the Binding.ConverterCulture reference page); set ConverterCulture explicitly when a binding must
not follow the element’s language.
A Details Panel in Both Languages
namespace Bookshelf.Desktop.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
// A reduced stand-in for the scenario's BookDetailsViewModel, with only the members this example formats.
public partial class BookDetailsViewModel : ObservableObject
{
[ObservableProperty]
public partial string Title { get; set; } = "";
[ObservableProperty]
public partial decimal Price { get; set; }
[ObservableProperty]
public partial DateTime LastOpened { get; set; }
}
Source: ObservableProperty attribute
<Grid xmlns:local="clr-namespace:Bookshelf.Desktop" Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Labels" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,12,4"
Text="{Binding [PriceLabel], Source={x:Static local:LocalizationSource.Instance}, Mode=OneWay}" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Price, StringFormat={}{0:C2}}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,12,0"
Text="{Binding [LastOpenedLabel], Source={x:Static local:LocalizationSource.Instance}, Mode=OneWay}" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding LastOpened, StringFormat={}{0:d}}" />
</Grid>
Source: Data binding overview (WPF)
ResourceDictionary per Language
A different approach to the same problem: keep the strings in XAML. Each language is a ResourceDictionary with the
same keys, and the application replaces the active dictionary at run time. Text elements use DynamicResource, so
they pick up the replacement immediately; this is the only technique here that needs no custom notification code.
<!-- Languages/Strings.en.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:String x:Key="AppTitle">Bookshelf Desktop</x:String>
<x:String x:Key="BooksTitle">My books</x:String>
<x:String x:Key="AddBook">Add book</x:String>
</ResourceDictionary>
<!-- Languages/Strings.es.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:String x:Key="AppTitle">Bookshelf Desktop</x:String>
<x:String x:Key="BooksTitle">Mis libros</x:String>
<x:String x:Key="AddBook">Añadir libro</x:String>
</ResourceDictionary>
namespace Bookshelf.Desktop;
using System.Windows;
public static class DictionaryLanguage
{
private static ResourceDictionary? current;
public static void Use(string tag)
{
var next = new ResourceDictionary
{
Source = new Uri($"pack://application:,,,/Languages/Strings.{tag}.xaml", UriKind.Absolute),
};
var merged = Application.Current.Resources.MergedDictionaries;
if (current is not null)
{
merged.Remove(current);
}
merged.Add(next);
current = next;
}
}
Source: Pack URIs in WPF
<TextBlock Text="{DynamicResource BooksTitle}" />
<Button Content="{DynamicResource AddBook}" />
Source: XAML resources
.resx + ResourceManager |
ResourceDictionary per language |
|
|---|---|---|
Typical translator tooling |
Wide: resx editors, translation-memory tools, Visual Studio Resource Explorer |
Plain XAML files; fewer tools understand them |
Compile-time key checking |
Yes, with the strongly typed class |
No: a mistyped key shows nothing |
Live switch |
Needs a notifying source |
Works with |
Sharing with view models and services |
Natural ( |
Awkward: needs |
Satellite assemblies |
Yes, translators can ship a DLL |
No: dictionaries are compiled into the main assembly unless you split them |
Other resource types (images, sounds) |
Yes |
Yes, any XAML object |
The lookup rules of DynamicResource, merged dictionaries and pack URIs are on
Resources & Resource Dictionaries.
x:Uid, UICulture and the Status of LocBaml
WPF has an older, BAML-based localization workflow, still described in the official WPF overview. The idea is to build the app once, then let localizers work on the compiled XAML:
-
Each element carries an
x:Uidthat identifies it across builds. The overview recommends generating them withmsbuild -t:updateuidand checking them withmsbuild -t:checkuid, and not editing them after localization starts. -
The project sets
<UICulture>en-US</UICulture>, which makes the build produce a language-neutral main assembly plus a satellite.resources.dllcontaining the localizable BAML and resources. -
A tool built on the
System.Windows.Markup.LocalizerAPIs extracts the key-value pairs, a localizer translates them (the sample tool writes a CSV), and the tool generates a new satellite assembly for the target culture. -
Localization attributes and comments in XAML (
Localization.AttributesandLocalization.Comments) mark which text is readable or modifiable and give translators context.
<TextBlock x:Uid="TextBlock_1"
Localization.Attributes="$Content(Readable Unmodifiable)"
Localization.Comments="$Content(Product name, do not translate)">
Bookshelf
</TextBlock>
The status that matters in 2026: the Microsoft WPF documentation carries a warning that the LocBaml tool only works
with WPF for .NET Framework projects and does not work with WPF for .NET. LocBaml has only ever been a sample (the
page calls it "a sample tool meant to help you get started in building a localization tool"), and the sample lives in
the WPF-Samples repository. On .NET 10 there is therefore no Microsoft-supported tool for the BAML-extraction
workflow. The same page describes the updateuid and checkuid MSBuild targets and the UICulture project property
in a .NET Framework context and does not say whether they behave identically in an SDK-style .NET 10 project, so
test them before relying on them. For new WPF apps on .NET the supported path is the one from the sections above:
.resx files with satellite assemblies, optionally with ResourceDictionary files.
x:Uid is not meaningless in WinUI 3, but it means something different: there it links an element to a .resw
entry, as the WinUI section below shows.
Right-to-Left Layout and FlowDirection
FlowDirection is an inherited property with two values, LeftToRight (the implicit value) and RightToLeft.
Setting it to RightToLeft on a window or page mirrors the layout of the element and its children: columns of a
Grid swap sides, text aligns to the right, and even gradient brushes are mirrored. An explicit value on any
descendant overrides the inherited one. Hebrew and Arabic are the usual right-to-left languages.
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft"
xml:lang="ar-SA"
Title="Bookshelf Desktop">
<DockPanel LastChildFill="True">
<!-- Dock="Left" now appears at the visual right because the layout is mirrored. -->
<Button DockPanel.Dock="Left" Content="+" Width="32" />
<!-- Content that is always left-to-right (a file name, code, a number) keeps its direction. -->
<TextBlock FlowDirection="LeftToRight" Text="C:\Books\library.json" />
</DockPanel>
</Window>
Rules from the official guide:
-
In navigation apps whose pages may be localized into a right-to-left language, set
FlowDirectionexplicitly on every page so that the page does not inherit it from theNavigationWindow. -
For stand-alone navigation apps, set
StartupUrito aNavigationWindowrather than to a page, so the window and the navigation bar can changeFlowDirection. -
Keep the
xml:langof the element tree in step with the language, since it drives spell checking, hyphenation, digit shapes and font fallback.
using System.Globalization;
using System.Windows;
static FlowDirection FlowFor(CultureInfo culture) =>
culture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
Application.Current.MainWindow.FlowDirection = FlowFor(CultureInfo.GetCultureInfo("ar-SA"));
Source: Globalization (WPF)
Mirroring does not flip every visual: an arrow icon, a Path geometry or a bitmap that implies direction (back and
forward arrows, progress bars with a fixed direction) must be handled on purpose, for example with a ScaleTransform
bound to the flow direction. Test the layout with a real right-to-left language even if you do not ship one yet.
Fonts, Text and Unicode
WPF handles Unicode text and the major writing systems. Its XAML parser defaults to UTF-8 when no encoding
declaration or byte-order mark is present, and character references (Ϩ style) are supported. The official
globalization topic lists several scripts as not supported (Khmer, Korean Old Hangul, Myanmar and Sinhala), and says
that for Thai the display and editing are supported but not word breaking; that page is old (the page header says
2017), so verify a script you need on a current Windows build before promising support. When the chosen font lacks a
character, WPF falls back through the Global User Interface composite font; you can create your own composite font
for per-language preferences.
Source: Globalization (WPF)
<!-- Mark language-specific text so spell checking, hyphenation and font fallback pick the right rules. -->
<TextBlock xml:lang="fr-CA" Text="Découvrir la France" />
Source: Globalization (WPF)
Localization in WinUI 3 and the Windows App SDK
WinUI 3 does not use .resx and ResourceManager. It uses the Windows Modern Resource Technology (MRT Core):
string resources live in .resw files, and the build compiles them into a .pri index that is matched against
qualifiers such as the language. The .resw format is also used for the package manifest strings, so the app name in
the Start menu can be localized.
Files and Folders
The convention is a Strings folder with one sub-folder per BCP-47 language tag, each with a Resources.resw:
Bookshelf.WinUI/
Strings/
en-US/Resources.resw
es-ES/Resources.resw
Package.appxmanifest
The default language must be set in the manifest (Application tab, Default language), and at minimum the default language’s strings must exist, since they are the fallback when nothing better matches. Resource identifiers are case insensitive and unique per file; do not change them after the strings go to translation, because that looks like a deletion plus an addition to localization tooling.
Linking XAML With x:Uid
In a .resw file, a property identifier has the form Identifier.Property. The x:Uid directive names the
identifier, and at run time every Identifier.Property entry is applied to the matching property of the element.
| Name in Resources.resw (en-US) | Value (en-US / es-ES) | Effect |
|---|---|---|
|
Add book / Añadir libro |
Sets the button content |
|
My books / Mis libros |
Sets the |
|
Search by title or author / Buscar por título o autor |
Sets the placeholder |
|
Search books / Buscar libros |
Sets an attached property (special syntax) |
|
Remove this book? / ¿Quitar este libro? |
A plain identifier, loaded from code |
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Bookshelf.WinUI.BooksPage">
<StackPanel Spacing="8" Padding="12">
<TextBlock x:Uid="BooksHeader" />
<AutoSuggestBox x:Uid="SearchBox" />
<Button x:Uid="AddBookButton" />
</StackPanel>
</Page>
Rules that follow from how the lookup works:
-
Every property identifier for an
x:Uidmust exist on the element type.Greeting.Texton aButtonis a run-time error because aButtonhas noTextproperty; use one identifier per control kind, such asButtonGreeting.Content. -
The values from the
.reswfile override the values set locally in the markup. -
A simple identifier (one without a property part) such as
ConfirmRemovecannot also have property identifiers. AddingConfirmRemove.Textwould cause a duplicate-entry build error. -
A string in another file is addressed as
x:Uid="/ErrorMessages/PasswordTooWeak";Resources.reswis the default file name and is omitted.
Loading Strings From Code
namespace Bookshelf.WinUI;
using Microsoft.Windows.ApplicationModel.Resources;
public sealed class WinUiStrings
{
private readonly ResourceLoader loader = new();
public string ConfirmRemove => loader.GetString("ConfirmRemove");
// A dotted identifier such as Fare.Well is addressed with a slash: Fare/Well.
public string Get(string key) => loader.GetString(key.Replace('.', '/'));
}
To override the language for a lookup (for example to show a tooltip in Spanish while the rest of the UI is English),
create a ResourceContext and set a qualifier:
using Microsoft.Windows.ApplicationModel.Resources;
var manager = new ResourceManager();
var context = manager.CreateResourceContext();
context.QualifierValues["Language"] = "es-ES";
var map = manager.MainResourceMap.GetSubtree("Resources");
string text = map.GetValue("ConfirmRemove", context).ValueAsString;
Packaged and Unpackaged WinUI 3 Apps
| Packaged | Unpackaged | |
|---|---|---|
Declaring languages |
In |
No manifest, nothing to declare after adding the resources |
Which language is used |
The user preferred-language list |
The system display language, not the preferred list |
PRI generation |
Done by the build |
Generate |
Constructing the loader |
Default |
Pass the |
The WinUI 3 & the Windows App SDK and MSIX Packaging & Signing pages of this section describe the project and manifest in full. The Windows App SDK documentation also covers a run-time override of the app’s preferred language; consult the MRT Core documentation for it before building a language switcher on WinUI 3.
Windows Forms
Windows Forms localizes through the same .resx and satellite-assembly mechanism, with the designer doing the work:
setting the form’s Localizable property to true and choosing a Language in the designer stores each
language’s property values in a culture-specific .resx file beside the form’s own. The .NET guidance adds that
Windows Forms dialogs should be created with the Visual Studio designer so they can be localized, not coded by hand.
WPF, WinUI 3 and WinForms Compared
| WPF | WinUI 3 | WinForms | |
|---|---|---|---|
String store |
|
|
|
XAML link |
|
|
None (designer-generated code) |
Language selection |
|
Qualifier matching against the user language list (or display language when unpackaged) |
|
Formatting culture |
|
|
|
Right-to-left |
|
|
|
Translator tooling |
Visual Studio Resource Explorer, any |
|
|
Switch language live |
Needs a notifying source or dynamic dictionaries |
Reload the page or use a |
Rebuild the form’s controls |
Testing Localization
Three kinds of test pay for themselves.
Completeness. Every key in the neutral file must exist in each supported language, or users see English. A unit test
can read the resource sets directly with ResourceManager.GetResourceSet, which returns the resources of one culture
without fallback.
using System.Collections;
using System.Globalization;
using Bookshelf.Desktop.Properties;
using Xunit;
public class StringsTests
{
[Theory]
[InlineData("es")]
public void Every_neutral_key_is_translated(string tag)
{
var neutral = Strings.ResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
var translated = Strings.ResourceManager.GetResourceSet(CultureInfo.GetCultureInfo(tag), true, false);
Assert.NotNull(neutral);
Assert.NotNull(translated);
var missing = new List<string>();
foreach (DictionaryEntry entry in neutral!)
{
var key = (string)entry.Key;
if (translated!.GetString(key) is null)
{
missing.Add(key);
}
}
Assert.Empty(missing);
}
[Fact]
public void Placeholders_match_in_confirm_remove()
{
var es = Strings.ResourceManager.GetString("ConfirmRemove", CultureInfo.GetCultureInfo("es"));
Assert.Contains("{0}", es);
}
}
Formatting. The .NET guidance says tests should not assert the exact output of culture formatting, because
globalization data changes between OS updates. Test that formatting returned something, that a value parses back
(round-trips), or use a custom CultureInfo created in the test setup when you must pin the pattern.
Layout and pseudo-localization. Run the app with longer-than-English text and with a right-to-left language. The .NET guidance mentions pseudolocalization with resource files as a benefit of custom cultures. Visual Studio’s Resource Explorer also flags missing translations as you edit.
The Testing View Models & UI page of this section covers the surrounding test setup.
Pitfalls
-
Languageisen-USunless you change it. Dates and numbers shown throughStringFormatcan look American on a Spanish PC. Override theLanguagemetadata at startup or setConverterCultureon the binding. -
x:Staticdoes not refresh. Text bound that way stays in the old language until the window is recreated. Use an indexer source orDynamicResourcefor live switching, or switch language on restart. -
Setting only one culture.
CurrentUICultureloads strings;CurrentCultureformats numbers. Setting only one gives a Spanish UI with English-format numbers (or the reverse). Set both, and on the default properties too, so that worker threads agree. -
Relying on
DefaultThreadCurrentUICulturefor the main thread. The documentation recommends setting the main thread’s culture explicitly, because the default property also changes other existing threads that have no explicit culture. -
Calling
OverrideMetadatatwice. It can be called once per type; do it once inOnStartupand not in a language-change handler. -
Missing neutral resources. If the neutral set is incomplete, the fallback throws. Keep the neutral file complete and set
NeutralResourcesLanguage. -
Concatenated sentences and singular/plural tricks. Word order and plural rules differ by language. Use one whole sentence per resource and separate plural keys.
-
Changing resource identifiers after translation. In
.reswand.resxworkflows this looks like a deletion and an addition and forces retranslation. -
Fixed widths. A German or Spanish label can be much longer than the English one. Use
Autoand shared size groups. -
Storing culture-formatted text. Persist numbers and dates with the invariant culture and an explicit round-trip format such as
"O". -
Expecting LocBaml. It does not work with WPF on .NET; plan for
.resxor dictionaries. -
Untrusted resource files. Resources in
.resxcan contain serialized objects; Microsoft says to include only.resxfiles you trust in the build, and that resource files must not hold passwords or private data. -
Unpackaged WinUI 3 apps. They use the system display language rather than the preferred language list, and the
.primust be rebuilt after every resource change. -
Images with text. They cannot be translated without a new image per language.