國際化
在 config/i18n.ts 啟用語言,在 locales JSON 管理文案。依產品需求新增語言並完成翻譯後,即可切換,不必另外整合 i18n 框架。
Saavo 範本已內建多語言支援,預設提供英文、簡體中文與繁體中文。不同語言透過 URL 前綴區分,頁面文案則統一從對應的 Locale 檔案讀取。
開發自己的產品時,通常不必再引入其他 i18n 函式庫。只要在設定中啟用需要支援的語言,並補上對應翻譯,就能在頁面與伺服器端程式碼中,直接使用專案提供的 Localized、localized 與 serverLocalized 輸出多語言文案。
既有功能
範本初始化後,即可使用下列國際化功能:
| 功能 | 預設值 | 說明 |
|---|---|---|
| 啟用語言 | en、zh-Hans、zh-Hant | config/i18n.ts |
| 預設語言 | en | 不含語言前綴的路由 |
| 其他語言前綴 | /{code}/... | 例如 /zh-Hans/docs |
| 文案檔案 | locales/en.json 等 | en.json 是鍵名與型別的依據 |
| 語言切換 | 頁面頂端的語言切換器 | 在已啟用的語言之間切換 |
頁面語言依 URL 路徑中的語言前綴決定,API 則透過 Accept-Language 請求標頭辨識語言。雖然配接器保留了 Locale Cookie 的設定名稱,目前的語言偵測流程並不會讀取 Cookie。也就是說,切換語言仍以 URL 與請求標頭為準,Cookie 不會影響語言切換。
除了頁面文案,網站名稱、方案名稱等設定內容也可以支援多語言。需要翻譯的設定可以使用 { value, key } 格式,value 是預設內容,key 則指向 Locale 檔案中的對應翻譯。
此外,瀏覽器端需要使用的設定,統一從 @/libs/config/client 讀取。deploy.ts、使用權益設定與各類金鑰等都屬於伺服器端資訊,不應直接匯入前端程式碼,以免被封裝到瀏覽器資源中。
先體驗語言切換
開啟首頁,使用語言切換器在英文與中文之間切換。預設語言的位址不含前綴:
/ 英文首頁
/zh-Hans 簡體中文首頁
/zh-Hant 繁體中文首頁
/docs 英文文件
/zh-Hans/docs 簡體中文文件
範本的註解中也預留了其他語言,包括使用 RTL 的 ar。這些語言預設未啟用,不會出現在語言清單中。
提示
修改品牌名稱時,需要同步更新所有已啟用語言的對應文案。如果只修改英文版,中文頁面仍可能顯示預設的「Saavo Starter」。
設定多語言
語言清單在 config/i18n.ts 設定,文案則統一儲存在 locales/ 目錄,每種語言對應一份獨立的 JSON 檔案。
新增語言
- 在
languages取消註解,或新增{ code, name, direction }。 - 新增
locales/{code}.json,鍵名須與locales/en.json一致。 - 若有文件或部落格,補齊
content/docs/{code}/與對應的部落格目錄。 - 翻譯法律頁面、電子郵件,以及產品設定中使用的所有 key。
- RTL 語言請設定
direction: 'rtl'。
defaultLanguage 決定沒有語言前綴的路由使用哪種預設語言,因此不要任意修改。變更後,原本預設顯示英文的無前綴頁面,會改成新的預設語言,既有英文存取路徑也可能因此改變。
完整步驟請參考:
修改品牌與介面文案
如果只需要修改某種語言的文案,直接編輯對應的 Locale JSON 即可。如果要同步調整品牌名稱等共用內容,則需要更新所有已啟用語言的翻譯檔案。例如,將產品名稱從「Saavo Starter」改為「My Product」時,除了 locales/en.json,也要同步修改 locales/zh-Hans.json 與 locales/zh-Hant.json。
設定中的 LocalizedText 可以同時包含 value 與 key。設定 key 後,系統會優先讀取 Locale 檔案的翻譯。如果目前語言缺少對應內容,就會使用 value 中的預設英文文案。
元件中提供給使用者的固定文案,也應統一放在 Locale 檔案管理,不要直接在 JSX 硬編碼中英文長句。例如,需要顯示「購買失敗,請重試」的錯誤提示時,應在 Locale 檔案定義 components.billing.purchaseFailed,不要直接在元件寫死「Purchase failed, please try again.」。
在業務程式碼中使用多語言
業務程式碼只使用專案提供的三個 API,全部從 @/components/server/Localized 匯入,不要直接讀取 Locale JSON。
路徑中的 server 只表示 <Localized /> 輸出純字串,不需要額外互動,頁面的用戶端與伺服器端都能使用,不是只能在伺服器端呼叫。localized() 也能在用戶端元件,以及點擊、提交等回呼中使用。只有 serverLocalized() 必須先取得 Ctx 或 WorkerCtx。
依文案出現的位置選擇 API:
| 位置 | API |
|---|---|
| JSX 中的可見文字 | <Localized /> |
aria-label、placeholder、alt、Toast、函式回傳值 | localized() |
| 頁面 SEO、API 回應、電子郵件等已有請求執行脈絡的地方 | serverLocalized() |
JSX 文字:
import { Localized } from '@/components/server/Localized';
<h2>
<Localized
id="components.profileSettings.title"
default="Profile Settings"
/>
</h2>屬性、Toast 等必須使用字串的位置:
import { localized } from '@/components/server/Localized';
import { toast } from 'react-toastify';
<img
alt={localized({
id: 'components.profileSettings.avatar.alt',
default: 'User avatar',
})}
/>
<input
placeholder={localized({
id: 'components.profileSettings.fullName.placeholder',
default: 'Enter your full name',
})}
/>
toast.error(localized({
id: 'components.billing.purchaseFailed',
default: 'Purchase failed, please refresh the page and try again.',
}));伺服器端頁面、API 與電子郵件已持有 c 時:
import { serverLocalized } from '@/components/server/Localized';
const pageTitle = serverLocalized({
c,
id: 'pages.home.title',
default: `${siteName} - A complete SaaS starter for Cloudflare`,
values: { siteName },
});id 必須是 locales/en.json 中已有的鍵名,default 則是缺少翻譯時使用的英文備用文案。電子郵件範本也使用 serverLocalized,例如 email.welcome.text。
需要插入姓名、數量等動態內容時,請在 Locale 中保留完整句子,使用 {name} 這類占位符,並在呼叫時透過 values 傳入。不要將一句話拆成多段翻譯後再組合。
localized({
id: 'components.account.products.cadence.everyMonths',
default: 'Every {count} months',
values: { count },
});日期、時間與金額不要直接寫入翻譯字串排版。先依目前語言格式化,再將結果放入 values。伺服器端使用 c.var.getLocale(),用戶端元件通常由頁面傳入 locale,金額則使用 formatMinorAmount。
import { formatMinorAmount } from '@/libs/utils/currency';
const date = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(paidAt);
const amount = formatMinorAmount(totalMinor, currency, locale);底層雖然使用 @intlify/core,業務程式碼不要直接呼叫它的 numberFormat / datetimeFormat,專案也沒有為這兩種格式另外設定對照表。具名插值使用上述 values,數字與日期則使用 Intl。
翻譯文案中的直線符號需要跳脫
目前 Saavo 的國際化功能使用 @intlify/core 處理翻譯文字。這個函式庫除了提供一般變數插值,也支援複數等訊息語法,其中 | 有特殊意義。
翻譯文字中未跳脫的 |,會被視為複數分支分隔符號,用來依數量選擇不同文案。例如:
{
"itemCount": "One item | {count} items"
}傳入不同數量時,會自動選擇對應內容:
// 輸出 One item
localized({
id: 'itemCount',
values: { count: 1 },
});
// 輸出 5 items
localized({
id: 'itemCount',
values: { count: 5 },
});常見寫法有兩種:
| 文案形式 | 分支意義 |
|---|---|
單數文案 | 複數文案 | 數量為 1 時使用前者,其他情況使用後者 |
零個文案 | 單數文案 | 複數文案 | 分別處理 0、1 與其他數量 |
例如:
{
"itemCount": "No items | One item | {count} items"
}因此,如果希望頁面實際顯示 | 字元,就不能直接寫在翻譯文字中。
一般文案較少遇到這個問題,但標題很容易出現,因為許多人習慣使用 | 分隔網站名稱與頁面標題。例如:
{
"pages": {
"home": {
"title": "{siteName} | Product Overview"
}
}
}這裡的 | 會被翻譯引擎辨識為複數分支分隔符號,最後可能只顯示其中一部分內容。
如果需要顯示實際的 |,應改為:
{
"pages": {
"home": {
"title": "{siteName} {'|'} Product Overview"
}
}
}這樣 | 就會以一般字元輸出,不再參與複數語法解析。
修改頁面標題時很容易遇到這個問題,使用 | 時請特別注意。
上線檢查
語言內容會直接呈現給使用者,上線前建議至少確認:
- 所有已啟用的語言都能透過語言切換器存取,且 URL 前綴正確。
-
locales/en.json、zh-Hans.json、zh-Hant.json中的品牌名稱與重要頁面文案,已換成自己的產品內容。 - 新增語言的 JSON 鍵名與英文一致,沒有缺少 key。
- 該語言有文件與部落格內容,或你可以接受缺少頁面。
- 法律頁面、電子郵件主旨與內文已翻譯。
- 未修改
defaultLanguage,除非已確認無前綴路由的新意義。
建議在每種啟用的語言下,都開啟首頁、登入頁、價格頁與一篇文件,不要只檢查英文。
常見問題
接下來
依接下來要開發的功能,可以繼續閱讀:
大多數產品完成本章後,只需要維護 Locale 檔案與內容目錄。
新頁面從一開始就使用 Localized 輸出文案,避免事後再抽出字串。