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 .resx files 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

CultureInfo.CurrentCulture

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

CultureInfo.CurrentUICulture

Which resources are loaded: the ResourceManager and satellite assemblies pick the language from it. Per thread.

Defaults to the Windows display language; assign it in code

FrameworkElement.Language (xml:lang)

The language WPF uses inside the element tree: binding and StringFormat conversions, spell checking, hyphenation, number substitution, font fallback. Inherited down the tree.

XAML attribute xml:lang, or code. Default en-US

FrameworkElement.FlowDirection

Whether the layout and text flow left-to-right or right-to-left. Inherited down the tree.

XAML or code. Default LeftToRight

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:

flowchart TD A["XAML binds to a resource key"] --> B["ResourceManager.GetString key"] B --> C{"Satellite assembly es-ES?"} C -->|found| H["Return Spanish string"] C -->|missing| D{"Satellite assembly es?"} D -->|found| H D -->|missing| E["Neutral resources in the main assembly or the NeutralResourcesLanguage satellite"] E --> I["Return English string"] H --> J["TextBlock shows the text"] I --> J

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.

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 Grid with SharedSizeGroup, SizeToContent and Auto sizes

Buttons keep equal widths when "Browse…​" becomes a longer word

Avoid Canvas for layout

Fixed coordinates clip translated text

Set TextWrapping on TextBlock

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 xml:lang correctly

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>

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

Strings.resx

Neutral (the default culture of the project)

English text, the complete set of keys

Strings.es.resx

Spanish (any region)

Spanish text

Strings.es-ES.resx

Spanish (Spain)

Only the strings that differ from es

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.

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

AppTitle

Bookshelf Desktop

Bookshelf Desktop

BooksTitle

My books

Mis libros

AddBook

Add book

Añadir libro

RemoveBook

Remove

Quitar

SearchPrompt

Search by title or author

Buscar por título o autor

ConfirmRemove

Remove "{0}" from your shelf?

¿Quitar "{0}" de tu estantería?

BookCountOne

1 book

1 libro

BookCountOther

{0} books

{0} libros

PriceLabel

Price

Precio

LastOpenedLabel

Last opened

Última apertura

LanguageLabel

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>

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) ?? "";
}

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) ?? "";

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?

x:Static on the generated property

Text="{x:Static p:Strings.BooksTitle}"

No: evaluated once when the XAML loads

Indexer binding to a localization source

Text="{Binding [BooksTitle], Source={x:Static local:LocalizationSource.Instance}}"

Yes, when the source raises PropertyChanged

DynamicResource with a swapped ResourceDictionary

Text="{DynamicResource BooksTitle}"

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));
}
<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}" />

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;
        }
    }
}
<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>

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)

1234.5 formatted with N2

1,234.50

1.234,50

1234.5 formatted with C2

$1,234.50

1.234,50 € (currency sign after the number)

new DateTime(2026, 10, 23) with d

10/23/2026

23/10/2026

The same date with D

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>

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; }
}
<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>

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;
    }
}
<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 DynamicResource

Sharing with view models and services

Natural (Strings.X)

Awkward: needs Application.Current.FindResource

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:

  1. Each element carries an x:Uid that identifies it across builds. The overview recommends generating them with msbuild -t:updateuid and checking them with msbuild -t:checkuid, and not editing them after localization starts.

  2. The project sets <UICulture>en-US</UICulture>, which makes the build produce a language-neutral main assembly plus a satellite .resources.dll containing the localizable BAML and resources.

  3. A tool built on the System.Windows.Markup.Localizer APIs 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.

  4. Localization attributes and comments in XAML (Localization.Attributes and Localization.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 FlowDirection explicitly on every page so that the page does not inherit it from the NavigationWindow.

  • For stand-alone navigation apps, set StartupUri to a NavigationWindow rather than to a page, so the window and the navigation bar can change FlowDirection.

  • Keep the xml:lang of 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"));

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 (&#1000; 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.

<!-- 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" />

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

AddBookButton.Content

Add book / Añadir libro

Sets the button content

BooksHeader.Text

My books / Mis libros

Sets the TextBlock text

SearchBox.PlaceholderText

Search by title or author / Buscar por título o autor

Sets the placeholder

SearchBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name

Search books / Buscar libros

Sets an attached property (special syntax)

ConfirmRemove

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:Uid must exist on the element type. Greeting.Text on a Button is a run-time error because a Button has no Text property; use one identifier per control kind, such as ButtonGreeting.Content.

  • The values from the .resw file override the values set locally in the markup.

  • A simple identifier (one without a property part) such as ConfirmRemove cannot also have property identifiers. Adding ConfirmRemove.Text would cause a duplicate-entry build error.

  • A string in another file is addressed as x:Uid="/ErrorMessages/PasswordTooWeak"; Resources.resw is 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 Package.appxmanifest, replace <Resource Language="x-generate"/> with one <Resource Language="en-US"/> per language (for example en-US and es-ES)

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 resources.pri with MakePri.exe and copy it next to the .exe; rebuild it whenever the resources change

Constructing the loader

Default ResourceLoader

Pass the .pri file name explicitly because there is no default view

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

.resx (satellite assemblies) or XAML dictionaries

.resw compiled into a .pri index

.resx per form, via the designer

XAML link

x:Static, bindings, DynamicResource; x:Uid only in the BAML workflow

x:Uid plus Identifier.Property entries

None (designer-generated code)

Language selection

CurrentUICulture of the thread

Qualifier matching against the user language list (or display language when unpackaged)

CurrentUICulture of the thread

Formatting culture

CurrentCulture, and FrameworkElement.Language inside bindings

CurrentCulture and the Windows regional settings

CurrentCulture

Right-to-left

FlowDirection

FlowDirection

RightToLeft and RightToLeftLayout

Translator tooling

Visual Studio Resource Explorer, any .resx tool; LocBaml only on .NET Framework

.resw editors; manifest strings localizable

.resx tools

Switch language live

Needs a notifying source or dynamic dictionaries

Reload the page or use a ResourceContext; not automatic

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

  • Language is en-US unless you change it. Dates and numbers shown through StringFormat can look American on a Spanish PC. Override the Language metadata at startup or set ConverterCulture on the binding.

  • x:Static does not refresh. Text bound that way stays in the old language until the window is recreated. Use an indexer source or DynamicResource for live switching, or switch language on restart.

  • Setting only one culture. CurrentUICulture loads strings; CurrentCulture formats 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 DefaultThreadCurrentUICulture for 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 OverrideMetadata twice. It can be called once per type; do it once in OnStartup and 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 .resw and .resx workflows 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 Auto and 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 .resx or dictionaries.

  • Untrusted resource files. Resources in .resx can contain serialized objects; Microsoft says to include only .resx files 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 .pri must be rebuilt after every resource change.

  • Images with text. They cannot be translated without a new image per language.