Skip to main content

Полное руководство по локализации ASP.NET Core

От IStringLocalizer до рабочей среды: настройте локализацию на основе ресурсов в ASP.NET Core, а затем автоматизируйте перевод с помощью ИИ.

1

Включить службы локализации

Зарегистрируйте службы локализации в Program.cs с помощью AddLocalization(), настройте поддерживаемые культуры и добавьте промежуточное ПО локализации запросов. Так Вы подключите весь конвейер локализации приложения ASP.NET Core.

AddLocalization() регистрирует IStringLocalizer и IStringLocalizerFactory в контейнере DI. ResourcesPath сообщает фреймворку, где искать файлы .resx. AddViewLocalization() включает IViewLocalizer в представлениях Razor, а AddDataAnnotationsLocalization() — локализованные сообщения проверки.
Program.cs
using Microsoft.AspNetCore.Localization;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);

// 1. Register localization services
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");

// 2. Add MVC with view/data-annotation localization
builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization();

var app = builder.Build();

// 3. Configure supported cultures
var supportedCultures = new[] { "en", "de", "ja", "es", "pt-BR" }
    .Select(c => new CultureInfo(c)).ToArray();

app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures,
});

app.UseStaticFiles();
app.UseRouting();
app.MapControllers();
app.Run();
2

Создать файлы ресурсов RESX

ASP.NET Core использует для переводов файлы RESX (ресурсы XML). Создайте по одному файлу для каждой культуры и класса: HomeController.en.resx, HomeController.de.resx и так далее. Фреймворк разрешает правильный файл на основе культуры текущего запроса.

Resources/Controllers/HomeController.{culture}.resx
<!-- Resources/Controllers/HomeController.en.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Welcome" xml:space="preserve">
    <value>Welcome to our application</value>
  </data>
  <data name="Greeting" xml:space="preserve">
    <value>Hello, {0}!</value>
  </data>
</root>

<!-- Resources/Controllers/HomeController.de.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Welcome" xml:space="preserve">
    <value>Willkommen in unserer Anwendung</value>
  </data>
  <data name="Greeting" xml:space="preserve">
    <value>Hallo, {0}!</value>
  </data>
</root>
Используйте класс SharedResource с собственными файлами RESX для строк, общих для контроллеров и представлений: названий кнопок, элементов навигации и распространённых сообщений проверки. Так Вы избежите дублирования ключей в десятках файлов RESX отдельных контроллеров.
Shared resources for cross-cutting strings
<!-- Resources/SharedResource.en.resx — shared across controllers -->
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="AppName" xml:space="preserve">
    <value>My Application</value>
  </data>
  <data name="Save" xml:space="preserve"><value>Save</value></data>
  <data name="Cancel" xml:space="preserve"><value>Cancel</value></data>
</root>

// Marker class (empty — only used for type lookup)
namespace MyApp;
public class SharedResource { }
3

Использовать IStringLocalizer в контроллерах и службах

Внедрите IStringLocalizer&lt;T&gt; в любой контроллер, службу или промежуточное ПО через внедрение зависимостей. Универсальный параметр типа T определяет загружаемый файл RESX. Получайте переведённые строки через синтаксис скобок localizer["Key"] с необязательными параметрами формата.

Controllers/HomeController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

public class HomeController(
    IStringLocalizer<HomeController> localizer,
    IStringLocalizer<SharedResource> shared) : Controller
{
    public IActionResult Index()
    {
        ViewData["Welcome"] = localizer["Welcome"];
        ViewData["AppName"] = shared["AppName"];

        // String interpolation with format parameters
        var greeting = localizer["Greeting", User.Identity?.Name ?? "Guest"];
        return View(new HomeViewModel { Greeting = greeting });
    }
}
Если IStringLocalizer возвращает имя ключа вместо переведённого значения, проверьте три аспекта: 1) имя файла RESX соответствует пространству имён класса; 2) ResourcesPath в AddLocalization() указывает на правильную папку; 3) для Build Action файла RESX в Visual Studio задано Embedded Resource.
4

Настроить промежуточное ПО культуры запросов

ASP.NET Core определяет культуру запроса через цепочку поставщиков: строку запроса, cookie и заголовок Accept-Language в таком порядке. Можно добавить собственных поставщиков, например считывать культуру из сегмента маршрута URL вроде /de/home.

Custom RouteDataRequestCultureProvider
// Culture resolved in order: QueryString, Cookie, Accept-Language
// Custom provider: read culture from URL route segment /de/home
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
    public override Task<ProviderCultureResult?> DetermineProviderCultureResult(
        HttpContext httpContext)
    {
        var culture = httpContext.GetRouteValue("culture")?.ToString();
        if (string.IsNullOrEmpty(culture))
            return NullProviderCultureResult;
        return Task.FromResult<ProviderCultureResult?>(
            new ProviderCultureResult(culture));
    }
}

