ASP.NET Core多语言配置实战:从原理到Cookie持久化语言切换

1. 项目缘起:为什么你的应用需要多语言支持?

几年前我接手一个内部工具项目,当时只支持中文,团队用着也挺好。后来公司业务拓展到海外,突然有一天,产品经理跑过来说:“下个月我们要给东南亚的客户做演示,界面和文档需要支持英文和泰语。” 那一刻,我才真正体会到“国际化”(i18n)和“本地化”(l10n)不是可选项,而是业务发展到一定阶段的必然需求。在 Asp .Net Core 框架下构建支持多语言的应用,远不止是把界面上的文字替换成另一种语言那么简单,它涉及到资源文件的管理、文化区域的自动识别、动态切换以及整个开发流程的适配。

简单来说,国际化就是让你的应用程序具备处理多种语言和文化习惯(如日期、货币格式)的能力,而本地化则是为特定的语言区域(如en-US,zh-CN)提供翻译好的资源。对于现代Web应用,无论是面向全球用户的电商平台、SaaS服务,还是企业内部需要支持多地区团队的管理系统,多语言支持都是提升用户体验、拓展市场的基础能力。Asp .Net Core 从设计之初就内置了对国际化的良好支持,通过IStringLocalizerIHtmlLocalizer等接口和中间件,我们可以相对优雅地实现这一功能,避免在代码中硬编码字符串。

但根据我的经验,很多开发者在配置多语言时,容易陷入几个误区:要么把所有资源都堆在一个巨大的JSON文件里,后期维护灾难;要么忽略了请求文化(Culture)的自动解析逻辑,导致切换不生效;再或者没有处理好共享资源与页面特定资源的关系。接下来,我将结合一个从零开始的实战项目,拆解 Asp .Net Core 国际化多语言配置的核心步骤、原理以及那些官方文档可能没细说的“坑”。

2. 环境搭建与基础项目结构规划

在开始编码之前,合理的项目结构能让你后续的维护工作轻松十倍。我建议创建一个全新的 Asp .Net Core MVC 项目作为演示,但其中的核心配置同样适用于 Web API 或 Razor Pages 项目。

首先,使用命令行或 IDE 创建一个新项目:

dotnet new mvc -n LocalizationDemo cd LocalizationDemo

创建完成后,我们首要任务是规划资源文件的存放位置。Asp .Net Core 支持多种资源存储方式,最常见的是.resx文件和 JSON 文件。.resx是 .NET 传统的二进制资源格式,与 Visual Studio 集成度好;而 JSON 文件更轻量,易于版本控制和跨平台编辑。为了更贴近现代开发流程,我选择使用 JSON 文件。在项目根目录下,创建以下文件夹结构:

LocalizationDemo/ ├── Resources/ │ ├── Controllers/ │ │ ├── HomeController.en.json │ │ ├── HomeController.zh.json │ │ └── HomeController.fr.json │ ├── Views/ │ │ └── Home/ │ │ ├── Index.en.json │ │ ├── Index.zh.json │ │ └── Index.fr.json │ └── Shared/ │ ├── _Layout.en.json │ ├── _Layout.zh.json │ ├── _Menu.en.json │ └── _Menu.fr.json └── ...

这种按“功能模块/页面”组织资源的方式,相比把所有字符串放在一个全局文件里,优势非常明显。当你要修改首页的某个按钮文字时,你很清楚只需要去Resources/Views/Home/Index.[culture].json里找,不会影响到用户管理或订单页面的文案。这对于大型项目、团队协作以及后续的翻译外包工作都非常友好。

接下来,我们需要安装一个关键的 NuGet 包来支持 JSON 资源文件。虽然 Asp .Net Core 内置了对IStringLocalizer的支持,但其默认的资源查找逻辑主要针对.resx文件。为了使用 JSON,我们可以使用Microsoft.Extensions.Localization包,它已经包含在元包中,但为了更灵活地配置,我们显式地添加对资源文件的支持。实际上,对于 JSON 文件,我们通常需要实现一个自定义的IStringLocalizerFactory,但社区已有成熟的方案。一个更简单直接的方法是使用Microsoft.Extensions.Localization配合特定的资源路径配置。在本例中,我们将使用内置机制,通过配置让其能读取我们指定目录下的 JSON 文件。

