feat: 前端 UI 国际化,支持中/英/日三语切换
之前语言设置只影响后端 AI 行为,前端 UI 文字始终为中文。 新增轻量 i18n 模块(React Context + 翻译字典),切换语言后所有 UI 文字即时刷新。 - 新增 i18n 核心模块和 zh-CN/en-US/ja-JP 翻译文件(约 72 个 key) - App.tsx 拆分为 App(Provider)+ AppContent(业务),确保 Hook 可访问 Context - 所有组件通过 useI18n().t() 获取翻译文本 - errors.ts 改为接受翻译函数参数 - storage.ts saveConfig 时派发事件通知 locale 变化
This commit is contained in:
@@ -5,20 +5,13 @@
|
||||
|
||||
import type { ErrorCode } from "../types";
|
||||
|
||||
const ERROR_MESSAGES: Record<ErrorCode, string> = {
|
||||
INVALID_MESSAGE: "消息格式异常,请重试",
|
||||
SESSION_NOT_FOUND: "会话已过期,请重新连接",
|
||||
RATE_LIMITED: "请求太频繁,请稍后再试",
|
||||
IMAGE_TOO_LARGE: "图像过大,请降低分辨率",
|
||||
AUDIO_TOO_SHORT: "语音太短,请再说一句",
|
||||
LLM_TIMEOUT: "AI 响应超时,请重试",
|
||||
LLM_ERROR: "AI 服务异常,请稍后重试",
|
||||
STT_ERROR: "语音识别失败,请重试",
|
||||
TTS_ERROR: "语音合成失败",
|
||||
INTERNAL_ERROR: "服务内部错误,请重试",
|
||||
};
|
||||
|
||||
/** 将错误码转为用户友好文案 */
|
||||
export function getErrorMessage(code: string): string {
|
||||
return ERROR_MESSAGES[code as ErrorCode] ?? `未知错误: ${code}`;
|
||||
/** 将错误码转为用户友好文案(需要传入翻译函数) */
|
||||
export function getErrorMessage(code: string, translate: (key: string) => string): string {
|
||||
const key = `error.${code}`;
|
||||
const translated = translate(key);
|
||||
// 如果翻译函数返回 key 本身(未找到翻译),用 fallback
|
||||
if (translated === key) {
|
||||
return translate("error.unknown") + `: ${code}`;
|
||||
}
|
||||
return translated;
|
||||
}
|
||||
|
||||
86
frontend/src/lib/i18n/en-US.ts
Normal file
86
frontend/src/lib/i18n/en-US.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { TranslationMap } from "./index";
|
||||
|
||||
export const enUS: TranslationMap = {
|
||||
// App header
|
||||
"app.title": "AI Vision Assistant",
|
||||
"app.stats.requests": "requests",
|
||||
|
||||
// Connection status
|
||||
"status.connected": "Connected",
|
||||
"status.connecting": "Connecting...",
|
||||
"status.disconnected": "Disconnected",
|
||||
|
||||
// Settings
|
||||
"settings.title": "Settings",
|
||||
"settings.appearance": "Appearance",
|
||||
"settings.theme": "Theme",
|
||||
"settings.theme.desc": "Switch between light and dark theme",
|
||||
"settings.theme.dark": "Dark",
|
||||
"settings.theme.light": "Light",
|
||||
"settings.session": "Session",
|
||||
"settings.tts": "Voice Response",
|
||||
"settings.tts.desc": "Play voice along with AI response",
|
||||
"settings.detail": "Image Quality",
|
||||
"settings.detail.desc": "High quality for text recognition, low saves bandwidth",
|
||||
"settings.detail.low": "Low",
|
||||
"settings.detail.high": "High",
|
||||
"settings.language": "Language",
|
||||
"settings.language.desc": "Interaction language preference",
|
||||
|
||||
// Video indicators
|
||||
"video.observing": "👁️ Observing",
|
||||
"video.listening": "🎤 Listening...",
|
||||
"video.playing": "🔊 Playing...",
|
||||
"video.initVad": "Initializing voice detection...",
|
||||
"video.clickToStart": "Click the button below to start",
|
||||
"video.cameraOff": "Camera is off",
|
||||
"video.cameraOff.hint": "You can type in the chat panel",
|
||||
|
||||
// Controls
|
||||
"controls.connecting": "Connecting...",
|
||||
"controls.start": "🎙️ Start Session",
|
||||
"controls.cameraOff": "Turn off camera",
|
||||
"controls.cameraOn": "Turn on camera",
|
||||
"controls.micOff": "Turn off microphone",
|
||||
"controls.micOn": "Turn on microphone",
|
||||
"controls.observing": "👁️ Observing",
|
||||
"controls.observation": "👁️ Observe",
|
||||
"controls.interrupt": "⏹ Interrupt",
|
||||
"controls.stop": "End Session",
|
||||
|
||||
// Chat panel
|
||||
"chat.title": "Chat",
|
||||
"chat.mode.observation": "Observing",
|
||||
"chat.reconnecting": "Connection lost, reconnecting...",
|
||||
"chat.empty.prompt": "Click the button below to start",
|
||||
"chat.empty.hint": "Type or speak with AI after connecting",
|
||||
"chat.welcome.prompt": "Type below to start chatting",
|
||||
"chat.welcome.hint": "Or enable microphone for voice chat",
|
||||
"chat.userLabel": "You",
|
||||
"chat.input.placeholder": "Type a message...",
|
||||
"chat.send": "Send",
|
||||
|
||||
// Session messages
|
||||
"session.changeDetected": "👁️ Scene change detected",
|
||||
"session.recognizing": "(Recognizing speech...)",
|
||||
"session.noSpeech": "(No speech detected)",
|
||||
"session.interrupted": "(Interrupted)",
|
||||
|
||||
// Errors
|
||||
"error.INVALID_MESSAGE": "Invalid message format, please retry",
|
||||
"error.SESSION_NOT_FOUND": "Session expired, please reconnect",
|
||||
"error.RATE_LIMITED": "Too many requests, please try again later",
|
||||
"error.IMAGE_TOO_LARGE": "Image too large, please reduce resolution",
|
||||
"error.AUDIO_TOO_SHORT": "Audio too short, please try again",
|
||||
"error.LLM_TIMEOUT": "AI response timed out, please retry",
|
||||
"error.LLM_ERROR": "AI service error, please try again later",
|
||||
"error.STT_ERROR": "Speech recognition failed, please retry",
|
||||
"error.TTS_ERROR": "Voice synthesis failed",
|
||||
"error.INTERNAL_ERROR": "Internal server error, please retry",
|
||||
"error.unknown": "Unknown error",
|
||||
|
||||
// Device errors
|
||||
"error.vadInit": "VAD initialization failed",
|
||||
"error.cameraAccess": "Cannot access camera",
|
||||
"error.micAccess": "Cannot access microphone",
|
||||
};
|
||||
47
frontend/src/lib/i18n/index.ts
Normal file
47
frontend/src/lib/i18n/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// ============================================================
|
||||
// i18n — 轻量国际化模块
|
||||
// 职责:提供翻译函数和 React Context,根据 locale 返回对应语言文本
|
||||
// ============================================================
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import { zhCN } from "./zh-CN";
|
||||
import { enUS } from "./en-US";
|
||||
import { jaJP } from "./ja-JP";
|
||||
|
||||
export type Locale = "zh-CN" | "en-US" | "ja-JP";
|
||||
|
||||
export type TranslationMap = Record<string, string>;
|
||||
|
||||
const translations: Record<Locale, TranslationMap> = {
|
||||
"zh-CN": zhCN,
|
||||
"en-US": enUS,
|
||||
"ja-JP": jaJP,
|
||||
};
|
||||
|
||||
/** 翻译函数:根据 key 和 locale 返回翻译文本 */
|
||||
export function t(key: string, locale: Locale): string {
|
||||
return translations[locale]?.[key] ?? translations["zh-CN"][key] ?? key;
|
||||
}
|
||||
|
||||
/** 从 config.language 值解析为合法 Locale */
|
||||
export function parseLocale(lang: string): Locale {
|
||||
if (lang === "en-US" || lang === "ja-JP") return lang;
|
||||
return "zh-CN";
|
||||
}
|
||||
|
||||
// ---- React Context ----
|
||||
|
||||
export interface I18nContextValue {
|
||||
locale: Locale;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export const I18nContext = createContext<I18nContextValue>({
|
||||
locale: "zh-CN",
|
||||
t: (key) => t(key, "zh-CN"),
|
||||
});
|
||||
|
||||
/** 组件内获取 i18n 的便捷 Hook */
|
||||
export function useI18n() {
|
||||
return useContext(I18nContext);
|
||||
}
|
||||
86
frontend/src/lib/i18n/ja-JP.ts
Normal file
86
frontend/src/lib/i18n/ja-JP.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { TranslationMap } from "./index";
|
||||
|
||||
export const jaJP: TranslationMap = {
|
||||
// App header
|
||||
"app.title": "AI ビジョンアシスタント",
|
||||
"app.stats.requests": "リクエスト",
|
||||
|
||||
// Connection status
|
||||
"status.connected": "接続済み",
|
||||
"status.connecting": "接続中...",
|
||||
"status.disconnected": "未接続",
|
||||
|
||||
// Settings
|
||||
"settings.title": "設定",
|
||||
"settings.appearance": "外観",
|
||||
"settings.theme": "テーマ",
|
||||
"settings.theme.desc": "ライト/ダークテーマを切り替え",
|
||||
"settings.theme.dark": "ダーク",
|
||||
"settings.theme.light": "ライト",
|
||||
"settings.session": "セッション",
|
||||
"settings.tts": "音声応答",
|
||||
"settings.tts.desc": "AI応答と同時に音声を再生",
|
||||
"settings.detail": "画像品質",
|
||||
"settings.detail.desc": "高品質は文字認識に最適、低品質は帯域節約",
|
||||
"settings.detail.low": "低",
|
||||
"settings.detail.high": "高",
|
||||
"settings.language": "言語",
|
||||
"settings.language.desc": "インタラクション言語の設定",
|
||||
|
||||
// Video indicators
|
||||
"video.observing": "👁️ 観察中",
|
||||
"video.listening": "🎤 聞き取り中...",
|
||||
"video.playing": "🔊 再生中...",
|
||||
"video.initVad": "音声検出を初期化中...",
|
||||
"video.clickToStart": "下のボタンをクリックして開始",
|
||||
"video.cameraOff": "カメラがオフです",
|
||||
"video.cameraOff.hint": "右側のチャットでテキスト対話ができます",
|
||||
|
||||
// Controls
|
||||
"controls.connecting": "接続中...",
|
||||
"controls.start": "🎙️ 対話を開始",
|
||||
"controls.cameraOff": "カメラをオフ",
|
||||
"controls.cameraOn": "カメラをオン",
|
||||
"controls.micOff": "マイクをオフ",
|
||||
"controls.micOn": "マイクをオン",
|
||||
"controls.observing": "👁️ 観察中",
|
||||
"controls.observation": "👁️ 観察モード",
|
||||
"controls.interrupt": "⏹ 中断",
|
||||
"controls.stop": "対話を終了",
|
||||
|
||||
// Chat panel
|
||||
"chat.title": "チャット",
|
||||
"chat.mode.observation": "観察モード",
|
||||
"chat.reconnecting": "接続が切断されました。再接続中...",
|
||||
"chat.empty.prompt": "下のボタンをクリックして開始",
|
||||
"chat.empty.hint": "接続後にテキストまたは音声でAIと対話できます",
|
||||
"chat.welcome.prompt": "下にテキストを入力して対話を開始",
|
||||
"chat.welcome.hint": "マイクを有効にして音声対話もできます",
|
||||
"chat.userLabel": "あなた",
|
||||
"chat.input.placeholder": "メッセージを入力...",
|
||||
"chat.send": "送信",
|
||||
|
||||
// Session messages
|
||||
"session.changeDetected": "👁️ シーン変化を検出",
|
||||
"session.recognizing": "(音声認識中...)",
|
||||
"session.noSpeech": "(音声が検出されませんでした)",
|
||||
"session.interrupted": "(中断済み)",
|
||||
|
||||
// Errors
|
||||
"error.INVALID_MESSAGE": "メッセージ形式が無効です。再試行してください",
|
||||
"error.SESSION_NOT_FOUND": "セッションが期限切れです。再接続してください",
|
||||
"error.RATE_LIMITED": "リクエストが多すぎます。しばらく待ってから再試行してください",
|
||||
"error.IMAGE_TOO_LARGE": "画像が大きすぎます。解像度を下げてください",
|
||||
"error.AUDIO_TOO_SHORT": "音声が短すぎます。もう一度お試しください",
|
||||
"error.LLM_TIMEOUT": "AI応答がタイムアウトしました。再試行してください",
|
||||
"error.LLM_ERROR": "AIサービスエラー。しばらく待ってから再試行してください",
|
||||
"error.STT_ERROR": "音声認識に失敗しました。再試行してください",
|
||||
"error.TTS_ERROR": "音声合成に失敗しました",
|
||||
"error.INTERNAL_ERROR": "サーバー内部エラー。再試行してください",
|
||||
"error.unknown": "不明なエラー",
|
||||
|
||||
// Device errors
|
||||
"error.vadInit": "VAD初期化に失敗しました",
|
||||
"error.cameraAccess": "カメラにアクセスできません",
|
||||
"error.micAccess": "マイクにアクセスできません",
|
||||
};
|
||||
86
frontend/src/lib/i18n/zh-CN.ts
Normal file
86
frontend/src/lib/i18n/zh-CN.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { TranslationMap } from "./index";
|
||||
|
||||
export const zhCN: TranslationMap = {
|
||||
// App header
|
||||
"app.title": "AI 视觉对话助手",
|
||||
"app.stats.requests": "次请求",
|
||||
|
||||
// Connection status
|
||||
"status.connected": "已连接",
|
||||
"status.connecting": "连接中...",
|
||||
"status.disconnected": "未连接",
|
||||
|
||||
// Settings
|
||||
"settings.title": "设置",
|
||||
"settings.appearance": "外观",
|
||||
"settings.theme": "主题",
|
||||
"settings.theme.desc": "切换明暗主题",
|
||||
"settings.theme.dark": "深色",
|
||||
"settings.theme.light": "浅色",
|
||||
"settings.session": "会话",
|
||||
"settings.tts": "语音回答",
|
||||
"settings.tts.desc": "AI 回答时同步播放语音",
|
||||
"settings.detail": "图像精度",
|
||||
"settings.detail.desc": "高精度适合识别文字,低精度省流量",
|
||||
"settings.detail.low": "低",
|
||||
"settings.detail.high": "高",
|
||||
"settings.language": "语言",
|
||||
"settings.language.desc": "交互语言偏好",
|
||||
|
||||
// Video indicators
|
||||
"video.observing": "👁️ 观察中",
|
||||
"video.listening": "🎤 正在聆听...",
|
||||
"video.playing": "🔊 正在播放...",
|
||||
"video.initVad": "正在初始化语音检测...",
|
||||
"video.clickToStart": "点击下方按钮开始对话",
|
||||
"video.cameraOff": "摄像头未开启",
|
||||
"video.cameraOff.hint": "可在右侧聊天框打字对话",
|
||||
|
||||
// Controls
|
||||
"controls.connecting": "连接中...",
|
||||
"controls.start": "🎙️ 开始对话",
|
||||
"controls.cameraOff": "关闭摄像头",
|
||||
"controls.cameraOn": "开启摄像头",
|
||||
"controls.micOff": "关闭麦克风",
|
||||
"controls.micOn": "开启麦克风",
|
||||
"controls.observing": "👁️ 观察中",
|
||||
"controls.observation": "👁️ 观察模式",
|
||||
"controls.interrupt": "⏹ 打断",
|
||||
"controls.stop": "结束对话",
|
||||
|
||||
// Chat panel
|
||||
"chat.title": "对话",
|
||||
"chat.mode.observation": "观察模式",
|
||||
"chat.reconnecting": "连接已断开,正在重连...",
|
||||
"chat.empty.prompt": "点击下方按钮开始对话",
|
||||
"chat.empty.hint": "连接后可打字或语音与 AI 交互",
|
||||
"chat.welcome.prompt": "在下方输入文字开始对话",
|
||||
"chat.welcome.hint": "也可以开启麦克风用语音对话",
|
||||
"chat.userLabel": "你",
|
||||
"chat.input.placeholder": "输入文字对话...",
|
||||
"chat.send": "发送",
|
||||
|
||||
// Session messages
|
||||
"session.changeDetected": "👁️ 画面变化检测",
|
||||
"session.recognizing": "(语音识别中...)",
|
||||
"session.noSpeech": "(未识别到语音)",
|
||||
"session.interrupted": "(已打断)",
|
||||
|
||||
// Errors
|
||||
"error.INVALID_MESSAGE": "消息格式异常,请重试",
|
||||
"error.SESSION_NOT_FOUND": "会话已过期,请重新连接",
|
||||
"error.RATE_LIMITED": "请求太频繁,请稍后再试",
|
||||
"error.IMAGE_TOO_LARGE": "图像过大,请降低分辨率",
|
||||
"error.AUDIO_TOO_SHORT": "语音太短,请再说一句",
|
||||
"error.LLM_TIMEOUT": "AI 响应超时,请重试",
|
||||
"error.LLM_ERROR": "AI 服务异常,请稍后重试",
|
||||
"error.STT_ERROR": "语音识别失败,请重试",
|
||||
"error.TTS_ERROR": "语音合成失败",
|
||||
"error.INTERNAL_ERROR": "服务内部错误,请重试",
|
||||
"error.unknown": "未知错误",
|
||||
|
||||
// Device errors
|
||||
"error.vadInit": "VAD 初始化失败",
|
||||
"error.cameraAccess": "无法访问摄像头",
|
||||
"error.micAccess": "无法访问麦克风",
|
||||
};
|
||||
@@ -30,6 +30,8 @@ export function loadConfig(): SessionConfig {
|
||||
export function saveConfig(config: SessionConfig): void {
|
||||
try {
|
||||
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
||||
// 派发自定义事件,通知 App 更新 locale
|
||||
window.dispatchEvent(new CustomEvent("camtalk-config-changed"));
|
||||
} catch {
|
||||
// localStorage 不可用时静默失败
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user