- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
本指南以 practical-aspnetcore 仓库的 Localization and Globalization 专题 为主体,系统讲解 ASP.NET Core 中基于语言与文化的本地化(Localization)与全球化(Globalization)实现方案。专题内含 6 个可直接运行的 Minimal API 示例,覆盖 resx 资源文件、QueryStringRequestCultureProvider、CookieRequestCultureProvider、Culture与UI Culture的区别,以及基于 Portable Object(PO)文件、带上下文(context)的高级翻译方案。读完本文,你将掌握 ASP.NET Core 请求本地化的完整配置链路、四种内置 Culture Provider 的取舍,以及从 resx 迁移到 PO 文件的完整实战路径。
专题概览:6 个示例的递进关系
该专题位于 projects/localization,README 明确其主题是 "all about languages, culture"(语言与文化)。6 个示例呈现清晰的递进脉络:
| 示例 | 目录 | 核心主题 | 支持语言 |
|---|---|---|---|
| Localization - 1 | projects/localization/localization-1 | 基于 resx 资源文件的最基础本地化 | 法语 fr-FR |
| Localization - 2 | projects/localization/localization-2 | 内置QueryStringRequestCultureProvider通过查询字符串切换文化 | 英语 en-US、法语 fr-FR |
| Localization - 3 | projects/localization/localization-3 | Culture与UI Culture的区别 | en-US、en-GB |
| Localization - 4 | projects/localization/localization-4 | 内置CookieRequestCultureProvider通过 Cookie 切换文化 | 英语 en-US、法语 fr-FR |
| Localization - 5 | projects/localization/localization-5 | 用 PO 文件替代繁琐的 resx 文件 | it-IT(附 en、fr 的 PO 文件) |
| Localization - 6 | projects/localization/localization-6 | PO 文件的延续:通过 context 让同一翻译键返回不同字符串 | it-IT(附 en、fr 的 PO 文件) |
需要说明的是:README 中 localization-1 标注了历史版本依赖Microsoft.AspNetCore.Localization与Microsoft.Extensions.Localization(2.1.0),localization-5 标注"requires ASP.NET Core 2"。当前仓库已将这些示例升级为net10.0(见各 csproj 文件),resx 示例不再需要单独引用上述 NuGet 包(相关类型已进入 ASP.NET Core 共享框架),而 PO 示例仍通过 OrchardCore.Localization.Core(当前版本 3.0.1)提供支持。
认识核心概念:Culture、UI Culture 与 RequestLocalizationOptions
在深入示例前,先厘清三个贯穿全部示例的核心概念。
Culture 与 UI Culture
- Culture(区域性):影响数字、日期、货币等格式化行为,例如日期
DateTime.Now.ToString()的输出格式。 - UI Culture(界面区域性):影响界面字符串的选取,即资源文件中哪一语言版本的翻译被加载。
在 ASP.NET Core 中,一次请求会同时携带这两个值,分别存放在IRequestCultureFeature.RequestCulture.Culture与RequestCulture.UICulture中。本地化 3 号示例对这一点做了专门演示,下文详述。
RequestLocalizationOptions:本地化的"总配置"
每个示例都通过RequestLocalizationOptions完成三项关键配置:
var options = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("fr-FR"), // 默认请求文化 SupportedCultures = supportedCultures, // 支持的 Culture 列表 SupportedUICultures = supportedCultures // 支持的 UI Culture 列表 }; app.UseRequestLocalization(options);DefaultRequestCulture:当无法从请求中解析出文化时使用的兜底值;SupportedCultures/SupportedUICultures:白名单,只有列表内的文化会被接受;UseRequestLocalization(options):注册请求本地化中间件,是所有示例的共同入口。
示例源码(如 localization-1/Program.cs)中注释特别强调:"This section is important otherwise aspnet won't be able to pick up the resource"(该配置很重要,否则 ASP.NET Core 无法加载对应资源)——即只有把文化列入SupportedCultures,中间件才会从资源文件中挑选匹配语言的字符串。
示例 1:基于 resx 资源文件的最基础本地化
这是整个专题的地基,位于 projects/localization/localization-1,演示使用资源文件完成最基础的本地化,仅支持法语(README 原话 "because we are fancy")。
完整的 Program.cs 实现
using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Localization; using System.Globalization; var builder = WebApplication.CreateBuilder(); builder.Services.AddLocalization(options => options.ResourcesPath = "resources"); var app = builder.Build(); var stringLocalizerFactory = app.Services.GetService<IStringLocalizerFactory>(); var local = stringLocalizerFactory.Create("Common", typeof(Program).Assembly.FullName); // 将支持的 Culture 显式列出,否则 ASP.NET Core 无法加载对应资源 var supportedCultures = new List<CultureInfo> { new CultureInfo("fr-FR") }; var options = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("fr-FR"), SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures }; app.UseRequestLocalization(options); app.Run(async context => { var requestCulture = context.Features.Get<IRequestCultureFeature>(); await context.Response.WriteAsync($"{requestCulture.RequestCulture.Culture} - {local["Hello"]} {local["Goodbye"]} {local["Yes"]} {local["No"]}"); }); app.Run();关键步骤拆解
注册本地化服务:
AddLocalization(options => options.ResourcesPath = "resources")指定资源文件的存放目录为resources。这是 resx 文件与运行时查找路径之间的约定,示例中资源文件全部位于 resources 目录。创建字符串本地化器:通过
IStringLocalizerFactory的Create("Common", typeof(Program).Assembly.FullName)创建本地化器。第一个参数是资源文件名基名Common,第二个参数是程序集限定名。这意味着程序会在resources目录下寻找名为Common.<culture>.resx的文件。配置并启用中间件:如前所述,配置
RequestLocalizationOptions后调用UseRequestLocalization。在响应中取用本地化字符串:
local["Hello"]等索引器语法按当前请求的 UI Culture 查找翻译。
resx 资源文件长什么样
示例目录下的 resources/Common.fr-FR.resx 是标准 ResX 格式的 XML 文件,核心数据段如下:
<data name="Hello" xml:space="preserve"> <value>Bonjour</value> </data> <data name="Goodbye" xml:space="preserve"> <value>Au Revoir</value> </data> <data name="Yes" xml:space="preserve"> <value>Oui</value> </data> <data name="No" xml:space="preserve"> <value>Non</value> </data>文件名中的fr-FR后缀(Common.fr-FR.resx)决定了该文件对应的区域性;若存在Common.en-US.resx,则匹配en-US。示例 1 只提供了法语资源,因此无论浏览器如何设置,输出都将是法语字符串。将DefaultRequestCulture设为fr-FR且SupportedCultures仅含fr-FR,正好保证了这一点。
示例 2:用 QueryStringRequestCultureProvider 切换请求文化
projects/localization/localization-2 在示例 1 基础上新增了en-US,并演示通过查询字符串切换请求文化,同时支持英语和法语。
源码与运行效果
核心代码与示例 1 几乎一致,差别仅在SupportedCultures加入了en-US,以及响应逻辑中动态输出切换链接:
var requestCulture = context.Features.Get<IRequestCultureFeature>().RequestCulture; if (requestCulture.Culture.TwoLetterISOLanguageName != "fr") await context.Response.WriteAsync($"<a href=\"/?culture=fr-FR&ui-culture=fr-FR\">Switch to French</a><br/>"); else if (requestCulture.Culture.TwoLetterISOLanguageName != "en") await context.Response.WriteAsync($"<a href=\"/?culture=en-US&ui-culture=en-US\">Switch to English</a><br/>"); await context.Response.WriteAsync($@" Request Culture: {requestCulture.Culture} <br/> Localized strings: {local["Hello"]} {local["Goodbye"]} {local["Yes"]} {local["No"]} ");页面标题明确点出所用机制:"QueryStringRequestCultureProvider —— We are using query string to switch the request culture."
工作原理
QueryStringRequestCultureProvider是 ASP.NET Core 内置的默认 Culture Provider 之一。它解析 URL 查询字符串中的两个约定参数:
culture:设置Culture(影响数字、日期等格式化);ui-culture:设置UI Culture(影响界面翻译字符串)。
因此访问/?culture=fr-FR&ui-culture=fr-FR即可切到法语,访问/?culture=en-US&ui-culture=en-US即切到英语。代码里通过TwoLetterISOLanguageName(fr/en)判断当前文化以决定渲染哪个切换链接。
该示例的资源文件位于 localization-2/resources,包含Common.fr-FR.resx与Common.en-US.resx两份翻译(英语版Hello译为 "Howdy"、Goodbye译为 "So long",法语版为 "Bonjour" / "Au Revoir"),是理解"一个键对应多语言"的最小完整样本。
示例 3:Culture 与 UI Culture 到底有什么区别
projects/localization/localization-3 专门演示Culture与UI Culture的差异,支持en-US与en-GB两种文化。页面用一句话概括核心差异:"UI Culture affects the resource string. Culture affects number formatting, dates, etc."(UI Culture 决定资源字符串,Culture 决定数字、日期等格式化)。
源码要点
var supportedCultures = new List<CultureInfo> { new CultureInfo("en-US"), new CultureInfo("en-GB") }; // 页面提供两个交叉切换链接 // <a href="/?culture=en-US&ui-culture=en-GB">Culture US - UI Culture GB</a> // <a href="/?culture=en-GB&ui-culture=en-US">Culture GB - UI Culture US</a> await context.Response.WriteAsync($@" Request Culture: {requestCulture.Culture} <br/> Today's Date (Culture): {DateTime.Now.ToString()}<br/><br/> Request UI Culture: {requestCulture.UICulture}<br/> Localized strings (UI Culture): {local["Hello"]} {local["Goodbye"]} {local["Yes"]} {local["No"]} ");验证方法
访问/?culture=en-US&ui-culture=en-GB时:
Request Culture为en-US,DateTime.Now.ToString()按美式格式输出日期(月/日/年 顺序、12 小时制等);Request UI Culture为en-GB,local["Hello"]等字符串按Common.en-GB.resx的翻译渲染。
访问/?culture=en-GB&ui-culture=en-US则完全相反。这样就直观验证了"两个维度独立生效"的结论:格式化看 Culture,翻译看 UI Culture。
值得注意,示例 3 的 resources/Common.en-US.resx 与示例 2 的英语资源内容相同(Howdy / So long / Yah / Nope);由于en-US与en-GB资源内容一致,这里的对照重点其实是日期格式化,而非翻译差异。
示例 4:用 CookieRequestCultureProvider 持久化语言选择
projects/localization/localization-4 演示另一种内置 Provider ——CookieRequestCultureProvider,通过 Cookie 切换请求文化,支持英语和法语。
写入 Cookie 的机制
示例没有像默认行为那样让 Provider 自动写 Cookie,而是显式地在代码中完成"写入 + 跳转"两步:
var languageSwitch = context.Request.Query["lang"]; if (languageSwitch == "fr") { var val = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("fr-FR")); context.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, val); context.Response.Redirect("/"); return; } else if (languageSwitch == "en") { var val = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("en-US")); context.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, val); context.Response.Redirect("/"); return; }关键 API 有两个:
CookieRequestCultureProvider.DefaultCookieName:默认 Cookie 名(即.AspNetCore.Culture)。页面会把它打印出来:"We are using a cookie named.AspNetCore.Cultureto switch the request culture."CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("fr-FR")):把文化信息序列化成 Provider 可识别的 Cookie 值格式。
交互流程为:用户点击/?lang=fr或/?lang=en→ 服务端写入对应文化的 Cookie → 重定向回/→CookieRequestCultureProvider从 Cookie 中解析文化并完成本地化渲染。由于文化保存在 Cookie 中,用户的语言选择具备持久性,这是与查询字符串方案(每次需带参数)的核心区别。
var requestCulture = context.Features.Get<IRequestCultureFeature>().RequestCulture; await context.Response.WriteAsync($@" Click <a href="/?lang=fr">here</a> for French cookie and <a href="/?lang=en">here</a> for English cookie.<br/><br/> Request Culture: {requestCulture.Culture} <br/> Localized strings: {local["Hello"]} {local["Goodbye"]} {local["Yes"]} {local["No"]} ");关于内置 Provider 的完整图景
从该专题的示例可以归纳出 ASP.NET Core 请求本地化中间件的 Provider 机制:中间件按顺序尝试从不同来源解析文化。示例中涉及的QueryStringRequestCultureProvider与CookieRequestCultureProvider均为内置实现;除它们之外框架还提供基于Accept-Language请求头的 Provider,以及支持自定义 Provider 的扩展点(通过RequestLocalizationOptions.RequestCultureProviders调整 Provider 列表与顺序)。示例代码从context.Features.Get<IRequestCultureFeature>()读取解析结果,正是中间件解析完成后写入请求特性集合的标准访问方式。
示例 5:用 PO 文件替代繁琐的 resx
projects/localization/localization-5 演示使用 Portable Object(PO)文件做本地化,README 明确其动机是替代 "cumbersome resx file"(繁琐的 resx 文件)。PO 是 GNU gettext 生态的标准翻译格式,比 XML 化的 resx 更轻量、更利于人工维护和版本管理。
依赖与注册方式
.csproj 中引入了 OrchardCore 的本地化核心包(当前版本 3.0.1):
<ItemGroup> <PackageReference Include="OrchardCore.Localization.Core" Version="3.0.1" /> </ItemGroup>Program.cs 的服务注册与示例 1~4 明显不同:
var builder = WebApplication.CreateBuilder(); builder.Services.AddMemoryCache(); builder.Services.AddPortableObjectLocalization(); var app = builder.Build(); var supportedCultures = new List<CultureInfo> { new CultureInfo("it-IT") // 默认文化;注释说明可改为 fr 或其他语言 }; var option = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("it-IT"), SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures }; app.UseRequestLocalization(option); app.Run(context => { var fac = context.RequestServices.GetService<IStringLocalizerFactory>(); var local = fac.Create(string.Empty, string.Empty); // 空字符串即使用默认/根资源 var requestCulture = context.Features.Get<IRequestCultureFeature>().RequestCulture; return context.Response.WriteAsync($"Request Culture `{requestCulture.UICulture}` = {local["Hello"]}"); });与 resx 版本的三处关键差异:
AddPortableObjectLocalization()替代AddLocalization(),PO 文件的查找约定也不同于ResourcesPath;AddMemoryCache():PO 文件解析结果会被缓存,需要内存缓存支撑;Create(string.Empty, string.Empty):两个参数均为空字符串,表示使用默认资源(PO 文件模式下的惯例写法),不再需要指定 "Common" 基名与程序集名。
示例刻意把支持文化限定为it-IT,以保证任何浏览器环境下都能稳定复现;注释说明:要切到法语或其他语言,只需把it-IT替换成目标文化或在SupportedCultures中追加更多文化。仓库同时提供了 en.po、fr.po、it.po 三个翻译文件。
PO 文件格式
PO 文件由msgid(源字符串/键)与msgstr(翻译)成对组成,例如:
# en.po msgid "Hello" msgstr "Howdy"# fr.po msgid "Hello" msgstr "Bonjour"# it.po msgid "Hello" msgstr "Ciao"对比 resx 的整段 XML 脚手架(schema、resheader、data 节点),PO 文件只需两三行即可表达一个翻译项,这正是 README 所称"替代繁琐 resx"的意义所在。
示例 6:PO 文件 + context,让同一键返回不同翻译
projects/localization/localization-6 是示例 5 的延续,核心新增点是context(上下文):通过msgctxt字段,让同一个翻译键Hello在不同上下文下返回不同字符串。
带上下文的 PO 文件
以 localization-6/fr.po 为例,同一个msgid "Hello"出现了三次,分别由msgctxt区分:
msgid "Hello" msgstr "Bonjour" msgctxt "Greet Friend" msgid "Hello" msgstr "Bonjour Mon Ami" msgctxt "Greet Lover" msgid "Hello" msgstr "Bonjour Mon Amour"英语版(en.po)与意大利语版(it.po)同样按此模式组织,例如Ciao Tesoro(Greet Friend)与Ciao Amore(Greet Lover)。
在代码中使用上下文
示例注册了两个路由端点,各自通过Create的第一个参数指定上下文,从而从同一Hello键取到不同翻译:
app.UseRouting(); app.MapGet("greet-friend", async context => { var fac = context.RequestServices.GetService<IStringLocalizerFactory>(); var local = fac.Create("Greet Friend", string.Empty); // 上下文:Greet Friend var requestCulture = context.Features.Get<IRequestCultureFeature>().RequestCulture; await context.Response.WriteAsync($"Request Culture `{requestCulture.UICulture}` = {local["Hello"]}"); }); app.MapGet("greet-lover", async context => { var fac = context.RequestServices.GetService<IStringLocalizerFactory>(); var local = fac.Create("Greet Lover", string.Empty); // 上下文:Greet Lover var requestCulture = context.Features.Get<IRequestCultureFeature>().RequestCulture; await context.Response.WriteAsync($"Request Culture `{requestCulture.UICulture}` = {local["Hello"]}"); });首页则保持Create(string.Empty, string.Empty)使用默认资源,并输出两个跳转链接:
app.Run(async context => { var local = fac.Create(string.Empty, string.Empty); ... await context.Response.WriteAsync($"Request Culture `{requestCulture.UICulture}` = {local["Hello"]}<br/><br/>"); await context.Response.WriteAsync($@"<a href=""/greet-friend"">Greet Friend</a><br/>"); await context.Response.WriteAsync($@"<a href=""/greet-lover"">Greet Lover</a><br/>"); ... });于是访问/greet-friend时同一键Hello输出 "Bonjour Mon Ami"(法语)/ "Ciao Tesoro"(意大利语),访问/greet-lover则输出 "Bonjour Mon Amour" / "Ciao Amore",而首页输出基础的 "Bonjour" / "Ciao"。这就是msgctxt上下文机制的价值:在词汇多义或需要按业务场景区分的场合,无需发明新键名,用上下文即可为同一翻译键提供不同译文。
运行方式与小结
运行方式
专题内每个示例都是独立的 Minimal API 项目(Microsoft.NET.Sdk.Web,目标框架net10.0),进入对应目录后执行:
dotnet run以 projects/localization/localization-1 为例,运行后浏览器访问默认地址即可看到按默认文化(法语)渲染的Hello / Goodbye / Yes / No翻译;示例 2 可通过?culture=fr-FR&ui-culture=fr-FR或?culture=en-US&ui-culture=en-US切换;示例 3 用交叉参数观察日期格式化与翻译的独立变化;示例 4 点击/?lang=fr//?lang=en写入文化 Cookie;示例 5、6 访问/、/greet-friend、/greet-lover观察 PO 文件与上下文翻译效果。
技术选型小结
综合 6 个示例,可提炼出本专题的核心决策点:
- 资源载体:小规模、团队熟悉 .NET 生态时,resx(示例 1~4)足够;追求翻译文件轻量、便于与翻译团队/外部工具协作时,选 PO 文件(示例 5、6,需引入 OrchardCore.Localization.Core 并注册
AddPortableObjectLocalization()); - 文化切换方式:临时切换、便于分享链接时用查询字符串(
QueryStringRequestCultureProvider);需要记住用户语言偏好时用 Cookie(CookieRequestCultureProvider,Cookie 名为DefaultCookieName,值由MakeCookieValue生成); - 语义分层:始终牢记
Culture管格式化、UI Culture管翻译这一基本分工(示例 3),并善用IRequestCultureFeature在端点内读取当前请求文化; - 必须显式声明:任何 Provider 生效的前提都是把目标文化列入
RequestLocalizationOptions.SupportedCultures/SupportedUICultures,否则资源不会被选中(示例源码注释中的核心提醒)。
如需进一步探索,可对照阅读仓库中其他涉及本地化的专题,例如 Blazor Server 的 Localization 示例 与 MVC 的 Localization 示例,以了解相同概念在不同 UI 框架下的落地方式。
- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
相关推荐
practical-aspnetcore 仓库完全指南:637 个 ASP.NET Core 实战示例的导航、运行与源码研读
practical aspnetcore 仓库完全指南:637 个 ASP.NET Core 实战示例的导航、运行与源码研读 导读 practical aspn
示例工程practical-aspnetcore 仓库实战:从固定路由到 IActionConstraint——ASP.NET Core MVC 路由 9 个渐进式示例全解
practical aspnetcore 仓库实战:从固定路由到 IActionConstraint——ASP.NET Core MVC 路由 9 个渐进式示例
示例工程ASP.NET Core 11 新特性微样例全解:practical-aspnetcore 仓库 net11 规划文档与 22 个落地示例深度解读
ASP.NET Core 11 新特性微样例全解:practical aspnetcore 仓库 net11 规划文档与 22 个落地示例深度解读 导读:本文围
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考