I18n 把“稳定机器语义”转换为“当前用户可读文案”。它拥有翻译读写、语言目录读取、运行时 Localizer 与缓存失效;请求语言解析位于 API Host,语言和翻译持久化记录实际由 MasterData Infrastructure 定义。
1. 已实现能力
- 请求语言按
Accept-Language → bitzorcas.lang Cookie → ?lang= → Setting → zh-CN解析; - 同步
ILanguageContext、CurrentCulture、CurrentUICulture、Content-Language与全局L()AsyncLocal; Platform、Tenant、TenantOffice三种翻译作用域;- 数据库读取按 Platform → Tenant → TenantOffice 后写覆盖前写;
- 翻译自然键由 Key、LanguageCode、ScopeType、TenantId、OfficeId 构成唯一索引;
SaveTranslation执行 upsert、当前实例 Tag 清理、跨实例资源同步通知和前端变更通知;- 单键
GetString支持数据库、模块 JSON、回退语言、Key 最后一段; - 翻译缓存按 language/tenant/office 分区,TTL 30 分钟;
- 语言与翻译 ReadModelStore 使用 ORM 中立
IEntitySet<T>,已有 SqlSugar/EF Core parity 场景; - 生产组合根对四个持久化端口采用 fail-closed 默认注册。
当前没有语言增删改/设默认 API、翻译删除/批量导入/审核/发布 API、参数格式化合同、ICU MessageFormat、复数/性别规则、版本历史、乐观并发、审计/outbox、资源 ETag、JSON 热重载、可用语言校验和完整 HTTP/多实例测试。
2. 真实运行结构
I18n Infrastructure 直接引用 MasterData Infrastructure 来复用 SysLanguageCatalogRecord 与 SysTranslationCatalogRecord。这与 I18nModule 只声明 [DependsOn("Authorization")] 的治理意图不一致,属于需要消除的物理依赖与治理事实偏差。
3. 四条 HTTP 路由
| 方法与路由 | Resource / Action | 当前行为 |
|---|---|---|
GET /api/i18n/languages | i18n/languages / View | 读取当前租户的启用或全部语言 |
GET /api/i18n/translations | i18n/translations / View | 按语言、当前租户/办公室合并数据库翻译,可下推 KeyPrefix |
GET /api/i18n/resources | i18n/translations / View | 计划供前端取资源;当前空键批量路径导致空结果 |
POST /api/i18n/translations | i18n/translations / Update | 保存 Active 翻译并执行三段通知 |
这些端点由 GenerateEndpoint 生成。权限目录登记三个稳定 code(i18n.languages.read、i18n.translations.read、i18n.translations.manage),请求使用通用 Resource/Action;必须用真实 HTTP 授权测试证明 Action 到 i18n.* code 的最终映射,不能仅凭常量存在推断。
4. 请求语言不是语言目录授权
中间件只用正则校验语言代码形状,例如 en-US 或 pt-BR,不会查询当前租户的 SysLanguage、禁用状态或默认语言。zz-ZZ 可能通过正则;若 .NET 不认识,Culture 设置被跳过,但 ILanguageContext 和响应头仍保留该值。
Accept-Language 只取列表第一项并去掉 q 参数,不会真正比较权重,也不进行 region fallback。Header 优先于 Cookie 和 Query,因此 ?lang= 不能覆盖浏览器自动 Header;这是当前合同,不是通常 UI 语言选择器最理想的策略。
5. 翻译覆盖链
Store 按 ScopeType 升序遍历并向同一字典赋值,因此数字更大的作用域覆盖更小作用域。它不验证行的 Scope、TenantId、OfficeId 组合是否合法;例如 Tenant 行若错误写了 OfficeId,读取仍按 Tenant 覆盖。数据库约束与写入校验需要共同守住不变量。
6. 保存一条办公室翻译
// 当前用户必须同时有租户和 Office 上下文;Handler 现在对缺失上下文 fail-closed。var command = new SaveTranslation.Command( Key: "Billing.Invoice.Actions.Submit", LanguageCode: "zh-CN", Scope: TranslationScope.TenantOffice, Value: "提交开票", Context: "发票列表批量操作按钮");
var result = await sender.Send(command, cancellationToken);
// 成功表示 upsert、Tag 清理、同步通知和前端通知都已返回成功;// 它不表示这些动作处于同一数据库事务或已经持久化到 Outbox。if (result.IsFailure) return result.Error;普通 User 不能写 Platform。Platform 作用域写入现在要求调用者同时持有 RootCrossTenant 权限并通过 ActorKey.TryCreatePlatformOperator 身份核验,两者满足后构造 RootCrossTenantOperation 能力票据(操作码 i18n.platform-translation.manage)下传到 Repository 复核。Tenant/Office 作用域缺失可信上下文时 Handler 直接失败,不再静默回退到 "0"。详细机制见翻译作用域与持久化。
7. 单键回退与批量差异
GetStringsAsync 没有复用这条链:它只加载请求语言数据库字典,再逐个输入 Key 返回命中值或 Key 最后一段;没有 JSON 或 fallback language。输入 Keys 为空时循环零次,即使 Store 已返回整包也会丢弃,所以前端资源 API 当前为空。
8. 语言目录边界
GetLanguages 只查询 TenantId == 当前租户 的 SysLanguage。平台 TenantId=0 或空租户的种子不会自动作为所有租户的 fallback。MasterData 的 110-sys_language.csv 含 zh-CN/en-US,但没有为每个新租户复制目录的证据。
语言目录只有读取用例(GetLanguages),模块没有 Create/Update/Disable/SetDefault Handler。ILanguageRepository 是空 marker;默认实现中存在的三段读取方法并不属于接口合同,不能当作可调用能力。
9. 缓存与一致性
Localizer 字典缓存键包含 language、tenant 和 office,采用 Global CacheScope 但通过 Key 分区;AreaTag 为 i18n,TTL 30 分钟。保存任意一个 Key 会清掉整个翻译 Tag,不是精确失效。
保存顺序是:数据库 upsert → 当前实例 RemoveByTag → ILocalResourceSyncNotifier → INotificationPublisher。后两个步骤失败时数据库已经改变,Command 返回异常/失败的具体表现取决于外围映射;没有 Outbox、补偿或重放状态。消费者失效失败会记录错误并吞掉异常,因此某个实例可继续读旧缓存直到 TTL。
JSON Loader 使用进程内 ConcurrentDictionary,只扫描运行目录顶层 Resources/i18n。当前源码树没有实际资源文件;单文件解析异常被静默跳过,Refresh() 也没有文件监视或管理端接线。
10. 状态与生命周期真相
TranslationStatus 定义 Active、NeedsReview、Deprecated,但 Save 永远写 Active,读取也只返回 Active。没有把现有 Active 转 NeedsReview、审核后发布、废弃、恢复或历史查询的用例。
TranslationEntry 只是公开 record,不是持久化聚合,也不是 Save 的返回值;Save 返回非泛型 Result。旧页把它描述为带乐观并发的聚合或 Draft/Published 生命周期,均不是源码事实。
11. 章节路线
- 请求语言解析与 Culture:优先级、BCP-47 校验、Setting、AsyncLocal、框架本地化与攻击面;
- 翻译作用域与持久化:表、唯一键、覆盖算法、写入权限、状态和跨模块 owner;
- Localizer、缓存与前端资源:单键/批量差异、JSON、fallback、缓存 fan-out 与资源 API 缺陷;
- 测试、运维与商业 GA:验证矩阵、容量、指标、恢复、迁移和发布阻断项。
12. 商业 GA 红线
- 修复前端资源空结果,并统一单键/批量/资源 API 回退合同;
- 语言协商尊重 q 权重、支持语言 allowlist 与明确的 region fallback;
- 语言目录具备租户初始化和受控管理生命周期;
- Tenant/Office/Language/Scope 组合在写入前严格校验;
- 平台翻译管理采用专用权限、可信平台身份、审批与审计;
- 翻译具有 Draft/Review/Published/Deprecated 或明确简化状态机、历史与回滚;
- 保存使用事务 + Outbox,缓存与前端通知可重放、可观测;
- JSON 资源有打包、校验、冲突、热重载与故障策略;
- 参数、复数、性别、时区、货币和 HTML 安全合同完整;
- 双 ORM、真实 HTTP、多实例、并发、故障、容量和恢复证据进入 GA 门禁。
13. 源码导航
# 四条生成路由、请求语言解析和单键/批量回退实现。rg -n "GenerateEndpoint\(|ResolveFromHeader|GetString\(|GetStringsAsync" \ src/Platform/I18n src/Hosts/BitzOrcas.Api/Middleware/LanguageResolutionMiddleware.cs -g '*.cs'
# 表、唯一索引、MasterData 物理 owner 和三作用域覆盖。rg -n "SysTranslation|SysLanguage|BitzIndex|OrderBy\(t => t.ScopeType\)" \ src/Platform/I18n src/Platform/MasterData -g '*.cs'
# 目标能力当前预期无命中。rg -n "SetDefaultLanguage|PublishTranslation|ExpectedVersion|MessageFormat|Outbox" \ src/Platform/I18n -g '*.cs'