// Register in Program.cs (route provider first = highest priority):
app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures,
    RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new RouteDataRequestCultureProvider(),
        new QueryStringRequestCultureProvider(),
        new CookieRequestCultureProvider(),
        new AcceptLanguageHeaderRequestCultureProvider(),
    }
});
Language Switcher Action
// Language switcher: persist choice in cookie
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
    return LocalRedirect(returnUrl);
}
Порядок промежуточного ПО имеет значение. UseRequestLocalization() необходимо вызывать после UseRouting(), но перед UseEndpoints() или MapControllers(). Если поместить его слишком поздно, при выполнении контроллеров культура не будет задана.
5

Локализовать аннотации данных

Атрибуты проверки, такие как [Required], [StringLength] и [Display], можно локализовать, задав их свойствам ErrorMessage или Name имена ключей RESX. Вызовите AddDataAnnotationsLocalization() в Program.cs, чтобы включить эту возможность.

ViewModels/RegisterViewModel.cs
using System.ComponentModel.DataAnnotations;

public class RegisterViewModel
{
    [Required(ErrorMessage = "NameRequired")]
    [Display(Name = "FullName")]
    [StringLength(100, ErrorMessage = "NameLength", MinimumLength = 2)]
    public string Name { get; set; } = string.Empty;

    [Required(ErrorMessage = "EmailRequired")]
    [EmailAddress(ErrorMessage = "EmailInvalid")]
    [Display(Name = "EmailAddress")]
    public string Email { get; set; } = string.Empty;

    [Required(ErrorMessage = "PasswordRequired")]
    [StringLength(100, ErrorMessage = "PasswordLength", MinimumLength = 8)]
    [Display(Name = "Password")]
    public string Password { get; set; } = string.Empty;
}
// RESX keys map to ErrorMessage/Name values:
// RegisterViewModel.de.resx: NameRequired = "Name ist erforderlich"
// RegisterViewModel.de.resx: FullName = "Vollständiger Name"
Локализация аннотаций данных ищет файлы RESX по имени класса ViewModel, а не контроллера. Для RegisterViewModel фреймворк ищет Resources/ViewModels/RegisterViewModel.de.resx. Если Ваши файлы RESX названы в честь контроллера, сообщения проверки не будут локализованы.
6

Обработать формы множественного числа и сообщения ICU

В .NET нет встроенной поддержки правил множественного числа, подобной ICU. В простых случаях используйте отдельные ключи RESX (ItemCount_One, ItemCount_Other) с переключением в коде. Для полной поддержки ICU MessageFormat со всеми категориями множественного числа CLDR применяйте библиотеку MessageFormat.NET.

Plural handling strategies
// Option 1: Separate RESX keys with code switch
// HomeController.en.resx: ItemCount_One = "You have {0} item"
// HomeController.en.resx: ItemCount_Other = "You have {0} items"
public string GetItemCount(int count)
{
    var key = count == 1 ? "ItemCount_One" : "ItemCount_Other";
    return _localizer[key, count];
}

// Option 2: ICU MessageFormat (dotnet add package MessageFormat.NET)
using Jeffijoe.MessageFormat;
var formatter = new MessageFormatter();

var pattern = "{count, plural, one {# item} other {# items}} in your cart";
var result = formatter.FormatMessage(pattern,
    new Dictionary<string, object> { { "count", 5 } });
// => "5 items in your cart"