首先,在Program.cs中,我们需要添加和配置本地化服务。这是整个多语言体系的“发动机”。

3. 核心服务配置:Program.cs 中的初始化逻辑

所有的魔法都始于启动配置。打开Program.cs文件,在builder.Services的服务容器配置区域,添加本地化服务。这里有几个关键点需要理解。

第一步:添加本地化服务

var builder = WebApplication.CreateBuilder(args); // 添加 MVC 服务(如果新建项目已默认添加) builder.Services.AddControllersWithViews(); // 配置本地化服务 builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

ResourcesPath = "Resources"这行代码至关重要。它告诉 Asp .Net Core 的本地化系统:“请去项目根目录下的Resources文件夹里寻找资源文件。” 系统会基于这个路径,按照约定的命名规则去查找文件。对于 JSON 文件,我们需要后续的配置来指定使用哪种资源格式。

第二步:配置请求本地化中间件服务添加后,还需要告诉应用如何根据每个 HTTP 请求来确定应该使用哪种语言文化。这通过配置请求本地化选项和添加中间件来实现。

// 配置支持的 cultures(语言文化)列表 const string defaultCulture = "en-US"; var supportedCultures = new[] { new CultureInfo(defaultCulture), new CultureInfo("zh-CN"), new CultureInfo("fr-FR") }; builder.Services.Configure<RequestLocalizationOptions>(options => { options.DefaultRequestCulture = new RequestCulture(defaultCulture); options.SupportedCultures = supportedCultures; // 用于日期、数字、货币格式 options.SupportedUICultures = supportedCultures; // 用于查找资源文件(UI字符串) options.RequestCultureProviders = new List<IRequestCultureProvider> { // 优先级1:QueryStringRequestCultureProvider(通过查询字符串切换,如 ?culture=en-US) new QueryStringRequestCultureProvider(), // 优先级2:CookieRequestCultureProvider(通过Cookie持久化语言选择) new CookieRequestCultureProvider(), // 优先级3:AcceptLanguageHeaderRequestCultureProvider(根据浏览器语言偏好) new AcceptLanguageHeaderRequestCultureProvider() }; });

这段代码做了几件重要的事:

  1. 定义支持的语言:我们声明支持美式英语 (en-US)、简体中文 (zh-CN) 和法语 (fr-FR)。CultureInfo对象包含了特定区域的语言和格式规则。
  2. 设置默认文化:当系统无法从请求中确定用户语言时,将回退到en-US
  3. 区分 Culture 与 UICultureSupportedCultures影响区域性相关的功能(如DateTime.ToString()的格式),而SupportedUICultures专门用于查找本地化的字符串资源。大多数情况下两者设为相同列表即可。
  4. 配置文化提供者顺序:这是一个非常实用的策略。它定义了系统尝试获取用户语言偏好的顺序。顺序决定了优先级。
    • QueryStringRequestCultureProvider:最高优先级。用户访问?culture=zh-CN&ui-culture=zh-CN可以立即切换语言,常用于开发测试或用户手动选择后的跳转链接。
    • CookieRequestCultureProvider:次优先级。一旦用户通过某种方式(比如查询字符串)选择了语言,我们可以将选择写入 Cookie,这样用户下次访问时无需再次选择。这是实现持久化语言偏好的关键。
    • AcceptLanguageHeaderRequestCultureProvider:最低优先级。读取浏览器发送的Accept-LanguageHTTP 头。这是最自动化的方式,但用户可能不清楚如何修改浏览器设置,且优先级最低,确保了手动选择的权力。

第三步:启用请求本地化中间件配置好选项后,必须在请求管道中启用它,而且位置很关键。它应该放在处理路由和端点的中间件之前,以确保在 MVC 开始执行控制器动作之前,当前请求的文化信息就已经被确定。

var app = builder.Build(); // 配置 HTTP 请求管道 if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); // *** 启用请求本地化中间件 *** app.UseRequestLocalization(); // 这会自动使用我们上面 Configure<RequestLocalizationOptions> 的配置 app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();

