Skip to main content

ASP.NET Core 在地化完整指南

從 IStringLocalizer 到生產環境:在 ASP.NET Core 中設定基於資源的在地化,再透過 AI 自動翻譯。

1

啟用在地化服務

在 Program.cs 中使用 AddLocalization() 註冊在地化服務,設定支援的區域性,並新增請求在地化中介軟體。這將為您的 ASP.NET Core 應用程式接通完整的在地化管線。

AddLocalization() 會在 DI 容器中註冊 IStringLocalizer 和 IStringLocalizerFactory。ResourcesPath 告訴框架在何處尋找 .resx 檔案。AddViewLocalization() 可在 Razor 檢視中啟用 IViewLocalizer,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>
對於控制器和檢視間共享的字串(按鈕標籤、導覽項和常見驗證訊息),請使用具有獨立 RESX 檔案的 SharedResource 類別。這樣可避免在數十個控制器專用 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)AddLocalization() 中的 ResourcesPath 是否指向正確資料夾;(3)RESX 檔案的 Build Action 在 Visual Studio 中是否設定為 Embedded Resource。
4

設定請求區域性中介軟體

ASP.NET Core 使用提供者鏈確定請求區域性:查詢字串、Cookie 和 Accept-Language 標頭(按此順序)。您也可以新增自訂提供者,例如從 /de/home 之類的 URL 路由段讀取區域性。

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

在地化資料註解

透過將 ErrorMessage 或 Name 屬性設定為 RESX 鍵名,可以在地化 [Required]、[StringLength] 和 [Display] 等驗證特性。在 Program.cs 中呼叫 AddDataAnnotationsLocalization() 以啟用此功能。

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"
資料註解在地化使用 ViewModel 類別名而非 Controller 來尋找 RESX 檔案。對於 RegisterViewModel,框架會尋找 Resources/ViewModels/RegisterViewModel.de.resx。如果 RESX 檔案按控制器命名,驗證訊息將不會在地化。
6

處理複數和 ICU 訊息

.NET 不像 ICU 那樣內建複數規則支援。對於簡單情況,可使用獨立的 RESX 鍵(ItemCount_One、ItemCount_Other)並透過程式碼切換。如需支援所有 CLDR 複數類別的完整 ICU MessageFormat,請使用 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 檢視

透過 @inject 在 Razor 檢視中使用 IViewLocalizer。它根據檢視檔案路徑解析 RESX 檔案。對於包含標記且可安全作為 HTML 使用的字串,請使用 IHtmlLocalizer。asp-for 和 asp-validation-for 等 Tag Helper 會自動使用在地化的 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

完成在地化設定後,使用 AI 翻譯 RESX 檔案。在 IDE 中讓 AI 助手翻譯源 RESX,或在 CI/CD 管線中使用 i18n Agent CLI,使翻譯保持同步。

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 產生偽譯文來測試 UI。

使用 LocaleChain.NET 修復語系回退

.NET 內建的 CultureInfo.Parent 層次結構僅使用 BCP 47 截斷:pt-BR 回退到 pt,再回退到 InvariantCulture,並跳過 pt-PT。LocaleChain.NET 為整個 .NET 生態系統提供可針對每個語系進行設定的回退鏈。

如果沒有 LocaleChain.NET,當葡萄牙語字串缺失時,即使您擁有完整的 pt-PT 翻譯,pt-BR 使用者仍會看到英語。相同問題還會影響 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 →

常見問題