// Arabic: 6 plural forms (zero, one, two, few, many, other)
var arPattern = @"{count, plural,
    zero {لا عناصر} one {عنصر واحد} two {عنصران}
    few {# عناصر} many {# عنصرًا} other {# عنصر}}";
Никогда не используйте count == 1 для определения единственного числа. Во французском 0 считается единственным числом. В русском есть отдельные формы few и many. В арабском шесть категорий множественного числа. Используйте правила, учитывающие CLDR, или библиотеку вроде MessageFormat.NET, которая обрабатывает их правильно.
7

Локализовать представления Razor

Используйте IViewLocalizer в представлениях Razor через @inject. Он разрешает файлы RESX по пути файла представления. Для строк с разметкой, безопасных для HTML, используйте IHtmlLocalizer. Вспомогательные теги, такие как asp-for и asp-validation-for, автоматически применяют локализованные атрибуты Display и ErrorMessage.

Views/Home/Index.cshtml
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedHtml

<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Greeting", User.Identity?.Name]</p>

@* IHtmlLocalizer: does NOT escape — use for RESX values with HTML *@
<p>@SharedHtml["TermsNotice"]</p>

@* Tag Helpers auto-localize Display/ErrorMessage attributes *@
<form asp-action="Register">
    <label asp-for="Name"></label>
    <input asp-for="Name" />
    <span asp-validation-for="Name"></span>
    <button type="submit">@Localizer["Submit"]</button>
</form>
IViewLocalizer разрешает файлы RESX по пути представления: Views/Home/Index.cshtml ищет Resources/Views/Home/Index.de.resx. Чтобы использовать строки в нескольких представлениях, отдельно внедрите IStringLocalizer&lt;SharedResource&gt;.
8

Автоматизировать перевод RESX

После завершения настройки локализации переведите файлы RESX с помощью ИИ. Попросите ИИ-помощника перевести исходный RESX в своей IDE или используйте CLI i18n Agent в конвейере CI/CD для синхронизации переводов.

Terminal
# In your IDE, ask your AI assistant:
> Translate Resources/Controllers/HomeController.en.resx to German, Japanese, Spanish

# HomeController.de.resx created (1.2s)
# HomeController.ja.resx created (1.5s)
# HomeController.es.resx created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate Resources/Controllers/HomeController.en.resx --lang de,ja,es
Переводите постепенно: добавив новые ключи в исходный файл RESX, переведите только различия, а не создавайте все файлы заново. Это сохранит переводы, уже проверенные людьми, и сократит ненужные изменения.

Автоматизировать контроль качества перевода

Выявляйте отсутствующие ключи и нарушенные заполнители до выпуска с помощью i18n-validate. Тестируйте интерфейс с псевдопереводами через i18n-pseudo, пока настоящие переводы ещё не готовы.

Исправить резервные локали с LocaleChain.NET

Встроенная иерархия CultureInfo.Parent в .NET использует только усечение BCP 47: pt-BR переходит на pt, а затем InvariantCulture, пропуская pt-PT. LocaleChain.NET предоставляет настраиваемые цепочки для каждой локали во всей экосистеме .NET.

Без LocaleChain.NET пользователь pt-BR при отсутствии португальской строки видит английский, даже если у Вас есть полный перевод pt-PT. Та же проблема затрагивает es-MX, который пропускает es-419, zh-Hant, который пропускает zh-Hans, и десятки других региональных вариантов.
Terminal
dotnet add package I18nAgent.LocaleChain
Program.cs
using I18nAgent.LocaleChain;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");

// Zero-config: built-in chains (pt-BR -> pt-PT -> pt -> en, etc.)
LocaleChain.Configure();

// Or customize specific chains:
LocaleChain.Configure(new Dictionary<string, string[]>
{
    ["pt-BR"] = new[] { "pt-PT", "pt", "en" },
    ["es-MX"] = new[] { "es-419", "es", "en" },
});

// Register the chain-aware string localizer
builder.Services.AddSingleton(
    typeof(IStringLocalizer<>),
    typeof(LocaleChainStringLocalizer<>));

Распространённые ошибки

Культура запроса не задана

Переводы всегда отображаются на языке по умолчанию. Проверьте вызов UseRequestLocalization() в конвейере промежуточного ПО и настройку поставщиков культуры. Убедитесь, что браузер отправляет заголовки Accept-Language. Проверьте работу промежуточного ПО с ?culture=de в строке запроса.

Файл RESX не найден

IStringLocalizer возвращает имя ключа вместо переведённого значения. Наиболее распространённая причина — имя файла RESX не соответствует полному пути пространства имён класса относительно ResourcesPath. Включите отладочную запись журналов для Microsoft.Extensions.Localization, чтобы увидеть пути, в которых ищет фреймворк.

Неверный порядок промежуточного ПО

UseRequestLocalization() должен находиться перед UseEndpoints() и MapControllers(). Если поместить его после них, при выполнении контроллеров культура запроса не будет задана. При минимальном размещении .NET 6+ вызывайте его до app.MapControllers().

Фоновый поток использует неверную культуру

CultureInfo.CurrentCulture и CurrentUICulture задаются для каждого потока. Фоновые задачи (Task.Run, размещённые службы) наследуют культуру пула потоков, а не запроса. При отправке фоновой работы явно захватывайте и задавайте культуру.

Рекомендуемая структура проекта

Project Structure
MyAspNetApp/
├── Controllers/
│   └── HomeController.cs
├── ViewModels/
│   └── RegisterViewModel.cs
├── Views/
│   └── Home/
│       └── Index.cshtml
├── Resources/
│   ├── Controllers/
│   │   ├── HomeController.en.resx     # English (source)
│   │   ├── HomeController.de.resx     # German
│   │   └── HomeController.ja.resx     # Japanese
│   ├── ViewModels/
│   │   ├── RegisterViewModel.en.resx
│   │   └── RegisterViewModel.de.resx
│   ├── Views/Home/
│   │   ├── Index.en.resx
│   │   └── Index.de.resx
│   └── SharedResource.en.resx
├── SharedResource.cs                  # Marker class
├── Program.cs
└── MyAspNetApp.csproj

Попробовать i18n Agent

Перетащите сюда файл перевода

JSON, YAML, PO, XML, CSV, Markdown, Properties

или нажмите, чтобы выбрать

Целевые языки

Регистрация не требуетсяМгновенный расчёт

Резервные локали с I18nAgent.LocaleChain

Когда в региональной локали, например pt-BR, отсутствует ключ перевода, .NET сразу переходит на инвариантную культуру, не проверяя сначала родительскую локаль pt.

Terminal
dotnet add package I18nAgent.LocaleChain
Configuration
using I18nAgent.LocaleChain;

LocaleChain.Configure(new Dictionary<string, string[]>
{
    ["pt-BR"] = new[] {"pt", "en"},
    ["zh-Hant-HK"] = new[] {"zh-Hant", "zh", "en"},
});

Полный список поддерживаемых фреймворков и 75 встроенных цепочек приведён в нашем руководстве по резервным локалям. Learn more →

Часто задаваемые вопросы