Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ jobs:
cache: npm
- name: Install frontend dependencies
run: npm ci
- name: Check version metadata
run: npm run check:versions
- name: Run frontend logic tests
run: npm run test:frontend
- name: Build frontend
run: npm run build

Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ API 模式下,`batch_size=auto` 使用 25,`concurrency=auto` 使用 6,并
- API 阶段结束后,前端必须提供“是,直接覆盖”和“否,人工校对”。两条路径最终都调用同一套 CMP 解析、格式守卫、备份和提交逻辑,不能为“直接覆盖”建立低校验旁路。
- 扫描结果按源文件列出条目数;CMP 也按 `lang/en_us.snbt` 或 `chapters/<文件名>.snbt` 分组。
- CMP v1 的人工可编辑内容仅为 `"英文" -> "中文"` 右侧 JSON 字符串。文件头、`# meta`、`## file`、`@` 回填位置和左侧英文都属于受保护内容。
- 新写出的 CMP 必须保存不包含右侧译文的 `protected_hash`;旧 v1 可缺少该字段。应用内校对另以整文件 revision 防止外部编辑与旧页面互相覆盖。
- CMP 元数据可以保存非敏感的提供商、模型、接口地址、翻译要求和词表指纹;禁止保存 API Key、Authorization、钥匙串值或完整 HTTP 请求/响应。
- CMP 元数据保存非敏感 `task_id`,用于串联 API、人工校对、写回和历史日志;兼容缺少该字段的早期 v1 文件。
- 应用 CMP 时必须重新扫描当前任务书并校验目录、模式、条目数、源内容 SHA-256、文件归属、条目 ID、JSON Pointer 和左侧英文。任务书变化、条目缺失/重复或元数据被修改时拒绝写入。
- 新任务的源指纹必须覆盖解析管线版本、源文件相对路径和完整字节;旧 v1 的条目 ID/英文指纹只保留读取兼容。
- 手工译文仍要经过换行、格式码、占位符、选择器、数字、URL、资源 ID、标签和 JSON 富文本结构校验;人工编辑不能绕过格式守卫。
- 所有验证通过后才创建备份并生成完整输出。语言文件重新解析完整 SNBT;章节文件验证替换位置、引号、转义和括号结构;多文件提交失败时回滚。
- 应用重新启动后,允许用户扫描同一目录并导入已有 CMP,不得强制重新调用翻译接口。
Expand Down Expand Up @@ -143,12 +145,14 @@ cargo test --manifest-path src-tauri/Cargo.toml
React、TypeScript 或设置页修改:

```bash
npm run test:frontend
npm run build
```

文档和所有改动:

```bash
npm run check:versions
git diff --check
```

Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ npm run tauri dev
2. 回到“翻译工作台”,选择整合包根目录,或直接选择 `config/ftbquests/quests`、`lang`、`chapters` 目录。
3. 点击“扫描任务书”,确认识别出的格式、文件数和待翻译条目数。
4. 点击“开始翻译”。程序调用所选服务,但不会立即覆盖任务书;完成后先生成可编辑的 `.cmp` 英文 → 中文校对文件。
5. 在弹窗中选择“是,直接覆盖”,或选择“否,人工校对”。人工校对时可以打开、另存或选择修改后的 CMP;只修改箭头右侧中文。
5. 在弹窗中选择“是,直接覆盖”,或选择“否,人工校对”。人工校对时可以打开、另存或选择修改后的 CMP;表格会标出同源多译、保持英文、疑似未汉化和异常状态,只修改箭头右侧中文。
6. 可以先点击“验证 CMP”查看任务书归属、源指纹、可应用/保持英文/格式失败数量和预计修改文件;这一步只在内存中验证当前表格译文,不修改 CMP、任务书、备份、缓存、历史或报告。
7. 点击“校验并覆盖”。程序会独立重新核对任务书指纹、英文原文、标签、数字、JSON 和 SNBT 结构,不依赖之前的验证结果;通过后才创建备份并写回。
8. 翻译与写回状态会保存在系统应用数据目录。重复点击、并发命令或重启后重新导入已应用的 CMP 都会由后端拒绝再次写回;这项保护不依赖前端按钮状态
8. 翻译与写回状态会保存在系统应用数据目录。重复点击、并发命令或重启后重新导入已应用的 CMP 都会由后端拒绝再次写回;若上次进程在翻译中异常退出,重新开始时可显式确认恢复中断状态,正在写回的状态不会自动解锁
9. 完成后检查格式告警。网页翻译只能作为机器初译,发布前仍应进行术语和语义审校。

