From dotnet-skills
Localizing .NET apps. .resx resources, IStringLocalizer, source generators, pluralization, RTL.
npx claudepluginhub wshaddix/dotnet-skillsThis skill uses the workspace's default tool permissions.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Captures architectural decisions in Claude Code sessions as structured ADRs. Auto-detects choices between alternatives and maintains a docs/adr log for codebase rationale.
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 boundary: This skill owns all cross-cutting localization concerns: resource formats, IStringLocalizer, formatting, RTL, pluralization. UI framework subsections provide architectural overview and cross-reference the framework-specific skills for deep implementation patterns.
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.
Resource files (.resx) are the standard .NET localization format. They compile into satellite assemblies resolved by ResourceManager with automatic culture fallback.
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.
<!-- 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>
<!-- 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>
// 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);
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 IStringLocalizerEmbedded.Json.Localization -- embedded JSON resourcesJSON resources are popular in ASP.NET Core but lack the built-in tooling support (Visual Studio designer, satellite assembly compilation) of .resx.
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.
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);
});
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;
}
}
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
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>
| 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 |
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")]
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"
// 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;
| 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 % |
bool isRtl = CultureInfo.CurrentCulture.TextInfo.IsRightToLeft;
// true for: ar-*, he-*, fa-*, ur-*, etc.
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].
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.
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"
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"
| 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 |
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 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:
CFBundleLocalizations to Info.plist<Resource Language="..."> entries to Package.appxmanifest<NeutralLanguage>en-US</NeutralLanguage> in csprojFor deep MAUI development patterns (controls, navigation, platform APIs), see [skill:dotnet-maui-development].
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].
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:
For WPF Host builder, MVVM Toolkit, and theming patterns, see [skill:dotnet-wpf-modern].
IHtmlLocalizer or IViewLocalizer in Blazor. These are MVC-only features. Use IStringLocalizer<T> in Blazor components.CultureInfo.CurrentCulture thread defaults in server code. Always pass explicit CultureInfo to formatting methods. Server thread culture may not match the request culture..resx files or resource dictionaries for modern WPF.BlazorWebAssemblyLoadAllGlobalizationData for Blazor WASM. Without it, only partial ICU data is loaded, causing incorrect date/number formatting for many cultures..resx file. The default resource is the single source of truth; satellite assemblies must be a subset.ResourceManager directly in AOT/trimmed apps. It relies on reflection. Use a source generator (ResXGenerator) for compile-time resource access.CFBundleLocalizations in Info.plist; Windows needs Resource Language entries.