Internationalization (i18n)
Overview
Internationalization (i18n) is the architectural process of designing .NET applications so they can be adapted to multiple languages and regions without code changes. In ASP.NET Core this is built on three pillars: IStringLocalizer<T> for string lookup, IViewLocalizer for Razor views, and IHtmlLocalizer<T> for HTML-safe localized content. Resource files (.resx) provide the translation storage, while the request localization middleware selects the active culture at runtime.
A well-internationalized application externalizes every user-facing string, uses culture-aware formatting, and supports right-to-left (RTL) layouts. The goal is to make adding a new language a content task (adding a .resx file) rather than a code change.
Service Registration and Resource Files
Configure localization services and point them at your resource directory. Resource files follow the naming convention {Type}.{culture}.resx.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
// Resources/Controllers/HomeController.en-US.resx
// Resources/Controllers/HomeController.fr-FR.resx
// Resources/Controllers/HomeController.ar-SA.resx
builder.Services.AddLocalization(options =>
options.ResourcesPath = "Resources");
builder.Services.AddControllersWithViews()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
new CultureInfo("ar-SA"),
};
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
var app = builder.Build();
app.UseRequestLocalization();
app.MapControllers();
app.Run();
Controller Localization with IStringLocalizer
Inject IStringLocalizer<T> to resolve localized strings in controllers and services. The type parameter T determines which .resx file is used for lookup.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
namespace MyApp.Controllers;
public class HomeController : Controller
{
private readonly IStringLocalizer<HomeController> _localizer;
public HomeController(IStringLocalizer<HomeController> localizer)
{
_localizer = localizer;
}
public IActionResult Index()
{
ViewData["Title"] = _localizer["WelcomeTitle"];
ViewData["Message"] = _localizer["WelcomeMessage",
DateTime.Now.ToString("D")];
return View();
}
public IActionResult Error()
{
var message = _localizer["ErrorNotFound"];
// message.ResourceNotFound == true if key is missing
return View("Error", message.Value);
}
}
Razor View Localization
Use IViewLocalizer in Razor views for inline localized content. It resolves resources based on the view file path.
@using Microsoft.AspNetCore.Mvc.Localization @inject IViewLocalizer Localizer <h1>@Localizer["PageHeading"]</h1> <p>@Localizer["WelcomeText", User.Identity?.Name ?? "Guest"]</p> <!-- HTML-safe localization --> <div>@Localizer["RichContent"]</div> <!-- RichContent value: "Click <a href='/help'>here</a> for help" --> <!-- IViewLocalizer returns IHtmlContent, so HTML is not escaped -->
Data Annotation Localization
Localize validation error messages by enabling data annotations localization.
using System.ComponentModel.DataAnnotations;
namespace MyApp.Models;
public class RegisterViewModel
{
[Required(ErrorMessage = "FieldRequired")]
[Display(Name = "EmailLabel")]
[EmailAddress(ErrorMessage = "InvalidEmail")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "FieldRequired")]
[Display(Name = "PasswordLabel")]
[StringLength(100, MinimumLength = 8,
ErrorMessage = "PasswordLength")]
public string Password { get; set; } = string.Empty;
}
The error message keys (FieldRequired, InvalidEmail, PasswordLength) are resolved from a shared resource file:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
builder.Services.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider =
(type, factory) =>
factory.Create(typeof(SharedResource));
});
namespace MyApp.Resources;
// Marker class for shared resources
// Resources/SharedResource.en-US.resx
// Resources/SharedResource.fr-FR.resx
public class SharedResource { }
Shared Resource Pattern
Use a shared resource class for strings that appear across multiple controllers and views.
using Microsoft.Extensions.Localization;
namespace MyApp.Services;
public class NotificationService
{
private readonly IStringLocalizer<SharedResource> _sharedLocalizer;
private readonly IStringLocalizer<NotificationService> _localizer;
public NotificationService(
IStringLocalizer<SharedResource> sharedLocalizer,
IStringLocalizer<NotificationService> localizer)
{
_sharedLocalizer = sharedLocalizer;
_localizer = localizer;
}
public string GetGreeting(string userName)
{
// From NotificationService.{culture}.resx
var greeting = _localizer["GreetingTemplate", userName];
// From SharedResource.{culture}.resx
var appName = _sharedLocalizer["ApplicationName"];
return $"{greeting} - {appName}";
}
}
Resource File Naming Conventions
| Scenario | File Path |
|---|---|
Controller HomeController | Resources/Controllers/HomeController.fr-FR.resx |
Service OrderService | Resources/Services/OrderService.fr-FR.resx |
View Views/Home/Index.cshtml | Resources/Views/Home/Index.fr-FR.resx |
| Shared resources | Resources/SharedResource.fr-FR.resx |
| Neutral fallback | Resources/Controllers/HomeController.resx |
Best Practices
- •Externalize every user-facing string into
.resxfiles from the start; retrofitting i18n into an existing codebase is significantly more expensive than designing for it up front. - •Use the
IStringLocalizer<T>generic pattern instead ofIStringLocalizerFactorydirectly, so the DI container automatically resolves the correct resource file based on the type. - •Create a shared resource class for strings used across multiple components (button labels, validation messages, app name) to avoid duplication across
.resxfiles. - •Always provide a neutral culture fallback (e.g.,
SharedResource.resxwithout a culture suffix) so missing translations return a meaningful default rather than the resource key. - •Use parameterized localization (
_localizer["Hello, {0}!", name]) instead of string concatenation to support word-order differences across languages. - •Enable data annotations localization with a shared resource provider so all validation messages are centralized and translatable.
- •Test with pseudo-localization (artificially lengthened strings, accented characters) to catch UI layout issues before real translations arrive.
- •Handle
ResourceNotFoundgracefully by checkingLocalizedString.ResourceNotFoundin development mode and logging missing keys for the translation team. - •Support RTL layouts by setting
dir="rtl"conditionally in your layout based onCultureInfo.CurrentUICulture.TextInfo.IsRightToLeft. - •Keep resource keys stable and descriptive (e.g.,
OrderConfirmation_Subjectrather thanString1) because renaming keys breaks existing translations.