翻译系统
本站以 17 种语言发布。这不是由一个翻译团队完成的,而是通过一个 Python 脚本、一个 Claude Opus 模型,以及一个精心设计的文件结构,让一切都能无缝衔接。以下是整个系统的工作原理。
文档存放在 src/content/docs/ 中。英文是根目录——其他每种语言都精确地镜像它:
src/content/docs/├── index.mdx ← English (root)├── getting-started.md├── features/│ ├── play-chess.md│ ├── multiplayer.md│ └── ...├── setup/│ ├── tts-overview.md│ └── ...├── under-the-hood/│ ├── architecture.md│ └── ...│├── fr/ ← French│ ├── index.mdx│ ├── getting-started.md│ ├── features/│ │ ├── play-chess.md│ │ └── ...│ └── ...│├── ja/ ← Japanese│ ├── index.mdx│ ├── getting-started.md│ └── ...│└── ... (16 language directories total)每个翻译文件都是其英文源文件的结构镜像。相同的文件名、相同的子目录路径、相同的 frontmatter 键。唯一的区别是正文内容使用了另一种语言。
为什么镜像结构很重要
Section titled “为什么镜像结构很重要”Starlight(文档框架)依赖这种对称性。当用户切换语言时,Starlight 会将 /docs/getting-started/ 替换为 /fr/docs/getting-started/——相同的路径,不同的语言前缀。如果法文文件不是精确地位于 fr/getting-started.md,语言切换器就会失效或静默回退到英文。
17 种语言
Section titled “17 种语言”语言按全球国际象棋玩家人口排序,数据来源于 Lichess、Chess.com 和 FIDE 注册信息。英语作为源语言排在第一位;其余语言按国际象棋排名排列:
| 排名 | 代码 | 语言 | 风格 |
|---|---|---|---|
| 1 | en | 英语 | 权威源语言 |
| 2 | es | 西班牙语 | 标准正式体 |
| 3 | hi | 印地语 | 天城文字体 |
| 4 | ru | 俄语 | 标准正式体 |
| 5 | de | 德语 | 标准正式体 |
| 6 | fr | 法语 | 标准正式体 |
| 7 | pt | 葡萄牙语 | 欧洲葡萄牙语 |
| 11 | pl | 波兰语 | 标准正式体 |
| 12 | it | 意大利语 | 标准正式体 |
| 13 | uk | 乌克兰语 | 标准正式体 |
| 14 | tr | 土耳其语 | 标准正式体 |
| 17 | ko | 韩语 | 합니다/습니다 体 |
| 18 | zh | 中文(简体) | 简体字 |
| — | zh-tw | 中文(繁体) | 繁体字 |
| 23 | nb | 挪威语(书面语) | 标准书面语 |
| — | be | 白俄罗斯语 | 标准白俄罗斯语 |
| 34 | ja | 日语 | です/ます 体 |
“风格”这一列很重要。日语和韩语有敬语等级的选择,这会影响到每一句话。翻译提示词中包含了这些指令,以确保模型产出自然、精炼的文本——而非生硬的机器翻译输出。
这个排序同时也决定了网站顶部语言下拉菜单的顺序。使用人数最多的国际象棋语言排在前面,这样用户更容易在不滚动的情况下找到自己的语言。
第 1 步:编写英文内容
Section titled “第 1 步:编写英文内容”所有文档最初以英文 markdown 形式写在 src/content/docs/ 中。Frontmatter 包含 title 和 description:
---title: "Getting Started"description: "Install En Parlant~ and play your first game."---
Download the latest release...第 2 步:运行脚本
Section titled “第 2 步:运行脚本”一个 Python 脚本(scripts/translate-docs.py)读取每个英文源文件,将其发送给 Claude API,然后写入翻译后的 markdown 文件:
python3 scripts/translate-docs.py \ --anthropic-key $ANTHROPIC_API_KEY \ --model claude-opus-4-6 \ --workers 5该脚本将全部 28 个源文件翻译成全部 16 种目标语言(共 448 个文件),大约需要 60–70 分钟。它同时运行 5 个并行 API 调用,以保持在速率限制范围内。
对于单独新增一种语言,大约需要 4 分钟。
第 3 步:构建
Section titled “第 3 步:构建”pnpm buildAstro 读取源 markdown 文件,通过 Starlight 的模板渲染,将静态 HTML 输出到 dist/。构建大约 500 个页面需要约 30 秒。
第 4 步:部署
Section titled “第 4 步:部署”pnpm run deploy将构建好的站点推送到 Cloudflare Workers。
脚本工作原理
Section titled “脚本工作原理”翻译脚本故意保持简洁——大约 300 行 Python 代码。以下是其流程:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐│ Read English │────▶│ Claude API │────▶│ Write target ││ source file │ │ (Opus 4.6) │ │ language file ││ │ │ │ │ ││ getting- │ │ "Translate into │ │ fr/getting- ││ started.md │ │ French..." │ │ started.md │└─────────────────┘ └──────────────────┘ └─────────────────┘ × 5 parallel × 16 languages × 28 files提示词精确地告诉 Claude 哪些内容需要翻译,哪些需要保持原样:
- 翻译: 正文、标题、表格标签、frontmatter 的 title 和 description
- 保留英文: 代码块、行内代码、命令示例、文件路径、URL、产品名称、人名、ASCII 图表
这种区分至关重要。像 pnpm build 这样的命令在每种语言中都必须保持 pnpm build。像 “En Parlant~” 或 “Stockfish” 这样的产品名称不翻译。但 “Getting Started” 在日语中变成”はじめに”,在俄语中变成”Начало работы”。
为什么选择 Opus?
Section titled “为什么选择 Opus?”Claude Opus 4.6 产出的翻译质量明显优于更快的模型。差异体现在:
- 自然的表达方式 —— Opus 写出来的文字像母语者,而非翻译腔。当英文的语序在目标语言中会显得别扭时,它会重组句子结构。
- 技术准确性 —— 国际象棋术语、TTS 专业用语和软件概念都使用了正确的领域专有术语。
- 一致性 —— 敬语等级在全文保持一致。日语始终使用です/ます体,不会在段落中间在随意体和礼貌体之间来回切换。
- MDX 处理 —— Opus 能正确保留
.mdx文件中的 JSX 组件标签(<Card>、<CardGrid>)和import语句,不会破坏它们。
成本差异是实实在在的——使用 Opus 进行全站翻译约需 $28,而使用 Sonnet 约需 $5——但对于用户真正会阅读的 448 个文件来说,质量是值得的。
语言路由架构
Section titled “语言路由架构”这是花费最长时间才搞定的部分。
Starlight 通过检查内容 slug 的第一个路径段来检测页面的语言。当它看到 fr/docs/getting-started 时,它知道这是法语,因为 fr 是第一个路径段。但最初的实现产生的 slug 类似于 docs/fr/getting-started——语言代码被埋在了 docs/ 之下。Starlight 将 docs 视为第一个路径段,把所有内容都当作英文处理,结果生成了 7,000 多个重复页面,而不是约 500 个。
src/content.config.ts 中的一个自定义 generateId 函数控制文件路径如何转换为内容 slug:
generateId({ entry }) { const slug = entry.replace(/\.[^.]+$/, ""); const firstSeg = slug.split("/")[0]; if (firstSeg && localeKeys.includes(firstSeg)) { return `${firstSeg}/docs/${slug.slice(firstSeg.length + 1)}`; } return `docs/${slug}`;}这将语言前缀放在了 docs/ 之前:
| 文件路径 | Slug | URL |
|---|---|---|
getting-started.md | docs/getting-started | /docs/getting-started/ |
fr/getting-started.md | fr/docs/getting-started | /fr/docs/getting-started/ |
ja/features/puzzles.md | ja/docs/features/puzzles | /ja/docs/features/puzzles/ |
英语使用 defaultLocale: "root",这意味着没有前缀——它直接位于 /docs/,而不是 /en/docs/。
localeKeys 数组
Section titled “localeKeys 数组”同一文件中的 localeKeys 数组必须列出每个非英语语言代码。如果某个语言代码存在于 Astro 的配置中但不在这个数组中,其翻译内容会被当作英文内容处理——语言切换器会失效,页面数量会暴增。
数据存储缓存
Section titled “数据存储缓存”Astro 将 slug 映射缓存在 .astro/data-store.json 中。任何语言配置更改后,必须在重新构建之前删除此文件,否则构建会成功但使用过期(错误)的路由。
侧边栏在 astro.config.mjs 中定义。指向页面的项目(slug 属性)会自动使用翻译页面 frontmatter 中的标题——无需手动翻译:
{ slug: "docs/getting-started" }// English: "Getting Started" (from English frontmatter)// French: "Premiers pas" (from French frontmatter)// Japanese: "はじめに" (from Japanese frontmatter)但分组标签和外部链接需要显式翻译:
{ label: "Features", translations: { fr: "Fonctionnalités", es: "Características", de: "Funktionen", ja: "機能", // ... all 16 languages }, items: [ { slug: "docs/features/play-chess" }, { slug: "docs/features/multiplayer" }, // ... ],}七个侧边栏元素需要手动翻译:Welcome、Features、App Menus、Setup Guides、Under the Hood、Credits 和 Accessibility。添加新语言意味着需要在这七个块中各添加一个条目。
应用程序连接
Section titled “应用程序连接”En Parlant~ 在其帮助菜单中链接到本文档。该链接是语言感知的——如果你正在使用法语版应用程序,它会打开法语文档:
const docsLocalePrefix = useMemo(() => { const lang = i18n.language; // e.g. "fr_FR", "zh_TW" if (!lang || lang.startsWith("en")) return ""; if (lang === "zh_TW") return "/zh-tw"; return `/${lang.slice(0, 2)}`;}, [i18n.language]);应用程序中可用的每种语言在网站上都有对应的翻译。映射是自动的——fr_FR 映射到 /fr/docs/,ja_JP 映射到 /ja/docs/。唯一的特殊情况是繁体中文:zh_TW 映射到 /zh-tw/docs/(使用连字符)。
为什么现在变得简单了
Section titled “为什么现在变得简单了”困难的部分已经解决:
-
路由架构已搞定。
generateId函数、localeKeys数组和defaultLocale: "root"配置协同工作,让 Starlight 生成正确的 URL 结构。这是最大的痛点——需要追踪 Starlight 和 Astro 中 6 个以上的源文件才找到并修复了问题。 -
翻译脚本处理一切。 全站重新翻译、单语言添加、单文件更新——都是同一个脚本配合不同的参数。它会在遇到速率限制时自动重试,跨 worker 并行处理,并清晰地报告错误。
-
添加新语言只需四处配置修改和一条命令。 在
astro.config.mjs、content.config.ts和translate-docs.py中添加语言代码,添加侧边栏翻译,运行脚本。大约 10 分钟的工作加上 4 分钟的翻译时间。 -
更新内容更加简单。 编辑英文源文件,使用
--files和--overwrite参数运行脚本只翻译修改过的文件,然后重新构建。或者对于小的文本修改,直接编辑翻译文件即可。 -
整个流水线已记录为一个技能。 运行
/translate_docs会引导你走完整个流程——使用哪种模式、传递什么参数、预检查、翻译后验证。无需任何隐性知识。
全部 448 个文件跨 16 种语言的翻译总共花费约 $28,耗时约 70 分钟。单独新增一种语言的费用约 $1.70。单个文件跨所有语言重新翻译的费用约 $1。这就是将文档维护为 17 种语言版本的持续成本。