為網站新增語言

以日文為例,補齊語言設定、介面資源、文件內容與發布驗證。

這一篇要為網站新增日文。目標不只是讓語言選單多一個選項,還要讓介面、連結、業務文案和準備發布的內容一起正常運作。

目前範本啟用英文、簡體中文和繁體中文,實際設定以你專案的 config/i18n.ts 為準。下方以 ja 作為新語言代碼,檔名、設定和內容目錄都使用相同代碼。

一、先區分介面與內容

內容所在位置範例
可用語言與預設語言config/i18n.ts語言代碼、名稱、書寫方向
介面文案locales/<語言>.json導覽、表單、提示、郵件範本文案
文件content/docs/<語言>/教學內文和側邊欄標題
部落格content/blog/<語言>/文章標題、摘要、內文

新增語言設定不會自動翻譯 JSON,也不會產生對應的文件和部落格文章。請先決定這次要發布哪些內容,再準備資源。

二、補齊日文資源

檢查 locales/ja.json 是否存在。有檔案不代表翻譯已完成,請先確認它是否只是空物件,或僅有部分占位內容。

以 locales/en.json 的鍵結構為基準,補齊日文檔案。保留物件層級、鍵名、陣列結構和插值變數,只修改要顯示的文字。

如果跟著前面幾篇教學操作,日文檔案還需要合併下列工作區文案:

"pages": {
  "workspace": {
    "title": "マイワークスペース",
    "description": "保存したリンクをここで管理できます。"
  }
}

這只是需要合併的片段,完整的日文檔案還應包含其他既有頁面的鍵。付費教學新增的 recipes.savedLinks 也要翻譯,不要只檢查範本原本包含的文案。

翻譯時請特別注意:

  • 插值名稱保持一致,例如 {count}、{name},不要翻譯括號中的名稱。
  • HTML 或格式化文字範本中的標籤、連結和變數須保持完整。
  • 多個複數分支之間的 | 是訊息語法,一般的直線符號需要依既有寫法跳脫為 {'|'}。
  • JSON 必須有效,不能加入註解或尾端逗號。

