Edit

Make an ASP.NET Core app's content localizable

Note

This isn't the latest version of this article. For the current release, see the .NET 10 version of this article.

Warning

This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 10 version of this article.

By Hisham Bin Ateya, Damien Bowden, Bart Calixto and Nadeem Afana

One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures.

IStringLocalizer

IStringLocalizer and IStringLocalizer<T> were architected to improve productivity when developing localized apps. IStringLocalizer uses the ResourceManager and ResourceReader to provide culture-specific resources at run time. The interface has an indexer and an IEnumerable for returning localized strings. IStringLocalizer doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development.

The following code example shows how to wrap the string "About Title" for localization.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.Controllers;

[Route("api/[controller]")]
public class AboutController : Controller
{
    private readonly IStringLocalizer<AboutController> _localizer;

    public AboutController(IStringLocalizer<AboutController> localizer)
    {
        _localizer = localizer;
    }

    [HttpGet]
    public string Get()
    {
        return _localizer["About Title"];
    }
}

In the preceding code, the IStringLocalizer<T> implementation comes from Dependency Injection. If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title".

You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file.

Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language .resx file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings.

IHtmlLocalizer

Use the IHtmlLocalizer<TResource> implementation for resources that contain HTML. IHtmlLocalizer HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the name parameter is HTML-encoded.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;

namespace Localization.Controllers;

public class BookController : Controller
{
    private readonly IHtmlLocalizer<BookController> _localizer;

