Topics

用 EmDash 建立多語言部落格的經驗談 ~在 SSR 中降低翻譯 API 成本的方法~

  • column

我們的部落格「ゆるろぐ」最近其實偷偷推出了英文版。這次就來分享幕後的故事,特別是「在 SSR 網站上實現多語言化時,API 成本會不會爆漲?」這個問題怎麼解決的。

前提條件:我們使用 EmDash 這個 CMS

這個部落格是用 EmDash(基於 Astro 的 CMS)運作的。在管理介面撰寫文章,前端則用 Astro 執行伺服器端轉譯(SSR)。

直觀的操作方式類似 WordPress,有時也被稱為「WordPress 的精神後繼者」。

EmDash 本來就內建「多語言內容」的機制,可以讓文章、頁面、選單、分類法(分類與標籤)等在各個語言版本中以不同記錄的形式存放。只需用 translationOf 欄位來標記「這篇英文文章是那篇日文文章的翻譯」,就能自動被視為同一文章的不同語言版本。

從 microCMS 遷移到 EmDash + Cloudflare

說到多語言化,SSG 的話其實滿簡單的

一般來說,若要「用 API 自動化多語言翻譯」,用 SSG(靜態網站生成器)的話就很簡單了。

  • 只在建置時呼叫一次翻譯 API
  • 將結果快取為靜態檔案
  • 然後只需配信該 HTML

因此,無論文章數量增加多少,「只有每次構建時」才會產生 API 成本。頻率也容易控制,快取也很直觀。

但 Yuru Log 是 SSR。那麼每次請求都要呼叫 API 嗎?

問題就在這裡。EmDash 的 CMS 頁面在規格上,全部都是 SSR(output: "server")進行渲染。不是用靜態構建方式來製作頁面,而是每當請求到來時,從 CMS 的 API 和資料庫中取得最新內容並進行繪製的機制。

那麼如果採用「顯示英文頁面時,在當下用 Gemini 進行翻譯並輸出」這樣樸素的實裝方式,會發生什麼呢?

  • 每次請求都會產生翻譯 API 費用
  • 延遲也會增加(必須等待 LLM 的回應)
  • 即使是同一篇文章,每次返回的翻譯結果也可能略有不同(缺乏再現性)

這完全是不可行的。即使是 SSR,採用「每次呼叫 API」這個方式,成本和使用者體驗都會崩潰。

解決方案:將翻譯從「請求時」移至「內容建立時」

答案很簡單,不在請求時進行翻譯。

具體來說,我們建立了這樣的流程。

  1. 在EmDash管理面板中撰寫並發布日文文章
  2. 只執行一次翻譯指令碼(scripts/translate.mjs)
    • 從EmDash API取得日文文章的Portable Text(結構化富文本)
    • 提取應翻譯的文字部分,並傳送至Gemini API(gemini-3.6-flash)
    • 圖片和Amazon商品卡等區塊不進行翻譯,直接通過
  3. 將翻譯結果儲存為新的英文文章到EmDash資料庫
    • 在translationOf中指定原始日文文章的ID,將其連結為同一文章群組的英文版本
    • 會以草稿形式保存,可以審查後再發布

也就是說,翻譯結果不是「即時生成的內容」,而是「保存在 CMS 中、使用另一種語言的內容」。

這樣做的話,SSR 的請求處理本身與平常的文章顯示沒有任何區別。只需使用 EmDash 的標準本地化查詢(getEmDashEntry(..., { locale: "en" }))來從資料庫取出已保存的英文內容。與 Gemini API 的通信只在翻譯文章時進行一次,瀏覽時完全不會發生。

換句話說就是這樣的結構

翻譯執行的時機

請求時的 API 成本

SSG + 每次翻譯

建置時

零(靜態檔案配送)

SSR + 每次翻譯(不建議做法)

每個請求

文章×存取次數次數發生 😱

SSR + 事前翻譯儲存於DB(此次方式)

內容建立時(1次)

零

我這次的領悟是,如果要把SSG「在構建時統一快取」的想法引入SSR的世界,那麼「將翻譯完成的內容作為持久化資料保存在DB中」就能成為其替代方案。即使是SSR,內容本身也不會持續動態生成,只要事先完成翻譯這種「繁重的處理」,配信本身就會和普通CMS頁面一樣輕快。

本機和正式環境DB不同,好幾次都「咦消失了!?」的故事