### 构建安装包
Expand All @@ -56,6 +56,7 @@ npm run tauri -- build
- 支持两种模式:语言文件(`lang/en_us.snbt`)和章节文件(`chapters/*.snbt`)
- 自动识别整合包目录结构,并按文件列出待翻译条目
- API 翻译后先生成可编辑、可导入导出的 `.cmp` 英文 → 中文校对文件
- CMP 表格提供同源多译、保持英文、疑似未汉化和异常状态筛选,帮助集中人工审校
- 只有确认应用 CMP 后才备份并写回;拒绝英文、回填位置或任务书指纹被修改的文件
- 官方 API 支持批量并发,网页翻译以大批次、低并发方式减少匿名请求
- 可选的版本化 Minecraft/模组词表,默认关闭;切换到 API 模式后可按需启用
Expand All @@ -72,13 +73,13 @@ npm run tauri -- build
工具支持两种文件格式,分别对应 FTB Quests 的两种任务书结构:

- **lang 模式**:解析 `lang/en_us.snbt`,这是一个类 JSON 的 SNBT 格式文件,值可以是字符串或字符串数组(多行描述)。工具自己实现了 SNBT 解析器(`snbt.rs`),保留键的原始顺序,写出时也生成合法 SNBT。
- **chapters 模式**:遍历 `chapters/*.snbt`,从每个章节文件中提取可翻译的字符串字段(任务标题、描述等)
- **chapters 模式**:遍历 `chapters/*.snbt`,用逐字符 token/span walker 跳过注释和字符串内伪字段,从嵌套结构提取任务标题、描述等目标字符串,同时保留原始字节位置用于写回

如果条目是 Minecraft JSON 富文本组件,程序不会把整个 JSON 交给翻译接口。它会先解析组件树,只抽取玩家可见的根字符串、`text`、`extra`、`with`、`separator`,以及 `hoverEvent` 中可显示的文本;`translate`、`keybind`、`clickEvent`、颜色、样式、命令、URL 和资源 ID 保持原值。每个抽取结果会形成一条带原条目 ID 和 JSON Pointer 回填路径的翻译单元,API 只翻译其中的纯文本,随后程序按路径写回原组件并再次比较结构。包含重复键或无法安全解析的疑似 JSON 组件会保留原文,不会交给翻译接口重新生成。

API 阶段完成后,程序在任务书目录的 `.ftb-translator/reviews/` 中生成玩家可见、可导出的 `.cmp` 校对文件。每个单元包含不可修改的 `@` 回填位置,以及一行 JSON 转义后的 `"英文" -> "中文"`。JSON 字符串转义让换行、引号和文本中原有的箭头不会破坏格式;人工校对只修改箭头右侧。应用 CMP 时会再次核对原任务书内容和每个回填位置,文件缺条目、重复位置、英文被修改或格式标签变化都会拒绝写入
API 阶段完成后,程序在任务书目录的 `.ftb-translator/reviews/` 中生成玩家可见、可导出的 `.cmp` 校对文件。每个单元包含不可修改的 `@` 回填位置,以及一行 JSON 转义后的 `"英文" -> "中文"`。新版 CMP 还保存不覆盖右侧译文的受保护区域哈希;人工校对只修改箭头右侧,修改 meta、状态、位置或左侧英文会被拒绝。内置表格使用整文件修订号,外部编辑后不会被旧页面静默覆盖

校对表格可以按状态筛选。接口限流会单独标记为 `rate_limited`,可用“重试限流项”仅重新请求这一批;重试前会再次校验 CMP 与当前任务书的一致性,成功后更新同一个 CMP,其他译文和人工编辑保持不变。
校对表格可以按状态和审校线索筛选。线索使用保守启发式发现同一英文对应多个中文、译文仍等于原文、译文没有中文以及异常处理状态;它们只用于缩小人工检查范围,不是语义准确率结论,也不参与写回权限判断。接口限流会单独标记为 `rate_limited`,可用“重试限流项”仅重新请求这一批;重试前会再次校验 CMP 与当前任务书的一致性,成功后更新同一个 CMP,其他译文和人工编辑保持不变。

