如何翻译 Markdown 文件而不破坏代码、链接或 Front Matter
Markdown 或 MDX 文件中哪些内容需要翻译、哪些应保持不变,以及如何检查结果。包含示例文件、免费检查工具和 9 次本地 LLM 测试。

要翻译 Markdown 文件,请翻译正文内容,并保持技术结构不变:front matter 键、代码、URL、链接标签、组件标签和脚注标签必须逐字节原样返回。然后检查三件即使仔细翻译仍可能出错的事情:指向标题的链接、代码注释,以及在中文、日文或韩文中粗体文本和数字范围周围的间距。
我们使用本地模型,以三种不同的提示词将一份 73 行的 MDX 页面翻译成简体中文九次,并用脚本检查了每个结果。人们通常担心的部分在全部九次运行中都得以保留:代码、行内代码、URL、表格、JSX 组件和任务列表。另外三件事出了问题。指向标题的链接在全部九次运行中都断了。代码注释在全部六次发送整个文件的运行中都被翻译了,包括三次提示词明确要求不要改动代码的运行。而将语法隐藏在占位符后面的提示词——听起来像是最安全的选择——在其全部三次运行中都破坏了 front matter。
快速回答:翻译什么,不翻译什么
| 文件部分 | 我们示例中的例子 | 是否翻译? |
|---|---|---|
| Front matter 键 | slug:, sidebar_position:, tags: | 否 |
| Front matter 值 | title: "Export Your Library" | 仅面向读者的值,例如 title 和 description |
| 导入和导出(MDX) | import Callout from '@site/src/components/Callout'; | 否 |
| 代码块 | shelfmark export --format epub | 否。单独决定注释和面向用户的字符串。 |
| 行内代码 | library.json, --covers | 否 |
| 链接和图像目标 | (./export-options.md), (https://www.w3.org/TR/epub-33/ "EPUB 3.3") | 否,引号中的可选标题除外 |
| 链接文本和图像替代文本 | [glossary section], ![Folder layout after an export] | 是 |
| 引用标签和定义 | [project glossary][glossary], [glossary]: ./glossary.md#terms | 保留标签 [glossary] 和 URL;翻译可见文本 |
| 标题 | ## Keep one glossary for the whole book | 可以,但请先为它们设置固定 ID(见下文) |
| JSX 组件 | <Callout type="warning" title="Back up first"> | 保留标签和属性名;翻译面向读者的属性值,例如 title |
| 提示标记 | :::tip 和 ::: | 否 |
| 脚注标签 | [^sync] | 否 |
| MDX 注释 | {/* Translator note: ... */} | 可选。读者永远看不到它们。 |
其余都是普通文本:段落、列表项、表格单元格和块引用。
我们测试了什么
我们编写了 sample-source.mdx,这是一个名为 Shelfmark 的虚构阅读应用的文档页面。它采用 Docusaurus 及类似文档站点使用的 MDX 风格编写,包含上表中每种结构的一个示例,外加一个两空格硬换行、一个粗体链接、一个以括号结尾的粗体短语、两个用短破折号书写的数字范围,以及一个指向标题“为整本书保留一份术语表”的页内链接。
然后我们让一个本地模型以三种方式将其翻译成简体中文:
- 整个文件。 粘贴整个文件并要求返回翻译后的文件。
- 指令式。 同上,外加六条规则:保留 front matter 键,不更改代码内的任何内容,保留 URL 和引用定义,保留导入、标签和注释,保留脚注标签和提示标记,并翻译标题、链接文本、替代文本和
title属性。 - 保护式。 将代码、行内代码、URL、标签、注释、脚注标签、提示标记和 front matter 行替换为带编号的占位符,例如
⟦12⟧,翻译其余部分,然后将原始内容放回。title和description的值被暴露出来以供翻译,结果 front matter 的---行和闭合引号也被暴露了。
模型是 Qwen 3.6 27B (Q4_K_M),运行在 LM Studio 中,关闭思考模式,温度设为 0.3,硬件为 Apple M4 Max 配 64 GB 内存。我们在 2026 年 10 月 25 日对每种条件运行了三次,共九次运行。脚本是 run_llm_markdown_test.py,所有九次输出均未经编辑发布;README 列出了每个文件。如果您从未设置过本地模型,我们的本地 LLM 翻译指南涵盖了相关设置。
这是一次针对一个模型、一个语言对和一个页面的小型测试。它展示了哪些问题会发生以及如何发现它们。它并不对模型或提示进行排名。
结果:语法幸存;链接和决策没有
我们用 check_markdown_translation.py 检查了每个输出,并用 remark 和 MDX 编译器渲染了每个输出。完整的检查器输出在 check-results.txt 中,results.csv 每次运行一行。
| 检查 | 整个文件 | 已指示 | 已保护 |
|---|---|---|---|
| 作为 MDX 编译 | 3/3 | 3/3 | 3/3 |
| 代码、行内代码、URL、表格、标签、任务列表保持不变 | 3/3 | 3/3 | 3/3 |
| 粗体和斜体仍正常渲染 | 3/3 | 3/3 | 3/3 |
| 前置元数据完好无损 | 3/3 | 3/3 | 0/3 |
| 页内链接仍指向其标题 | 0/3 | 0/3 | 0/3 |
| 代码注释保持原样 | 0/3 | 0/3 | 3/3 |
标注 title 已翻译 | 3/3 | 3/3 | 0/3 |
前几行是好消息。当我们粘贴整个文件时,这个模型完整保留了 Markdown 语法;我们在代码、URL 或表格中没有发现任何需要修复的地方。最后四行才是问题所在,而且它们在你构建网站时都不会显示为错误。
1. 翻译后的标题会破坏指向它们的链接
示例用 [glossary section](#keep-one-glossary-for-the-whole-book) 链接到其术语表部分。每次运行都翻译了标题(这是应该的),并且每次运行都保留了链接目标(这也是应该的)。结果是一个指向已不存在 ID 的链接。
大多数 Markdown 工具根据标题文本生成标题 ID。Pandoc 的文档将标识符描述为“基于标题文本”,而 Docusaurus 警告说,你可能想要更改或翻译标题,“而不更新现有 ID 以避免破坏链接”。一旦标题变为“为整本书保留一个术语表”,其生成的 ID 就是中文,而 #keep-one-glossary-for-the-whole-book 不会滚动到任何地方。页面仍然可以构建,所以没有任何提示。
你也不能通过在翻译后编辑链接来解决这个问题。在九次运行中,模型用四种不同的方式写了同一个标题,因此生成的 ID 会随着每次重新翻译而再次改变。修复应该在翻译之前进行:为每个被链接的标题指定一个固定 ID。
- 在 Docusaurus MDX 中,使用
## Keep one glossary for the whole book {/* #keep-one-glossary */}。Docusaurus 指出,旧的{#id}形式“不再是有效的 MDX 语法”,并且其write-heading-ids命令会为你为每个文档添加 ID。 - 在 Pandoc 和遵循 PHP Markdown Extra 的 Markdown 处理器中,在标题末尾使用
{#keep-one-glossary}。
这样,ID 在每种语言中都保持不变,检查器会将其视为链接目标。
2. 代码注释被翻译,而可见字符串却没有
示例中有一个 shell 注释和一个 YAML 注释:
library:
name: "My books" # shown in the sidebar
sort: title
所有六次发送整个文件的运行都翻译了这两个注释,包括三次提示中写着“不要更改围栏代码块内的任何内容”的运行。代码本身在九次运行中均未改动。与此同时,注释中说是显示在侧边栏的 "My books" 在九次运行中均保持英文。模型做了本地化页面所需的反面:它翻译了给开发者的注释,却留下了读者会看到的标签。
这两种选择并非在所有情况下都是错的。对于读者会将代码复制到自己项目中的教程,翻译注释是有帮助的。对于代码必须与仓库匹配的参考文档,它们会使页面与源文件不一致。为项目做一次决定,并手动检查配置示例中的字符串值,因为没有任何工具能区分显示标签和标识符。出于这个原因,检查器将仅注释的更改报告为警告。
3. 看似安全的方法破坏了 front matter
受保护的条件实现了它的承诺:所有 32 个占位符在三次运行中都返回了,因此代码、URL 和标签都被完美恢复。但我们的脚本将两件小事留在了占位符之外:打开 front matter 的 --- 行,以及翻译后的标题和描述后的结束引号。模型在三次运行中都丢弃了它们。文件开头变成了这样:
title: "导出您的书库
description: "在更换设备之前,请备份您的笔记和书籍。
slug: export-library
没有开头的 ---,Markdown 解析器不再将 front matter 视为 front matter。它将六行 YAML 加上结束的 --- 读取为一个标题,因此页面会以一个包含 YAML 的大标题开头。它仍然作为 MDX 编译。把 --- 放回去也不够:YAML 解析器会拒绝两个未闭合的引号。
教训不是要避免占位符。而是,你暴露在外的任何内容都可能被更改后返回,并且 front matter 根本不应该作为页面的一部分发送给模型。解析它,只将面向读者的值(title、description)作为单独的字符串发送,并将翻译写回原始结构。这只需几行代码,就能消除整类问题。
中文、日文和韩文中的粗体文本和范围
另外两个问题从未在我们的九次运行中出现,但它们值得检查,因为它们会悄无声息地破坏页面,而且这两个问题都曾影响 BookTranslator 自己的博客。该博客用 MDX 编写,并被机器翻译成 27 种语言,在一次定时翻译运行中,两篇韩文文章连续三次重试都未能通过我们的结构检查。原因是下面两种模式。
以标点结尾的粗体。 CommonMark 通过查看 ** 两侧的字符来决定它能否开启或闭合粗体。紧跟在标点后的闭合 ** 后面必须跟一个空格、标点或行尾;这就是右翼规则。英文中那里总是有空格。中文、日文和韩文则常常没有。因此 打开**设置 → 导出(测试版)**并选择 会显示字面星号,因为粗体以全角括号结尾,后面直接跟着“并”。去掉括号的同一行会渲染为粗体,在 ** 后加空格的版本也是如此。粗体链接也有同样的问题,因为链接以 ) 结尾:点击**[导出](./export-options.md)**打开 会失败,韩文 **[API](./api.md)**는 也一样。
我们的模型恰好在每个粗体短语两侧都加了空格,所以它的输出渲染正确。如果编辑后来为了遵循中文排版惯例而移除这些空格,这个 bug 就会重新出现。你可以在 render-cases.md 中尝试全部七种变体。
使用波浪号表示数字范围。 中文和日文文本常将范围写作 3~5。基于 remark 构建的 GitHub Flavored Markdown 站点默认会将单个波浪号转换为删除线:remark-gfm 的 singleTilde 选项默认为 true,除非你更改它。同一段落中的两个范围,例如 需要 3~5 分钟,生成 10~20 MB 的文件夹,会将它们之间的文本渲染为删除线。我们的示例使用了短破折号,八次运行保留了它们,而第九次写成了 3 到 5。请要求使用短破折号或文字表示范围,或在 remark 配置中设置 singleTilde: false。
结构检查器会标记这两种模式。要确认实际渲染效果,render_check.mjs 会统计源文件和译文中的粗体、斜体、删除线和残留星号,并尝试进行 MDX 编译:
npm install @mdx-js/mdx@3 unified@11 remark-parse@11 remark-gfm@4
node render_check.mjs source.mdx translated.mdx
译文中粗体节点更少或字面星号更多,意味着某些强调未能闭合。
经得起考验的工作流程
-
首先修复标题 ID。 为每个被链接的标题添加显式 ID,使用你的站点生成器支持的语法。在源语言中一次性完成,并在翻译任何内容之前提交。
-
将 front matter 从页面中取出。 只翻译面向读者的值,如
title和description,作为单独的字符串,然后将它们写回原始 YAML。 -
确定代码策略。 注释是否翻译;配置示例中的显示字符串是否翻译。将其与不得更改的产品名称(例如我们示例中的 Shelfmark)一起写入你的翻译词汇表。
-
翻译正文。 使用能解析 Markdown 的工具,或发送整个页面并附上明确规则。在我们的测试中,发送整个页面保持了语法完整;如果使用占位符,请保护完整的结构,不要留下任何半暴露的部分。
-
运行结构检查。
python3 check_markdown_translation.py source.mdx translated.mdx --keep Shelfmark它会比较 front matter 的键和值、导入、代码块、行内代码、链接目标、引用定义、标题级别、页内链接目标、JSX 标签和属性、MDX 注释、警告标记、脚注标签、表格形状、任务列表复选框、硬换行、无法闭合的强调、波浪号范围以及你要求保留的术语。它不评判翻译质量。
-
使用
render_check.mjs检查 CJK 目标的渲染效果,并在任何跟在标点后的闭合**后添加一个空格。 -
构建站点并点击链接。 打开每种语言的页面,点击每个页内链接,并阅读配置示例。构建成功只能证明文件可以编译。
对于准确性至关重要的文档,我们的技术翻译工作流程中的术语和审校步骤也适用。
BookTranslator 的适用场景
BookTranslator 的 TXT 翻译器接受 .txt、.md 和 .mdx 文件。根据产品页面,它在翻译时保留段落、换行、特殊字符和文本结构,并将大文件分割成段进行上下文感知翻译,因此你可以上传整本手册或用 Markdown 写的书,而不必分段粘贴。
产品页面没有描述对 front matter、代码块或 MDX 组件的特殊处理,我们也没有为本文通过 BookTranslator 运行此示例。无论使用什么工具,请在翻译前执行步骤 1 到 3,翻译后执行步骤 5 到 7。没有固定 ID 的标题仍然会丢失链接,用波浪号写的范围仍然会变成删除线。如果你要发布多种语言的书籍,我们的多语言自助出版指南涵盖了该任务的其余部分。
获取测试文件
本次测试的所有内容均可免费重用:
- sample-source.mdx:73 行的英文示例;
- check_markdown_translation.py:结构检查器(仅使用 Python 标准库);
- render_check.mjs 和 render-cases.md:渲染检查和七个 CJK 测试用例;
- run_llm_markdown_test.py:适用于任何 OpenAI 兼容本地服务器的三种提示条件;
- outputs 文件夹中的九个输出文件,以及 check-results.txt 和 results.csv。





