| name | i18n |
| description | Guidance for internationalization (i18n) architecture in .NET applications. USE FOR: designing i18n-ready applications, externalizing user-facing strings, building multi-language ASP.NET Core apps, Razor view localization, data annotation localization. DO NOT USE FOR: low-level culture formatting (use globalization-localization), ICU plural/gender patterns (use messageformat-net), basic resource file operations (use resources-localization).
|
| license | MIT |
| metadata | {"displayName":"Internationalization (i18n)","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | ["claude","copilot","cursor"] |
| references | [{"title":"Globalization and Localization in ASP.NET Core","url":"https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization"},{"title":"Globalize and Localize .NET Applications","url":"https://learn.microsoft.com/en-us/dotnet/core/extensions/globalization-and-localization"},{"title":".NET Localization Documentation","url":"https://learn.microsoft.com/en-us/dotnet/core/extensions/localization"}] |
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);
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"];
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;
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)
{
var greeting = _localizer["GreetingTemplate", userName];
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
.resx files 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 of IStringLocalizerFactory directly, 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
.resx files.
- Always provide a neutral culture fallback (e.g.,
SharedResource.resx without 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
ResourceNotFound gracefully by checking LocalizedString.ResourceNotFound in development mode and logging missing keys for the translation team.
- Support RTL layouts by setting
dir="rtl" conditionally in your layout based on CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft.
- Keep resource keys stable and descriptive (e.g.,
OrderConfirmation_Subject rather than String1) because renaming keys breaks existing translations.