先前我寫過一篇文章,介紹如何使用 Google 翻譯 API 為 Astro SSG 網站實現多語化。這套機制會使用 Google Cloud Translation API,翻譯建置後的靜態 HTML,並產生各語言版本。作為控制成本的簡易多語化方案,它已經相當實用。
不過,經過一段時間的運作後,我在 SEO 與翻譯品質兩方面都開始有些在意的地方。關於「為什麼要放棄 Google 翻譯」的內容,請參閱另一篇文章。
文章:將機器翻譯從 Google 翻譯遷移至 Claude API
本文是關於「實際上是如何重新打造的」這一實作內容。我們將翻譯引擎從 Google 翻譯換成 LLM(Claude API),同時也一併清除了上次遺留下來、需要手動處理的營運作業。
建置時使用 LLM 翻譯,並將差異快取存入 R2
基本方針與上次相同,翻譯都在建置階段(伺服器端)完成。我們不會從瀏覽器呼叫 Claude API。不同之處在於翻譯引擎與快取的存放位置。
建置流程如下。
npm run build
├── fetch-microcms (microCMSから記事データ取得)
├── astro build (日本語HTMLを生成 → dist/)
├── translate (各ロケールのHTMLを生成 → dist/{locale}/)
└── update-xml (sitemap更新)
translate 的內容(translate-html-llm.mjs)大致依照以下順序運作。
- 從 Cloudflare R2 下載翻譯快取(單一 JSON 檔案)
- 逐一讀取
dist/內的每個 HTML,透過 cheerio 擷取需要翻譯的文字 - 若快取中已有結果則直接使用,僅將沒有快取的內容送至 Claude API
- 以翻譯結果取代文字,並輸出至
dist/{locale}/ - 將新翻譯的內容合併進快取,並上傳至 R2
重點有兩項:只翻譯有變動的部分(差異翻譯),以及將該快取存放於 R2。
① 如何讓跨越行內標籤的譯文自然通順
這次最想改善的就是這一點。
例如,正文中有以下這樣的 HTML。
<p>私たちは<strong>ウェブアクセシビリティ</strong>を重視しています</p>
如果直接逐段翻譯,我們 / 網頁無障礙 / 的重視 這三個片段會被分開處理。日文和中文的語序不同,把翻譯後的片段放回原位時,<strong> 套用的位置可能會錯位,整句話也可能讀起來不自然。標記越多的句子,越容易崩壞。這正是舊做法最大的問題。
改善方法分為以下兩個階段。
第一點:以區塊元素為單位進行分塊。 翻譯的最小單位不是「標籤之間的空隙」,而是 p、h1〜h6、li、td、blockquote 等區塊元素的完整內容(innerHTML)。不會將句子切斷。
第二點:將行內標籤替換為標記,把整個句子作為一個翻譯單位傳遞。 區塊中的 <strong>、<a> 等標籤,會先替換為 ... 這類標記。
私たちは[[T1]]ウェブアクセシビリティ[[/T1]]を重視しています
我們會指示 LLM:「這是一個完整的句子。請自然地翻譯,並可為需要強調的詞語重新加上相同的標記。標記的位置可依譯文的語序調整。」譯文返回後,再將標記還原為原本的 <strong> 或 <a href="...">。屬性(如 href 或 class)則原樣保留。
如此一來,<strong> 在英文版中也能正確套用到對應的詞語上,句子讀起來也更自然。
② 差分翻譯與 R2 快取的設計
如果每次都翻譯全部內容,成本再高也不夠用。前一版也有 translate-cache.json 快取,但這次我們重新檢視了快取鍵的產生方式與儲存位置。
修改提示詞後會重新產生譯文的機制
快取鍵是由「原始日文 + 語系 + 翻譯提示詞內容」組合而成。這樣一來,若原始日文有變動,只有該筆條目會被視為「快取中不存在」;若翻譯指示或術語方針(提示詞)有變動,則所有條目都會自動被視為「快取中不存在」,並重新翻譯。
此設計的目的,是防止「已改善提示詞,但舊譯文仍殘留在快取中」的問題。實際上,我們會以 sha256 對上述內容進行雜湊,並將雜湊後的字串作為鍵值。
快取內容的結構如下。
{"<sha256のキー>":{"value":"翻訳結果(マーカー込み)","locale":"en","model":"claude-haiku-4-5-20251001","translatedAt":"2026-06-12T..."}}
將快取放到 R2,免去手動作業
以下是上一版遺留問題的解決方式。
上一版的快取是以儲存庫中的 JSON 檔案管理。因此,在 CMS 新增文章並透過部署 Hook 觸發建置時,伺服器只能讀取 Git 上舊的快取。無奈之下,我們採用了一項操作規則來應付:「新增文章後在本機建置,將更新過的快取檔案 push 到 Git」。
這次我們將快取放在 Cloudflare R2 上的單一 JSON blob 中。每次建置時從 R2 取得,完成後再寫回。如此一來,透過 webhook 觸發的建置也能讓快取持續保存,本機建置再 push 的手動作業也完全消失。只要新增文章並 push,就只會翻譯新增的部分,快取也會自動更新。
翻譯的優先順序
譯詞不一致(例如公司名稱 Liberogic 的寫法不統一)一直是上一版的困擾。這次我們分four階段處理。
- 手動覆寫(
data-i18n-key)― 對於在 HTML 中加上data-i18n-key的元素,會固定使用預先準備的手寫譯文。不交給 LLM 或辭典處理,因此可以實現「這裡一定要用這個譯法」的對應。 - 術語辭典(glossary)― 對於導覽列或頁面標題等反覆出現的固定標籤,以 JSON 辭典固定譯詞。修改辭典後重新建置即可立即生效。
- R2 快取― 若以上兩者皆無,則查看快取。
- Claude API― 只有在前述皆無對應時,才最後交由 LLM 處理。
本文中出現的術語統一,無法單靠精確詞典涵蓋(不支援部分比對),因此我們透過系統提示中的術語提示,採取寬鬆的方式來統一。
與 LLM 的「怪癖」共處
機器翻譯產出怪異譯文的問題,上次也遇到過,但 LLM 自有其獨特的怪癖。以下列舉幾項在實際運用中觀察到的現象。
- 技術性的句子整段保留英文。 當「不翻譯的術語」清單(如 API、React、Vue)被過度套用時,模型有時會把整句都以英文回傳。我們在使用者提示中加強要求「必須以目標語言輸出」來因應。
- 標記位置在語意上顛倒。 模型可能誤判應強調的詞語,導致
<strong>套用到錯誤的範圍。只要刪除該筆條目並重新翻譯,即可修正。 - CJK 語言的回應會中途截斷。 漢字每個字消耗的 token 較多,若一次送出過多內容,輸出會達到上限,導致 JSON 損壞。我們調高
max_tokens,並減少每批次的筆數來因應。 - 原始的
以及 CJK 日期格式錯亂。 換行符號可能以字串的形式混入,或是像2026 年 05 月 22 日這樣多出空白。這些問題將透過後處理腳本統一清除。
實務中有效的一項經驗是:有問題的部分逐筆刪除後重新翻譯,成功率最高。 若一次嘗試修正多筆,LLM 往往會以相同方式錯誤判斷相同結構。欲速則不達,這是我們的體會。
關於成本
翻譯引擎統一採用 Claude Haiku 4.5。以成本為優先,若品質出現問題,我們的方針是先透過提示與詞典加以改善。系統提示中也啟用了 prompt caching,以壓縮每次都相同的固定部分所產生的費用。
實測的參考數據如下。
內容 | 成本 |
|---|---|
單一語系的首次完整翻譯 | 約 $2.5-3 |
所有語系的首次完整翻譯 | 約 $20 |
一般部署(僅快取命中) | 接近 $0 |
新增一篇文章(數種語系的翻譯) | 約 $0.01 |
日常的部署幾乎免費,新增文章每篇也只需約 1 到數塊日圓。初次重建確實需要一筆費用,但過了這一關,營運成本反而比以前更低。
總結
我們從 Google 翻譯換成 LLM 翻譯,結果營運成本幾乎沒有增加,翻譯品質也大幅提升。
使用 LLM 並不代表一切都會自動完美完成。我們仍需持續踏實地找出並修正各種翻譯偏差。不過,可調整的部分已集中在提示詞與詞彙表這兩個一目了然的地方,因此改善的循環變得更容易運作。
從 DTP 踏入網頁世界,不知不覺間,標記、前端、專案指導與無障礙,樣樣精通的「技藝仙人」。自 Liberogic 創業初期就跨領域活躍,如今是公司內部的活字典般的存在。最近沉迷於運用提示詞提升效率的探索,想著「無障礙相關的工作,能不能更依賴 AI 呢?」技術與思考,仍在持續進化中
二俣 步
IAAP認證網頁無障礙專員(WAS) / 標記工程師 / 前端工程師 / 網站總監