Skip to content
bitzorcas
中EN

Reference

I18n Localizer、缓存与前端资源

深入解释 LocalizerService 的单键/批量算法、数据库与 JSON 回退、Setting、同步阻塞、缓存键/Tag/TTL、预热贡献者、跨实例失效和 GetI18nResources 空结果缺陷。

Last updated

运行时 Localizer 是 I18n 最容易被调用、也最容易被误解的部分。单键、批量和前端资源三个入口目前不是同一个语义;选择 API 前必须先看清回退、错误和缓存合同。

LocalizerService 标注 [BitzCacheArea(translations)]。可选启动 / Host 重建由 I18nCacheWarmupContributor 预热启用语言 × office=0 的有限键空间,见缓存区域目录。

1. 三个入口

入口输入当前输出语义
GetString(key)当前 Language/Tenant/Office单键完整回退链
GetString(key, language, ...)显式上下文单键完整回退链
GetStringsAsync(keys, ...)Key 集合只查请求语言 DB;缺失取 Key 最后一段
GET /api/i18n/resourceslanguage + prefix以空 Keys 调批量;当前结果为空

GetTranslations 不经过 Localizer,而是直接返回数据库合并字典。它不会合并 JSON 或 fallback language,但空 Keys 能正确表示“全部”。

2. 单键算法

读取一个终端展示文案
// 使用稳定、分层的 Key;不要把中文原文当 Key。
var title = localizer.GetString(
key: "Identity.Login.Title",
language: resolvedLanguage,
tenantId: currentTenantId,
officeId: currentOfficeId);
// 若所有层都未命中,当前返回 "Title",不是 null 或错误。
// 因此调用者无法仅靠返回值区分真实翻译与最终兜底。
response.Title = title;

数据库读取失败会被 LoadDictionaryAsync 转为空字典,然后继续 JSON/fallback/key;这是可用性优先的降级。系统没有同时返回 hit source 或 degraded flag,生产诊断只能靠另加指标。

3. 回退语言

platform.i18n.fallbackLanguage 通过 SettingsManager 读取,传入 TenantId,默认 en-US。若它与请求语言忽略大小写相同,则跳过第二轮。

回退只支持一个精确语言,没有链式 parent fallback。例如 pt-BR 不会先试 pt;zh-HK 不会按配置尝试 zh-Hant。Setting 值也没有先验证 Culture 或启用目录。

4. 最终 Key 文本

ToFallbackText 取最后一个点后的片段:Identity.Login.Title → Title;Key 无点则原样返回,末尾为点也原样返回。

这个策略保证 UI 不出现空字符串,却可能把内部错误码或技术字段直接展示。商业产品应决定:开发环境显示 Key、生产显示安全通用文案、可观测性记录 missing key;不能把“最后一段”当翻译质量保障。

5. 批量算法缺失的两层

GetStringsAsync 先加载请求语言数据库字典,再只遍历传入 Keys。它计算了 fallback 变量却没有使用;也没有访问 JsonResourceLoader。

当前批量行为的可执行思维模型
// Store 的空 Keys 约定是加载整包;这里先得到请求语言的完整 DB 字典。
var databaseValues = await LoadDictionaryAsync(
requestedLanguage, tenantId, officeId, cancellationToken);
var result = new Dictionary<string, string>();
foreach (var key in keys)
{
// 没有 JSON,也没有 fallback-language 查询。
result[key] = databaseValues.TryGetValue(key, out var value)
? value
: ToFallbackText(key);
}
return result;

单键和批量对同一 Key 可返回不同值,这是 API 一致性缺陷。调用方不应通过换 API 获得不同本地化结果。

6. GetI18nResources 空结果

Handler 调用 localizer.GetStringsAsync(Array.Empty<string>(), ...),期望空集合代表“加载全部”;但批量方法的结果是按输入 Keys 循环构造,空集合自然返回空字典。随后 KeyPrefix 只会过滤这个空结果。

GET /api/i18n/resources

Keys = []

Store 加载整包 DB

foreach(Keys): 0 次

{}

Prefix 过滤仍为 {}

修复不能只把 Store 字典直接返回,否则仍会遗漏 JSON/fallback 合并。应定义“完整资源包”如何处理缺失、冲突、来源与 prefix,并用合同测试固定。

7. 数据库字典缓存

同步单键 API 经 ICacheStore.GetOrCreateAsync 缓存完整语言字典。Key 由:

area=i18n / scope=Global / lang / language / tenant / tenant-or-0 / office / office-or-0

Policy TTL 为 30 分钟,AreaTag 为 i18n。虽然 CacheScope 是 Global,tenant/office 已进入 Key;审查工具不要看到 Global 就误判跨租户共享,也不要忽略 Key builder 是否对分隔符、长度和哈希做稳定处理。

8. 同步阻塞异步

