dotnet-localization
npx skills add https://github.com/novotnyllc/dotnet-artisan --skill dotnet-localization
Agent 安装分布
Skill 文档
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-backedIStringLocalizerEmbedded.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.resxfile)
// 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
CFBundleLocalizationstoInfo.plist - Windows: Add
<Resource Language="...">entries toPackage.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
IHtmlLocalizerorIViewLocalizerin Blazor. These are MVC-only features. UseIStringLocalizer<T>in Blazor components. - Do not rely on
CultureInfo.CurrentCulturethread defaults in server code. Always pass explicitCultureInfoto 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
.resxfiles or resource dictionaries for modern WPF. - Do not forget
BlazorWebAssemblyLoadAllGlobalizationDatafor 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
.resxfile. The default resource is the single source of truth; satellite assemblies must be a subset. - Do not use
ResourceManagerdirectly 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
CFBundleLocalizationsinInfo.plist; Windows needsResource Languageentries.