Skip to main content

Hướng dẫn đầy đủ về bản địa hóa ASP.NET Core

Từ IStringLocalizer đến môi trường production: thiết lập bản địa hóa dựa trên tài nguyên trong ASP.NET Core rồi tự động dịch bằng AI.

1

Bật dịch vụ bản địa hóa

Đăng ký dịch vụ bản địa hóa trong Program.cs bằng AddLocalization(), cấu hình các văn hóa được hỗ trợ và thêm middleware bản địa hóa yêu cầu. Thao tác này kết nối toàn bộ quy trình bản địa hóa cho ứng dụng ASP.NET Core của bạn.

AddLocalization() đăng ký IStringLocalizer và IStringLocalizerFactory trong DI container. ResourcesPath cho framework biết nơi tìm các tệp .resx. AddViewLocalization() bật IViewLocalizer trong các view Razor và AddDataAnnotationsLocalization() bật thông báo xác thực đã bản địa hóa.
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

Tạo tệp tài nguyên RESX

ASP.NET Core dùng tệp RESX (tài nguyên XML) cho bản dịch. Tạo một tệp cho mỗi văn hóa và mỗi lớp: HomeController.en.resx, HomeController.de.resx, v.v. Framework phân giải đúng tệp dựa trên văn hóa yêu cầu hiện tại.

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>
Dùng lớp SharedResource cùng các tệp RESX riêng cho những chuỗi dùng chung giữa controller và view — nhãn nút, mục điều hướng và thông báo xác thực chung. Cách này tránh lặp khóa trong hàng chục tệp RESX dành riêng cho từng controller.
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

Dùng IStringLocalizer trong controller và dịch vụ

Chèn IStringLocalizer&lt;T&gt; vào bất kỳ controller, dịch vụ hoặc middleware nào thông qua dependency injection. Tham số kiểu generic T xác định tệp RESX cần tải. Dùng cú pháp ngoặc vuông localizer["Key"] để truy xuất chuỗi đã dịch cùng các tham số định dạng tùy chọn.

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 });
    }
}
Nếu IStringLocalizer trả về tên khóa thay vì giá trị đã dịch, hãy kiểm tra ba điều: (1) cách đặt tên tệp RESX khớp với namespace của lớp, (2) ResourcesPath trong AddLocalization() trỏ đến đúng thư mục và (3) Build Action của tệp RESX được đặt thành Embedded Resource trong Visual Studio.
4

Cấu hình middleware văn hóa yêu cầu

ASP.NET Core xác định văn hóa yêu cầu bằng một chuỗi nhà cung cấp: chuỗi truy vấn, cookie và header Accept-Language (theo thứ tự đó). Bạn có thể thêm nhà cung cấp tùy chỉnh — chẳng hạn đọc văn hóa từ một phân đoạn định tuyến URL như /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);
}
Thứ tự middleware rất quan trọng. Bạn phải gọi UseRequestLocalization() sau UseRouting() nhưng trước UseEndpoints() hoặc MapControllers(). Nếu đặt quá muộn, văn hóa sẽ chưa được thiết lập khi controller thực thi.
5

Bản địa hóa chú thích dữ liệu

Bạn có thể bản địa hóa các thuộc tính xác thực như [Required], [StringLength] và [Display] bằng cách đặt thuộc tính ErrorMessage hoặc Name thành tên khóa RESX. Gọi AddDataAnnotationsLocalization() trong Program.cs để bật tính năng này.

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"
Tính năng bản địa hóa chú thích dữ liệu dùng tên lớp ViewModel để tìm tệp RESX chứ không dùng Controller. Với RegisterViewModel, framework tìm Resources/ViewModels/RegisterViewModel.de.resx. Nếu tệp RESX được đặt tên theo controller, thông báo xác thực sẽ không được bản địa hóa.
6

Xử lý dạng số nhiều và thông báo ICU