```text
## file "chapters/example.snbt"
Expand Down Expand Up @@ -196,7 +197,7 @@ API 返回后,每条译文经过两步处理:

**校验失败**:该条译文**不写入**,原文被保留,条目进入「人工修正」列表。校验通过的译文才写入文件并存入缓存。

生成 CMP 时不会创建备份或修改任务书。用户确认应用后,程序会先验证 CMP 与当前任务书的 SHA-256 内容指纹、英文原文和回填位置;验证通过才创建备份。最终写回时,语言文件会重新解析生成后的完整 SNBT;章节文件会验证每个替换位置唯一且存在,并检查写回后的引号、转义和括号结构。多文件写入任一失败时会回滚本次已写文件。
生成 CMP 时不会创建备份或修改任务书。用户确认应用后,程序会验证受保护区域哈希,以及覆盖源文件路径、完整字节、解析管线和提取身份的 SHA-256 内容指纹,再核对英文原文和回填位置;验证通过才创建备份。最终写回时,语言文件会重新解析生成后的完整 SNBT;章节文件会验证每个替换位置唯一且存在,并检查写回后的引号、转义和括号结构。多文件写入任一失败时会回滚本次已写文件。

### 5. 翻译缓存

Expand Down
12 changes: 6 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ React 界面(src/{App,components,services,types}.tsx/ts)
扫描先把所选目录解析为任务书根目录,并选择一种模式。若同一目录同时存在 `lang/en_us.snbt` 和 `chapters/*.snbt`,当前实现优先使用 `lang`。

- `lang`:`snbt.rs` 解析根 compound,值只接受字符串或字符串数组,使用有序 `Vec` 保持键顺序;写回生成完整 `lang/zh_cn.snbt` 并重新解析。
- `chapters`:`chapters.rs` 当前用受限正则寻找 `title`、`subtitle`、`description`、`text`、`name` 字段,记录原字符串字面量的字节区间、引号和序号。替换按倒序 span 完成,并在替换前后检查引号、转义和括号结构。它不是通用 SNBT 解析器
- `chapters`:`chapters.rs` 使用面向任务的逐字符 token/span walker,跳过注释和字符串内部的伪字段,识别嵌套 compound/list 中的 `title`、`subtitle`、`description`、`text`、`name`,并记录原字面量字节区间、引号和序号。替换按倒序 span 完成,并在替换前后检查引号、转义和括号结构;它仍不是覆盖所有 SNBT 类型的通用 AST
- 普通条目进入一个 `$` 翻译单元;可安全解析的 Minecraft JSON 富文本按 JSON Pointer 拆为多个玩家可见单元;疑似富文本但无法安全解析或含重复键时整条保留英文。

翻译单元携带 `entry_id`、`path`、英文原文、保护后的文本和占位符映射。CMP 再加入源文件归属、状态与译文,使每个译文都能回到唯一位置。
Expand All @@ -46,7 +46,7 @@ React 界面(src/{App,components,services,types}.tsx/ts)

“直接覆盖”和“人工校对”只是在 UI 中到达应用阶段的方式不同,后端最终都调用相同的 CMP 应用路径。详细流程分别见 [翻译流水线](translation-pipeline.md) 与 [写回事务](writeback-transaction.md)。

后端另以 `created → translating → review_ready → applying → applied` 表示成功路径,操作失败进入 `failed`;未修改任务书的 apply 失败恢复为 `review_ready`。命令在启动异步任务或写回前先原子转换状态,不能只依赖前端按钮防止重复操作。
后端另以 `created → translating → review_ready → applying → applied` 表示成功路径,操作失败进入 `failed`;未修改任务书的 apply 失败恢复为 `review_ready`。命令在启动异步任务或写回前先原子转换状态,不能只依赖前端按钮防止重复操作。状态诊断会列出活动任务、更新时间以及翻译记录是否早于本次进程:只有旧 `translating` 可在用户确认后标记为中断;本次进程后的翻译视为可能仍存活,`applying` 则始终不自动解锁。

## 持久化边界

Expand All @@ -66,7 +66,7 @@ React 界面(src/{App,components,services,types}.tsx/ts)

`providers.rs` 适配 Google 网页、DeepL 网页、DeepL 官方 API 和 OpenAI 兼容接口。API 模式 `auto` 批大小为 25、并发为 6,并发硬上限 12;两个网页提供商的有效并发固定收敛为 1。提供商层负责各自协议、拆分和最多三次递增等待的 HTTP 尝试,核心层负责批次并发、失败状态、恢复、格式守卫和 CMP 汇总。

缓存键包含原文和提供商身份;OpenAI 兼容模式包含模型,其他模式还包含接口地址。开启词表时,词表内容指纹参与缓存键;富文本另带处理管线版本,避免复用旧的整段 JSON 结果。
缓存键包含原文、提供商、模型和规范化接口地址,避免两个 OpenAI 兼容端点使用同名模型时交叉复用。默认 DeepSeek 地址仍可只读迁移旧缓存,自定义地址不读取旧式键。开启词表时,词表内容指纹参与缓存键;富文本另带处理管线版本,避免复用旧的整段 JSON 结果。

## 必须保持的不变量

Expand All @@ -80,10 +80,10 @@ React 界面(src/{App,components,services,types}.tsx/ts)

## 已知边界与演进方向

- 当前 `chapters` 提取仍依赖受限正则,不等于完整 SNBT token walker。历史 Python 实现曾使用 token span walker;为何不应扩大为“用正则解析所有 SNBT”见 [ADR-001](decisions/001-token-span-over-regex.md)。
- 历史 Python 格式守卫有轻量颜色/样式 AST;当前 Rust 版只把颜色码当不透明 token 并比较排序后的多重集合,尚不能证明样式作用域等价。见 [ADR-002](decisions/002-colour-ast.md)。
- `chapters` 已恢复轻量 token/span walker,但仍只理解本工具需要的字段和值边界,不是完整 SNBT AST;新增 typed array 或未知合法语法时仍要补 fixture。见 [ADR-001](decisions/001-token-span-over-regex.md)。
- Rust 已恢复轻量颜色/样式 AST,并与 token 多重集合共同验证活动样式作用域。见 [ADR-002](decisions/002-colour-ast.md)。
- 文件提交是应用级补偿事务,不是文件系统原子事务;进程崩溃或回滚自身失败仍需使用已创建的备份人工恢复。
- 当前已有 Rust 单元测试、临时目录流程测试,以及使用确定性 Mock 响应的 `lang`/`chapters` 扫描→CMP→备份→写回 Golden fixture;尚无前端组件/浏览器测试,异步 HTTP provider 也未通过可注入客户端贯穿 Golden。见 [测试策略](testing-strategy.md)。
- 当前已有 Rust 单元测试、临时目录流程测试,使用确定性 Mock 响应的 `lang`/`chapters` 扫描→CMP→备份→写回 Golden fixture,以及前端纯逻辑和 SSR 组件测试;尚无真实浏览器交互测试,异步 HTTP provider 也未通过可注入客户端贯穿 Golden。见 [测试策略](testing-strategy.md)。

## 决策索引

Expand Down
10 changes: 6 additions & 4 deletions docs/cmp-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ CMP 是 FTB Translator 面向玩家和校对者的翻译工程文件。它把“
```text
# FTB Translator CMP v1
# 只修改箭头右侧的中文;保留 @ 行、英文原文、引号与 JSON 转义。
# meta {"version":1,"task_id":"20260714T120000.000Z-0001","quests_dir":"/pack/config/ftbquests/quests","mode":"chapters","source_fingerprint":"...","provider":"google_web","base_url":"https://translate.googleapis.com","model":"google-web","style":"自然玩家向简体中文汉化","glossary_enabled":false,"glossary_fingerprint":"","total_entries":2,"cache_hits":0}
# meta {"version":1,"task_id":"20260714T120000.000Z-0001","quests_dir":"/pack/config/ftbquests/quests","mode":"chapters","source_fingerprint":"v2:...","provider":"google_web","base_url":"https://translate.googleapis.com","model":"google-web","style":"自然玩家向简体中文汉化","glossary_enabled":false,"glossary_fingerprint":"","total_entries":2,"cache_hits":0,"protected_hash":"..."}

## file "chapters/example.snbt"

Expand Down Expand Up @@ -72,7 +72,7 @@ CMP 语法层允许 JSON 空字符串 `""`,以便兼容旧 v1 文件并完整
| 2 | `task_id` | 可选 | 串联 API 请求、CMP 操作、写回与历史日志;早期 v1 文件缺少时按空字符串读取,应用时为本次操作生成新编号 |
| 3 | `quests_dir` | 必需 | 生成 CMP 时的任务书目录 |
| 4 | `mode` | 必需 | `lang` 或 `chapters` |
| 5 | `source_fingerprint` | 必需 | 按条目 ID 和完整英文原文计算的 SHA-256 指纹 |
| 5 | `source_fingerprint` | 必需 | 新文件使用 `v2:` 前缀,覆盖解析管线版本、源文件相对路径、完整字节以及提取后的条目身份;旧 v1 的条目 ID/英文指纹继续兼容 |
| 6 | `provider` | 必需 | 生成机器译文的提供商 |
| 7 | `base_url` | 必需 | 非敏感接口地址,用于缓存与历史归属 |
| 8 | `model` | 必需 | 模型或网页翻译标识 |
Expand All @@ -81,8 +81,9 @@ CMP 语法层允许 JSON 空字符串 `""`,以便兼容旧 v1 文件并完整
| 11 | `glossary_fingerprint` | 必需 | 当次词表内容指纹;不包含词表正文 |
| 12 | `total_entries` | 必需 | 原始任务书条目数,不是富文本拆分后的单元数 |
| 13 | `cache_hits` | 必需 | 当次 API 阶段的完整条目缓存命中数 |
| 14 | `protected_hash` | 可选 | 写入器生成的受保护区域 SHA-256;覆盖 meta 与每条记录除右侧译文外的字段,旧 v1 可缺少 |

应用写出时,元数据始终使用表中的字段顺序。除 `task_id` 外,CMP v1 当前没有其他可选元数据字段
应用写出时,元数据始终使用表中的字段顺序。人工修改右侧译文不会改变 `protected_hash`;修改 `task_id`、提供商信息、状态、位置或左侧英文会使校验失败。早期 v1 缺少 `task_id` 或 `protected_hash` 时仍按兼容规则读取

CMP 禁止包含 API Key、Authorization、钥匙串内容或完整 HTTP 请求/响应。

Expand Down Expand Up @@ -118,6 +119,7 @@ CMP 禁止包含 API Key、Authorization、钥匙串内容或完整 HTTP 请求/
出现以下任一情况时,不创建备份、不写入任何任务书文件:

- CMP 版本、文件头或 JSON 语法无效;
- 新版 CMP 的 `protected_hash` 与受保护内容不一致;
- CMP 不属于当前扫描的任务书目录或模式;
- 当前任务书的条目数量或 SHA-256 指纹已经变化;
- 翻译单元缺失、重复或包含未知回填位置;
Expand All @@ -132,7 +134,7 @@ CMP 禁止包含 API Key、Authorization、钥匙串内容或完整 HTTP 请求/
## 兼容性规则

- 写入器只生成 `# FTB Translator CMP v1`;解析器同时接受更名前的旧拼写文件头。未来不兼容变化必须提升版本号。
- 旧 v1 缺少 `task_id` 时仍可读取;其余必需字段缺失会被拒绝。
- 旧 v1 缺少 `task_id`、`protected_hash` 或仍使用旧式源指纹时仍可读取;其余必需字段缺失会被拒绝。
- v1 允许增加普通 `#` 注释,但 `# meta` 和 `@` 的未知字段会被拒绝,不能改变翻译对照行的语义。
- 写出时按 `file` 的字典序生成 `## file` 分组,同一文件内保持原翻译单元顺序;元数据与 `@` 字段按上表顺序生成。规范写出的 CMP 再次读取和写出时,数据与字节内容都保持稳定。
- 不要把内部的 `translation-units-latest.jsonl` 当作 CMP。JSONL 仅用于诊断实际接口调用,CMP 才是人工校对和导入导出格式。
Expand Down
Loading