ASP.NET Core Globalization and a custom RequestCultureProvider
Jürgen Gutsch - 25 October, 2025
In this post, I'm going to write about how to enable and use Globalization in ASP.NET Core. Since you don't can change the culture depending on route values by default, I show you how to create and register a custom RequestCultureProvider that does this job
Resources Files
Like in the old time of the .NET Framework, the resources (strings, images, icons, etc.) for different languages are stored in so-called resource files that end with resx
stored in a folder called Resources
by default.
Unlike in the good old time of the .NET Framework, the right resource files will be fetched automatically by the implementation of the specific Localizer
if you follow some naming conventions.
- If you inject the Localizer into a controller, the localizer should be named like
Controllers.ControllerClassName.[Culture].resx
or put to a subfolder calledControllers
and named likeControllerClassName.[Culture].resx
. - If you are injecting the Localizer into a view, it is almost the same as for the controllers. The difference is just to have a view name in the resource path instead of a controller name:
Views.ControllerName.ViewName.[culture].resx
orViews/ControllerName/ViewName.[culture].resx
.
It is up to you to decide how you like to structure your resource files. Personally, I prefer the folder option. Also, an autogenerated code file as you might know from the past is no longer needed since you need to use a localizer to access the resources.
Unfortunately there is now way yet to add a resource file via the .NET CLI. Maybe there will be a template in the future. I created the resource file with the Visual Studio 2022 and copied it to create the other files needed.
Localizers
You no longer need to use the resource manager to read the actual localized strings from the resource files. You can now use an IStringLocalizer
or an IHtmlLocalizer
. The latter doesn't HTML-encode the strings that are stored in the resource files and can be used to localize strings that contain HTML code if needed.:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
namespace Internationalization.Controllers;
public class HomeController : Controller
{
private readonly IStringLocalizer<HomeController> _localizer;
public AboutController(IStringLocalizer<HomeController> localizer)
{
_localizer = localizer;
}
public IActionResult Index()
{
return View(new { Title = _localizer["About Title"]});
}
}
The resource key named "About Title" doesn't need to exist or even the resource file doesn't need to exist. If the Localizer doesn't find the key, the key itself gets returned as a string. You can use any kind of string as a key. This can help you to develop the application without having the resource files in place.
You can even inject a localizer in the Razor View like this:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@model HomeIndexViewModel
@{
ViewData["Title"] = Localizer["Title"];
}
<h1>@ViewData["Title"]</h1>
In this case, it is an HtmlLocalizer
the key can also contain HTML that doesn't get encoded when writing it out to the view. Even if it's not recommended to save HTML in resource files it might be needed in some cases. You shouldn't do that because HTML should be part of the frontend templates like Razor, Blazor, etc.
Instead of using the ViewLocalizer
in the Razor Templates, you can also localize the entire View. Therefore you need to suffix the view name with the needed culture or put the view in a subfolder called like the culture. How localized views are handled needs to be configured when enabling Localization and Globalization.
Enabling Globalization in ASP.NET Core
As usual, you need to add the required services to enable localization:
builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
builder.Services.AddControllersWithViews()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
The first line adds general localization to be used in the C# code, like controllers, etc. setting the ResourcePath
in the options is optionally and just added to the snippets, to show you that you can change the path where the resources are stored.
After that, the ViewLocalization
, as well as the DataAnnotationLocalization,
was added to the Service Collection. The LanguageViewLocationExpanderFormat
tells the View localizer that in the case of localized views, the culture was added as a suffix to the filename instead of being part of the folder structure.
After adding the needed services to the service collection the required middleware needs to be added as well:
app.UseRequestLocalization(options =>
{
var culture = new List<CultureInfo> {
new CultureInfo("en-en"),
new CultureInfo("fr-fr"),
new CultureInfo("de-de")
};
options.DefaultRequestCulture = new RequestCulture("en-en");
options.SupportedCultures = culture;
options.SupportedUICultures = culture;
});
This Middleware uses pre-configured RequestCultureProviders
to set the Culture
and the UICulture
to the current process. With this culture set, the localizers can select the right resource files or the right localized views.
That's it with enabling Localization and Globalization. With this information, you should be able to create multilanguage applications already.
Culture vs. UI Culture
Setting the culture will set the application to a specific language and optional a region. If you also set the UI Culture
, you make a distinction between translating texts and between how numbers, dates, and currencies are displayed. The UI culture
is used to load resources from a corresponding resource file and the Culture
is used to change the way how numbers, dates, and currencies are formatted and displayed.
In some cases, it makes sense to handle that separately. If you only like to translate your page without taking care of number and date formats, etc. you should only change the UI Culture
.
Localize ViewModels
While enabling view localization, we also enabled DataAnnotationsLocalization
. This helps you to translate labels for form fields in case you use the @Html.LabelFor()
method. This doesn't need to specify the ResourceType
anymore. Since there is no longer an autogenerated C#-File, there is also no ResourceType
specified. Inside the ViewModel
you just need to specify the DisplayAttribute
:
public class EmployeeViewModel
{
[Display(Name = "Number")]
public int Number { get; set; }
[Display(Name = "Firstname")]
public string? Firstname { get; set; }
[Display(Name = "Lastname")]
public string? Lastname { get; set; }
[Display(Name = "Department")]
public string? Department { get; set; }
[Display(Name = "Phone")]
public string? Phone { get; set; }
[Display(Name = "Email")]
public string? Email { get; set; }
[Display(Name = "Date of birth")]
public DateTime DateOfBirth { get; set; }
[Display(Name = "Size")]
public decimal Size { get; set; }
[Display(Name = "Salery")]
public decimal Salery { get; set; }
}
The DataAnnotationsLocalizer
will automatically use the string that is set in the Name
property as a key to search for the relevant resource. This also works for the Description
and the ShortName
properties.
The resource file that is used to translate the display names has to be placed inside subfolders called ViewModels/ControllerName
. Example: /Resources/ViewModels/Home/EmployeeModel.de-DE.resx
RequestCultureProviders
As mentioned RequestCultureProviders
retrieve the culture from somewhere and prepare it for the process to work with the culture. The RequestCultureProviders
return a ProviderCultureResult
that has the property Culture
and UICulture
set. Both cultures can differ if needed. In most cases, it will be the same.
There are three preconfigured RequestCultureProviders
:
-
QueryStringRequestCultureProvider
This provider extracts theCulture
andUICulture
from query string values if there are any. This means you can switch the language by just setting the query strings.?culture-de-DE&ui-culture=de-DE
-
CookieRequestCultureProvider
This provider extracts the culture information from a specific cookie. The cookie-name is.AspNetCore.Culture
and the value of the cookie might look like this:c=es-MX|uic=es-MX
(c
is the culture anduic
is the ui-culture) -
AcceptLanguageHeaderRequestCultureProvider
That provider extracts the language information from theAccept-Language
Header that gets sent by the browsers. Every browser has preferred languages configured and sends those languages to the server. With this information, you can localize your application-specific to the user's language
As you have seen in the previews section, not every language sent by the accept-language header, cookie, or query string gets accepted by your application. You need to define a list of supported cultures and a default request culture that is used if the language sent by the client isn't supported by your application.
Creating a custom RequestCultureProvider
What I am missing in the list of RequestCultureProviders
is a RouteValueRequestCultureProvider
. A provider that is getting the culture information from a route value in case it is part of the route like this /en-US/Home/Index/
Let's assume we have a route configured like this:
app.MapControllerRoute(
name: "default",
pattern: "{culture=en-us}/{controller=Home}/{action=Index}/{id?}");
This adds the culture as part of the route.
Actually, I built a RouteValueRequestCultureProvider
that handles the route values:
using Microsoft.AspNetCore.Localization;
namespace Internationalization.Providers;
// <summary>
/// Determines the culture information for a request via values in the route values.
/// </summary>
public class RouteValueRequestCultureProvider : RequestCultureProvider
{
/// <summary>
/// The key that contains the culture name.
/// Defaults to "culture".
/// </summary>
public string RouteValueKey { get; set; } = "culture";
/// <summary>
/// The key that contains the UI culture name. If not specified or no value is found,
/// <see cref="RouteValueKey"/> will be used.
/// Defaults to "ui-culture".
/// </summary>
public string UIRouteValueKey { get; set; } = "ui-culture";
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var request = httpContext.Request;
if (!request.RouteValues.Any())
{
return NullProviderCultureResult;
}
string? queryCulture = null;
string? queryUICulture = null;
if (!string.IsNullOrWhiteSpace(RouteValueKey))
{
queryCulture = request.RouteValues[RouteValueKey]?.ToString();
}
if (!string.IsNullOrWhiteSpace(UIRouteValueKey))
{
queryUICulture = request.RouteValues[UIRouteValueKey]?.ToString();
}
if (queryCulture == null && queryUICulture == null)
{
// No values specified
return NullProviderCultureResult;
}
if (queryCulture != null && queryUICulture == null)
{
// Value for culture but not for UI culture so default to culture value for both
queryUICulture = queryCulture;
}
else if (queryCulture == null && queryUICulture != null)
{
// Value for UI culture but not for culture so default to UI culture value for both
queryCulture = queryUICulture;
}
var providerResultCulture = new ProviderCultureResult(queryCulture, queryUICulture);
return Task.FromResult<ProviderCultureResult?>(providerResultCulture);
}
}
This RouteValueRequestCultureProvider
reads the culture and the ui-culture out of the route values and returns a ProviderCultureResult
that will be used by the Localizers.
The route engine handles the generation of the route URLs for us if we use the MVC mechanisms to create links and tags. We'll now have the selected language and region everywhere in the routes.
To create a language changer, we Just need to change the culture in the route value like this:
<ul class="navbar-nav flex-grow-1 justify-content-end">
<li class="nav-item">
<a class="nav-link text-dark" asp-area=""
asp-controller="@Context.GetRouteValue("Controller")"
asp-action="@Context.GetRouteValue("Action")"
asp-route-culture="en-US">EN</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area=""
asp-controller="@Context.GetRouteValue("Controller")"
asp-action="@Context.GetRouteValue("Action")"
asp-route-culture="de-DE">DE</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area=""
asp-controller="@Context.GetRouteValue("Controller")"
asp-action="@Context.GetRouteValue("Action")"
asp-route-culture="fr-FR">FR</a>
</li>
</ul>
Changing the culture and the UI culture also changes the way how dates, numbers, and currencies are displayed. This means the language changer is also changing the region and will display the currency in Euro in case you change the region to a region that uses the Euro as local currency. You need to keep this in mind when working with financial data because just changing the currency doesn't make sense if you don't convert the actual numbers to the local currency as well. If you don't want to change the currency, you should hard code the way how to format and display currency. Just fixing the culture of a region and changing the UI culture would also set the numbers and dates to a fixed format which is not what we want to have.
This is the start page of the sample project in French.
(I apologies for wrong translations. Unfortunately it is more than 25 years in the past when I was learning French in school.)
Sample application and Conclusion
This is actually working and I created a small application to demonstrate this. This sample includes all the topics of this post. You will find the sample project in Github.
Microsoft reduces the complexity a lot. On the other hand, if you were used to work with more complex resource handling in the past, you will stumble upon small things you won't expect like me. However, adding Globalization and Localization in .NET 7 is easy and I like the way how to get it working.