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 значення Embedded Resource у Visual Studio.
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, а не Controller. Для 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 за допомогою ШІ. У своїй IDE попросіть асистента на основі ШІ перекласти вихідний файл RESX або використовуйте i18n Agent CLI у своєму CI/CD pipeline, щоб синхронізувати переклади.

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-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 →

Поширені запитання