dotnet-localization
Comprehensive .NET internationalization and localization: .resx resource files and satellite assemblies, modern alternatives (JSON resources, source generators for AOT), IStringLocalizer patterns, date/number/currency formatting with CultureInfo, RTL layout support, pluralization engines, and per-framework localization integration for Blazor, MAUI, Uno Platform, and WPF.
Version assumptions: .NET 8.0+ baseline. IStringLocalizer stable since .NET Core 1.0; localization APIs stable since .NET 5. .NET 9+ features explicitly marked.
Scope
-
.resx resource files, satellite assemblies, culture fallback chains
-
IStringLocalizer patterns and DI registration
-
Modern alternatives: JSON resources, source generators for AOT
-
Date/number/currency formatting with CultureInfo
-
RTL layout support per framework (Blazor, MAUI, Uno, WPF)
-
Pluralization engines (MessageFormat.NET, SmartFormat.NET)
Out of scope
-
Deep Blazor component patterns -- see [skill:dotnet-blazor-components]
-
Deep MAUI development patterns -- see [skill:dotnet-maui-development]
-
Uno Platform project structure and Extensions ecosystem -- see [skill:dotnet-uno-platform]
-
WPF Host builder and MVVM patterns -- see [skill:dotnet-wpf-modern]
-
Source generator authoring (Roslyn API) -- see [skill:dotnet-csharp-source-generators]
Cross-references: [skill:dotnet-blazor-components] for Blazor component lifecycle, [skill:dotnet-maui-development] for MAUI app structure, [skill:dotnet-uno-platform] for Uno Extensions and x:Uid, [skill:dotnet-wpf-modern] for WPF on modern .NET.
.resx Resource Files
Overview
Resource files (.resx ) are the standard .NET localization format. They compile into satellite assemblies resolved by ResourceManager with automatic culture fallback.
Culture Fallback Chain
Resources resolve in order of specificity, falling back until a match is found:
sr-Cyrl-RS.resx -> sr-Cyrl.resx -> sr.resx -> Resources.resx (default/neutral)
The default .resx file (no culture suffix) is the single source of truth. Translation files must not contain keys absent from the default file.
Project Setup
<!-- MyApp.csproj --> <PropertyGroup> <NeutralLanguage>en-US</NeutralLanguage> </PropertyGroup>
<ItemGroup> <!-- Default resources --> <EmbeddedResource Include="Resources\Messages.resx" /> <!-- Culture-specific resources --> <EmbeddedResource Include="Resources\Messages.fr-FR.resx" /> <EmbeddedResource Include="Resources\Messages.de-DE.resx" /> </ItemGroup>
Resource File Structure
<!-- Resources/Messages.resx (default/neutral) --> <?xml version="1.0" encoding="utf-8"?> <root> <data name="Welcome" xml:space="preserve"> <value>Welcome to the application</value> <comment>Shown on the home page</comment> </data> <data name="ItemCount" xml:space="preserve"> <value>You have {0} item(s)</value> <comment>{0} = number of items</comment> </data> </root>
Accessing Resources
// Via generated strongly-typed class (ResXFileCodeGenerator custom tool) string welcome = Messages.Welcome;
// Via ResourceManager directly var rm = new ResourceManager("MyApp.Resources.Messages", typeof(Messages).Assembly); string welcome = rm.GetString("Welcome", CultureInfo.CurrentUICulture);
Modern Alternatives
JSON-Based Resources
Lightweight alternative for projects already using JSON for configuration. Libraries provide IStringLocalizer implementations backed by JSON files.
// Resources/en-US.json { "Welcome": "Welcome to the application", "ItemCount": "You have {0} item(s)" }
Libraries:
-
Senlin.Mo.Localization -- JSON-backed IStringLocalizer
-
Embedded.Json.Localization -- embedded JSON resources
JSON resources are popular in ASP.NET Core but lack the built-in tooling support (Visual Studio designer, satellite assembly compilation) of .resx .
Source Generators for AOT Compatibility
Traditional .resx with ResourceManager uses reflection at runtime, which is problematic for Native AOT and trimming. Source generators eliminate runtime reflection by generating strongly-typed accessor classes at compile time.
Recommended source generators:
Generator Description AOT-Safe
ResXGenerator (ycanardeau) Strongly-typed classes with IStringLocalizer support and DI registration Yes
VocaDb.ResXFileCodeGenerator Original strongly-typed .resx source generator Yes
Built-in ResXFileCodeGenerator
Visual Studio custom tool (not a Roslyn source generator) No -- generates static properties but still uses ResourceManager
<!-- Using ResXGenerator --> <ItemGroup> <PackageReference Include="ResXGenerator" Version="1.*" PrivateAssets="all" /> </ItemGroup>
// Generated at compile time -- no runtime reflection string welcome = Messages.Welcome;
// With DI registration (ResXGenerator) services.AddResXLocalization();
Recommendation: Use .resx files as the resource format (broadest tooling support) with a source generator for AOT/trimming scenarios. Use JSON resources only for lightweight or config-heavy projects.
IStringLocalizer Patterns
Registration
var builder = WebApplication.CreateBuilder(args);
// Register localization services builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
var app = builder.Build();
// Configure request localization middleware var supportedCultures = new[] { "en-US", "fr-FR", "de-DE", "ja-JP" }; app.UseRequestLocalization(options => { options.SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); });
IStringLocalizer
The primary localization interface. Injectable via DI. Use everywhere: services, controllers, Blazor components, middleware.
public class OrderService { private readonly IStringLocalizer<OrderService> _localizer;
public OrderService(IStringLocalizer<OrderService> localizer)
{
_localizer = localizer;
}
public string GetConfirmation(int orderId)
{
// Indexer returns LocalizedString with implicit string conversion
return _localizer["OrderConfirmed", orderId];
// Resolves: "Order {0} confirmed" with orderId substituted
}
public bool IsTranslated(string key)
{
LocalizedString result = _localizer[key];
return !result.ResourceNotFound;
}
}
IViewLocalizer (MVC Razor Views Only)
Auto-resolves resource files matching the view path. Not supported in Blazor.
@* Views/Home/Index.cshtml *@ @inject IViewLocalizer Localizer
<h1>@Localizer["Welcome"]</h1> <p>@Localizer["ItemCount", Model.Count]</p>
Resource file location: Resources/Views/Home/Index.en-US.resx
IHtmlLocalizer (MVC Only)
HTML-aware variant that HTML-encodes format arguments but preserves HTML in the resource string itself. Not supported in Blazor.
@inject IHtmlLocalizer<SharedResource> HtmlLocalizer
@* Resource: "Read our <a href='/terms'>terms</a>, {0}" @ @ {0} is HTML-encoded, the <a> tag is preserved *@ <p>@HtmlLocalizer["TermsNotice", Model.UserName]</p>
When to Use Each
Interface Scope HTML-Safe Blazor MVC
IStringLocalizer<T>
Everywhere No (plain text) Yes Yes
IViewLocalizer
View-local strings No No Yes
IHtmlLocalizer<T>
HTML in resources Yes No Yes
Namespace Resolution
If resource lookup fails, check namespace alignment. IStringLocalizer<T> resolves resources using the full type name of T relative to the ResourcesPath . Use RootNamespaceAttribute to fix namespace/assembly mismatches:
[assembly: RootNamespace("MyApp")]
Date, Number, and Currency Formatting
CultureInfo
CultureInfo is the central class for culture-specific formatting. Two distinct properties control behavior:
-
CultureInfo.CurrentCulture -- controls formatting (dates, numbers, currency)
-
CultureInfo.CurrentUICulture -- controls resource lookup (which .resx file)
// Always pass explicit CultureInfo -- never rely on thread defaults in server code var date = DateTime.Now.ToString("D", new CultureInfo("fr-FR")); // "vendredi 14 fevrier 2026"
var price = 1234.56m.ToString("C", new CultureInfo("de-DE")); // "1.234,56 EUR" (uses NumberFormatInfo.CurrencySymbol)
var number = 1234567.89.ToString("N2", new CultureInfo("ja-JP")); // "1,234,567.89"
Server-Side Best Practices
// Use useUserOverride: false in server scenarios to avoid // picking up user-customized formats var culture = new CultureInfo("en-US", useUserOverride: false);
// Set culture per-request (ASP.NET Core middleware handles this) CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture;
Format Specifiers
Specifier Type Example (en-US) Example (de-DE)
"d"
Short date 2/14/2026 14.02.2026
"D"
Long date Friday, February 14, 2026 Freitag, 14. Februar 2026
"C"
Currency $1,234.56 1.234,56 EUR
"N2"
Number 1,234.57 1.234,57
"P1"
Percent 85.5% 85,5 %
RTL Support
Detecting RTL Cultures
bool isRtl = CultureInfo.CurrentCulture.TextInfo.IsRightToLeft; // true for: ar-, he-, fa-, ur-, etc.
Per-Framework RTL Patterns
Blazor: No native FlowDirection -- use CSS dir attribute:
// wwwroot/js/app.js window.setDocumentDirection = (dir) => document.documentElement.dir = dir;
// Set via named JS function (avoid eval -- causes CSP unsafe-eval violations) await JSRuntime.InvokeVoidAsync("setDocumentDirection", isRtl ? "rtl" : "ltr");
For deep Blazor component patterns, see [skill:dotnet-blazor-components].
MAUI: FlowDirection property on VisualElement and Window :
// Set at window level -- cascades to all children window.FlowDirection = isRtl ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
Android requires android:supportsRtl="true" in AndroidManifest.xml (set by default in MAUI). For deep MAUI patterns, see [skill:dotnet-maui-development].
Uno Platform: Inherits WinUI FlowDirection model:
<Page FlowDirection="RightToLeft"> <!-- All children inherit RTL layout --> </Page>
For Uno Extensions and x:Uid binding, see [skill:dotnet-uno-platform].
WPF: FlowDirection property on FrameworkElement :
<Window FlowDirection="RightToLeft"> <!-- All children inherit RTL layout --> </Window>
For WPF on modern .NET patterns, see [skill:dotnet-wpf-modern].
Pluralization
The Problem
Simple string interpolation fails for pluralization across languages:
// WRONG: English-only, breaks in languages with complex plural rules $"You have {count} item{(count != 1 ? "s" : "")}"
Languages like Arabic have six plural forms (zero, one, two, few, many, other). Polish distinguishes "few" from "many" based on number ranges.
ICU MessageFormat (MessageFormat.NET)
CLDR-compliant pluralization using ICU plural categories. Recommended for internationalization-first projects.
// Package: jeffijoe/messageformat.net (v5.0+, ships CLDR pluralizers) var formatter = new MessageFormatter();
string pattern = "{count, plural, " + "=0 {No items}" + "one {# item}" + "other {# items}}";
formatter.Format(pattern, new { count = 0 }); // "No items" formatter.Format(pattern, new { count = 1 }); // "1 item" formatter.Format(pattern, new { count = 42 }); // "42 items"
SmartFormat.NET
Flexible text templating with built-in pluralization. Good for projects wanting maximum flexibility.
// Package: axuno/SmartFormat (v3.6.1+) using SmartFormat;
Smart.Format("{count:plural:No items|# item|# items}", new { count = 0 }); // "No items" Smart.Format("{count:plural:No items|# item|# items}", new { count = 1 }); // "1 item" Smart.Format("{count:plural:No items|# item|# items}", new { count = 5 }); // "5 items"
Choosing a Pluralization Engine
Engine CLDR Compliance API Style Best For
MessageFormat.NET Full (CLDR categories) ICU pattern strings Multi-locale apps needing standard compliance
SmartFormat.NET Partial (extensible) .NET format string extension Flexible templating with pluralization
Manual conditional None string.Format
- branching Simple English-only dual forms
UI Framework Integration
Blazor Localization
Blazor supports IStringLocalizer only -- IHtmlLocalizer and IViewLocalizer are not available.
Component injection:
@inject IStringLocalizer<MyComponent> Loc
<h1>@Loc["Welcome"]</h1> <p>@Loc["ItemCount", items.Count]</p>
Culture configuration by render mode:
Render Mode Culture Source
Server / SSR RequestLocalizationMiddleware (server-side)
WebAssembly CultureInfo.DefaultThreadCurrentCulture
- Blazor start option applicationCulture
Auto Both -- server middleware for initial load, WASM culture for client-side
WASM globalization data:
<!-- Required for full ICU data in Blazor WASM --> <PropertyGroup> <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData> </PropertyGroup>
Without this property, Blazor WASM loads only a subset of ICU data. For minimal download size, use InvariantGlobalization=true (disables localization entirely).
Dynamic culture switching:
// CultureSelector component pattern: // 1. Store selected culture in browser local storage // 2. Set culture cookie via controller redirect (server-side) // 3. Read cookie in RequestLocalizationMiddleware
For deep Blazor component patterns (lifecycle, state management, JS interop), see [skill:dotnet-blazor-components].
MAUI Localization
MAUI uses .resx files with strongly-typed generated properties.
Resource setup:
Resources/ Strings/ AppResources.resx # Default (neutral) culture AppResources.fr-FR.resx # French AppResources.ja-JP.resx # Japanese
XAML binding:
<!-- Import namespace --> <ContentPage xmlns:strings="clr-namespace:MyApp.Resources.Strings">
<!-- Use x:Static for strongly-typed access --> <Label Text="{x:Static strings:AppResources.Welcome}" /> <Button Text="{x:Static strings:AppResources.LoginButton}" /> </ContentPage>
Code access:
string welcome = AppResources.Welcome;
Platform requirements:
-
iOS/Mac Catalyst: Add CFBundleLocalizations to Info.plist
-
Windows: Add <Resource Language="..."> entries to Package.appxmanifest
-
All platforms: Set <NeutralLanguage>en-US</NeutralLanguage> in csproj
For deep MAUI development patterns (controls, navigation, platform APIs), see [skill:dotnet-maui-development].
Uno Platform Localization
Uno uses .resw files (Windows resource format) with x:Uid for automatic XAML resource binding.
Resource structure:
Strings/ en/Resources.resw fr-FR/Resources.resw ja-JP/Resources.resw
Registration:
// In Host builder configuration .UseLocalization()
XAML binding with x:Uid:
<!-- x:Uid maps to resource keys: "MainPage_Title.Text", "LoginButton.Content" --> <TextBlock x:Uid="MainPage_Title" /> <Button x:Uid="LoginButton" />
Runtime culture switching:
var localizationService = serviceProvider .GetRequiredService<ILocalizationService>(); await localizationService.SetCurrentCultureAsync( new CultureInfo("fr-FR")); // Note: XAML x:Uid bindings retain old culture until app restart
Known limitation: x:Uid -based localization keeps the old culture until app restart, even after calling SetCurrentCultureAsync . Code-based IStringLocalizer updates immediately.
For Uno Extensions ecosystem configuration and MVUX patterns, see [skill:dotnet-uno-platform].
WPF Localization
Recommended approach for .NET 8+: .resx files with DynamicResource binding for runtime locale switching. Avoid LocBaml (works only on .NET Framework).
Resource dictionary approach:
<!-- Resources/Strings.en-US.xaml --> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:sys="clr-namespace:System;assembly=System.Runtime"> <sys:String x:Key="Welcome">Welcome</sys:String> <sys:String x:Key="LoginButton">Log In</sys:String> </ResourceDictionary>
<!-- MainWindow.xaml --> <TextBlock Text="{DynamicResource Welcome}" /> <Button Content="{DynamicResource LoginButton}" />
Runtime locale switching:
// Swap resource dictionary at runtime var dict = new ResourceDictionary { Source = new Uri($"Resources/Strings.{cultureName}.xaml", UriKind.Relative) }; Application.Current.Resources.MergedDictionaries.Clear(); Application.Current.Resources.MergedDictionaries.Add(dict);
ResX approach (simpler, works on all .NET versions):
// Standard .resx with generated class string welcome = Strings.Welcome;
// Runtime switch Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-FR"); // Re-read after culture change string welcomeFr = Strings.Welcome; // Now returns French
Community options:
-
WPF Localization Extensions -- RESX files with XAML markup extensions for declarative localization
-
LocBamlCore (h3xds1nz) -- unofficial port supporting .NET 9, for BAML localization on modern .NET
For WPF Host builder, MVVM Toolkit, and theming patterns, see [skill:dotnet-wpf-modern].
Agent Gotchas
-
Do not use IHtmlLocalizer or IViewLocalizer in Blazor. These are MVC-only features. Use IStringLocalizer<T> in Blazor components.
-
Do not rely on CultureInfo.CurrentCulture thread defaults in server code. Always pass explicit CultureInfo to formatting methods. Server thread culture may not match the request culture.
-
Do not hardcode plural forms. English "singular/plural" does not work for Arabic (6 forms), Polish, or other languages. Use MessageFormat.NET or SmartFormat.NET for proper CLDR pluralization.
-
Do not use LocBaml for WPF on .NET 8+. LocBaml is a .NET Framework-only sample tool. Use .resx files or resource dictionaries for modern WPF.
-
Do not forget BlazorWebAssemblyLoadAllGlobalizationData for Blazor WASM. Without it, only partial ICU data is loaded, causing incorrect date/number formatting for many cultures.
-
Do not add translation keys absent from the default .resx file. The default resource is the single source of truth; satellite assemblies must be a subset.
-
Do not use ResourceManager directly in AOT/trimmed apps. It relies on reflection. Use a source generator (ResXGenerator) for compile-time resource access.
-
Do not forget platform-specific setup for MAUI. iOS/Mac Catalyst need CFBundleLocalizations in Info.plist ; Windows needs Resource Language entries.