至此,服务的配置就完成了。但我们现在只有“发动机”,还没有“燃料”(即资源文件)。接下来,我们创建具体的资源文件并了解其命名规范。

4. 资源文件创建、命名规范与内容组织

资源文件是翻译内容的载体。Asp .Net Core 查找资源文件的规则基于一个“资源名称”(通常是类的全名或视图的路径)和当前UICulture

对于控制器和普通类:系统会查找名为[Namespace].[ClassName].[culture].json的资源文件。例如,对于LocalizationDemo.Controllers.HomeController类,系统会查找Resources/Controllers/HomeController.en-US.json。为了简化,我们通常使用不带区域的“中性文化”文件名,如HomeController.en.json,系统会自动匹配en-USen-GB

对于视图:系统会查找与视图路径匹配的资源文件。例如,对于/Views/Home/Index.cshtml视图,系统会查找Resources/Views/Home/Index.[culture].json

让我们创建第一个资源文件。在Resources/Views/Home/目录下,创建Index.en.json

{ "Title": "Welcome", "HelloWorld": "Hello, world!", "CurrentTime": "The current server time is: {0}", "LearnMore": "Learn More" }

接着,创建对应的中文文件Index.zh.json

{ "Title": "欢迎", "HelloWorld": "你好,世界!", "CurrentTime": "当前服务器时间是:{0}", "LearnMore": "了解更多" }

以及法语文件Index.fr.json

{ "Title": "Bienvenue", "HelloWorld": "Bonjour le monde !", "CurrentTime": "L'heure actuelle du serveur est : {0}", "LearnMore": "En savoir plus" }

注意CurrentTime中的{0},这是一个占位符,允许我们在运行时传入动态值(如实际的日期时间),这是资源字符串的常见需求。

注意:JSON 文件的属性名(如"Title")是大小写敏感的,必须与你在视图中引用的键名完全一致。一个常见的错误是键名拼写不一致,导致回退到默认语言甚至抛出异常。

5. 在视图中使用 IStringLocalizer:三种注入方式

配置好服务和资源后,就可以在视图中使用本地化的字符串了。Asp .Net Core 提供了IStringLocalizer<T>泛型接口,其中T通常是与之关联的类。对于视图,通常使用IViewLocalizer,它是IStringLocalizer的视图特化版本,能自动根据视图路径定位资源文件。

方式一:使用依赖注入在视图中获取 IViewLocalizer这是最直接的方式。在视图文件(如Index.cshtml)的顶部,通过@inject指令注入IViewLocalizer

@using Microsoft.AspNetCore.Mvc.Localization @inject IViewLocalizer Localizer

然后,在 HTML 中使用Localizer对象:

@{ ViewData["Title"] = Localizer["Title"]; } <div class="text-center"> <h1 class="display-4">@Localizer["HelloWorld"]</h1> <p>@string.Format(Localizer["CurrentTime"], DateTime.Now)</p> <a href="#" class="btn btn-primary">@Localizer["LearnMore"]</a> </div>

Localizer["Key"]会返回一个LocalizedString对象,在 Razor 中直接输出时,其Value属性会自动被调用,显示当前语言下的字符串。对于带占位符的字符串,我们使用string.Format方法进行格式化。

方式二:在控制器中注入并传递到视图有时,你可能需要在控制器逻辑中获取本地化字符串,然后再传递给视图。在HomeController.cs中:

using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; namespace LocalizationDemo.Controllers { public class HomeController : Controller { private readonly IStringLocalizer<HomeController> _localizer; public HomeController(IStringLocalizer<HomeController> localizer) { _localizer = localizer; } public IActionResult Index() { ViewData["Greeting"] = _localizer["HelloWorld"]; ViewData["FormattedTime"] = string.Format(_localizer["CurrentTime"], DateTime.Now); return View(); } } }

然后在视图中,你可以通过ViewData访问这些已经翻译好的字符串。这种方式将本地化逻辑放在了控制器,视图更干净,但增加了控制器的复杂度。

