本地化与国际化
大约 10 分钟约 3092 字
本地化与国际化
简介
ASP.NET Core 内置了本地化(Localization)和国际化(Internationalization,简称 i18n)支持。通过资源文件(.resx)、字符串本地化器、视图本地化等机制,可以让应用支持多语言。适用于面向全球用户的产品、多区域部署的系统。
特点
基本配置
注册本地化服务
/// <summary>
/// 本地化基本配置
/// </summary>
var builder = WebApplication.CreateBuilder(args);
// 添加本地化支持,指定资源文件所在程序集
builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
// 支持的文化列表
var supportedCultures = new[]
{
new CultureInfo("zh-CN"),
new CultureInfo("en-US"),
new CultureInfo("ja-JP"),
};
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture("zh-CN");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
// 文化检测顺序
options.RequestCultureProviders = new IRequestCultureProvider[]
{
new QueryStringRequestCultureProvider(), // ?culture=en-US
new CookieRequestCultureProvider(), // Cookie
new AcceptLanguageHeaderRequestCultureProvider() // Accept-Language 头
};
});
var app = builder.Build();
// 使用本地化中间件
app.UseRequestLocalization();
app.Run();资源文件结构
Resources/
├── Controllers/
│ ├── UserController.zh-CN.resx # 中文
│ └── UserController.en-US.resx # 英文
├── Views/
│ └── Home.zh-CN.resx
├── Messages.zh-CN.resx # 共享资源
├── Messages.en-US.resx
└── Messages.ja-JP.resx字符串本地化
IStringLocalizer
/// <summary>
/// IStringLocalizer — 按键获取本地化字符串
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IStringLocalizer<UserController> _localizer;
public UserController(IStringLocalizer<UserController> localizer)
{
_localizer = localizer;
}
[HttpGet("welcome")]
public IActionResult Welcome()
{
// 根据当前文化自动选择资源文件
var message = _localizer["Welcome"];
return Ok(new { Message = message.Value });
}
[HttpPost("create")]
public IActionResult Create([FromBody] CreateUserRequest request)
{
// 带参数的本地化
var message = _localizer["UserCreated", request.UserName];
return Ok(new { Message = message.Value });
}
[HttpGet("notfound")]
public IActionResult NotFound()
{
var message = _localizer["UserNotFound"];
return NotFound(new { Message = message.Value });
}
}资源文件示例
<!-- Resources/Controllers/UserController.zh-CN.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="Welcome" xml:space="preserve">
<value>欢迎访问用户管理系统</value>
</data>
<data name="UserCreated" xml:space="preserve">
<value>用户 {0} 创建成功</value>
</data>
<data name="UserNotFound" xml:space="preserve">
<value>用户不存在</value>
</data>
</root>
<!-- Resources/Controllers/UserController.en-US.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="Welcome" xml:space="preserve">
<value>Welcome to User Management System</value>
</data>
<data name="UserCreated" xml:space="preserve">
<value>User {0} created successfully</value>
</data>
<data name="UserNotFound" xml:space="preserve">
<value>User not found</value>
</data>
</root>共享资源
/// <summary>
/// 共享本地化资源 — 多处复用的字符串
/// </summary>
// 定义一个空类作为资源定位标记
public class SharedResources { }
// 使用共享资源
public class NotificationService
{
private readonly IStringLocalizer<SharedResources> _localizer;
public NotificationService(IStringLocalizer<SharedResources> localizer)
{
_localizer = localizer;
}
public string GetError(string key) => _localizer[$"Error_{key}"];
public string GetSuccess(string key) => _localizer[$"Success_{key}"];
}
// 资源文件
// Resources/SharedResources.zh-CN.resx
// Error_Timeout → "请求超时,请重试"
// Error_NoPermission → "没有操作权限"
// Success_Saved → "保存成功"
// Success_Deleted → "删除成功"数据注解本地化
验证消息本地化
/// <summary>
/// 数据注解的本地化 — 验证错误消息多语言
/// </summary>
public class RegisterViewModel
{
[Required(ErrorMessage = "UserName_Required")]
[StringLength(50, ErrorMessage = "UserName_Length")]
public string UserName { get; set; } = "";
[Required(ErrorMessage = "Email_Required")]
[EmailAddress(ErrorMessage = "Email_Invalid")]
public string Email { get; set; } = "";
[Required(ErrorMessage = "Password_Required")]
[MinLength(8, ErrorMessage = "Password_MinLength")]
public string Password { get; set; } = "";
}
// 注册数据注解本地化
builder.Services.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResources));
});手动切换文化
API 方式切换
/// <summary>
/// 手动设置当前请求的文化
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class LanguageController : ControllerBase
{
private readonly IStringLocalizer<SharedResources> _localizer;
public LanguageController(IStringLocalizer<SharedResources> localizer)
{
_localizer = localizer;
}
// 切换语言
[HttpPost("switch")]
public IActionResult SwitchLanguage([FromBody] SwitchLanguageRequest request)
{
// 设置 Cookie
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(request.Culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
return Ok(new { Message = "语言切换成功", Culture = request.Culture });
}
// 获取当前语言
[HttpGet("current")]
public IActionResult GetCurrentLanguage()
{
var culture = CultureInfo.CurrentCulture.Name;
var uiCulture = CultureInfo.CurrentUICulture.Name;
return Ok(new { Culture = culture, UICulture = uiCulture });
}
// 指定文化的翻译
[HttpGet("translate")]
public IActionResult Translate([FromQuery] string key, [FromQuery] string? culture = null)
{
if (!string.IsNullOrEmpty(culture))
{
// 临时切换文化
var originalCulture = CultureInfo.CurrentCulture;
var originalUICulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentCulture = new CultureInfo(culture);
CultureInfo.CurrentUICulture = new CultureInfo(culture);
var value = _localizer[key];
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUICulture;
return Ok(new { Key = key, Value = value.Value, Culture = culture });
}
return Ok(new { Key = key, Value = _localizer[key].Value });
}
}
public record SwitchLanguageRequest(string Culture);自定义资源存储
JSON 资源文件
/// <summary>
/// 从 JSON 文件加载本地化字符串
/// </summary>
public class JsonStringLocalizer : IStringLocalizer
{
private readonly Dictionary<string, string> _strings;
private readonly string _culture;
public JsonStringLocalizer(string basePath, string culture)
{
_culture = culture;
var filePath = Path.Combine(basePath, $"{culture}.json");
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath);
_strings = JsonSerializer.Deserialize<Dictionary<string, string>>(json)
?? new Dictionary<string, string>();
}
else
{
_strings = new Dictionary<string, string>();
}
}
public LocalizedString this[string name]
{
get
{
var value = _strings.GetValueOrDefault(name, name);
return new LocalizedString(name, value, ! _strings.ContainsKey(name));
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = _strings.GetValueOrDefault(name, name);
var value = string.Format(format, arguments);
return new LocalizedString(name, value, !_strings.ContainsKey(name));
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return _strings.Select(s => new LocalizedString(s.Key, s.Value, false));
}
}
// JSON 资源文件示例
// Resources/zh-CN.json
// {
// "Welcome": "欢迎访问",
// "UserCreated": "用户 {0} 创建成功",
// "Error_Timeout": "请求超时"
// }日期/数字格式化
文化相关的格式化
/// <summary>
/// 日期和数字的文化敏感格式化
/// </summary>
public class FormatService
{
public void ShowFormats()
{
var now = DateTime.Now;
var amount = 1234567.89m;
// 中文格式
var zhCulture = new CultureInfo("zh-CN");
Console.WriteLine(now.ToString("D", zhCulture)); // 2026年4月11日
Console.WriteLine(amount.ToString("C", zhCulture)); // ¥1,234,567.89
// 英文格式
var enCulture = new CultureInfo("en-US");
Console.WriteLine(now.ToString("D", enCulture)); // Saturday, April 11, 2026
Console.WriteLine(amount.ToString("C", enCulture)); // $1,234,567.89
// 日文格式
var jaCulture = new CultureInfo("ja-JP");
Console.WriteLine(now.ToString("D", jaCulture)); // 2026年4月11日
Console.WriteLine(amount.ToString("C", jaCulture)); // ¥1,234,567.89
}
}路由级别的文化配置
基于URL的文化切换
/// <summary>
/// 通过路由实现文化切换 — /en-US/api/products
/// </summary>
// Program.cs
builder.Services.AddRouting(options =>
{
options.ConstraintMap.Add("culture", typeof(CultureRouteConstraint));
});
builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
var app = builder.Build();
// 使用路由参数指定文化
app.MapControllerRoute(
name: "localized",
pattern: "{culture:culture}/{controller=Home}/{action=Index}/{id?}");
app.UseRequestLocalization();
// 自定义路由约束
public class CultureRouteConstraint : IRouteConstraint
{
private static readonly string[] SupportedCultures = { "zh-CN", "en-US", "ja-JP" };
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var value) || value == null)
return false;
return SupportedCultures.Contains(Convert.ToString(value));
}
}数据库资源存储
从数据库加载本地化字符串
/// <summary>
/// 数据库资源提供器 — 支持运行时动态更新翻译
/// </summary>
public class DatabaseStringLocalizer : IStringLocalizer
{
private readonly IResourceRepository _repository;
private readonly string _resourceType;
private readonly ILogger<DatabaseStringLocalizer> _logger;
private readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new();
public DatabaseStringLocalizer(
IResourceRepository repository,
string resourceType,
ILogger<DatabaseStringLocalizer> logger)
{
_repository = repository;
_resourceType = resourceType;
_logger = logger;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = format != null ? string.Format(format, arguments) : name;
return new LocalizedString(name, value, format == null);
}
}
private string? GetString(string name)
{
var culture = CultureInfo.CurrentUICulture.Name;
var cacheKey = $"{_resourceType}:{culture}";
var strings = _cache.GetOrAdd(cacheKey, _ =>
{
try
{
return _repository.GetStrings(_resourceType, culture)
.GetAwaiter().GetResult();
}
catch (Exception ex)
{
_logger.LogError(ex, "加载本地化资源失败: {Type}, {Culture}", _resourceType, culture);
return new Dictionary<string, string>();
}
});
return strings.GetValueOrDefault(name);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
var culture = CultureInfo.CurrentUICulture.Name;
var strings = _cache.GetOrAdd($"{_resourceType}:{culture}", _ =>
_repository.GetStrings(_resourceType, culture).GetAwaiter().GetResult());
return strings.Select(s => new LocalizedString(s.Key, s.Value, false));
}
// 刷新缓存(翻译更新后调用)
public void RefreshCache() => _cache.Clear();
}
// 资源仓储接口
public interface IResourceRepository
{
Task<Dictionary<string, string>> GetStrings(string resourceType, string culture);
Task SetString(string resourceType, string key, string culture, string value);
}
// 注册
builder.Services.AddSingleton<IResourceRepository, SqlResourceRepository>();
builder.Services.AddSingleton<IStringLocalizerFactory, DatabaseLocalizerFactory>();本地化中间件深入
自定义文化解析中间件
/// <summary>
/// 自定义文化解析策略
/// </summary>
public class CustomCultureProvider : IRequestCultureProvider
{
public Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
// 优先级:URL路径 > QueryString > Header > Cookie > 默认
// 1. 从 URL 路径解析
var pathSegments = httpContext.Request.Path.Value?.Split('/');
if (pathSegments?.Length > 1)
{
var candidate = pathSegments[1];
if (IsValidCulture(candidate))
{
return Task.FromResult<ProviderCultureResult?>(
new ProviderCultureResult(candidate, candidate));
}
}
// 2. 从自定义 Header 解析
if (httpContext.Request.Headers.TryGetValue("X-Culture", out var headerCulture))
{
var culture = headerCulture.ToString();
if (IsValidCulture(culture))
{
return Task.FromResult<ProviderCultureResult?>(
new ProviderCultureResult(culture, culture));
}
}
// 3. 从用户设置解析(已登录用户)
var user = httpContext.User;
if (user.Identity?.IsAuthenticated == true)
{
var userCulture = user.FindFirst("culture")?.Value;
if (userCulture != null && IsValidCulture(userCulture))
{
return Task.FromResult<ProviderCultureResult?>(
new ProviderCultureResult(userCulture, userCulture));
}
}
return Task.FromResult<ProviderCultureResult?>(null);
}
private static bool IsValidCulture(string culture)
{
var supported = new[] { "zh-CN", "en-US", "ja-JP" };
return supported.Contains(culture);
}
}
// 注册自定义提供者
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.RequestCultureProviders.Insert(0, new CustomCultureProvider());
});本地化测试策略
单元测试本地化字符串
/// <summary>
/// 本地化功能的单元测试
/// </summary>
public class LocalizationTests
{
[Fact]
public void Chinese_Welcome_ReturnsCorrectTranslation()
{
// Arrange
var culture = new CultureInfo("zh-CN");
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
var localizer = CreateLocalizer("zh-CN");
// Act
var result = localizer["Welcome"];
// Assert
Assert.Equal("欢迎访问用户管理系统", result.Value);
}
[Fact]
public void English_Welcome_ReturnsCorrectTranslation()
{
var culture = new CultureInfo("en-US");
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
var localizer = CreateLocalizer("en-US");
var result = localizer["Welcome"];
Assert.Equal("Welcome to User Management System", result.Value);
}
[Fact]
public void MissingTranslation_FallsBackToKey()
{
var culture = new CultureInfo("fr-FR"); // 不支持的文化
CultureInfo.CurrentUICulture = culture;
var localizer = CreateLocalizer("fr-FR");
var result = localizer["NonExistentKey"];
// 回退到键本身
Assert.Equal("NonExistentKey", result.Value);
Assert.True(result.ResourceNotFound);
}
[Fact]
public void FormattedString_ReplacesParameters()
{
CultureInfo.CurrentUICulture = new CultureInfo("zh-CN");
var localizer = CreateLocalizer("zh-CN");
var result = localizer["UserCreated", "张三"];
Assert.Contains("张三", result.Value);
}
private IStringLocalizer CreateLocalizer(string culture)
{
// 创建测试用的本地化器
var strings = new Dictionary<string, string>
{
["Welcome"] = culture == "zh-CN" ? "欢迎访问用户管理系统" : "Welcome to User Management System",
["UserCreated"] = culture == "zh-CN" ? "用户 {0} 创建成功" : "User {0} created successfully"
};
return new TestLocalizer(strings);
}
}
// 测试用本地化器
public class TestLocalizer : IStringLocalizer
{
private readonly Dictionary<string, string> _strings;
public TestLocalizer(Dictionary<string, string> strings) => _strings = strings;
public LocalizedString this[string name] =>
new(name, _strings.GetValueOrDefault(name, name), !_strings.ContainsKey(name));
public LocalizedString this[string name, params object[] arguments] =>
new(name, string.Format(_strings.GetValueOrDefault(name, name), arguments),
!_strings.ContainsKey(name));
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>
_strings.Select(s => new LocalizedString(s.Key, s.Value, false));
}请求文化提供者
| 提供者 | 优先级 | 示例 |
|---|---|---|
| QueryString | 最高 | ?culture=en-US&ui-culture=en-US |
| Cookie | 中 | `.AspNetCore.Culture=c=en-US |
| Accept-Language | 最低 | Accept-Language: en-US,en;q=0.9 |
| 自定义路由 | 自定义 | /en-US/api/users |
优点
缺点
总结
本地化是全球化产品的基础能力。ASP.NET Core 的本地化方案覆盖了字符串、视图、验证消息等场景。资源文件推荐用 JSON 替代 resx,翻译管理建议接入专业平台。核心原则:默认语言要完整,其他语言按需翻译,缺失时优雅降级。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《本地化与国际化》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《本地化与国际化》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《本地化与国际化》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《本地化与国际化》最大的收益和代价分别是什么?
