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 →

常见问题