既有的伺服器端和用戶端載入器會從 locales/*.json 找到資源,不需要在載入器中手動加入日文分支。

三、啟用語言

在 config/i18n.ts 的 websiteI18nDefinitions.languages 中啟用或新增:

{
    code: 'ja',
    name: '日本語',
    direction: 'ltr',
},

如果檔案中已有這段被註解的設定,取消註解即可,不要再新增重複項目。本次只新增可選語言,保留原本的 defaultLanguage。變更預設語言會影響預設入口和回退行為,應另行確認後,作為獨立變更處理。

語言型別會從設定推導。元件透過專案既有的翻譯介面讀取文案,不需要撰寫 locale === 'ja' 的判斷。

完成設定後,重新啟動開發伺服器,開啟 /ja/,再前往上一篇教學的 /ja/workspace。確認頁面路由和文案都正確。

四、檢查元件是否使用翻譯資源

伺服器端頁面繼續使用:

serverLocalized({ c, id: 'pages.workspace.title' })

既有的用戶端元件繼續使用 Localized、localized 或專案的翻譯 Hook,不另外建立一套語言對應物件。

如果某段文案仍是英文或中文,先找出它是否直接寫在 JSX 或設定的 value 中。將需要翻譯的文案放進語言資源,再透過資源鍵引用。日期和數字顯示也應沿用既有的格式化工具或 Intl,以目前語言作為參數。

頁面連結使用既有的國際化路由工具,例如 getFullPath('/workspace', locale)。業務 API 通常仍是 /api/...,不要為日文另外建立 /ja/api/...。既有的請求封裝負責帶入語言資訊,自行撰寫請求時,則需要依 API 的語言約定傳遞 Accept-Language。

五、準備日文文件和部落格文章

如果網站會發布文件,請建立 content/docs/ja/。從既有語言目錄複製實際要發布的文章與目錄設定,保留對應檔案的編號和路徑,再翻譯 frontmatter、內文與 meta.json 中的顯示標題。

建議先準備一組精簡但完整的內容:

content/docs/ja/
├── meta.json
└── 000_index.mdx

從同一專案的既有語言目錄複製這兩個檔案最穩妥,再將 meta.json 的 pages 調整為日文目錄中實際存在的頁面。不要直接沿用一份指向尚未翻譯章節的完整側邊欄。

新增語言時,逐一檢查文件內連結。日文目錄中的連結只能指向 /ja/docs/... 下實際存在的日文文章,或目前文章內部的錨點。目標尚未翻譯時,先補齊譯文,或暫時將連結改為純文字說明。不要以其他語言的文章作為替代,也不要產生不存在的頁面位址。各語言對應文章的錨點 ID 統一使用相同的英文名稱。

部落格也採用相同做法,在 content/blog/ja/ 中新增準備發布的文章。只有專案已啟用部落格,而且這次確實要發布日文部落格文章時,才需要新增這些內容。每種語言有哪些文章,以自己的內容目錄為準。

六、檢查缺少的資源鍵

可以在專案根目錄新增暫存檔案 check-ja-keys.mjs,用來比較英文和日文資源的結構:

import { readFileSync } from 'node:fs';

const readLocale = (name) => JSON.parse(
    readFileSync(new URL(`./locales/${name}.json`, import.meta.url), 'utf8'),
);

function collect(value, prefix = '', result = new Map()) {
    if (value !== null && typeof value === 'object') {
        result.set(prefix, Array.isArray(value) ? 'array' : 'object');
        for (const [key, child] of Object.entries(value)) {
            collect(child, prefix ? `${prefix}.${key}` : key, result);
        }
    } else {
        result.set(prefix, typeof value);
    }
    return result;
}

const source = collect(readLocale('en'));
const target = collect(readLocale('ja'));
const missing = [...source.keys()].filter((key) => !target.has(key));
const mismatched = [...source.keys()].filter(
    (key) => target.has(key) && target.get(key) !== source.get(key),
);
const extra = [...target.keys()].filter((key) => !source.has(key));
console.log({ missing, mismatched, extra });
if (missing.length || mismatched.length) process.exitCode = 1;

執行:

node check-ja-keys.mjs

missing 是缺少的鍵,mismatched 表示結構或值的型別不一致,extra 則需要判斷是否為過時的鍵。這項檢查只能找出結構問題,無法確認翻譯品質,也無法取代插值變數和實際介面的人工檢查。使用完畢後,可以刪除暫存指令碼。

七、產生內容並驗證發布結果

執行專案檢查:

npm run lint
npm run typecheck
npm run build

建置會產生內容集合和搜尋索引。需要單獨排查文件編譯或搜尋問題時,可以執行:

npm run gen:search-index

如果部署使用 KV 文件內容,本機同步使用 npm run kv:sync:local,遠端同步使用 npm run kv:sync:remote。既有的 deploy:update 流程會處理發布所需的同步,單獨執行建置不代表遠端內容已更新。

最後依實際使用者流程驗收:

  • 從語言選單切換到日文,能開啟首頁和工作區。
  • 登入、註冊、表單驗證、空白狀態和失敗提示都有對應文案。
  • 購買區域顯示日文名稱和說明,金額仍與實際方案一致。
  • 文件側邊欄只列出有效頁面,內文連結和圖片正常。
  • 日文搜尋能找到已發布的日文文件。
  • 郵件等不在目前頁面上顯示的文案,也依對應流程檢查。
  • 英文、簡體中文和繁體中文入口仍可正常使用。

遇到部分頁面回退到其他語言時,先檢查資源鍵、內容是否存在,以及發布同步的情況。不要在元件中為日文加入暫時的分支,否則下次新增語言時,還得再次修改業務程式碼。更多機制說明請見國際化和文件系統。