This commit is contained in:
2025-08-06 11:32:29 +08:00
parent 90afd6e520
commit 351a79d54c
21 changed files with 915 additions and 16 deletions

View File

@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Toast, ToastContainer } from "@/components/ui/toast";
import { useTranslation } from "@/hooks/useTranslation";
import { api, type Agent } from "@/lib/api";
import { cn } from "@/lib/utils";
import MDEditor from "@uiw/react-md-editor";
@@ -43,6 +44,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
onAgentCreated,
className,
}) => {
const { t } = useTranslation();
const [name, setName] = useState(agent?.name || "");
const [selectedIcon, setSelectedIcon] = useState<AgentIconName>((agent?.icon as AgentIconName) || "bot");
const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || "");
@@ -92,9 +94,9 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
onAgentCreated();
} catch (err) {
console.error("Failed to save agent:", err);
setError(isEditMode ? "Failed to update agent" : "Failed to create agent");
setError(isEditMode ? t('agents.updateFailed') : t('agents.createFailed'));
setToast({
message: isEditMode ? "Failed to update agent" : "Failed to create agent",
message: isEditMode ? t('agents.updateFailed') : t('agents.createFailed'),
type: "error"
});
} finally {
@@ -108,7 +110,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
systemPrompt !== (agent?.system_prompt || "") ||
defaultTask !== (agent?.default_task || "") ||
model !== (agent?.model || "sonnet")) &&
!confirm("You have unsaved changes. Are you sure you want to leave?")) {
!confirm(t('messages.unsavedChanges'))) {
return;
}
onBack();
@@ -135,10 +137,10 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
</Button>
<div>
<h2 className="text-lg font-semibold">
{isEditMode ? "Edit CC Agent" : "Create CC Agent"}
{isEditMode ? t('agents.editAgent') : t('agents.createAgent')}
</h2>
<p className="text-xs text-muted-foreground">
{isEditMode ? "Update your Claude Code agent" : "Create a new Claude Code agent"}
{isEditMode ? t('agents.updateAgentDescription') : t('agents.createAgentDescription')}
</p>
</div>
</div>
@@ -153,7 +155,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
) : (
<Save className="mr-2 h-4 w-4" />
)}
{saving ? "Saving..." : "Save"}
{saving ? t('app.loading') : t('app.save')}
</Button>
</motion.div>

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { Globe, Check } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useTranslation } from '@/hooks/useTranslation';
import { api } from '@/lib/api';
import { cn } from '@/lib/utils';
interface LanguageSwitcherProps {
className?: string;
showText?: boolean;
}
/**
* 语言切换组件
*
* @example
* <LanguageSwitcher />
* <LanguageSwitcher showText={true} className="ml-2" />
*/
export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
className,
showText = false
}) => {
const { changeLanguage, currentLanguage, supportedLanguages } = useTranslation();
const handleLanguageChange = async (languageCode: string) => {
try {
// 映射前端语言代码到后端格式
const backendLocale = languageCode === 'zh' ? 'zh-CN' : 'en-US';
// 同步到后端
await api.setLanguage(backendLocale);
// 更新前端
changeLanguage(languageCode);
} catch (error) {
console.error('Failed to change language:', error);
// 即使后端同步失败,也要尝试更新前端
changeLanguage(languageCode);
}
};
const getCurrentLanguageDisplay = () => {
const currentLang = supportedLanguages.find(lang => lang.code === currentLanguage);
return currentLang?.nativeName || currentLanguage.toUpperCase();
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn("gap-2", className)}
>
<Globe className="h-4 w-4" />
{showText && <span className="hidden sm:inline">{getCurrentLanguageDisplay()}</span>}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{supportedLanguages.map((language) => (
<DropdownMenuItem
key={language.code}
onClick={() => handleLanguageChange(language.code)}
className="flex items-center justify-between cursor-pointer"
>
<div className="flex flex-col">
<span className="font-medium">{language.nativeName}</span>
<span className="text-xs text-muted-foreground">{language.name}</span>
</div>
{currentLanguage === language.code && (
<Check className="h-4 w-4 text-primary" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -3,6 +3,8 @@ import { motion } from "framer-motion";
import { Circle, FileText, Settings, ExternalLink, BarChart3, Network, Info, Bot } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover } from "@/components/ui/popover";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { useTranslation } from "@/hooks/useTranslation";
import { api, type ClaudeVersionStatus } from "@/lib/api";
import { cn } from "@/lib/utils";
@@ -57,6 +59,7 @@ export const Topbar: React.FC<TopbarProps> = ({
onAgentsClick,
className,
}) => {
const { t } = useTranslation();
const [versionStatus, setVersionStatus] = useState<ClaudeVersionStatus | null>(null);
const [checking, setChecking] = useState(true);
@@ -130,7 +133,7 @@ export const Topbar: React.FC<TopbarProps> = ({
trigger={statusContent}
content={
<div className="space-y-3 max-w-xs">
<p className="text-sm font-medium">Claude Code not found</p>
<p className="text-sm font-medium">{t('messages.claudeCodeNotFound')}</p>
<div className="rounded-md bg-muted p-3">
<pre className="text-xs font-mono whitespace-pre-wrap">
{versionStatus.output}
@@ -142,7 +145,7 @@ export const Topbar: React.FC<TopbarProps> = ({
className="w-full"
onClick={onSettingsClick}
>
Select Claude Installation
{t('messages.selectClaudeInstallation')}
</Button>
<a
href="https://www.anthropic.com/claude-code"
@@ -150,7 +153,7 @@ export const Topbar: React.FC<TopbarProps> = ({
rel="noopener noreferrer"
className="flex items-center space-x-1 text-xs text-primary hover:underline"
>
<span>Install Claude Code</span>
<span>{t('messages.installClaudeCode')}</span>
<ExternalLink className="h-3 w-3" />
</a>
</div>
@@ -186,7 +189,7 @@ export const Topbar: React.FC<TopbarProps> = ({
className="text-xs"
>
<Bot className="mr-2 h-3 w-3" />
Agents
{t('navigation.agents')}
</Button>
)}
@@ -197,7 +200,7 @@ export const Topbar: React.FC<TopbarProps> = ({
className="text-xs"
>
<BarChart3 className="mr-2 h-3 w-3" />
Usage Dashboard
{t('navigation.usage')}
</Button>
<Button
@@ -217,7 +220,7 @@ export const Topbar: React.FC<TopbarProps> = ({
className="text-xs"
>
<Network className="mr-2 h-3 w-3" />
MCP
{t('navigation.mcp')}
</Button>
<Button
@@ -227,15 +230,18 @@ export const Topbar: React.FC<TopbarProps> = ({
className="text-xs"
>
<Settings className="mr-2 h-3 w-3" />
Settings
{t('navigation.settings')}
</Button>
{/* Language Switcher */}
<LanguageSwitcher />
<Button
variant="ghost"
size="icon"
onClick={onInfoClick}
className="h-8 w-8"
title="About"
title={t('navigation.about')}
>
<Info className="h-4 w-4" />
</Button>

View File

@@ -24,3 +24,4 @@ export {
useAsyncPerformanceTracker
} from './usePerformanceMonitor';
export { TAB_SCREEN_NAMES } from './useAnalytics';
export { useTranslation, getLanguageDisplayName } from './useTranslation';

View File

@@ -0,0 +1,55 @@
import { useTranslation as useI18nTranslation } from 'react-i18next';
/**
* 自定义 i18n Hook提供类型安全的翻译功能
*/
export const useTranslation = (namespace?: string) => {
const { t, i18n } = useI18nTranslation(namespace || 'common');
/**
* 切换语言
* @param language 语言代码 ('en' | 'zh')
*/
const changeLanguage = (language: string) => {
i18n.changeLanguage(language);
};
/**
* 获取当前语言
*/
const currentLanguage = i18n.language;
/**
* 检查是否是中文
*/
const isChineseLang = currentLanguage.startsWith('zh');
/**
* 获取支持的语言列表
*/
const supportedLanguages = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
];
return {
t,
changeLanguage,
currentLanguage,
isChineseLang,
supportedLanguages,
i18n,
};
};
/**
* 语言选择器组件的辅助函数
*/
export const getLanguageDisplayName = (code: string, displayInNative = false) => {
const languages = {
en: displayInNative ? 'English' : 'English',
zh: displayInNative ? '中文' : 'Chinese',
};
return languages[code as keyof typeof languages] || code;
};

View File

@@ -1879,5 +1879,49 @@ export const api = {
console.error("Failed to delete slash command:", error);
throw error;
}
},
// ================================
// Language Settings
// ================================
/**
* Gets the current language setting
* @returns Promise resolving to the current language locale
*/
async getCurrentLanguage(): Promise<string> {
try {
return await invoke<string>("get_current_language");
} catch (error) {
console.error("Failed to get current language:", error);
throw error;
}
},
/**
* Sets the language setting
* @param locale - Language locale to set (e.g., 'en-US', 'zh-CN')
* @returns Promise resolving when language is set
*/
async setLanguage(locale: string): Promise<void> {
try {
await invoke<void>("set_language", { locale });
} catch (error) {
console.error("Failed to set language:", error);
throw error;
}
},
/**
* Gets the list of supported languages
* @returns Promise resolving to array of supported language locales
*/
async getSupportedLanguages(): Promise<string[]> {
try {
return await invoke<string[]>("get_supported_languages");
} catch (error) {
console.error("Failed to get supported languages:", error);
throw error;
}
}
};

57
src/lib/i18n.ts Normal file
View File

@@ -0,0 +1,57 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// 引入语言资源文件
import en from '@/locales/en/common.json';
import zh from '@/locales/zh/common.json';
// 配置语言检测器
const languageDetectorOptions = {
// 检测顺序
order: ['localStorage', 'navigator', 'htmlTag'],
// 缓存语言到localStorage
caches: ['localStorage'],
// 检查所有可用语言
checkWhitelist: true,
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
// 回退语言
fallbackLng: 'en',
// 调试模式(开发环境)
debug: process.env.NODE_ENV === 'development',
// 语言资源
resources: {
en: {
common: en,
},
zh: {
common: zh,
},
},
// 命名空间配置
defaultNS: 'common',
ns: ['common'],
// 语言检测选项
detection: languageDetectorOptions,
// 插值配置
interpolation: {
escapeValue: false, // React 已经默认防止XSS
},
// 白名单支持的语言
supportedLngs: ['en', 'zh'],
// 非显式支持的语言回退到en
nonExplicitSupportedLngs: true,
});
export default i18n;

156
src/locales/en/common.json Normal file
View File

@@ -0,0 +1,156 @@
{
"app": {
"name": "Claudia",
"welcome": "Welcome to Claudia",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"update": "Update",
"remove": "Remove",
"add": "Add",
"confirm": "Confirm",
"back": "Back",
"next": "Next",
"previous": "Previous",
"refresh": "Refresh",
"close": "Close",
"open": "Open"
},
"navigation": {
"projects": "CC Projects",
"agents": "CC Agents",
"settings": "Settings",
"usage": "Usage Dashboard",
"mcp": "MCP Manager",
"about": "About"
},
"projects": {
"title": "Projects",
"noProjects": "No projects found",
"selectProject": "Select a project",
"openSession": "Open Session",
"newSession": "New Session",
"sessions": "Sessions",
"noSessions": "No sessions found",
"lastModified": "Last Modified",
"sessionHistory": "Session History"
},
"agents": {
"title": "CC Agents",
"newAgent": "New Agent",
"createAgent": "Create Agent",
"editAgent": "Edit Agent",
"deleteAgent": "Delete Agent",
"executeAgent": "Execute Agent",
"agentName": "Agent Name",
"agentIcon": "Agent Icon",
"systemPrompt": "System Prompt",
"defaultTask": "Default Task",
"model": "Model",
"permissions": "Permissions",
"fileAccess": "File Access",
"networkAccess": "Network Access",
"noAgents": "No agents found",
"agentCreated": "Agent created successfully",
"agentUpdated": "Agent updated successfully",
"agentDeleted": "Agent deleted successfully",
"confirmDelete": "Are you sure you want to delete this agent?",
"executionHistory": "Execution History",
"runAgent": "Run Agent",
"agentRuns": "Agent Runs",
"createAgentDescription": "Create a new Claude Code agent",
"updateAgentDescription": "Update your Claude Code agent",
"createFailed": "Failed to create agent",
"updateFailed": "Failed to update agent"
},
"settings": {
"title": "Settings",
"general": "General",
"appearance": "Appearance",
"language": "Language",
"theme": "Theme",
"checkpointSettings": "Checkpoint Settings",
"autoCheckpoint": "Auto Checkpoint",
"checkpointInterval": "Checkpoint Interval",
"maxCheckpoints": "Max Checkpoints",
"proxySettings": "Proxy Settings",
"enableProxy": "Enable Proxy",
"httpProxy": "HTTP Proxy",
"httpsProxy": "HTTPS Proxy",
"noProxy": "No Proxy",
"analyticsConsent": "Analytics Consent",
"enableAnalytics": "Enable Analytics",
"disableAnalytics": "Disable Analytics"
},
"mcp": {
"title": "MCP Server Management",
"addServer": "Add Server",
"serverName": "Server Name",
"serverCommand": "Server Command",
"serverArgs": "Server Arguments",
"testConnection": "Test Connection",
"connectionSuccess": "Connection successful",
"connectionFailed": "Connection failed",
"importFromClaude": "Import from Claude Desktop",
"exportConfig": "Export Configuration",
"noServers": "No MCP servers configured"
},
"usage": {
"title": "Usage Dashboard",
"totalTokens": "Total Tokens",
"totalCost": "Total Cost",
"byModel": "By Model",
"byProject": "By Project",
"byDate": "By Date",
"last7Days": "Last 7 Days",
"last30Days": "Last 30 Days",
"allTime": "All Time",
"exportData": "Export Data"
},
"checkpoint": {
"title": "Checkpoints",
"createCheckpoint": "Create Checkpoint",
"restoreCheckpoint": "Restore Checkpoint",
"deleteCheckpoint": "Delete Checkpoint",
"checkpointName": "Checkpoint Name",
"checkpointMessage": "Checkpoint Message",
"timeline": "Timeline",
"diff": "Diff",
"noCheckpoints": "No checkpoints found"
},
"placeholders": {
"searchProjects": "Search projects...",
"searchAgents": "Search agents...",
"enterAgentName": "Enter agent name...",
"enterSystemPrompt": "Enter system prompt...",
"enterDefaultTask": "Enter default task...",
"enterURL": "Enter URL...",
"searchCommands": "Search commands...",
"enterCommand": "Enter command...",
"enterDescription": "Enter description..."
},
"validation": {
"required": "This field is required",
"invalidEmail": "Invalid email address",
"invalidUrl": "Invalid URL",
"minLength": "Minimum {{count}} characters required",
"maxLength": "Maximum {{count}} characters allowed"
},
"messages": {
"saveSuccess": "Saved successfully",
"deleteSuccess": "Deleted successfully",
"operationFailed": "Operation failed",
"confirmAction": "Are you sure you want to perform this action?",
"unsavedChanges": "You have unsaved changes",
"networkError": "Network error occurred",
"unknownError": "Unknown error occurred",
"claudeCodeNotFound": "Claude Code not found",
"selectClaudeInstallation": "Select Claude Installation",
"installClaudeCode": "Install Claude Code"
}
}

156
src/locales/zh/common.json Normal file
View File

@@ -0,0 +1,156 @@
{
"app": {
"name": "Claudia",
"welcome": "欢迎使用 Claudia",
"loading": "加载中...",
"error": "错误",
"success": "成功",
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"create": "创建",
"update": "更新",
"remove": "移除",
"add": "添加",
"confirm": "确认",
"back": "返回",
"next": "下一步",
"previous": "上一步",
"refresh": "刷新",
"close": "关闭",
"open": "打开"
},
"navigation": {
"projects": "CC 项目",
"agents": "CC 智能体",
"settings": "设置",
"usage": "用量仪表板",
"mcp": "MCP 管理器",
"about": "关于"
},
"projects": {
"title": "项目",
"noProjects": "未找到项目",
"selectProject": "选择项目",
"openSession": "打开会话",
"newSession": "新建会话",
"sessions": "会话",
"noSessions": "未找到会话",
"lastModified": "最近修改",
"sessionHistory": "会话历史"
},
"agents": {
"title": "CC 智能体",
"newAgent": "新建智能体",
"createAgent": "创建智能体",
"editAgent": "编辑智能体",
"deleteAgent": "删除智能体",
"executeAgent": "执行智能体",
"agentName": "智能体名称",
"agentIcon": "智能体图标",
"systemPrompt": "系统提示",
"defaultTask": "默认任务",
"model": "模型",
"permissions": "权限",
"fileAccess": "文件访问",
"networkAccess": "网络访问",
"noAgents": "未找到智能体",
"agentCreated": "智能体创建成功",
"agentUpdated": "智能体更新成功",
"agentDeleted": "智能体删除成功",
"confirmDelete": "确认要删除此智能体吗?",
"executionHistory": "执行历史",
"runAgent": "运行智能体",
"agentRuns": "智能体运行记录",
"createAgentDescription": "创建新的 Claude Code 智能体",
"updateAgentDescription": "更新您的 Claude Code 智能体",
"createFailed": "创建智能体失败",
"updateFailed": "更新智能体失败"
},
"settings": {
"title": "设置",
"general": "常规",
"appearance": "外观",
"language": "语言",
"theme": "主题",
"checkpointSettings": "检查点设置",
"autoCheckpoint": "自动检查点",
"checkpointInterval": "检查点间隔",
"maxCheckpoints": "最大检查点数",
"proxySettings": "代理设置",
"enableProxy": "启用代理",
"httpProxy": "HTTP 代理",
"httpsProxy": "HTTPS 代理",
"noProxy": "无代理",
"analyticsConsent": "分析同意",
"enableAnalytics": "启用分析",
"disableAnalytics": "禁用分析"
},
"mcp": {
"title": "MCP 服务器管理",
"addServer": "添加服务器",
"serverName": "服务器名称",
"serverCommand": "服务器命令",
"serverArgs": "服务器参数",
"testConnection": "测试连接",
"connectionSuccess": "连接成功",
"connectionFailed": "连接失败",
"importFromClaude": "从 Claude Desktop 导入",
"exportConfig": "导出配置",
"noServers": "未配置 MCP 服务器"
},
"usage": {
"title": "用量仪表板",
"totalTokens": "总令牌数",
"totalCost": "总成本",
"byModel": "按模型",
"byProject": "按项目",
"byDate": "按日期",
"last7Days": "最近 7 天",
"last30Days": "最近 30 天",
"allTime": "全部时间",
"exportData": "导出数据"
},
"checkpoint": {
"title": "检查点",
"createCheckpoint": "创建检查点",
"restoreCheckpoint": "恢复检查点",
"deleteCheckpoint": "删除检查点",
"checkpointName": "检查点名称",
"checkpointMessage": "检查点消息",
"timeline": "时间线",
"diff": "差异",
"noCheckpoints": "未找到检查点"
},
"placeholders": {
"searchProjects": "搜索项目...",
"searchAgents": "搜索智能体...",
"enterAgentName": "输入智能体名称...",
"enterSystemPrompt": "输入系统提示...",
"enterDefaultTask": "输入默认任务...",
"enterURL": "输入 URL...",
"searchCommands": "搜索命令...",
"enterCommand": "输入命令...",
"enterDescription": "输入描述..."
},
"validation": {
"required": "此字段为必填项",
"invalidEmail": "无效的邮箱地址",
"invalidUrl": "无效的 URL",
"minLength": "至少需要 {{count}} 个字符",
"maxLength": "最多允许 {{count}} 个字符"
},
"messages": {
"saveSuccess": "保存成功",
"deleteSuccess": "删除成功",
"operationFailed": "操作失败",
"confirmAction": "确认要执行此操作吗?",
"unsavedChanges": "您有未保存的更改",
"networkError": "网络错误",
"unknownError": "未知错误",
"claudeCodeNotFound": "未找到 Claude Code",
"selectClaudeInstallation": "选择 Claude 安装",
"installClaudeCode": "安装 Claude Code"
}
}

View File

@@ -5,6 +5,7 @@ import { ErrorBoundary } from "./components/ErrorBoundary";
import { AnalyticsErrorBoundary } from "./components/AnalyticsErrorBoundary";
import { analytics, resourceMonitor } from "./lib/analytics";
import { PostHogProvider } from "posthog-js/react";
import "./lib/i18n"; // 初始化国际化
import "./assets/shimmer.css";
import "./styles.css";