From Legacy Resources to Modern Localization
In classic ASP.NET, you managed globalization through Global.asax, scattered resource files, and manual culture detection from browser headers. Each request required custom code to parse Accept-Language headers and set thread culture. Resource loading was brittle, and testing different cultures meant changing browser settings or deploying multiple builds.
ASP.NET Core streamlines this with middleware that automatically detects culture from multiple sources, dependency injection for IStringLocalizer that loads resources cleanly, and built-in support for culture-specific views and validation messages. Modern .NET 8 handles culture negotiation, fallback chains, and resource loading with minimal configuration.
You'll learn how to configure request localization middleware, use IStringLocalizer for type-safe resource loading, detect culture from query strings and cookies, and build a minimal API that responds in multiple languages. This replaces manual culture management with declarative configuration.
Setting Up Request Localization Middleware
Request localization middleware examines each HTTP request and sets the current thread's culture based on configured providers. You specify which cultures your app supports and which providers to check. The middleware runs early in the pipeline so all subsequent middleware and endpoints see the correct culture.
Configure supported cultures and default culture in Program.cs. The middleware checks providers in order until one returns a valid culture, then sets CurrentCulture and CurrentUICulture for the request.
using Microsoft.AspNetCore.Localization;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
// Configure supported cultures
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
new CultureInfo("de-DE"),
new CultureInfo("ja-JP")
};
builder.Services.Configure(options =>
{
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
// Check query string, then cookie, then Accept-Language header
options.RequestCultureProviders = new List
{
new QueryStringRequestCultureProvider(),
new CookieRequestCultureProvider(),
new AcceptLanguageHeaderRequestCultureProvider()
};
});
builder.Services.AddControllers();
builder.Services.AddLocalization();
var app = builder.Build();
// Enable localization middleware
app.UseRequestLocalization();
app.MapControllers();
app.Run();
This configuration makes your app support four cultures with English as the default. The middleware first checks for a culture query string parameter, then looks for a culture cookie, and finally examines the Accept-Language header. Users can switch languages with URLs like /products?culture=fr-FR or by setting cookies through a language selector.
Using IStringLocalizer for Resources
IStringLocalizer provides type-safe access to resource files with automatic culture detection. You inject IStringLocalizer<T> into controllers or services, and it loads the appropriate resource file based on the current request culture. Resource files follow the naming pattern TypeName.Culture.resx.
Create resource files for each type you want to localize. The framework maps IStringLocalizer<HomeController> to Resources/Controllers/HomeController.resx and its culture-specific variants automatically.
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="Welcome" xml:space="preserve">
<value>Welcome to our store</value>
</data>
<data name="ProductsFound" xml:space="preserve">
<value>Found {0} products</value>
</data>
</root>
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
namespace MyApp.Controllers;
[ApiController]
[Route("api/[controller]")]
public class HomeController : ControllerBase
{
private readonly IStringLocalizer _localizer;
public HomeController(IStringLocalizer localizer)
{
_localizer = localizer;
}
[HttpGet]
public IActionResult Get()
{
var welcomeMessage = _localizer["Welcome"];
var productCount = 42;
var productsMessage = _localizer["ProductsFound", productCount];
return Ok(new
{
message = welcomeMessage.Value,
products = productsMessage.Value
});
}
}
The localizer indexer returns a LocalizedString with the translated value. You can pass format arguments directly to the indexer for parameterized strings. If a translation is missing, it returns the resource key itself as a fallback, preventing crashes but signaling that translation is needed.
Custom Culture Providers
Sometimes you need culture detection beyond query strings and headers. You might store user language preferences in a database or determine culture from subdomain. Custom providers implement IRequestCultureProvider and can check any aspect of the request.
using Microsoft.AspNetCore.Localization;
public class RouteValueRequestCultureProvider : RequestCultureProvider
{
public override Task DetermineProviderCultureResult(
HttpContext httpContext)
{
if (httpContext?.Request?.RouteValues == null)
{
return Task.FromResult(null);
}
var culture = httpContext.Request.RouteValues["culture"]?.ToString();
if (string.IsNullOrEmpty(culture))
{
return Task.FromResult(null);
}
return Task.FromResult(
new ProviderCultureResult(culture));
}
}
builder.Services.Configure(options =>
{
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
// Custom provider first, then defaults
options.RequestCultureProviders.Insert(0,
new RouteValueRequestCultureProvider());
});
// Map routes with culture parameter
app.MapGet("/{culture}/products", (string culture) =>
{
return Results.Ok(new { culture });
})
.WithName("GetProducts");
This provider extracts culture from route parameters, enabling URLs like /fr-FR/products or /de-DE/about. Insert it at the beginning of the providers list so it takes precedence over query strings and headers. This pattern works well for SEO-friendly URLs with language segments.
Production Integration Patterns
In production apps, combine multiple detection strategies and provide language switchers. Store user preferences in cookies so they persist across sessions. Add endpoints to change language explicitly and redirect back to the original page.
Include proper fallback handling and logging for missing translations. Configure resource file locations and enable resource reloading in development for faster iteration.
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Localization;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
var supportedCultures = new[] { "en-US", "fr-FR", "de-DE", "ja-JP" }
.Select(c => new CultureInfo(c))
.ToArray();
builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
builder.Services.Configure(options =>
{
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
var app = builder.Build();
app.UseRequestLocalization();
// Language switcher endpoint
app.MapPost("/set-language", (string culture, HttpContext context) =>
{
if (supportedCultures.Any(c => c.Name == culture))
{
context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
}
return Results.Ok(new { culture });
});
app.MapGet("/", (IStringLocalizer localizer) =>
{
return Results.Ok(new { message = localizer["Welcome"].Value });
});
app.Run();
This setup provides a language switcher endpoint that sets cookies with a one-year expiration. Users can change language explicitly and their choice persists. The cookie provider reads this value on subsequent requests, maintaining language preference across sessions.
Try It Yourself
Build a minimal API with localization support and test culture switching through query strings.
Steps
- Initialize:
dotnet new web -n LocalizationDemo
- Change directory:
cd LocalizationDemo
- Replace Program.cs with the code below
- Run:
dotnet run
- Test with curl:
curl "http://localhost:5000/"
curl "http://localhost:5000/?culture=fr-FR"
curl "http://localhost:5000/?culture=de-DE"
using Microsoft.AspNetCore.Localization;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
new CultureInfo("de-DE")
};
builder.Services.Configure(options =>
{
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
var app = builder.Build();
app.UseRequestLocalization();
app.MapGet("/", (HttpContext context) =>
{
var culture = CultureInfo.CurrentCulture;
var now = DateTime.Now;
var amount = 1234.56m;
return Results.Ok(new
{
culture = culture.Name,
date = now.ToString("D", culture),
currency = amount.ToString("C", culture),
greeting = culture.Name switch
{
"fr-FR" => "Bonjour",
"de-DE" => "Guten Tag",
_ => "Hello"
}
});
});
app.Run();
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Console
The default request returns English formatting with "Hello" and dates like "Monday, November 4, 2025". Adding ?culture=fr-FR switches to French with "Bonjour" and "lundi 4 novembre 2025". German shows "Guten Tag" with period-separated thousands. The middleware detects culture from the query string and sets it for the entire request automatically.