From 9f04671dcf6b2be3cdc1c635629b753a8ef0655d Mon Sep 17 00:00:00 2001 From: anomixer Date: Tue, 21 Apr 2026 01:52:28 +0800 Subject: [PATCH] feat: add Traditional Chinese support for web UI --- web/src/components/LanguageSwitcher.tsx | 12 +- web/src/i18n/context.tsx | 5 +- web/src/i18n/en.ts | 2 +- web/src/i18n/tw.ts | 288 ++++++++++++++++++++++++ web/src/i18n/types.ts | 2 +- web/src/i18n/zh.ts | 2 +- 6 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 web/src/i18n/tw.ts diff --git a/web/src/components/LanguageSwitcher.tsx b/web/src/components/LanguageSwitcher.tsx index bf2d300b0c636..9b86acd36ca3d 100644 --- a/web/src/components/LanguageSwitcher.tsx +++ b/web/src/components/LanguageSwitcher.tsx @@ -3,12 +3,16 @@ import { useI18n } from "@/i18n/context"; /** * Compact language toggle — shows a clickable flag that switches between - * English and Chinese. Persists choice to localStorage. + * English, Simplified Chinese, and Traditional Chinese. Persists choice to localStorage. */ export function LanguageSwitcher() { const { locale, setLocale, t } = useI18n(); - const toggle = () => setLocale(locale === "en" ? "zh" : "en"); + const toggle = () => { + if (locale === "en") setLocale("zh"); + else if (locale === "zh") setLocale("tw"); + else setLocale("en"); + }; return ( ); diff --git a/web/src/i18n/context.tsx b/web/src/i18n/context.tsx index 6fc6f6e56a05d..4996c44f8063f 100644 --- a/web/src/i18n/context.tsx +++ b/web/src/i18n/context.tsx @@ -2,14 +2,15 @@ import { createContext, useContext, useState, useCallback, type ReactNode } from import type { Locale, Translations } from "./types"; import { en } from "./en"; import { zh } from "./zh"; +import { tw } from "./tw"; -const TRANSLATIONS: Record = { en, zh }; +const TRANSLATIONS: Record = { en, zh, tw }; const STORAGE_KEY = "hermes-locale"; function getInitialLocale(): Locale { try { const stored = localStorage.getItem(STORAGE_KEY); - if (stored === "en" || stored === "zh") return stored; + if (stored === "en" || stored === "zh" || stored === "tw") return stored; } catch { // SSR or privacy mode } diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 8941fcda4a92b..f27dfe7765809 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -278,7 +278,7 @@ export const en: Translations = { }, language: { - switchTo: "Switch to Chinese", + switchTo: "Switch language", }, theme: { diff --git a/web/src/i18n/tw.ts b/web/src/i18n/tw.ts new file mode 100644 index 0000000000000..61f910ee7e489 --- /dev/null +++ b/web/src/i18n/tw.ts @@ -0,0 +1,288 @@ +import type { Translations } from "./types"; + +export const tw: Translations = { + common: { + save: "儲存", + saving: "儲存中...", + cancel: "取消", + close: "關閉", + delete: "刪除", + refresh: "重新整理", + retry: "重試", + search: "搜尋...", + loading: "載入中...", + create: "建立", + creating: "建立中...", + set: "設定", + replace: "替換", + clear: "清除", + live: "連線中", + off: "離線", + enabled: "已啟用", + disabled: "已停用", + active: "活躍", + inactive: "未啟動", + unknown: "未知", + untitled: "無標題", + none: "無", + form: "表單", + noResults: "無結果", + of: "/", + page: "頁", + msgs: "訊息", + tools: "工具", + match: "相符", + other: "其他", + configured: "已配置", + removed: "已移除", + failedToToggle: "切換失敗", + failedToRemove: "移除失敗", + failedToReveal: "顯示失敗", + collapse: "折疊", + expand: "展開", + general: "一般", + messaging: "訊息平台", + }, + + app: { + brand: "Hermes Agent", + brandShort: "HA", + webUi: "管理面板", + footer: { + name: "Hermes Agent", + org: "Nous Research", + }, + nav: { + status: "狀態", + sessions: "會話", + analytics: "分析", + logs: "日誌", + cron: "排程任務", + skills: "技能", + config: "設定", + keys: "金鑰", + }, + }, + + status: { + agent: "代理", + gateway: "網關", + activeSessions: "活躍會話", + recentSessions: "最近會話", + connectedPlatforms: "已連接平台", + running: "執行中", + starting: "啟動中", + failed: "失敗", + stopped: "已停止", + connected: "已連接", + disconnected: "已中斷", + error: "錯誤", + notRunning: "未執行", + startFailed: "啟動失敗", + pid: "進程", + runningRemote: "執行中(遠端)", + noneRunning: "無", + gatewayFailedToStart: "網關啟動失敗", + lastUpdate: "最後更新", + platformError: "錯誤", + platformDisconnected: "已中斷", + }, + + sessions: { + title: "會話", + searchPlaceholder: "搜尋訊息內容...", + noSessions: "暫無會話", + noMatch: "沒有相符的會話", + startConversation: "開始對話後將顯示在此處", + noMessages: "暫無訊息", + untitledSession: "無標題會話", + deleteSession: "刪除會話", + previousPage: "上一頁", + nextPage: "下一頁", + roles: { + user: "使用者", + assistant: "助理", + system: "系統", + tool: "工具", + }, + }, + + analytics: { + period: "時間範圍:", + totalTokens: "總 Token 數", + totalSessions: "總會話數", + apiCalls: "API 呼叫", + dailyTokenUsage: "每日 Token 用量", + dailyBreakdown: "每日明細", + perModelBreakdown: "模型用量明細", + topSkills: "常用技能", + skill: "技能", + loads: "代理載入", + edits: "代理管理", + lastUsed: "最近使用", + input: "輸入", + output: "輸出", + total: "總計", + noUsageData: "該時間段暫無使用數據", + startSession: "開始會話後將在此顯示分析數據", + date: "日期", + model: "模型", + tokens: "Token", + perDayAvg: "/天 平均", + acrossModels: "共 {count} 個模型", + inOut: "輸入 {input} / 輸出 {output}", + }, + + logs: { + title: "日誌", + autoRefresh: "自動重新整理", + file: "檔案", + level: "層級", + component: "組件", + lines: "行數", + noLogLines: "未找到日誌記錄", + }, + + cron: { + newJob: "新建排程任務", + nameOptional: "名稱(可選)", + namePlaceholder: "例如:每日總結", + prompt: "提示詞", + promptPlaceholder: "代理每次執行時應執行什麼操作?", + schedule: "排程運算式(cron)", + schedulePlaceholder: "0 9 * * *", + deliverTo: "投遞至", + scheduledJobs: "已排程任務", + noJobs: "暫無排程任務。在上方建立一個。", + last: "上次", + next: "下次", + pause: "暫停", + resume: "恢復", + triggerNow: "立即觸發", + delivery: { + local: "本地", + telegram: "Telegram", + discord: "Discord", + slack: "Slack", + email: "郵件", + }, + }, + + skills: { + title: "技能", + searchPlaceholder: "搜尋技能和工具集...", + enabledOf: "已啟用 {enabled}/{total}", + all: "全部", + noSkills: "未找到技能。技能從 ~/.hermes/skills/ 載入", + noSkillsMatch: "沒有相符的技能。", + skillCount: "{count} 個技能", + resultCount: "{count} 個結果", + noDescription: "暫無描述。", + toolsets: "工具集", + toolsetLabel: "{name} 工具集", + noToolsetsMatch: "沒有相符的工具集。", + setupNeeded: "需要設定", + disabledForCli: "CLI 已停用", + more: "還有 {count} 個", + }, + + config: { + configPath: "~/.hermes/config.yaml", + exportConfig: "匯出設定為 JSON", + importConfig: "從 JSON 匯入設定", + resetDefaults: "恢復預設值", + rawYaml: "原始 YAML 設定", + searchResults: "搜尋結果", + fields: "個欄位", + noFieldsMatch: '沒有相符"{query}"的欄位', + configSaved: "設定已儲存", + yamlConfigSaved: "YAML 設定已儲存", + failedToSave: "儲存失敗", + failedToSaveYaml: "YAML 儲存失敗", + failedToLoadRaw: "載入原始設定失敗", + configImported: "設定已匯入 — 請檢查後儲存", + invalidJson: "無效的 JSON 檔案", + categories: { + general: "一般", + agent: "代理", + terminal: "終端", + display: "顯示", + delegation: "委派", + memory: "記憶", + compression: "壓縮", + security: "安全", + browser: "瀏覽器", + voice: "語音", + tts: "文字轉語音", + stt: "語音轉文字", + logging: "日誌", + discord: "Discord", + auxiliary: "輔助", + }, + }, + + env: { + description: "管理儲存在以下位置的 API 金鑰和憑據", + changesNote: "更改會立即儲存到磁碟。活躍會話將自動獲取新金鑰。", + hideAdvanced: "隱藏進階選項", + showAdvanced: "顯示進階選項", + llmProviders: "LLM 提供商", + providersConfigured: "已設定 {configured}/{total} 個提供商", + getKey: "獲取金鑰", + notConfigured: "{count} 個未設定", + notSet: "未設定", + keysCount: "{count} 個金鑰", + enterValue: "輸入值...", + replaceCurrentValue: "替換當前值({preview})", + showValue: "顯示實際值", + hideValue: "隱藏值", + }, + + oauth: { + title: "提供商登入(OAuth)", + providerLogins: "提供商登入(OAuth)", + description: "已連接 {connected}/{total} 個 OAuth 提供商。登入流程目前透過 CLI 執行;點擊「複製命令」並貼到終端機中進行設定。", + connected: "已連接", + expired: "已過期", + notConnected: "未連接。在終端機中執行 {command}。", + runInTerminal: "在終端機中。", + noProviders: "未偵測到支援 OAuth 的提供商。", + login: "登入", + disconnect: "中斷連接", + managedExternally: "外部管理", + copied: "已複製 ✓", + cli: "CLI", + copyCliCommand: "複製 CLI 命令(用於外部/備用方式)", + connect: "連接", + sessionExpires: "會話將在 {time} 後過期", + initiatingLogin: "正在啟動登入流程…", + exchangingCode: "正在交換令牌…", + connectedClosing: "已連接!正在關閉…", + loginFailed: "登入失敗。", + sessionExpired: "會話已過期。點擊重試以開始新的登入。", + reOpenAuth: "重新開啟授權頁面", + reOpenVerification: "重新開啟驗證頁面", + submitCode: "提交代碼", + pasteCode: "貼上授權代碼(包含 #state 後綴也可以)", + waitingAuth: "等待您在瀏覽器中授權…", + enterCodePrompt: "已在新分頁中打開。如果需要,請輸入以下代碼:", + pkceStep1: "已在新分頁打開 claude.ai。請登入並點擊「授權」。", + pkceStep2: "複製授權後顯示的授權代碼。", + pkceStep3: "將代碼貼到下方並提交。", + flowLabels: { + pkce: "瀏覽器登入(PKCE)", + device_code: "裝置代碼", + external: "外部 CLI", + }, + expiresIn: "{time}後過期", + }, + + language: { + switchTo: "切換語言", + }, + + theme: { + title: "主題", + switchTheme: "切換主題", + }, +}; diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index 5ae559c9cd751..cf1e25637a86a 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -1,4 +1,4 @@ -export type Locale = "en" | "zh"; +export type Locale = "en" | "zh" | "tw"; export interface Translations { // ── Common ── diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 34941f616a261..d22b97219c5d9 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -278,7 +278,7 @@ export const zh: Translations = { }, language: { - switchTo: "切换到英文", + switchTo: "切换语言", }, theme: {