方式三:使用共享资源对于多个控制器或视图共用的字符串(比如网站名称、通用按钮文字“提交”、“取消”),创建共享资源是更好的选择。首先,创建一个空的类作为共享资源的标记。在项目根目录创建SharedResource.cs

namespace LocalizationDemo { // 这是一个标记类,仅用于定位共享资源文件。 public class SharedResource { } }

然后在Resources/目录下创建SharedResource.en.jsonSharedResource.zh.json等文件。在任何需要的地方,注入IStringLocalizer<SharedResource>即可使用共享的字符串。

实操心得:我强烈建议为每个视图和控制器创建独立的资源文件,仅将真正的全局字符串放入共享资源。这能最大程度地保持模块化,避免一个资源文件的改动影响过多地方,在大型项目中尤其重要。

6. 实现前端语言切换器与 Cookie 持久化

一个友好的多语言网站必须提供直观的语言切换方式。我们将创建一个简单的语言切换下拉菜单,并利用 Cookie 来记住用户的选择。

首先,我们在_Layout.cshtml或一个共享的局部视图中创建切换器。为了获取当前支持的语言列表,我们需要将之前在Program.cs中配置的RequestLocalizationOptions注入进来。修改_Layout.cshtml的头部:

@using Microsoft.AspNetCore.Localization @using Microsoft.Extensions.Options @inject IOptions<RequestLocalizationOptions> LocOptions @{ var requestCulture = Context.Features.Get<IRequestCultureFeature>(); var cultureItems = LocOptions.Value.SupportedUICultures .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName }) .ToList(); var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}{Context.Request.QueryString}"; }

然后,在导航栏合适的位置(比如右上角)添加一个表单:

<div class="nav-item dropdown"> <form id="cultureForm" asp-controller="Home" asp-action="SetLanguage" asp-route-returnUrl="@returnUrl" method="post"> <select name="culture" onchange="this.form.submit()" class="form-control form-control-sm"> @foreach (var item in cultureItems) { <option value="@item.Value" selected="@(requestCulture?.RequestCulture?.UICulture?.Name == item.Value)"> @item.Text </option> } </select> </form> </div>

这个表单包含一个下拉选择框,选项来自SupportedUICultures。当用户选择不同选项时,通过onchange事件自动提交表单。表单会提交到HomeControllerSetLanguage动作,并携带当前页面 URL 作为returnUrl,以便语言切换后能回到原页面。

现在,在HomeController中实现SetLanguage动作方法:

[HttpPost] public IActionResult SetLanguage(string culture, string returnUrl) { // 验证请求的文化是否在支持列表中 var supportedCultures = new[] { "en-US", "zh-CN", "fr-FR" }; if (!supportedCultures.Contains(culture)) { culture = "en-US"; // 回退到默认 } // 将用户选择的 Culture 存入 Cookie Response.Cookies.Append( CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)), new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1), IsEssential = true } // 设置长期有效 ); // 重定向回原页面 return LocalRedirect(returnUrl); }

这个方法的核心是Response.Cookies.Append。我们使用CookieRequestCultureProvider.DefaultCookieName(默认为.AspNetCore.Culture)作为 Cookie 的名称,并使用MakeCookieValue方法生成符合中间件解析格式的 Cookie 值。设置IsEssential = true是为了确保即使用户拒绝了非必要 Cookie,这个语言选择 Cookie 依然能被写入,这对核心功能至关重要。

踩坑点LocalRedirect方法会防止开放重定向攻击,确保returnUrl是应用内的本地 URL。直接使用Redirect(returnUrl)是不安全的。

完成以上步骤后,运行应用。你应该能看到页面右上角有一个语言下拉框。选择“中文(简体中国)”后,页面内容应即时切换为中文,并且刷新页面或关闭浏览器再打开,语言选择依然有效,这就是 Cookie 持久化的作用。

7. 高级话题:资源文件回退机制与结构化 JSON

在实际开发中,你不可能为每个语言都翻译所有字符串。Asp .Net Core 的本地化系统提供了智能的回退机制。

  1. 特定文化回退到中性文化:当请求fr-FR(法国法语)时,系统会按顺序查找:

    • Resource.fr-FR.json
    • Resource.fr.json(回退到中性法语)
    • Resource.en.json(或你设置的默认文化资源)
    • 最后,如果都找不到,直接返回键名本身(避免抛出异常,便于开发)。
  2. 父资源回退:对于视图资源,如果在Resources/Views/Home/Index.fr.json中找不到某个键,系统会去父目录或共享资源中查找吗?默认行为不会。视图本地化器 (IViewLocalizer) 严格限定在对应视图路径的资源文件中查找。这是为了保持明确的边界。如果你希望共享,应该使用IStringLocalizer<SharedResource>

结构化 JSON 资源:对于复杂的 UI,有时一个键对应的不是简单字符串,而是一段带有 HTML 或复杂格式的文本。你可以这样做:

{ "PromoBanner": { "Title": "Summer Sale!", "Description": "Up to <strong>50% off</strong> on selected items. <a href='/sales'>Shop now</a>.", "CssClass": "alert-success" } }

在视图中,你可以通过Localizer["PromoBanner.Title"]来访问嵌套属性。但请注意,如果值中包含 HTML,在输出时需要使用@Html.Raw(Localizer["PromoBanner.Description"])来防止 Razor 自动进行 HTML 编码。务必谨慎使用Html.Raw,并确保资源文件的内容是可信的,以防止跨站脚本(XSS)攻击。更好的做法是将样式和结构留在视图中,资源文件只提供纯文本内容。

8. 常见问题排查与调试技巧

即使按照步骤配置,有时多语言功能也可能不生效。以下是我总结的几个常见问题及排查思路:

问题一:切换语言后,页面内容没有变化。

  • 检查点1:Cookie 是否成功写入。打开浏览器的开发者工具(F12),在“应用”(Application)或“存储”(Storage)标签页中查看 Cookies。你应该能看到一个名为.AspNetCore.Culture的 Cookie,其值类似于c=en-US|uic=en-US。如果没有,检查SetLanguage动作是否被正确调用,以及 Cookie 设置代码是否有误。
  • 检查点2:中间件顺序。确保app.UseRequestLocalization()app.UseRouting()app.UseEndpoints()之前。如果顺序错了,路由已经确定了控制器和动作,此时再设置文化可能就晚了。
  • 检查点3:资源文件命名和位置。确认资源文件是否放在Resources文件夹(或你配置的ResourcesPath)的正确子目录下,并且文件名完全匹配(包括大小写)。例如,对于HomeController,文件应为Resources/Controllers/HomeController.en.json,而不是Resources/Controllers/HomeController.en-US.json(除非你请求的就是en-US)。

问题二:某些字符串显示为键名(如“HelloWorld”),而不是翻译后的文本。

  • 检查点1:键名拼写。确认视图中Localizer["Key"]里的Key与 JSON 文件中的属性名完全一致,包括大小写。
  • 检查点2:资源文件是否被发布。在开发环境下,修改 JSON 文件通常能热重载。但在生产环境(如发布到 IIS),你需要确保资源文件被包含在发布输出中。检查.csproj文件,确保类似以下内容存在,或者所有.json文件的“复制到输出目录”属性设置为“始终复制”或“如果较新则复制”。
    <ItemGroup> <Content Update="Resources\**" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>
  • 检查点3:回退行为。系统找不到对应语言的翻译时,会尝试回退到中性文化,再回退到默认文化资源。如果连默认文化资源文件里都没有这个键,那么就会返回键名本身。请检查你的默认文化(如en.json)资源文件是否包含了所有必需的键。

问题三:日期、货币格式没有随语言切换。

  • 检查点:确认你在RequestLocalizationOptions中同时正确设置了SupportedCulturesSupportedUICulturesSupportedCultures控制CultureInfo.CurrentCulture,它影响DateTime.ToString()decimal.ToString(“C”)等格式。如果只设置了SupportedUICultures,那么字符串翻译会变,但数字日期格式不会变。

调试技巧:你可以在视图中临时添加调试代码来查看当前的文化信息:

<p>Current Culture: @System.Globalization.CultureInfo.CurrentCulture.Name</p> <p>Current UI Culture: @System.Globalization.CultureInfo.CurrentUICulture.Name</p> <p>Request Culture Provider: @Context.Features.Get<IRequestCultureFeature>()?.Provider?.GetType().Name</p>

这能帮你快速确认当前生效的文化是哪个,以及是由哪个 Provider(查询字符串、Cookie 还是浏览器头)提供的。

9. 在 Web API 与类库中的本地化实践

多语言需求不仅限于 MVC 视图,在 Web API 控制器和共享的类库中同样常见。

在 Web API 控制器中:用法与 MVC 控制器完全一样。注入IStringLocalizer<T>,然后在返回错误信息、状态描述时使用本地化字符串。

[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IStringLocalizer<ProductsController> _localizer; public ProductsController(IStringLocalizer<ProductsController> localizer) { _localizer = localizer; } [HttpGet("{id}")] public IActionResult Get(int id) { var product = _productService.Get(id); if (product == null) { // 返回本地化的错误信息 return NotFound(_localizer["ProductNotFound", id]); } return Ok(product); } }

你需要为ProductsController创建对应的资源文件Resources/Controllers/ProductsController.[culture].json

在独立的类库中:如果你想在一个被多个项目引用的类库中实现本地化,最佳实践是:

  1. 在类库项目中创建Resources文件夹和资源文件。
  2. 为需要本地化的类创建对应的资源文件,例如MyUtilityClass.en.json
  3. 在类库中通过依赖注入获取IStringLocalizer<MyUtilityClass>。如果类库本身不处理依赖注入,通常由调用方(如主 Web 项目)在构造服务时传入IStringLocalizer实例。
  4. 关键一步:确保类库中的资源文件能被主项目发现。你需要修改主项目的.csproj文件,添加对类库资源文件的引用,或者确保类库的资源文件被复制到主项目的输出目录。一种更清晰的方式是,将资源文件作为“嵌入式资源”嵌入类库的 DLL 中,然后使用IStringLocalizerFactory从程序集中读取。但这涉及更高级的配置,对于大多数场景,将资源文件放在主项目中统一管理可能更简单。

10. 性能考量与最佳实践总结

当资源文件非常多时,可能会对性能产生轻微影响,尤其是在应用启动时加载所有资源。以下是一些优化建议:

  1. 按需加载:Asp .Net Core 的本地化系统默认是惰性加载的,只有在第一次请求某个本地化器 (IStringLocalizer) 时,才会加载对应的资源文件。这本身就是一个优化。
  2. 避免巨型资源文件:坚持按功能模块拆分资源文件,而不是使用一个全局文件。这不仅能提高可维护性,也能减少单次加载和解析的数据量。
  3. 使用缓存:翻译内容本质上是静态的(在发布后)。虽然本地化框架内部可能有缓存机制,但在极高并发场景下,可以考虑使用分布式缓存(如 Redis)来存储常用的、翻译结果固定的字符串,但这会引入缓存一致性问题(更新翻译后需要清除缓存)。
  4. 预编译资源:对于.resx文件,.NET 支持在编译时生成强类型资源类,这能提供编译时检查和高性能。对于 JSON 文件,没有直接的预编译支持,但其文本格式在解析上通常也足够快。
  5. 监控缺失的翻译:在开发阶段,可以注册一个MissingTranslation事件或通过自定义IStringLocalizer实现来记录哪些键没有找到对应语言的翻译,方便查漏补缺。

回顾整个配置过程,从服务注册、中间件配置、资源文件组织、视图注入到语言切换器实现,每一步都有其设计用意。我个人的体会是,前期花时间设计好资源文件的结构和命名规范,远比后期在混乱的翻译中挣扎要高效得多。对于大多数项目,使用 JSON 文件、按视图/控制器组织资源、配合 Cookie 持久化的语言切换器,是一个平衡了灵活性、可维护性和开发体验的方案。当你的应用需要走向世界时,这套建立在 Asp .Net Core 之上的多语言体系,将成为你坚实的后盾。