我們的部落格「ゆるろぐ」最近其實偷偷推出了英文版。這次就來分享幕後的故事,特別是「在 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」這個方式,成本和使用者體驗都會崩潰。
解決方案:將翻譯從「請求時」移至「內容建立時」
答案很簡單,不在請求時進行翻譯。
具體來說,我們建立了這樣的流程。
- 在EmDash管理面板中撰寫並發布日文文章
- 只執行一次翻譯指令碼(
scripts/translate.mjs)- 從EmDash API取得日文文章的Portable Text(結構化富文本)
- 提取應翻譯的文字部分,並傳送至Gemini API(
gemini-3.6-flash) - 圖片和Amazon商品卡等區塊不進行翻譯,直接通過
- 將翻譯結果儲存為新的英文文章到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年入職