Fix remaining hardcoded Chinese UI strings for English locale

This commit is contained in:
Matheus Gois
2026-06-08 17:19:44 +08:00
committed by GitHub
parent e8c7338c31
commit fa93fd6398
7 changed files with 278 additions and 89 deletions
+4 -2
View File
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { buildGatewayUrl } from "@/lib/gateway-url";
import { getPlatformDisplayName } from "@/lib/platforms";
import { useI18n } from "@/lib/i18n";
export interface AgentPlatform {
name: string;
@@ -102,6 +103,7 @@ async function copyText(text: string): Promise<boolean> {
}
function ErrorStatusWithCopy({ error, className }: { error?: string; className?: string }) {
const { t } = useI18n();
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle");
const errorText = (error || "Unknown error").trim() || "Unknown error";
@@ -130,10 +132,10 @@ function ErrorStatusWithCopy({ error, className }: { error?: string; className?:
onClick={onCopy}
className="rounded border border-[var(--border)] px-1.5 py-0.5 text-[10px] text-[var(--text-muted)] hover:text-[var(--text)] hover:border-[var(--accent)]"
>
{copyState === "copied" ? "已复制" : "复制"}
{copyState === "copied" ? t("common.copied") : t("common.copy")}
</button>
{copyState === "failed" && (
<span className="text-[10px] text-amber-300"></span>
<span className="text-[10px] text-amber-300">{t("common.copyFailed")}</span>
)}
</span>
<span className="whitespace-pre-wrap break-words">{errorText}</span>
+24 -31
View File
@@ -2,6 +2,7 @@
'use client';
import { useEffect, useState } from 'react';
import { useI18n, localeToBcp47 } from '@/lib/i18n';
interface DailyItem {
time: string;
@@ -10,6 +11,7 @@ interface DailyItem {
}
function DailyBoard() {
const { t, locale } = useI18n();
const [items, setItems] = useState<DailyItem[]>([]);
const [loading, setLoading] = useState(true);
const [speaking, setSpeaking] = useState(false);
@@ -33,14 +35,16 @@ function DailyBoard() {
const completedItems = items.filter(i => i.status === 'completed');
const inProgressItems = items.filter(i => i.status === 'in-progress');
const reportText = completedItems.length > 0
? `今日共完成 ${completedItems.length} 项任务。${completedItems.map(i => i.content).join('。')}`
: '今日暂无完成记录';
? t('daily.reportCompleted')
.replace('{count}', String(completedItems.length))
.replace('{items}', completedItems.map(i => i.content).join(locale === 'en' ? '. ' : '。'))
: t('daily.reportEmpty');
const speak = () => {
if (!('speechSynthesis' in window)) {
alert('您的浏览器不支持语音合成');
alert(t('daily.noSpeechSupport'));
return;
}
@@ -51,25 +55,18 @@ function DailyBoard() {
}
const u = new SpeechSynthesisUtterance(reportText);
u.lang = 'zh-CN';
u.lang = localeToBcp47(locale);
u.rate = 1.0;
u.onstart = () => setSpeaking(true);
u.onend = () => setSpeaking(false);
u.onerror = () => setSpeaking(false);
setUtterance(u);
window.speechSynthesis.speak(u);
};
const stop = () => {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
}
setSpeaking(false);
};
const today = new Date().toLocaleDateString('zh-CN', {
const today = new Date().toLocaleDateString(localeToBcp47(locale), {
year: 'numeric',
month: 'long',
day: 'numeric',
@@ -78,10 +75,9 @@ function DailyBoard() {
return (
<main className="min-h-screen p-4 md:p-8 max-w-3xl mx-auto">
{/* 头部 */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">📅 </h1>
<h1 className="text-2xl font-bold">📅 {t('daily.title')}</h1>
<p className="text-sm text-[var(--text-muted)] mt-1">{today}</p>
</div>
<div className="flex gap-2">
@@ -96,42 +92,40 @@ function DailyBoard() {
>
{speaking ? (
<>
<span></span>
<span></span> {t('daily.stop')}
</>
) : (
<>
<span>🎤</span>
<span>🎤</span> {t('daily.report')}
</>
)}
</button>
</div>
</div>
{/* 统计 */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
<div className="text-2xl font-bold text-green-400">{completedItems.length}</div>
<div className="text-xs text-[var(--text-muted)]"></div>
<div className="text-xs text-[var(--text-muted)]">{t('daily.completed')}</div>
</div>
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
<div className="text-2xl font-bold text-yellow-400">{inProgressItems.length}</div>
<div className="text-xs text-[var(--text-muted)]"></div>
<div className="text-xs text-[var(--text-muted)]">{t('daily.inProgress')}</div>
</div>
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
<div className="text-2xl font-bold text-gray-400">
{items.length - completedItems.length - inProgressItems.length}
</div>
<div className="text-xs text-[var(--text-muted)]"></div>
<div className="text-xs text-[var(--text-muted)]">{t('daily.pending')}</div>
</div>
</div>
{/* 播报列表 */}
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] overflow-hidden">
{loading ? (
<div className="p-8 text-center text-[var(--text-muted)]">...</div>
<div className="p-8 text-center text-[var(--text-muted)]">{t('common.loading')}</div>
) : items.length === 0 ? (
<div className="p-8 text-center text-[var(--text-muted)]">
{t('daily.noRecords')}
</div>
) : (
<div className="divide-y divide-[var(--border)]">
@@ -143,13 +137,13 @@ function DailyBoard() {
}`}
>
<span className="text-lg mt-0.5">
{item.status === 'completed' ? '✅' :
{item.status === 'completed' ? '✅' :
item.status === 'in-progress' ? '🔄' : '⏳'}
</span>
<div className="flex-1 min-w-0">
<p className={`text-sm ${
item.status === 'completed'
? 'text-[var(--text-muted)] line-through'
item.status === 'completed'
? 'text-[var(--text-muted)] line-through'
: ''
}`}>
{item.content}
@@ -164,10 +158,9 @@ function DailyBoard() {
)}
</div>
{/* 提示 */}
{speaking && (
<div className="mt-4 p-3 rounded-lg bg-blue-500/10 border border-blue-500/30 text-xs text-blue-400">
🎤 ...
🎤 {t('daily.speakingHint')}
</div>
)}
</main>
+13 -13
View File
@@ -133,21 +133,21 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
const res = await fetch("/api/gateway-restart", { method: "POST" });
const data = await res.json();
if (data.ok) {
setRestartMsg("✅ 重啟指令已送出,稍後自動重新檢查…");
setRestartMsg(t("gateway.restartSent"));
setTimeout(() => {
checkHealth();
setRestartMsg(null);
setShowDetail(false);
}, 4000);
} else {
setRestartMsg(`❌ 重啟失敗:${data.error || "未知錯誤"}`);
setRestartMsg(t("gateway.restartFailed").replace("{error}", data.error || t("common.unknownError")));
}
} catch (err: any) {
setRestartMsg(`❌ 重啟失敗:${err.message}`);
setRestartMsg(t("gateway.restartFailed").replace("{error}", err.message));
} finally {
setRestarting(false);
}
}, [restarting, checkHealth]);
}, [restarting, checkHealth, t]);
const handleRestore = useCallback(async (filename: string) => {
if (restoring) return;
@@ -252,7 +252,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
) : showWarning ? (
<span
className={compact ? "text-yellow-400 text-xs cursor-pointer" : "text-yellow-400 text-sm cursor-pointer"}
title="Telegram 連線異常,建議重啟"
title={t("gateway.telegramStall")}
onClick={handleDetailClick}
></span>
) : (
@@ -270,9 +270,9 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
className={`inline-flex items-center gap-1 rounded-full border font-medium transition-colors ${
compact ? "px-1.5 py-0.5 text-[10px]" : "px-2 py-0.5 text-xs"
} bg-orange-500/20 text-orange-300 border-orange-500/40 hover:bg-orange-500/35 cursor-pointer`}
title="查看問題並重啟 Gateway"
title={t("gateway.viewAndRestart")}
>
🔄{!compact && " 重啟"}
🔄{!compact && ` ${t("gateway.restart")}`}
</button>
)}
@@ -280,7 +280,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
{showDetail && (
<div className="absolute top-full left-0 mt-1 z-50 rounded-lg bg-[var(--card)] border border-[var(--border)] shadow-xl text-xs w-72 overflow-hidden">
<div className="px-3 py-2 border-b border-[var(--border)] flex items-center justify-between">
<span className="font-semibold text-[var(--text)]">Gateway </span>
<span className="font-semibold text-[var(--text)]">{t("gateway.statusTitle")}</span>
<button onClick={() => setShowDetail(false)} className="text-[var(--text-muted)] hover:text-[var(--text)] cursor-pointer"></button>
</div>
@@ -289,7 +289,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
<div className="flex items-center gap-2">
<span className="text-[var(--text-muted)]">Process</span>
<span className={health?.ok ? "text-green-400" : "text-red-400"}>
{health?.ok ? "✅ 運作中" : "❌ 無回應"}
{health?.ok ? t("gateway.processRunning") : t("gateway.processDown")}
</span>
</div>
@@ -298,10 +298,10 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
<div className="flex items-start gap-2">
<span className="text-[var(--text-muted)] shrink-0">Telegram</span>
<span className="text-yellow-400">
Polling
{t("gateway.telegramPolling")}
{logResult.lastStallAt && (
<span className="text-[var(--text-muted)] ml-1">
({new Date(logResult.lastStallAt).toLocaleTimeString("zh-TW", { hour: "2-digit", minute: "2-digit" })})
({new Date(logResult.lastStallAt).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })})
</span>
)}
</span>
@@ -312,7 +312,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
{logResult && logResult.issues.includes("subagent_timeout") && (
<div className="flex items-center gap-2">
<span className="text-[var(--text-muted)]">Subagent</span>
<span className="text-orange-400"> timeout </span>
<span className="text-orange-400"> {t("gateway.subagentTimeout")}</span>
</div>
)}
@@ -483,7 +483,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
disabled={restarting}
className="w-full py-1.5 rounded-lg bg-orange-500/20 text-orange-300 border border-orange-500/40 hover:bg-orange-500/35 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer font-medium"
>
{restarting ? "⏳ 重啟中…" : "🔄 重啟 Gateway"}
{restarting ? t("gateway.restarting") : t("gateway.restartGateway")}
</button>
</div>
</div>
+1 -1
View File
@@ -268,7 +268,7 @@ export default function SkillsPage() {
<div>
<h1 className="text-2xl font-bold">{t("skills.title")}</h1>
<p className="text-[var(--text-muted)] text-sm mt-1">
{skills.length} {t("skills.count")}{t("skills.builtin")} {builtinCount} / {t("skills.extension")} {extensionCount} / {t("skills.custom")} {customCount} / {t("skills.workspace")} {workspaceCount}
{t("skills.totalPrefix")} {skills.length} {t("skills.count")}{t("skills.builtin")} {builtinCount} / {t("skills.extension")} {extensionCount} / {t("skills.custom")} {customCount} / {t("skills.workspace")} {workspaceCount}
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
+65 -41
View File
@@ -2,6 +2,7 @@
'use client';
import { useEffect, useState } from 'react';
import { useI18n, localeToBcp47, type Locale } from '@/lib/i18n';
interface AgentState {
agentId: string;
@@ -16,51 +17,76 @@ interface Skill {
status: string;
}
const STATE_CONFIG = {
working: { label: '工作中', emoji: '🔥', color: 'bg-green-500' },
online: { label: '在线', emoji: '🟢', color: 'bg-blue-500' },
idle: { label: '空闲', emoji: '😴', color: 'bg-yellow-500' },
offline: { label: '离线', emoji: '⚫', color: 'bg-gray-500' },
};
function AgentCard({
agent,
t,
locale,
}: {
agent: AgentState;
t: (key: string) => string;
locale: Locale;
}) {
const stateLabels: Record<AgentState['state'], string> = {
working: t('agent.status.working'),
online: t('agent.status.online'),
idle: t('agent.status.idle'),
offline: t('agent.status.offline'),
};
const stateEmojis: Record<AgentState['state'], string> = {
working: '🔥',
online: '🟢',
idle: '😴',
offline: '⚫',
};
const stateColors: Record<AgentState['state'], string> = {
working: 'bg-green-500',
online: 'bg-blue-500',
idle: 'bg-yellow-500',
offline: 'bg-gray-500',
};
function AgentCard({ agent }: { agent: AgentState }) {
const config = STATE_CONFIG[agent.state] || STATE_CONFIG.offline;
const label = stateLabels[agent.state] || stateLabels.offline;
const emoji = stateEmojis[agent.state] || stateEmojis.offline;
const color = stateColors[agent.state] || stateColors.offline;
const lastActiveText = agent.lastActive
? new Date(agent.lastActive).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
: '无记录';
? new Date(agent.lastActive).toLocaleTimeString(localeToBcp47(locale), {
hour: '2-digit',
minute: '2-digit',
})
: t('workspace.noRecord');
return (
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 hover:border-[var(--accent)]/50 transition-all">
<div className="flex items-center gap-3 mb-3">
<div className={`w-3 h-3 rounded-full ${config.color}`} />
<span className="text-2xl">{config.emoji}</span>
<div className={`w-3 h-3 rounded-full ${color}`} />
<span className="text-2xl">{emoji}</span>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-sm truncate">{agent.agentId}</h3>
<p className="text-xs text-[var(--text-muted)]">{config.label}</p>
<p className="text-xs text-[var(--text-muted)]">{label}</p>
</div>
</div>
<div className="text-xs text-[var(--text-muted)]">
: {lastActiveText}
{t('workspace.lastActive')} {lastActiveText}
</div>
</div>
);
}
function SkillCard({ skill }: { skill: Skill }) {
function SkillCard({ skill, t }: { skill: Skill; t: (key: string) => string }) {
return (
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-3 hover:border-[var(--accent)]/50 transition-all">
<div className="flex items-start justify-between gap-2 mb-2">
<h4 className="font-medium text-sm truncate">{skill.name}</h4>
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium ${
skill.status === 'running'
? 'bg-green-500/20 text-green-400'
skill.status === 'running'
? 'bg-green-500/20 text-green-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
{skill.status === 'running' ? '运行中' : '已禁用'}
{skill.status === 'running' ? t('workspace.skillRunning') : t('workspace.skillDisabled')}
</span>
</div>
<p className="text-xs text-[var(--text-muted)] line-clamp-2">
{skill.description || '无描述'}
{skill.description || t('skills.noDesc')}
</p>
<p className="text-[10px] text-[var(--accent)] mt-2">
v{skill.version}
@@ -70,6 +96,7 @@ function SkillCard({ skill }: { skill: Skill }) {
}
export default function WorkspacePage() {
const { t, locale } = useI18n();
const [agents, setAgents] = useState<AgentState[]>([]);
const [skills, setSkills] = useState<Skill[]>([]);
const [loading, setLoading] = useState(true);
@@ -84,17 +111,16 @@ export default function WorkspacePage() {
]);
const agentData = await agentRes.json();
const skillsData = await skillsRes.json();
// Handle both old and new skill format
if (Array.isArray(skillsData.skills)) {
setSkills(skillsData.skills.map((s: any) => ({
name: s.name || s.id || '未知',
setSkills(skillsData.skills.map((s: { name?: string; id?: string; description?: string; version?: string }) => ({
name: s.name || s.id || 'unknown',
description: s.description || '',
version: s.version || 'unknown',
status: 'running',
})));
}
setAgents(agentData.statuses || []);
} catch (error) {
console.error('Failed to fetch data:', error);
@@ -105,71 +131,69 @@ export default function WorkspacePage() {
}
fetchData();
const interval = setInterval(fetchData, 15000); // 15秒刷新
const interval = setInterval(fetchData, 15000);
return () => clearInterval(interval);
}, []);
const workingCount = agents.filter(a => a.state === 'working').length;
const onlineCount = agents.filter(a => a.state === 'online' || a.state === 'working').length;
const refreshTime = lastRefresh.toLocaleTimeString(localeToBcp47(locale));
return (
<main className="min-h-screen p-4 md:p-8 max-w-7xl mx-auto">
{/* 头部 */}
<div className="flex flex-col gap-4 mb-8 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-2xl font-bold">🤖 Agent </h1>
<h1 className="text-2xl font-bold">🤖 {t('nav.workspace')}</h1>
<p className="text-sm text-[var(--text-muted)] mt-1">
Agent · {lastRefresh.toLocaleTimeString('zh-CN')}
{t('workspace.subtitle').replace('{time}', refreshTime)}
</p>
</div>
<div className="flex gap-3">
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
<span className="text-[var(--text-muted)]">线:</span>
<span className="text-[var(--text-muted)]">{t('workspace.online')}</span>
<span className="font-semibold ml-1">{onlineCount}/{agents.length}</span>
</div>
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
<span className="text-[var(--text-muted)]">:</span>
<span className="text-[var(--text-muted)]">{t('workspace.workingCount')}</span>
<span className="font-semibold ml-1 text-green-400">{workingCount}</span>
</div>
</div>
</div>
{/* Agent 网格 */}
<section className="mb-8">
<h2 className="text-lg font-semibold mb-4">📊 Agent </h2>
<h2 className="text-lg font-semibold mb-4">📊 {t('workspace.agentStatus')}</h2>
{loading ? (
<div className="text-center py-8 text-[var(--text-muted)]">...</div>
<div className="text-center py-8 text-[var(--text-muted)]">{t('common.loading')}</div>
) : agents.length === 0 ? (
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
Agent
{t('workspace.noAgents')}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{agents.map((agent) => (
<AgentCard key={agent.agentId} agent={agent} />
<AgentCard key={agent.agentId} agent={agent} t={t} locale={locale} />
))}
</div>
)}
</section>
{/* 技能列表 */}
<section>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">📦 </h2>
<h2 className="text-lg font-semibold">📦 {t('workspace.installedSkills')}</h2>
<span className="text-xs text-[var(--text-muted)]">
{skills.length}
{t('workspace.totalSkills').replace('{count}', String(skills.length))}
</span>
</div>
{loading ? (
<div className="text-center py-8 text-[var(--text-muted)]">...</div>
<div className="text-center py-8 text-[var(--text-muted)]">{t('common.loading')}</div>
) : skills.length === 0 ? (
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
{t('workspace.noSkills')}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{skills.map((skill) => (
<SkillCard key={skill.name} skill={skill} />
<SkillCard key={skill.name} skill={skill} t={t} />
))}
</div>
)}
+168
View File
@@ -13,6 +13,8 @@ const translations: Record<Locale, Record<string, string>> = {
// nav sidebar
"nav.overview": "總覽",
"nav.agents": "機器人",
"nav.workspace": "Agent 工作台",
"nav.daily": "今日播報",
"nav.models": "模型列表",
"nav.monitor": "監控",
"nav.sessions": "會話列表",
@@ -276,6 +278,45 @@ const translations: Record<Locale, Record<string, string>> = {
"skills.contentTitle": "SKILL.md 內容",
"skills.loadingContent": "正在載入技能內容...",
"skills.contentLoadFailed": "技能內容載入失敗",
"skills.totalPrefix": "共",
// workspace page
"workspace.subtitle": "即時監控所有 Agent 狀態 · {time} 更新",
"workspace.online": "在線:",
"workspace.workingCount": "工作中:",
"workspace.agentStatus": "Agent 狀態",
"workspace.noAgents": "暫無 Agent 資料",
"workspace.installedSkills": "已安裝技能",
"workspace.totalSkills": "共 {count} 個技能",
"workspace.noSkills": "暫無技能",
"workspace.lastActive": "最後活躍:",
"workspace.noRecord": "無記錄",
"workspace.skillRunning": "執行中",
"workspace.skillDisabled": "已停用",
"workspace.unknown": "未知",
// daily report page
"daily.title": "今日播報板",
"daily.report": "播報",
"daily.stop": "停止",
"daily.completed": "已完成",
"daily.inProgress": "進行中",
"daily.pending": "待開始",
"daily.noRecords": "今日暫無記錄",
"daily.speakingHint": "正在語音播報今日完成事項...",
"daily.noSpeechSupport": "您的瀏覽器不支援語音合成",
"daily.reportCompleted": "今日共完成 {count} 項任務。{items}",
"daily.reportEmpty": "今日暫無完成記錄",
// theme
"theme.switchToLight": "切換到亮色模式",
"theme.switchToDark": "切換到暗色模式",
// copy
"common.copy": "複製",
"common.copied": "已複製",
"common.copyFailed": "複製失敗",
"common.unknownError": "未知錯誤",
// gateway status
"gateway.healthy": "Gateway 運作正常",
@@ -300,6 +341,18 @@ const translations: Record<Locale, Record<string, string>> = {
"gateway.reloadNow": "立即重新整理",
"gateway.reloadHint": "重新整理後,可點選機器人卡片上的「測試」確認是否正常運作",
"gateway.configCorruptHint": "設定檔可能損毀,請使用上方 Gateway 面板還原備份",
"gateway.restart": "重啟",
"gateway.restartGateway": "🔄 重啟 Gateway",
"gateway.restarting": "⏳ 重啟中…",
"gateway.restartSent": "✅ 重啟指令已送出,稍後自動重新檢查…",
"gateway.restartFailed": "❌ 重啟失敗:{error}",
"gateway.viewAndRestart": "查看問題並重啟 Gateway",
"gateway.telegramStall": "Telegram 連線異常,建議重啟",
"gateway.statusTitle": "Gateway 狀態",
"gateway.processRunning": "✅ 運作中",
"gateway.processDown": "❌ 無回應",
"gateway.telegramPolling": "⚠️ Polling 異常",
"gateway.subagentTimeout": "⚠️ 有 timeout 記錄",
// pixel office
"pixelOffice.title": "OpenClaw Agents 辦公室",
@@ -621,6 +674,45 @@ const translations: Record<Locale, Record<string, string>> = {
"skills.contentTitle": "SKILL.md 内容",
"skills.loadingContent": "正在加载技能内容...",
"skills.contentLoadFailed": "技能内容加载失败",
"skills.totalPrefix": "共",
// workspace page
"workspace.subtitle": "实时监控所有 Agent 状态 · {time} 更新",
"workspace.online": "在线:",
"workspace.workingCount": "工作中:",
"workspace.agentStatus": "Agent 状态",
"workspace.noAgents": "暂无 Agent 数据",
"workspace.installedSkills": "已安装技能",
"workspace.totalSkills": "共 {count} 个技能",
"workspace.noSkills": "暂无技能",
"workspace.lastActive": "最后活跃:",
"workspace.noRecord": "无记录",
"workspace.skillRunning": "运行中",
"workspace.skillDisabled": "已禁用",
"workspace.unknown": "未知",
// daily report page
"daily.title": "今日播报板",
"daily.report": "播报",
"daily.stop": "停止",
"daily.completed": "已完成",
"daily.inProgress": "进行中",
"daily.pending": "待开始",
"daily.noRecords": "今日暂无记录",
"daily.speakingHint": "正在语音播报今日完成事项...",
"daily.noSpeechSupport": "您的浏览器不支持语音合成",
"daily.reportCompleted": "今日共完成 {count} 项任务。{items}",
"daily.reportEmpty": "今日暂无完成记录",
// theme
"theme.switchToLight": "切换到亮色模式",
"theme.switchToDark": "切换到暗色模式",
// copy
"common.copy": "复制",
"common.copied": "已复制",
"common.copyFailed": "复制失败",
"common.unknownError": "未知错误",
// gateway status
"gateway.healthy": "Gateway 运行正常",
@@ -645,6 +737,18 @@ const translations: Record<Locale, Record<string, string>> = {
"gateway.reloadNow": "立即刷新",
"gateway.reloadHint": "刷新后,可点击机器人卡片上的「测试」确认是否正常运作",
"gateway.configCorruptHint": "配置文件可能损坏,请使用上方 Gateway 面板还原备份",
"gateway.restart": "重启",
"gateway.restartGateway": "🔄 重启 Gateway",
"gateway.restarting": "⏳ 重启中…",
"gateway.restartSent": "✅ 重启指令已送出,稍后自动重新检查…",
"gateway.restartFailed": "❌ 重启失败:{error}",
"gateway.viewAndRestart": "查看问题并重启 Gateway",
"gateway.telegramStall": "Telegram 连接异常,建议重启",
"gateway.statusTitle": "Gateway 状态",
"gateway.processRunning": "✅ 运行中",
"gateway.processDown": "❌ 无响应",
"gateway.telegramPolling": "⚠️ Polling 异常",
"gateway.subagentTimeout": "⚠️ 有 timeout 记录",
// pixel office
"pixelOffice.title": "OpenClaw Agents办公室",
@@ -970,6 +1074,45 @@ const translations: Record<Locale, Record<string, string>> = {
"skills.contentTitle": "SKILL.md",
"skills.loadingContent": "Loading skill content...",
"skills.contentLoadFailed": "Failed to load skill content",
"skills.totalPrefix": "Total",
// workspace page
"workspace.subtitle": "Real-time monitoring of all agent statuses · updated {time}",
"workspace.online": "Online:",
"workspace.workingCount": "Working:",
"workspace.agentStatus": "Agent Status",
"workspace.noAgents": "No agent data",
"workspace.installedSkills": "Installed Skills",
"workspace.totalSkills": "{count} skills total",
"workspace.noSkills": "No skills",
"workspace.lastActive": "Last active:",
"workspace.noRecord": "No record",
"workspace.skillRunning": "Running",
"workspace.skillDisabled": "Disabled",
"workspace.unknown": "Unknown",
// daily report page
"daily.title": "Daily Report Board",
"daily.report": "Report",
"daily.stop": "Stop",
"daily.completed": "Completed",
"daily.inProgress": "In Progress",
"daily.pending": "Pending",
"daily.noRecords": "No records today",
"daily.speakingHint": "Speaking today's completed items...",
"daily.noSpeechSupport": "Your browser does not support speech synthesis",
"daily.reportCompleted": "Completed {count} tasks today. {items}",
"daily.reportEmpty": "No completed records today",
// theme
"theme.switchToLight": "Switch to light mode",
"theme.switchToDark": "Switch to dark mode",
// copy
"common.copy": "Copy",
"common.copied": "Copied",
"common.copyFailed": "Copy failed",
"common.unknownError": "Unknown error",
// gateway status
"gateway.healthy": "Gateway is running",
@@ -994,6 +1137,18 @@ const translations: Record<Locale, Record<string, string>> = {
"gateway.reloadNow": "Refresh now",
"gateway.reloadHint": "After refresh, click the Test button on each bot card to verify it's working.",
"gateway.configCorruptHint": "Config may be corrupt. Use the Gateway panel above to restore a backup.",
"gateway.restart": "Restart",
"gateway.restartGateway": "🔄 Restart Gateway",
"gateway.restarting": "⏳ Restarting…",
"gateway.restartSent": "✅ Restart command sent, rechecking shortly…",
"gateway.restartFailed": "❌ Restart failed: {error}",
"gateway.viewAndRestart": "View issues and restart Gateway",
"gateway.telegramStall": "Telegram connection issue, restart recommended",
"gateway.statusTitle": "Gateway Status",
"gateway.processRunning": "✅ Running",
"gateway.processDown": "❌ Not responding",
"gateway.telegramPolling": "⚠️ Polling issue",
"gateway.subagentTimeout": "⚠️ Timeout records",
// pixel office
"pixelOffice.title": "OpenClaw Agents Office",
@@ -1386,6 +1541,19 @@ export function useI18n() {
return useContext(I18nContext);
}
export function localeToBcp47(locale: Locale): string {
switch (locale) {
case "en":
return "en-US";
case "zh-TW":
return "zh-TW";
case "vi":
return "vi-VN";
default:
return "zh-CN";
}
}
export function LanguageSwitcher() {
const { locale, setLocale } = useI18n();
return (
+3 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react";
import { useI18n } from "@/lib/i18n";
export type Theme = "dark" | "light";
@@ -50,11 +51,12 @@ export function useTheme() {
export function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
const { t } = useI18n();
return (
<button
onClick={toggleTheme}
className="px-2 py-1.5 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm hover:border-[var(--accent)] transition cursor-pointer"
title={theme === "dark" ? "切换到亮色模式" : "切换到暗色模式"}
title={theme === "dark" ? t("theme.switchToLight") : t("theme.switchToDark")}
>
{theme === "dark" ? "☀️" : "🌙"}
</button>