這次實裝中,最消耗心力的其實是這個。EmDash在本機開發環境和正式環境各自擁有獨立的DB。說起來是理所當然的事,但正因為如此,我多次體驗了「剛才還有的功能消失了!」。

症狀大致如下

  • Header的nav突然變空了
  • 頁腳的 Categories 清單消失了
  • 文章頁面的側邊欄小工具消失了
  • 好不容易建立的本地端選單項目、小工具又恢復成原來的樣子了

而且都在「剛才還在動作」的時機發生,所以一開始我還懷疑自己是不是破壞了什麼,因此花時間尋找原因。

原因 1:將生產環境資料庫匯入本地端的處理(pull.sh)

開發期間,為了「想在本地端也看到生產環境的最新資料」,我多次執行了一個把生產環境資料庫整個匯入本地端的指令稿。這樣做的話,本地端的資料庫就會被完全覆蓋成生產環境的狀態(例如英文文章還是草稿、或者選單結構還在公開前的狀態)。

也就是說,即使我想在本地端確認「生產環境還沒公開的功能」,但在執行 pull 之後,本地端也會恢復成生產環境的狀態,所以在本地端先行進行的工作就會暫時無法看到。

原因 2:每次進入本地端管理介面時都會執行的「種子」處理

另一個原因是,每次重新登入本地端管理介面(開發用的繞過登入機制)時,順帶會重新讀取 seed.json(初始資料定義檔)。

這很麻煩,

  • 選單項目、小工具→ 會被種子端的內容「完全覆蓋」(= 本地手動新增的部分會消失)
  • 分類、標籤等分類法→ 「既存的項目會被跳過」所以不會消失

這樣的話,各個項目的行為規格就不同。所以會出現「分類還在,但選單和小工具每次都消失」這種乍看莫名其妙的現象。

採取的對策

要根本消除這種雙重結構(pull 覆蓋/種子覆蓋)很困難,所以在實務層面上採取了一個折中方案

  • 將 seed.json 中所有示範用的虛擬內容(選單項目、小工具、分類等)全部清空
  • 每次進入本地環境時,都透過 API 重新建立必要的選單和小工具

用這種方式來應對。這不是根治,只是緩解方案,但至少可以防止「被意外的示範資料覆蓋」的事故發生。

總結

  • EmDash 原本就是一個針對每個語言環境都能保有內容的多語言 CMS
  • 如果是 SSG,「在編譯時翻譯→快取」就完成了,但 SSR 的話「每次翻譯」會變成計費地獄
  • 解決方案是在內容建立時而非請求時只執行一次翻譯,並將結果作為一般 CMS 內容存入資料庫
  • 使用的翻譯引擎是 Gemini API(gemini-3.6-flash)
  • 但將 CMS 多語言化時,光是整頓翻譯流程還不夠,「本機與正式環境資料庫分離」本身會衍生新的陷阱,這點也別忘了
  • 特別是「在本機搶先建立正式環境還沒有的狀態進行作業」這類開發方式,容易被環境同步機制(pull、seed、快取等)的副作用所困擾

這次的教訓是,只要事前掌握「翻譯在何時執行」和「環境同步何時、覆蓋什麼」這兩項時序設計,即使在 SSR 環境下也能不被營運成本和事故左右,順利實現多語言對應。

你的網站也考慮多語言化嗎!

本文作者

我主要從事標記語言、JavaScript、React 和 Next.js 的前端開發。看到自己參與的網站順利上線時最開心!興趣是彈吉他。喜歡貓咪和烤地瓜🐱🍠

Hiracchi

前端工程師 / 2022年入職

查看此員工的文章

信心十足的團隊體制與迅速的應對能力是我們的優勢

Liberogic 擁有經驗豐富的人員積極推進專案,因而獲得客戶的高度評價。
我們恰當地安排專案經理和總監,致力於順利推進整個專案。 我們避免不必要的全面投入而導致成本增加,而是採用適材適所配置資源的方式,因此在業務把握到估價制作與提交的速度上也備受好評。

※ 請注意,我們不積極進行 SES 形式的駐場業務。

Slack、Teams、Redmine、Backlog、Asana、Jira、Notion、Google Workspace、Zoom、Webex 等幾乎所有主要的專案管理工具和聊天工具都可供您使用。

請諮詢我們解決您的網站問題。

案例分析