    public BookController(IHtmlLocalizer<BookController> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Hello(string name)
    {
        ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

        return View();
    }

NOTE: Generally, only localize text, not HTML.

IStringLocalizerFactory

At the lowest level, IStringLocalizerFactory can be retrieved from of Dependency Injection:

public class TestController : Controller
{
    private readonly IStringLocalizer _localizer;
    private readonly IStringLocalizer _localizer2;

    public TestController(IStringLocalizerFactory factory)
    {
        var type = typeof(SharedResource);
        var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
        _localizer = factory.Create(type);
        _localizer2 = factory.Create("SharedResource", assemblyName.Name);
    }       

    public IActionResult About()
    {
        ViewData["Message"] = _localizer["Your application description page."] 
            + " loc 2: " + _localizer2["Your application description page."];

        return View();
    }

The preceding code demonstrates each of the two factory create methods.

Shared resources

You can partition your localized strings by controller or area, or have just one container. In the sample app, a marker class named SharedResource is used for shared resources. The marker class is never called:

// Dummy class to group shared resources

namespace Localization;

public class SharedResource
{
}

In the following sample, the InfoController and the SharedResource localizers are used:

public class InfoController : Controller
{
    private readonly IStringLocalizer<InfoController> _localizer;
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public InfoController(IStringLocalizer<InfoController> localizer,
                   IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _localizer = localizer;
        _sharedLocalizer = sharedLocalizer;
    }

    public string TestLoc()
    {
        string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
                     " Info resx " + _localizer["Hello!"];
        return msg;
    }

View localization

The IViewLocalizer service provides localized strings for a view. The ViewLocalizer class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of IViewLocalizer:

@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>

The default implementation of IViewLocalizer finds the resource file based on the view's file name. There's no option to use a global shared resource file. ViewLocalizer implements the localizer using IHtmlLocalizer, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and IViewLocalizer HTML-encodes the parameters but not the resource string. Consider the following Razor markup:

@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]

A French resource file could contain the following values:

Key Value
<i>Hello</i> <b>{0}!</b> <i>Bonjour</i> <b>{0} !</b>

The rendered view would contain the HTML markup from the resource file.

Generally, only localize text, not HTML.

To use a shared resource file in a view, inject IHtmlLocalizer<T>:

@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>

DataAnnotations localization

DataAnnotations error messages are localized with IStringLocalizer<T>. Using the option ResourcesPath = "Resources", the error messages in RegisterViewModel can be stored in either of the following paths:

  • Resources/ViewModels.Account.RegisterViewModel.fr.resx
  • Resources/ViewModels/Account/RegisterViewModel.fr.resx
using System.ComponentModel.DataAnnotations;

namespace Localization.ViewModels.Account;

public class RegisterViewModel
{
    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password field is required.")]
    [StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.",
                                                                 MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage =
                            "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

Non-validation attributes are localized.

How to use one resource string for multiple classes

The following code shows how to use one resource string for validation attributes with multiple classes:

    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });

In the preceding code, SharedResource is the class corresponding to the .resx file where the validation messages are stored. With this approach, DataAnnotations only uses SharedResource, rather than the resource for each class.

DataAnnotations localization in Minimal APIs and Blazor

Validation localization is available for Minimal API and Blazor apps that opt into the Microsoft.Extensions.Validation pipeline by calling AddValidation() in Program.cs. Localize validation error messages and the display names of validated properties and parameters by also calling AddValidationLocalization:

builder.Services.AddValidation();
builder.Services.AddValidationLocalization<ValidationResources>();

The localization integration does not apply to MVC and Razor Pages apps, or to Blazor forms that don't include AddValidation.

Note

The integration is provided by the Microsoft.Extensions.Validation.Localization package, which builds on the Microsoft.Extensions.Validation package. Both packages are included in the Web SDK (Microsoft.NET.Sdk.Web) and the Razor SDK (Microsoft.NET.Sdk.Razor), so apps that use those SDKs don't need explicit package references. Standalone Blazor WebAssembly apps and other project that do not use the Web SDK or the Razor SDK must reference both packages explicitly:

<PackageReference Include="Microsoft.Extensions.Validation" Version="11.0.0" />
<PackageReference Include="Microsoft.Extensions.Validation.Localization" Version="11.0.0" />

Resource file lookup

By default, validation localization resolves messages and display names from .resx resource files using ASP.NET Core's standard IStringLocalizer infrastructure. For an overview of authoring and naming .resx files, see Provide localized resources for languages and cultures in an ASP.NET Core app.

To use a shared resource file for every validated type, pass a marker type argument to AddValidationLocalization:

builder.Services.AddValidationLocalization<ValidationResources>();

The marker type identifies the .resx file the framework uses (for example, ValidationResources.resx for the default culture and ValidationResources.fr.resx for French).

Important

A shared resource file is necessary for Minimal APIs, because top-level parameters on Minimal API endpoints don't have a containing type that the default per-type convention can key on.

For per-type resource file resolution, use the non-generic overload of AddValidationLocalization:

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddValidationLocalization();

This approach follows the standard ASP.NET Core convention: under the project's configured ResourcesPath, the type's full name (without the project's root namespace prefix) is used as a dotted path. For example, with ResourcesPath = "Resources", a project whose root namespace is Contoso looks up validation messages for Contoso.Models.Customer in Resources/Models/Customer.fr.resx (or equivalently Resources/Models.Customer.fr.resx) for French. For a full description of the .resx naming and placement conventions, see Provide localized resources for languages and cultures in an ASP.NET Core app.

Customize the localizer creation

For full control over which .resx file to use for a given validated type, set ValidationLocalizationOptions.LocalizerProvider. The delegate receives the validated type and an IStringLocalizerFactory, and returns the IStringLocalizer to use:

builder.Services.AddValidationLocalization(options =>
{
    options.LocalizerProvider = (type, factory) =>
        type is not null && type.Namespace?.StartsWith("Contoso.Admin") == true
            ? factory.Create(typeof(AdminValidationResources))
            : factory.Create(typeof(SharedValidationResources));
});

Localizing from other sources

The localization data doesn't have to come from .resx files. Validation localization resolves strings through whichever IStringLocalizerFactory is registered in DI. Registering a custom factory implementation switches validation messages to that factory's backing store, with no further configuration:

builder.Services.AddSingleton<IStringLocalizerFactory, MyJsonStringLocalizerFactory>();
builder.Services.AddValidation();
builder.Services.AddValidationLocalization();

This can be used to load localized messages from JSON files, databases, remote translation services, and other sources.

What gets localized

When validation localization is configured:

  • Error messages whose ErrorMessage property is set to a resource key are looked up by that key. If no resource entry matches, the literal value of ErrorMessage is used as the error message.
  • Display names supplied as literal strings through [Display(Name = "...")] or [DisplayName("...")] are looked up by the literal value as a resource key. If no resource entry matches, the literal value is used as the display name.

Attributes that use static resource localization (via the DisplayAttribute.ResourceType and ValidationAttribute.ErrorMessageResourceType properties) are not processed by the validation localizer registered by AddValidationLocalization.

Localize the built-in validation messages

Some applications might find it useful to translate or override the default error messages of attributes like RequiredAttribute and StringLengthAttribute without setting ErrorMessage on every attribute instance. This can be achieved by an ErrorMessageKeyProvider that derives a resource key programmatically:

builder.Services.AddValidationLocalization(options =>
{
    options.ErrorMessageKeyProvider = ctx => ctx.Attribute.ErrorMessage is not null
        ? ctx.Attribute.ErrorMessage
        : $"{ctx.Attribute.GetType().Name}_Error";
});

With the preceding configuration, a [Required] attribute with no ErrorMessage looks up the resource key RequiredAttribute_Error, a [StringLength(50)] looks up StringLengthAttribute_Error, and so on. The key provider runs only when ErrorMessage isn't set on the attribute instance, so model-specific overrides via ErrorMessage = "MyKey" continue to take precedence.

Configure localization services

Localization services are configured in Program.cs:

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

builder.Services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
  • AddLocalization adds the localization services to the services container, including implementations for IStringLocalizer<T> and IStringLocalizerFactory. The preceding code also sets the resources path to "Resources".

  • AddViewLocalization adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the Index.fr.cshtml file.

  • AddDataAnnotationsLocalization adds support for localized DataAnnotations validation messages through IStringLocalizer abstractions.

Note

You may not be able to enter decimal commas in decimal fields. To support jQuery validation for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. See this GitHub comment 4076 for instructions on adding decimal comma.

Next steps

Localizing an app also involves the following tasks:

Additional resources

By Rick Anderson, Damien Bowden, Bart Calixto, Nadeem Afana, and Hisham Bin Ateya

One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures.

IStringLocalizer

IStringLocalizer and IStringLocalizer<T> were architected to improve productivity when developing localized apps. IStringLocalizer uses the ResourceManager and ResourceReader to provide culture-specific resources at run time. The interface has an indexer and an IEnumerable for returning localized strings. IStringLocalizer doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development.

The following code example shows how to wrap the string "About Title" for localization.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.Controllers
{
    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private readonly IStringLocalizer<AboutController> _localizer;

        public AboutController(IStringLocalizer<AboutController> localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _localizer["About Title"];
        }
    }
}

In the preceding code, the IStringLocalizer<T> implementation comes from Dependency Injection. If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title".

You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file.

Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language .resx file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings.

IHtmlLocalizer

Use the IHtmlLocalizer<T> implementation for resources that contain HTML. IHtmlLocalizer HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the name parameter is HTML-encoded.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;

namespace Localization.Controllers
{
    public class BookController : Controller
    {
        private readonly IHtmlLocalizer<BookController> _localizer;

        public BookController(IHtmlLocalizer<BookController> localizer)
        {
            _localizer = localizer;
        }

        public IActionResult Hello(string name)
        {
            ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

            return View();
        }

Note

Generally, only localize text, not HTML.

IStringLocalizerFactory

At the lowest level, you can get IStringLocalizerFactory out of Dependency Injection:

{
    public class TestController : Controller
    {
        private readonly IStringLocalizer _localizer;
        private readonly IStringLocalizer _localizer2;

        public TestController(IStringLocalizerFactory factory)
        {
            var type = typeof(SharedResource);
            var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
            _localizer = factory.Create(type);
            _localizer2 = factory.Create("SharedResource", assemblyName.Name);
        }       

        public IActionResult About()
        {
            ViewData["Message"] = _localizer["Your application description page."] 
                + " loc 2: " + _localizer2["Your application description page."];

The preceding code demonstrates each of the two factory create methods.

Shared resources

You can partition your localized strings by controller or area, or have just one container. In the sample app, a dummy class named SharedResource is used for shared resources.

// Dummy class to group shared resources

namespace Localization
{
    public class SharedResource
    {
    }
}

Some developers use the Startup class to contain global or shared strings. In the following sample, the InfoController and the SharedResource localizers are used:

public class InfoController : Controller
{
    private readonly IStringLocalizer<InfoController> _localizer;
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public InfoController(IStringLocalizer<InfoController> localizer,
                   IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _localizer = localizer;
        _sharedLocalizer = sharedLocalizer;
    }

    public string TestLoc()
    {
        string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
                     " Info resx " + _localizer["Hello!"];
        return msg;
    }

View localization

The IViewLocalizer service provides localized strings for a view. The ViewLocalizer class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of IViewLocalizer:

@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>

The default implementation of IViewLocalizer finds the resource file based on the view's file name. There's no option to use a global shared resource file. ViewLocalizer implements the localizer using IHtmlLocalizer, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and IViewLocalizer HTML-encodes the parameters but not the resource string. Consider the following Razor markup:

@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]

A French resource file could contain the following values:

Key Value
<i>Hello</i> <b>{0}!</b> <i>Bonjour</i> <b>{0} !</b>

The rendered view would contain the HTML markup from the resource file.

Note

Generally, only localize text, not HTML.

To use a shared resource file in a view, inject IHtmlLocalizer<T>:

@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>

DataAnnotations localization

DataAnnotations error messages are localized with IStringLocalizer<T>. Using the option ResourcesPath = "Resources", the error messages in RegisterViewModel can be stored in either of the following paths:

  • Resources/ViewModels.Account.RegisterViewModel.fr.resx
  • Resources/ViewModels/Account/RegisterViewModel.fr.resx
public class RegisterViewModel
{
    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password field is required.")]
    [StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

In ASP.NET Core MVC 1.1.0 or later, non-validation attributes are localized.

How to use one resource string for multiple classes

The following code shows how to use one resource string for validation attributes with multiple classes:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });
}

In the preceding code, SharedResource is the class corresponding to the .resx file where the validation messages are stored. With this approach, DataAnnotations only uses SharedResource, rather than the resource for each class.

Configure localization services

Localization services are configured in the Startup.ConfigureServices method:

services.AddLocalization(options => options.ResourcesPath = "Resources");

services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
  • AddLocalization adds the localization services to the services container, including implementations for IStringLocalizer<T> and IStringLocalizerFactory. The preceding code also sets the resources path to "Resources".

  • AddViewLocalization adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the Index.fr.cshtml file.

  • AddDataAnnotationsLocalization adds support for localized DataAnnotations validation messages through IStringLocalizer abstractions.

Note

You may not be able to enter decimal commas in decimal fields. To support jQuery validation for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. See this GitHub comment 4076 for instructions on adding decimal comma.

Next steps

Localizing an app also involves the following tasks:

Additional resources