写回防护与修复
让模型生成文本很容易,难的是不让它破坏已有成果。插件在写回路径上串了一整条防护链:候选筛选 → 响应校验 → 内容校验 → 语义判定 → 结构修复 → 失败隔离。
防护一:只补缺失,绝不覆盖
| 条目状态 | 是否进入翻译候选 |
|---|---|
status: initial 或无 target | ✅ |
status: translated,且判定为自翻译(见下) | ✅(Retranslate Self Translations 开启时) |
status: translated,已有正常译文 | ⛔ 跳过 |
status: reviewed / final | ⛔ 永不触碰 |
插件不提供「覆盖已有译文」的开关。要重译某条,请在 .cliff 里把它改回 status: initial 或删除 target——这个动作会留痕、可评审。
NOTE
非原生 Culture 才参与翻译;原生语言由人工维护。
防护二:自翻译判定
问题:项目源码常常是混合语种撰写的(英文错误消息 + 中文 UI 标签),而 CLIFF header 的 source-language 取自 NativeCulture。此时「源语言 = 目标语言」,指令自相矛盾,模型可能把英文原样返回——这就是「自翻译」:target == source,且文本其实并不属于目标语言。
判定(FCliffAITranslator::IsSelfTranslation):target == source 且源文本的 Unicode 脚本族(拉丁 / CJK / 阿拉伯 / 西里尔)与目标语言不符 → 视为自翻译,拒绝写回并计入失败。
重译候选(IsRetranslationCandidate):status 恰为 translated、判定为自翻译、且 Retranslate Self Translations 开启 → 下次 AI 翻译时重新翻译。reviewed / final 即使满足条件也不重译。
提示词加固:system prompt 与 user content 都明确「条目可能与声明的源语言不同;凡不属于目标语言的条目必须翻译;已属于目标语言的原样返回;除该情形外禁止原样返回源文本」,并在 source == target 时追加说明。
防护三:空译文
模型返回 "" 时:
- 源文本已属于目标语言 → 本地回填源文本(模型本应原样返回)
- 否则 → 拒绝写回并走重试
导入器同样拒绝写入空译文,并把历史遗留的空译文 archive 条目就地恢复为源文本。原因是空译文会被编译成空字符串,Slate UI 直接显示空白,看起来像「漏翻」。
防护四:响应与内容校验
| 检查 | 失败处理 |
|---|---|
响应是合法 JSON,且能从「JSON 数组」或对象中的 translations / results / data / items 取到条目 | 该批请求重试(最多 Max Retries 次);仍失败则记 warning 并跳过该批 |
{"translations": []} 空数组 | 视为合法(该批无需翻译),不再误报「响应无法解析」 |
每条含 id 且能在本批候选中匹配 | 该条记为失败并跳过 |
translation 非空 | 见「防护三」 |
占位符一致:译文占位符数量相等且源文中的每个 {…} 都出现在译文中(支持嵌套花括号) | 拒绝写回该条:Placeholder mismatch for '…'; skipped. |
| 译文与源文完全相同且源文不属于目标语言 | 拒绝写回该条:AI returned the source unchanged for '…'; self-translation refused. |
单条失败不会中断整次任务:只记 warning,其余条目照常写回。一轮结束后若仍有失败条目,插件会自动再跑一轮(最多 Max Retries 轮),只处理仍然缺失译文的条目。
防护五:CLIFF 自动修复闭环
AI 产出的 .cliff 不假设第一次就合法:
① FCliffDocument::ParseAndValidate 校验
② 通过 → 进入导入
③ 失败且 Auto Repair CLIFF 开启:
把「原始 CLIFF 文本 + issue 列表 + 修复要求」发回 DeepSeek
(专用 system prompt:只修报告的问题、保持已正确内容不变、只返回 CLIFF 纯文本)
要求:只修语法/结构/占位符/必填字段;
不得改动已正确条目的 id / namespace / clan / source / 已有 target / status 语义
④ 收到修复结果后剥掉可能的 ``` 代码围栏,再次 ParseAndValidate
⑤ 最多 Max Repair Attempts 轮(默认 2)
⑥ 仍失败 → 该文件标记失败(记入 FailedFiles),**不写入、不导入**,避免污染官方数据
⑦ 修复成功后仍要过「防护三/四」的校验
部分成功与失败口径
| 情况 | 结果 |
|---|---|
| 本轮部分条目失败 | 告警 + 继续写回成功的条目,任务照常进入导入阶段 |
| 整次任务(含全部重试轮次)译文数为 0 | 命令let 报错,任务失败 |
{"translations": []} 空数组响应 | 视为合法结果(不再误报「响应无法解析」) |
| HTTP 401 / 403 | 立即停止,提示检查 Key |
| HTTP 429 | 按 Retry-After 或指数退避后重试 |
| HTTP 5xx / 超时 | 按 Max Retries 重试,仍失败则跳过该批 |
| 未配置 Key | 直接报错,不发起任何请求 |
IMPORTANT
判断失败的阈值是「整个任务的译文总数为 0」,而不是「某一批失败」。这条规则是为了让
导出 → AI 翻译 → 导入管线不会因为个别批次抖动而中途放弃,导致已翻译的内容无法落盘。
日志与密钥安全
- 日志绝不打印 API Key、
Authorization头,或含密钥的完整请求体 - 失败信息只包含:目标语言、文件路径、条目 id、issue 类别与消息
- 进度窗口的
Copy Log/Save Log...可以直接归档整次执行的日志,便于复盘
与官方数据的隔离
AI 只写 .cliff;写入 manifest / archive 的动作仍然由 CliffImport 步骤完成(FCliffImporter),因此:
- AI 永远不会直接改二进制
.locres - 修复失败的文件不会被导入
- 任何时刻都可以用「重新导入上一版
.cliff」来回滚