本地化(Localization,简称 L10n)是指将软件产品针对特定区域市场的语言、文化习俗及技术标准进行适配的过程。它建立在国际化(I18n)的基础之上。在 .NET 中,这主要涉及两个核心概念:
- Culture (文化):影响数字、日期、时间、货币等数据的格式化显示(例如 en-US 使用 $,zh-CN 使用 ¥)。
- UICulture (UI 文化):影响资源文件(如字符串翻译)的查找和加载。
本地化的目标是让应用程序能够根据用户的偏好,动态展示相应的文本和格式,从而提升用户体验。
多语言支持 (Minimal APIs + .resx)
这是 .NET 最传统的本地化方式。通过 .resx (Resource XML) 文件存储键值对翻译。在 Minimal API 中,我们需要注册 AddLocalization 服务,并注入 IStringLocalizer<T> 来获取翻译后的字符串。
步骤:
创建资源文:
- 在项目根目录创建 Resources 文件夹。
- 创建 SharedResource.resx (默认/英文)。
- 创建 SharedResource.zh-CN.resx (中文)。
- 创建一个空类 SharedResource.cs 用于定位资源。
SharedResource.resx 内容:
| Name | Value |
|---|---|
| Hello | Hello World |
SharedResource.zh-CN.resx 内容:
| Name | Value |
|---|---|
| Hello | 你好世界 |
Program.cs 配置:
cs
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Localization;
var builder = WebApplication.CreateBuilder(args);
// 1. 添加本地化服务,指定资源路径为 "Resources"
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
// 2. 配置支持的语言
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { "en-US", "zh-CN" };
options.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
});
var app = builder.Build();
// 3. 启用请求本地化中间件
app.UseRequestLocalization();
// 4. 使用 IStringLocalizer 获取翻译
app.MapGet("/", (IStringLocalizer<SharedResource> localizer) =>
{
return Results.Ok(new { Message = localizer["Hello"] });
});
app.Run();
// 标记类,用于定位资源文件
public class SharedResource { }
RequestLocalizationMiddleware 中间件
UseRequestLocalization() 中间件负责检测当前请求的文化特性(Culture)。它内部维护了一个 RequestCultureProvider 列表,按顺序尝试确定用户的语言偏好。一旦某个提供者成功识别,后续提供者将被跳过。
检测顺序:
QueryStringRequestCultureProvider URL 参数 (e.g., ?culture=zh-CN)。
CookieRequestCultureProviderCookie (e.g., .AspNetCore.Culture)。
AcceptLanguageHeaderRequestCultureProvider HTTP Header (Accept-Language)。
示例 (Program.cs):
cs
// 在 Program.cs 中启用中间件
// 注意:必须放在任何可能依赖 Culture 的中间件之前
app.UseRequestLocalization();
// 此时访问 http://localhost:5000/?culture=zh-CN
// 中间件会自动将 CurrentCulture 和 CurrentUICulture 设置为 zh-CN
RouteDataRequestCultureProvider
RouteDataRequestCultureProvider 允许通过 URL 路由数据来确定语言。例如,URL 结构为 /zh-CN/home 或 /en-US/home。这需要自定义提供者顺序,并确保路由模板中包含文化参数。
PS: 标准的 RouteDataRequestCultureProvider 查找路由数据中的 culture 和 ui-culture 键。
示例:
配置提供者顺序:
cs
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { "en-US", "zh-CN" };
options.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
// 清除默认提供者,将 RouteData 提供者置顶
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider());
// 可选:保留其他提供者作为 fallback
options.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
});
定义包含文化的路由:
cs
// 路由模板中包含 {culture}
app.MapGet("/{culture}/hello", (string culture, IStringLocalizer<SharedResource> localizer) =>
{
// 在 Minimal API 中,RouteDataRequestCultureProvider 会自动从路由数据中提取 culture
// 并设置 HttpContext 中的 CultureInfo。
// 如果自动设置未生效(取决于版本),可能需要手动设置或使用 Action Filter。
// 但在标准配置下,中间件会处理此逻辑。
return Results.Ok(localizer["Hello"]);
});
访问 http://localhost:5000/zh-CN/hello 将显示中文。
自定义 RequestCultureProvider 类
当默认策略(Query, Cookie, Header, Route)无法满足需求时(例如从 JWT Token、自定义 Header X-Lang 或数据库中读取用户偏好),可以创建自定义提供者。需继承 RequestCultureProvider 并重写 DetermineProviderCultureResult 方法。
示例:
创建自定义提供者类:
cs
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Http;
public class CustomHeaderRequestCultureProvider : RequestCultureProvider
{
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
throw new ArgumentNullException(nameof(httpContext));
// 从自定义 Header "X-Custom-Lang" 中读取语言代码
var langHeader = httpContext.Request.Headers["X-Custom-Lang"].ToString();
if (string.IsNullOrEmpty(langHeader))
{
// 返回 null 表示此提供者无法确定文化,中间件将尝试下一个提供者
return Task.FromResult<ProviderCultureResult?>(null);
}
// 返回解析出的文化结果
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(langHeader));
}
}
注册自定义提供者:
cs
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { "en-US", "zh-CN" };
options.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
// 将自定义提供者插入到列表最前面,优先执行
options.RequestCultureProviders.Insert(0, new CustomHeaderRequestCultureProvider());
});
多样化的数据源
除了 .resx,我们可以实现 IStringLocalizer 和 IStringLocalizerFactory 来支持 JSON 文件或 Redis。
基于 JSON 格式的本地化
创建一个 JSON 文件存储所有语言的翻译,并通过自定义 Localizer 读取。适合小型应用或静态配置。
创建 JSON 文件 (Resources/lang.json):
bash
{
"zh-CN": {
"Hello": "你好,JSON!",
"Welcome": "欢迎"
},
"en-US": {
"Hello": "Hello from JSON!",
"Welcome": "Welcome"
}
}
实现 JSON Localizer:
cs
using Microsoft.Extensions.Localization;
using System.Text.Json;
public class JsonStringLocalizer : IStringLocalizer
{
private readonly Dictionary<string, string> _resources;
public JsonStringLocalizer(string cultureName)
{
_resources = LoadResources(cultureName);
}
private Dictionary<string, string> LoadResources(string cultureName)
{
var path = Path.Combine("Resources", "lang.json");
if (!File.Exists(path)) return new Dictionary<string, string>();
var json = File.ReadAllText(path);
var allLangs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(json);
// 尝试获取指定语言,否则回退到英文,再否则为空
if (allLangs != null)
{
if (allLangs.ContainsKey(cultureName)) return allLangs[cultureName];
if (allLangs.ContainsKey("en-US")) return allLangs["en-US"];
}
return new Dictionary<string, string>();
}
public LocalizedString this[string name]
{
get
{
if (_resources.TryGetValue(name, out var value))
return new LocalizedString(name, value, false);
return new LocalizedString(name, name, true); // Not found
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = this[name].Value;
return new LocalizedString(name, string.Format(format, arguments), false);
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return _resources.Select(kvp => new LocalizedString(kvp.Key, kvp.Value));
}
}
public class JsonStringLocalizerFactory : IStringLocalizerFactory
{
public IStringLocalizer Create(Type resourceSource)
{
return new JsonStringLocalizer(CultureInfo.CurrentUICulture.Name);
}
public IStringLocalizer Create(string baseName, string location)
{
return new JsonStringLocalizer(CultureInfo.CurrentUICulture.Name);
}
}
注册服务:
cs
// 替换默认的 Factory
builder.Services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
// 注意:不再需要 AddLocalization,因为我们要完全接管 Localizer 的创建
基于 Redis 存储的本地化
使用 StackExchange.Redis 将翻译存储在 Redis 中。适合需要动态更新翻译且无需重启应用的场景。
安装 NuGet包:
StackExchange.Redis
实现 Redis Localizer:
cs
using Microsoft.Extensions.Localization;
using StackExchange.Redis;
using System.Collections.Concurrent;
public class RedisStringLocalizer : IStringLocalizer
{
private readonly IDatabase _db;
private readonly string _culture;
// 简单内存缓存,避免频繁请求 Redis
private static readonly ConcurrentDictionary<string, string> _cache = new();
public RedisStringLocalizer(IConnectionMultiplexer redis, string culture)
{
_db = redis.GetDatabase();
_culture = culture;
}
public LocalizedString this[string name]
{
get
{
var cacheKey = $"{_culture}:{name}";
// 检查内存缓存
if (_cache.TryGetValue(cacheKey, out var cachedValue))
{
return new LocalizedString(name, cachedValue, false);
}
// Redis Key 格式: lang:{culture}:{key}
var redisKey = $"lang:{_culture}:{name}";
var value = _db.StringGet(redisKey);
if (value.HasValue)
{
var strValue = value.ToString();
_cache.TryAdd(cacheKey, strValue);
return new LocalizedString(name, strValue, false);
}
return new LocalizedString(name, name, true);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = this[name].Value;
return new LocalizedString(name, string.Format(format, arguments), false);
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
// Redis 扫描所有键性能较差,生产环境建议实现特定索引或禁用此方法
return Enumerable.Empty<LocalizedString>();
}
}
public class RedisStringLocalizerFactory : IStringLocalizerFactory
{
private readonly IConnectionMultiplexer _redis;
public RedisStringLocalizerFactory(IConnectionMultiplexer redis)
{
_redis = redis;
}
public IStringLocalizer Create(Type resourceSource)
{
return new RedisStringLocalizer(_redis, CultureInfo.CurrentUICulture.Name);
}
public IStringLocalizer Create(string baseName, string location)
{
return new RedisStringLocalizer(_redis, CultureInfo.CurrentUICulture.Name);
}
}
注册服务:
cs
// 1. 注册 Redis 连接
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
return ConnectionMultiplexer.Connect("localhost:6379");
});
// 2. 替换 Localizer Factory
builder.Services.AddSingleton<IStringLocalizerFactory, RedisStringLocalizerFactory>();
测试数据写入 (Redis CLI):
SET lang:zh-CN:Hello "你好,Redis!"
SET lang:en-US:Hello "Hello from Redis!"