diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 58ac759..241bfd0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,17 +2,20 @@ // CamTalk — 主应用组件(Web 端双栏布局) // ============================================================ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useVisionSession } from "./hooks/useVisionSession"; import { VideoPreview } from "./components/VideoPreview"; import { ChatPanel } from "./components/ChatPanel"; import { ConfigPanel } from "./components/ConfigPanel"; import { ToastContainer } from "./components/Toast"; -import { loadTheme, saveTheme } from "./lib/storage"; +import { loadConfig, loadTheme, saveTheme } from "./lib/storage"; +import { I18nContext, parseLocale, t } from "./lib/i18n"; +import type { Locale } from "./lib/i18n"; import type { Theme } from "./types"; import "./App.css"; -function App() { +/** 内部组件,确保在 I18nContext.Provider 内部使用 useVisionSession */ +function AppContent() { const [showConfig, setShowConfig] = useState(false); const [theme, setTheme] = useState(loadTheme); const [elapsed, setElapsed] = useState(0); @@ -88,28 +91,30 @@ function App() { return () => clearInterval(id); }, [isConnected]); + const { t: tr } = useMemo(() => ({ t: (key: string) => t(key, parseLocale(config.language)) }), [config.language]); + return (
{/* ---- 顶部导航栏 ---- */}

CamTalk

- AI 视觉对话助手 + {tr("app.title")}
{isConnected && (stats.queryCount > 0 || stats.totalTokens > 0) && ( - {stats.queryCount} 次请求 · {stats.totalTokens} tokens + {stats.queryCount} {tr("app.stats.requests")} · {stats.totalTokens} tokens )} - {isConnected ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"} + {isConnected ? tr("status.connected") : connectionStatus === "connecting" ? tr("status.connecting") : tr("status.disconnected")}
)} {isConnected && !isCameraOn && ( @@ -179,8 +184,8 @@ function App() { - 摄像头未开启 - 可在右侧聊天框打字对话 + {tr("video.cameraOff")} + {tr("video.cameraOff.hint")}
)} @@ -189,21 +194,21 @@ function App() {
{!isConnected ? ( ) : (
@@ -211,15 +216,15 @@ function App() { className={`btn ${mode === "observation" ? "btn--active" : "btn--secondary"}`} onClick={toggleMode} > - {mode === "observation" ? "👁️ 观察中" : "👁️ 观察模式"} + {mode === "observation" ? tr("controls.observing") : tr("controls.observation")} {isProcessing && ( )}
)} @@ -229,15 +234,15 @@ function App() { {/* 右侧:聊天面板 */}
- 对话 + {tr("chat.title")} {isConnected && mode === "observation" && ( - 观察模式 + {tr("chat.mode.observation")} )}
{connectionStatus === "disconnected" && messages.length > 0 && (
- 连接已断开,正在重连... + {tr("chat.reconnecting")}
)} (() => parseLocale(loadConfig().language)); + + const i18nValue = useMemo(() => ({ + locale, + t: (key: string) => t(key, locale), + }), [locale]); + + // 监听配置变化以更新 locale + useEffect(() => { + const handler = () => { + const cfg = loadConfig(); + setLocale(parseLocale(cfg.language)); + }; + // 自定义事件,由 saveConfig 触发 + window.addEventListener("camtalk-config-changed", handler); + return () => window.removeEventListener("camtalk-config-changed", handler); + }, []); + + return ( + + + + ); +} + export default App; diff --git a/frontend/src/components/CameraManager/index.tsx b/frontend/src/components/CameraManager/index.tsx index 0cb76e0..7b950a4 100644 --- a/frontend/src/components/CameraManager/index.tsx +++ b/frontend/src/components/CameraManager/index.tsx @@ -4,6 +4,7 @@ // ============================================================ import { useCallback, useRef, useState } from "react"; +import { useI18n } from "../../lib/i18n"; export interface CameraManagerHandle { /** 获取当前视频轨道 */ @@ -13,6 +14,7 @@ export interface CameraManagerHandle { } export function useCamera() { + const { t } = useI18n(); const videoRef = useRef(null); const [stream, setStream] = useState(null); const [error, setError] = useState(null); @@ -29,7 +31,7 @@ export function useCamera() { } setError(null); } catch (err) { - const message = err instanceof Error ? err.message : "无法访问摄像头"; + const message = err instanceof Error ? err.message : t("error.cameraAccess"); setError(message); console.error("[Camera] 获取摄像头失败:", err); } diff --git a/frontend/src/components/ChatPanel/index.tsx b/frontend/src/components/ChatPanel/index.tsx index 5836226..e19ada5 100644 --- a/frontend/src/components/ChatPanel/index.tsx +++ b/frontend/src/components/ChatPanel/index.tsx @@ -4,6 +4,7 @@ // ============================================================ import { useEffect, useRef, useState } from "react"; +import { useI18n } from "../../lib/i18n"; import type { ChatMessage } from "../../types"; import type { ConnectionStatus } from "../../lib/websocket"; @@ -19,6 +20,7 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText const containerRef = useRef(null); const isAutoScroll = useRef(true); const [inputText, setInputText] = useState(""); + const { t } = useI18n(); const isConnected = connectionStatus === "connected"; @@ -56,8 +58,8 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText return (
💬 -

点击下方按钮开始对话

- 连接后可打字或语音与 AI 交互 +

{t("chat.empty.prompt")}

+ {t("chat.empty.hint")}
); } @@ -69,15 +71,15 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText {messages.length === 0 && !currentReply && (
💬 -

在下方输入文字开始对话

- 也可以开启麦克风用语音对话 +

{t("chat.welcome.prompt")}

+ {t("chat.welcome.hint")}
)} {messages.map((msg, index) => (
- {msg.role === "user" ? "你" : "AI"} + {msg.role === "user" ? t("chat.userLabel") : "AI"}
{msg.content}
{msg.role === "assistant" && msg.tokensUsed !== undefined && ( @@ -110,7 +112,7 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText setInputText(e.target.value)} /> @@ -118,7 +120,7 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText type="submit" className="chat-input__send" disabled={!inputText.trim()} - title="发送" + title={t("chat.send")} > ➤ diff --git a/frontend/src/components/ConfigPanel/index.tsx b/frontend/src/components/ConfigPanel/index.tsx index 3844e6d..9523ec6 100644 --- a/frontend/src/components/ConfigPanel/index.tsx +++ b/frontend/src/components/ConfigPanel/index.tsx @@ -3,6 +3,7 @@ // 职责:主题切换、TTS 开关、detail level 切换、语言选择 // ============================================================ +import { useI18n } from "../../lib/i18n"; import type { SessionConfig, Theme } from "../../types"; interface ConfigPanelProps { @@ -14,40 +15,42 @@ interface ConfigPanelProps { } export function ConfigPanel({ config, theme, onUpdate, onThemeChange, onClose }: ConfigPanelProps) { + const { t } = useI18n(); + return (
e.stopPropagation()}>
- 设置 + {t("settings.title")}
-
外观
+
{t("settings.appearance")}
-
会话
+
{t("settings.session")}