I18n 的 GA 不是“能返回中文”就完成。它必须证明同一请求在不同租户、办公室、实例、数据库、部署版本与故障阶段都得到可解释结果,而且翻译运营不会破坏全站可用性。
1. 当前自动化证据
| 测试面 | 已有证据 |
|---|---|
| Query Handler | 语言启用列表、Store Failure 传播、翻译 Prefix 调用 |
| Localizer | Store Failure 后回退 Key 最后一段 |
| Architecture | Query 依赖 ReadModelStore、生产默认端口 fail-closed、生成适配器存在 |
| 双 ORM parity | Platform/Tenant/Office 覆盖、删除后平台 fallback、语言启用/全部/默认 |
| API Shell smoke | ReadModelStore 在组合根可解析/调用 |
没有 LanguageResolutionMiddleware 测试、SaveTranslation 测试、GetI18nResources 成功测试、JSON 测试、授权 HTTP 测试、缓存同步消费者测试、多实例一致性、并发、故障注入、容量与恢复证据。
2. 最小源码回归组合
# I18n 的三个证据面使用不同过滤式,避免把宽泛 parity 当成模块单测。I18N_APP_FILTER="FullyQualifiedName~I18n"I18N_ARCH_FILTER="FullyQualifiedName~I18n|FullyQualifiedName~MasterDataInfrastructure"I18N_PARITY_FILTER="FullyQualifiedName~PortRepositoryParity"
# 应用层先证明 Handler 与 Localizer 的确定性行为。dotnet test tests/BitzOrcas.Application.Tests/BitzOrcas.Application.Tests.csproj --filter "$I18N_APP_FILTER"
# 架构与集成层分别证明端口边界和双 ORM 适配器。dotnet test tests/BitzOrcas.Architecture.Tests/BitzOrcas.Architecture.Tests.csproj --filter "$I18N_ARCH_FILTER"dotnet test tests/BitzOrcas.Integration.Tests/BitzOrcas.Integration.Tests.csproj --filter "$I18N_PARITY_FILTER"过滤器能快速定位,但 GA 流水线还应执行完整测试集,避免 I18n 对 Settings、MasterData、Authorization、缓存和 Host 的回归被漏掉。
3. HTTP 协商矩阵
用真实 WebApplicationFactory 覆盖 Header/Cookie/Query/Setting 的每种组合、q 权重、大小写、无效 Culture、禁用语言和无租户调用。对每个结果断言:响应值、Content-Language、Culture、缓存键和降级原因一致。
// 故意让 Header 第一项质量值更低,用来证明解析器真正比较 q 值。using var request = new HttpRequestMessage(HttpMethod.Get, "/api/i18n/translations?LanguageCode=en-US");request.Headers.TryAddWithoutValidation("Accept-Language", "fr-FR;q=0.1,en-US;q=1.0");request.Headers.Add("Cookie", "bitzorcas.lang=en-US");
var response = await client.SendAsync(request, cancellationToken);
// 目标实现的响应头和可观测解析原因必须报告同一个规范语言。response.EnsureSuccessStatusCode();response.Content.Headers.ContentLanguage.ShouldContain("en-US");await evidence.AssertResolvedLanguageAsync("en-US", reason: "explicit-user-preference");这是目标行为示例,不是当前优先级断言。最终优先级必须由产品合同固定。
4. 授权与租户矩阵
分别测试匿名、普通 User、平台管理员、System、受信 Application、租户 A/B、Office 1/2。读取与保存都要验证 Resource/Action 到稳定 permission code 的真实映射。
关键攻击包括:无 Tenant 调用读取所有租户候选、TenantOffice 无 Office 退化为 0、Application caller 任意改平台文案、Tenant A 构造 Tenant B 标识、禁用语言仍可读写、未知 Scope int 绕过规则。
5. 资源 API 回归
先用数据库、JSON 与 fallback language 构造互补 Key,再分别调用 GetString、GetStringsAsync、GetTranslations 和 GetI18nResources。期望同一公开合同下值一致,并验证 Prefix 不泄漏其他模块资源。
// 三个来源故意各提供不同 Key,避免同值掩盖错误的优先级。await database.SaveAsync("Identity.Login.Title", "zh-CN", "登录", cancellationToken);files.WriteJson("Identity.zh-CN.json", new { Identity_Login_Submit = "继续" });await database.SaveAsync("Identity.Login.Help", "en-US", "Need help?", cancellationToken);
// Prefix 只能裁剪最终合并结果,不能让某个来源绕过模块边界。var resources = await api.GetResourcesAsync("zh-CN", "Identity.", cancellationToken);
resources["Identity.Login.Title"].ShouldBe("登录"); // 请求语言 DBresources["Identity.Login.Submit"].ShouldBe("继续"); // 请求语言 JSONresources["Identity.Login.Help"].ShouldBe("Need help?"); // fallback language测试辅助器应保留真实点分 Key;示例中的匿名对象字段只是表达来源,不是建议的 JSON 写法。
6. 保存故障注入
在 Repository Upsert、当前缓存清除、Sync Notify、前端 Notification 四个位置逐点失败。每次记录数据库、各实例缓存、同步事件和客户端响应,证明重试不会重复污染也不会永久失联。
当前顺序下 Upsert 后失败会部分成功。GA 目标应在事务内写 Translation + Outbox,提交后由独立消费者重试缓存/前端事件;Command 返回包含稳定 resource version。
7. 多实例一致性
至少启动两个真实 Host 和共享消息/数据库:实例 A 预热翻译,实例 B 保存,随后 A 必须在 SLO 内读到新值。再丢弃、延迟、乱序和重复同步事件,验证版本比较与 TTL 最终收敛。
不能只直接调用 Consumer;要覆盖消息路由、序列化、ResourceType、作用域、重试和 poison 策略。Platform 变更应影响所有租户缓存,Tenant/Office 变更不应全局惊群。
8. 双 ORM 与数据库差异
在每个支持数据库验证唯一索引、大小写 Collation、StartsWith SQL、软删除过滤、并发 upsert、长值/Unicode、OfficeId 字符串比较和事务隔离。SqlSugar 与 EF Core 最终外部 Result/Problem Details 必须一致。
Prefix 查询要保存实际执行计划与 P95;Contains(keys) 在大集合下可能生成超长 IN,应限制 Key 数量或使用临时表/批次策略。
9. 容量基线
建议维度:100/1万/100万 Key、2/10/50 语言、1/1000 租户、1/100 办公室、1/1000 并发、1/100 Key 批量、10/1000 次每分钟编辑。
记录 P50/P95/P99、Store 行数/扫描、缓存 hit/miss、字典对象大小、GC、线程池排队、同步 L() 阻塞、广播延迟、资源包 bytes、压缩率和 CDN hit。全语言字典按每个 Office 缓存可能形成显著内存乘数。
10. 指标与日志
i18n_language_resolution_total{source,result,fallback_reason};i18n_translation_hit_total{source,scope,language},不带 tenant 原值;i18n_missing_key_total{module,language};i18n_cache_hit/miss/evict与 key cardinality;i18n_sync_lag_seconds、consumer failure、stale snapshot age;- save/review/publish/conflict/rollback;
- resource package keys/bytes/generation duration;
- invalid tag、disabled language、format placeholder error;
- JSON load/parse/conflict/version。
日志记录 Key、规范化语言、scope、correlation 与 version,不默认记录 Value;翻译可能包含客户品牌、姓名模板或敏感运营内容。
11. 告警
重点告警:资源 API 连续返回空包、missing-key 比例激增、平台翻译批量改变、同步延迟超过 SLO、实例 snapshot version 分叉、缓存 cardinality/内存突增、JSON 解析失败、无租户读取、平台写身份异常和占位符格式错误。
告警应区分“翻译缺失但安全回退”和“持久化不可用导致降级”,否则表面 200 会掩盖数据库故障。
12. 资源发布流程
JSON 资源应在构建期校验 schema、Key 命名、重复、语言覆盖率、占位符一致性、HTML 策略和 UTF-8。生成 manifest:resource version、commit、语言、文件 hash、Key count。
滚动发布期间资源版本可能不同。API 可返回 ETag/manifest version,前端按版本缓存;数据库覆盖也应记录基于哪个基线资源版本制作,防止升级后 override 语义漂移。
13. 备份与恢复
备份 SysTranslation、SysLanguage、翻译 Revision/Published pointer、审计与 Outbox(完成 GA 后)。恢复后逐租户验证默认语言唯一、启用语言、自然键、Scope 组合、Active/Published 投影、软删除和 resource version。
数据库恢复与应用 JSON 版本必须兼容。若恢复到旧数据库但部署了新基线资源,需要 reconciliation 报告:新增/删除 Key、过期 override、占位符变化和冲突。
14. 迁移与回滚
规范化 LanguageCode 前先生成 dry-run 清单,检测大小写/别名合并冲突;迁移自然键时先修复非法 Tenant/Office/Scope 行。不要直接加唯一约束让生产部署在脏数据上失败。
发布状态机迁移应把现有 Active 行创建为初始 Published Revision,并保留旧 ID 映射。回滚应用版本时,数据库 schema、资源 manifest 与旧客户端缓存协议都要保持兼容。
15. 商业 GA 阻断项
GetI18nResources当前空结果;- 单键与批量 JSON/fallback 语义不一致;
- 语言协商不处理 q 值、支持目录与 canonical tag;
- 默认语言种子与多租户语言目录没有闭环;
- Tenant/Office 缺失与非法 Scope 未失败关闭;
- Platform 写仅依赖 caller type,缺专权、审批与审计;
- TranslationStatus 没有真实审核/发布生命周期;
- 保存与通知无事务 Outbox、版本与补偿;
- 全 Tag 失效带来跨租户惊群,广播失败可陈旧 30 分钟;
- JSON 无资源文件、嵌入读取、确定冲突、热更新和错误可观测性;
- 参数、复数、富文本、时区/货币合同缺失;
- HTTP、多实例、并发、故障、容量、迁移和恢复证据不足。
16. 发布门禁清单
# 不允许资源端点继续以空 Keys 调用会丢弃结果的批量方法。rg -n "GetStringsAsync\(\s*Array.Empty<string>" src/Platform/I18n -g '*.cs'
# 目标生命周期、版本、Outbox 与协商能力。rg -n "PublishedRevision|ExpectedVersion|Outbox|ResolvedLanguage|Quality" \ src/Platform/I18n src/Hosts/BitzOrcas.Api -g '*.cs'
# I18n 测试面应从当前两类文件扩展到 HTTP、缓存、JSON 与故障套件。rg -n "I18n|LanguageResolution|JsonResourceLoader|TranslationCacheSync" \ tests -g '*.cs'