.NET không tích hợp sẵn quy tắc số nhiều như ICU. Với trường hợp đơn giản, hãy dùng các khóa RESX riêng (ItemCount_One, ItemCount_Other) cùng câu lệnh chuyển nhánh trong mã. Để hỗ trợ đầy đủ ICU MessageFormat cho mọi nhóm số nhiều CLDR, hãy dùng thư viện 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 {# عنصر}}";
Đừng bao giờ dùng count == 1 để phát hiện dạng số ít. Tiếng Pháp coi 0 là số ít. Tiếng Nga có dạng riêng cho 'few' và 'many'. Tiếng Ả Rập có sáu nhóm số nhiều. Hãy dùng quy tắc số nhiều nhận biết CLDR hoặc thư viện như MessageFormat.NET để xử lý chính xác.
7

Bản địa hóa view Razor

Dùng IViewLocalizer trong view Razor qua @inject. Công cụ này phân giải tệp RESX dựa trên đường dẫn tệp của view. Với chuỗi chứa mã đánh dấu và an toàn cho HTML, hãy dùng IHtmlLocalizer. Các Tag Helper như asp-for và asp-validation-for tự động dùng thuộc tính Display và ErrorMessage đã bản địa hóa.

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 phân giải tệp RESX theo đường dẫn view: Views/Home/Index.cshtml tìm Resources/Views/Home/Index.de.resx. Nếu muốn dùng chung chuỗi giữa các view, hãy chèn riêng IStringLocalizer&lt;SharedResource&gt;.
8

Tự động dịch RESX

Sau khi hoàn tất thiết lập bản địa hóa, hãy dịch các tệp RESX bằng AI. Trong IDE, yêu cầu trợ lý AI dịch RESX nguồn hoặc dùng i18n Agent CLI trong quy trình CI/CD để luôn đồng bộ bản dịch.

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
Dịch tăng dần — khi thêm khóa mới vào tệp RESX nguồn, chỉ dịch phần khác biệt thay vì tạo lại mọi tệp. Cách này giữ nguyên các bản dịch đã được con người duyệt và giảm thiểu thay đổi không cần thiết.

Tự động đảm bảo chất lượng bản dịch

Phát hiện khóa bị thiếu và placeholder bị hỏng bằng i18n-validate trước khi phát hành. Kiểm thử UI bằng bản dịch giả với i18n-pseudo trước khi có bản dịch thật.

Khắc phục phương án dự phòng locale bằng LocaleChain.NET

Hệ thống phân cấp CultureInfo.Parent tích hợp sẵn của .NET chỉ dùng phép rút gọn BCP 47: pt-BR dự phòng về pt rồi InvariantCulture và bỏ qua pt-PT. LocaleChain.NET cung cấp chuỗi dự phòng có thể cấu hình cho từng locale trong toàn bộ hệ sinh thái .NET.

Nếu không có LocaleChain.NET, người dùng pt-BR sẽ thấy tiếng Anh khi thiếu chuỗi tiếng Bồ Đào Nha — ngay cả khi bạn có bản dịch pt-PT hoàn chỉnh. Vấn đề tương tự ảnh hưởng đến es-MX (bỏ qua es-419), zh-Hant (bỏ qua zh-Hans) và hàng chục biến thể khu vực khác.
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<>));

Lỗi thường gặp

Chưa thiết lập văn hóa cho yêu cầu

Bản dịch luôn hiển thị ngôn ngữ mặc định. Hãy kiểm tra UseRequestLocalization() đã được gọi trong quy trình middleware và các nhà cung cấp văn hóa đã được cấu hình. Xác minh trình duyệt gửi header Accept-Language. Kiểm thử với ?culture=de trong chuỗi truy vấn để xác nhận middleware hoạt động.

Không tìm thấy tệp RESX

IStringLocalizer trả về tên khóa thay vì giá trị đã dịch. Nguyên nhân phổ biến nhất là tên tệp RESX không khớp với đường dẫn namespace đầy đủ của lớp tính từ ResourcesPath. Bật ghi nhật ký gỡ lỗi cho Microsoft.Extensions.Localization để xem framework tìm kiếm những đường dẫn nào.

Thứ tự middleware không chính xác

UseRequestLocalization() phải xuất hiện trước UseEndpoints() và MapControllers(). Nếu đặt sau, văn hóa yêu cầu chưa được thiết lập khi controller thực thi. Trong mô hình lưu trữ tối giản của .NET 6+, hãy gọi hàm này trước app.MapControllers().

Luồng nền dùng sai văn hóa

CultureInfo.CurrentCulture và CurrentUICulture áp dụng theo từng luồng. Tác vụ nền (Task.Run, dịch vụ được lưu trữ) kế thừa văn hóa của thread pool chứ không phải văn hóa yêu cầu. Hãy thu thập và thiết lập rõ ràng văn hóa khi phân phối công việc nền.

Cấu trúc dự án đề xuất

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

Dùng thử i18n Agent ngay

Thả tệp bản dịch của bạn vào đây

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

hoặc nhấp để duyệt

Ngôn ngữ đích

Không cần đăng kýBáo giá tức thì

Dự phòng locale với I18nAgent.LocaleChain

Khi thiếu khóa bản dịch trong locale khu vực như pt-BR, .NET chuyển thẳng sang văn hóa bất biến thay vì kiểm tra locale cha pt trước.

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"},
});

Xem Hướng dẫn dự phòng locale để biết danh sách đầy đủ các framework được hỗ trợ và 75 chuỗi tích hợp sẵn. Learn more →

Câu hỏi thường gặp