跳到內容

翻譯系統

本網站以 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 鍵值。唯一的差異是正文內容是另一種語言。

Starlight(文件框架)依賴這種對稱性。當使用者切換語言時,Starlight 會將 /docs/getting-started/ 替換為 /fr/docs/getting-started/——相同的路徑,不同的語系前綴。如果法文檔案不是精確地位於 fr/getting-started.md,語言切換器就會壞掉或默默回退到英文。

語言按全球西洋棋玩家人口排序,資料來自 LichessChess.com 和 FIDE 註冊數據。英文作為來源語言排在首位;其餘依照西洋棋排名排列:

排名代碼語言風格
1en英文真實來源
2es西班牙文標準正式
3hi印地文天城文字體
4ru俄文標準正式
5de德文標準正式
6fr法文標準正式
7pt葡萄牙文歐洲葡萄牙文
11pl波蘭文標準正式
12it義大利文標準正式
13uk烏克蘭文標準正式
14tr土耳其文標準正式
17ko韓文합니다/습니다 形式
18zh中文(簡體)簡體字
zh-tw中文(繁體)繁體字
23nb挪威語(書面語)標準 Bokmål
be白俄羅斯文標準白俄羅斯文
34ja日文です/ます 形式

「風格」欄位很重要。日文和韓文有語體層級的選擇,會影響每一句話。翻譯提示詞包含這些指示,讓模型產出自然、精緻的文字——而不是僵硬的機器翻譯。

這個排序也控制了網站標題列的語言下拉選單。使用人數最多的西洋棋語言排在最前面,使用者更可能不需要捲動就找到自己的語言。

所有文件都從 src/content/docs/ 中的英文 markdown 開始。Frontmatter 包含 titledescription

---
title: "Getting Started"
description: "Install En Parlant~ and play your first game."
---
Download the latest release...

一個 Python 腳本(scripts/translate-docs.py)讀取每個英文來源檔案,將其傳送到 Claude API,然後寫出翻譯後的 markdown:

Terminal window
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 分鐘。

Terminal window
pnpm build

Astro 讀取來源 markdown,透過 Starlight 的模板渲染,並將靜態 HTML 輸出到 dist/。所有約 500 個頁面的建構大約需要 30 秒。

Terminal window
pnpm run deploy

將建構完成的網站推送到 Cloudflare Workers。

翻譯腳本刻意保持簡單——大約 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」在日文中變成「はじめに」,在俄文中變成「Начало работы」。

Claude Opus 4.6 產出的翻譯品質明顯優於較快的模型。差異體現在:

  • 自然的措辭——Opus 寫起來像母語者,而非翻譯者。當英文的語序在目標語言中聽起來不自然時,它會重新組織句子結構。
  • 技術準確度——西洋棋術語、TTS 專業用語和軟體概念都使用正確的領域特定詞彙來翻譯。
  • 一致性——正式語體自始至終保持一致。日文全程使用 です/ます 形式,不會在段落中間切換正式與隨意語體。
  • MDX 處理——Opus 能正確保留 .mdx 檔案中的 JSX 元件標籤(<Card><CardGrid>)和 import 陳述式,不會破壞它們。

成本差異是真實的——使用 Opus 進行全站翻譯約 $28,而 Sonnet 約 $5——但對於使用者實際會閱讀的 448 個檔案而言,品質是值得的。

這是花最長時間才搞定的部分。

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/ 之前

檔案路徑SlugURL
getting-started.mddocs/getting-started/docs/getting-started/
fr/getting-started.mdfr/docs/getting-started/fr/docs/getting-started/
ja/features/puzzles.mdja/docs/features/puzzles/ja/docs/features/puzzles/

英文使用 defaultLocale: "root",意味著沒有前綴——它直接位於 /docs/,而不是 /en/docs/

同一檔案中的 localeKeys 陣列必須列出每個非英文語系。如果某個語系存在於 Astro 的設定中但不在這個陣列中,其翻譯內容會被當作英文內容處理——語言切換器會壞掉,頁面數量會暴增。

Astro 會在 .astro/data-store.json 中快取 slug 對應關係。在任何語系設定變更後,必須先刪除此檔案再重新建構,否則建構會以過時(錯誤)的路由成功完成。

側邊欄定義在 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。新增一種語言意味著在這七個區塊中各新增一個條目。

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/(使用連字號)。

困難的部分已經完成:

  1. 路由架構已解決。 generateId 函式、localeKeys 陣列和 defaultLocale: "root" 設定協同運作,使 Starlight 產生正確的 URL 結構。這是最大的痛點——需要追蹤 Starlight 和 Astro 中 6 個以上的原始檔才找到並修復。

  2. 翻譯腳本處理一切。 全站重新翻譯、新增單一語言、更新個別檔案——全部使用同一個腳本配合不同的旗標。它會在遇到速率限制時自動重試、跨工作執行緒平行處理,並清楚地報告錯誤。

  3. 新增一種語言只需四處設定修改和一個命令。 將語系新增到 astro.config.mjscontent.config.tstranslate-docs.py,新增側邊欄翻譯,執行腳本。大約 10 分鐘的工作加上 4 分鐘的翻譯時間。

  4. 更新內容更加簡單。 編輯英文來源,使用 --files--overwrite 旗標只針對變更的檔案執行腳本,重新建構。或者對於小幅的文字修正,直接編輯翻譯檔案。

  5. 整個流程已被記錄為技能。 執行 /translate_docs 會引導你走完整個過程——使用哪種模式、傳遞什麼旗標、飛行前檢查、翻譯後驗證。不需要任何隱性知識。

448 個檔案跨 16 種語言的完整翻譯花費約 $28,耗時約 70 分鐘。新增一種語言約花費 $1.70。將一個檔案重新翻譯成所有語言約花費 $1。這些就是將文件維持在 17 種語言版本的持續成本。