Skip to content
bitzorcas
中EN

Concept

I18n 国际化与运行时本地化

源码校验的 I18n 总览,讲清请求语言解析、平台/租户/办公室翻译覆盖、数据库与 JSON 回退、本地缓存同步、前端资源投影以及当前商业 GA 缺口。

Last updated

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. 真实运行结构

HTTP 请求

LanguageResolutionMiddleware
Header · Cookie · Query · Setting

CultureInfo + ILanguageContext + L()

3 个查询端点 + 1 个保存端点

LocalizerService

SysTranslation
MasterData owner

Resources/i18n/*.lang.json

ICacheStore
30 分钟 · i18n Tag

LocalResourceSync
Translation

I18n Infrastructure 直接引用 MasterData Infrastructure 来复用 SysLanguageCatalogRecord 与 SysTranslationCatalogRecord。这与 I18nModule 只声明 [DependsOn("Authorization")] 的治理意图不一致,属于需要消除的物理依赖与治理事实偏差。

3. 四条 HTTP 路由

方法与路由Resource / Action当前行为
GET /api/i18n/languagesi18n/languages / View读取当前租户的启用或全部语言
GET /api/i18n/translationsi18n/translations / View按语言、当前租户/办公室合并数据库翻译,可下推 KeyPrefix
GET /api/i18n/resourcesi18n/translations / View计划供前端取资源;当前空键批量路径导致空结果
POST /api/i18n/translationsi18n/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. 翻译覆盖链

是否

Key + Language + Tenant + Office

只读 Active 行

Platform: TenantId = 0

Tenant: 当前 TenantId 覆盖

TenantOffice 且 OfficeId 匹配?

办公室值覆盖

保留租户或平台值

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. 单键回退与批量差异

是否是否是否是否

GetString(key, requestedLanguage)

请求语言 DB 命中?

返回值

请求语言 JSON 命中?

读取 fallbackLanguage Setting
默认 en-US

回退语言 DB 命中?

回退语言 JSON 命中?

返回 Key 最后一段

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. 章节路线

12. 商业 GA 红线

  1. 修复前端资源空结果,并统一单键/批量/资源 API 回退合同;
  2. 语言协商尊重 q 权重、支持语言 allowlist 与明确的 region fallback;
  3. 语言目录具备租户初始化和受控管理生命周期;
  4. Tenant/Office/Language/Scope 组合在写入前严格校验;
  5. 平台翻译管理采用专用权限、可信平台身份、审批与审计;
  6. 翻译具有 Draft/Review/Published/Deprecated 或明确简化状态机、历史与回滚;
  7. 保存使用事务 + Outbox,缓存与前端通知可重放、可观测;
  8. JSON 资源有打包、校验、冲突、热重载与故障策略;
  9. 参数、复数、性别、时区、货币和 HTML 安全合同完整;
  10. 双 ORM、真实 HTTP、多实例、并发、故障、容量和恢复证据进入 GA 门禁。

13. 源码导航

Terminal window
# 四条生成路由、请求语言解析和单键/批量回退实现。
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'

返回模块目录 · MasterData 模块 · Authorization 模块

100%

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