GetString 为保持同步签名,内部多次 .GetAwaiter().GetResult() 调 Settings、Cache 和 Store。注释说明 ASP.NET Core 没有经典 SynchronizationContext 死锁,但这不消除线程池阻塞、尾延迟放大或在非 ASP.NET Host 中的风险。

高吞吐路径应优先异步 API;若保留同步 L(),可在请求前异步预热不可变快照,或使用进程内只读快照避免同步等待 I/O。必须以并发基准而非注释证明容量。

9. JSON 资源加载

Loader 扫描 AppDomain.BaseDirectory/Resources/i18n 顶层,文件名匹配 *.{lang}.json,每个文件是扁平字符串字典。多个文件同 Key 时,目录枚举后写覆盖前写,但文件枚举顺序未排序,冲突结果不应视为确定。

模块 JSON 资源格式
{
"Identity.Login.Title": "登录",
"Identity.Login.Submit": "继续"
}

当前源码树没有实际 Resources/i18n 文件。Loader 注释提到嵌入资源,代码却只读取文件系统;没有 Assembly resource 读取。解析异常全部吞掉,无法区分目录不存在、JSON 损坏和空语言包。

10. JSON 缓存与刷新

每种 language 的合并字典保存在 Singleton ConcurrentDictionary。Refresh() 清空全部语言,但没有 FileSystemWatcher、管理 API、LocalResourceSync consumer 或部署钩子调用它。

因此部署后替换 JSON 文件不会自动生效。蓝绿/滚动发布还可能让不同实例运行不同资源版本;资源应随应用构建不可变发布,或具备带版本、校验和与广播的显式热更新协议。

11. 保存后的三种通知

  1. RemoveByTagAsync("i18n"):当前实例本地缓存;
  2. LocalResourceSync Translation:其他实例收到后清同一 Tag;
  3. Notification i18n.translations.changed:计划由前端重新拉资源。

消费者捕获非取消异常、记录后吞掉,因此广播处理不会反复毒化队列,但会允许陈旧缓存存活到 TTL。通知没有资源版本/ETag,乱序事件也无法判断新旧;sourceVersion 只是发送时 Unix 毫秒。

12. 精确失效与惊群

保存任意租户、语言、Key 都清除整个 i18n Tag,所有租户/办公室字典在下一次访问重建。高频翻译编辑可制造缓存惊群和数据库尖峰。

目标可按 tenant/language/office 分层 tag,使用版本化 snapshot、single-flight 和抖动 TTL。精确化时要考虑 Platform 变更影响所有租户,而 Tenant 变更只影响一个租户,Office 变更只影响一个办公室。

13. 参数与 HTML 安全

当前 Localizer 返回原始 string,没有格式参数类型、占位符校验、HTML-safe 类型、ICU plural/select 或富文本 sanitizer。调用方自行 string.Format 可能因翻译占位符错误抛异常,也可能把不可信值拼进 HTML。

翻译值默认应作为纯文本并由 UI 编码。富文本必须使用独立资源类型和 allowlist sanitizer;参数应按名称/类型验证,禁止翻译改变模板执行语义。

14. 目标统一解析接口

目标:返回可诊断的解析结果
// 目标接口把完整上下文一次传入,避免三个公开入口分别实现回退链。
var resolved = await localizer.ResolveAsync(new LocalizationRequest(
Keys: ["Identity.Login.Title", "Identity.Login.Submit"],
Language: language,
TenantId: tenantId,
OfficeId: officeId,
IncludeFallback: true), cancellationToken);
// 值与诊断分离,业务日志只记录 Key/来源,不记录可能敏感的翻译正文。
foreach (var item in resolved.Items)
metrics.RecordHit(item.Key, item.ResolvedLanguage, item.Source);

单键、批量和资源包都应委托同一 Resolver;差别只在输入集合和输出投影,不能复制回退算法。

15. 测试矩阵

场景必须证明
DB/JSON/fallback/key 四层单键与批量结果相同
空 Keys 资源包返回完整合并集,不是空
PrefixDB/JSON/fallback 一致裁剪
DB 失败降级来源可观测且无错误缓存污染
JSON 损坏/冲突确定性策略与告警
Platform/Tenant/Office 变更只失效正确受众
广播丢失/乱序/重复TTL 或版本最终收敛
高频编辑无惊群,P99 受控
同步 L() 并发线程池与尾延迟达标
格式参数/HTML占位符安全、输出编码

16. 检查命令

Terminal window
# 单键、批量、JSON 与缓存实现。
rg -n "GetString\(|GetStringsAsync|GetOrLoadDictionary|JsonResourceLoader|CacheTtl" \
src/Platform/I18n src/Framework/BitzOrcas.Application/Localization -g '*.cs'
# 空 Keys 资源路径与三段失效。
rg -n "Array.Empty<string>|RemoveByTagAsync|NotifyAsync|i18n.translations.changed" \
src/Platform/I18n -g '*.cs'

I18n 总览 · 请求语言与 Culture · 测试与 GA

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%