diff --git a/bun.lock b/bun.lock index 276c066..3f5e70e 100644 --- a/bun.lock +++ b/bun.lock @@ -33,11 +33,14 @@ "diff": "^8.0.2", "framer-motion": "^12.0.0-alpha.1", "html2canvas": "^1.4.1", + "i18next": "^25.3.2", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.468.0", "posthog-js": "^1.258.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", + "react-i18next": "^15.6.1", "react-markdown": "^9.0.3", "react-syntax-highlighter": "^15.6.1", "recharts": "^2.14.1", @@ -677,12 +680,18 @@ "highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="], + "html-parse-stringify": ["html-parse-stringify@3.0.1", "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + "i18next": ["i18next@25.3.2", "https://registry.npmmirror.com/i18next/-/i18next-25.3.2.tgz", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA=="], + + "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], + "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], @@ -891,6 +900,8 @@ "react-hook-form": ["react-hook-form@7.58.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA=="], + "react-i18next": ["react-i18next@15.6.1", "https://registry.npmmirror.com/react-i18next/-/react-i18next-15.6.1.tgz", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="], @@ -1023,6 +1034,8 @@ "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "void-elements": ["void-elements@3.1.0", "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], diff --git a/package.json b/package.json index bffa53e..2331db4 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,14 @@ "diff": "^8.0.2", "framer-motion": "^12.0.0-alpha.1", "html2canvas": "^1.4.1", + "i18next": "^25.3.2", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.468.0", "posthog-js": "^1.258.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", + "react-i18next": "^15.6.1", "react-markdown": "^9.0.3", "react-syntax-highlighter": "^15.6.1", "recharts": "^2.14.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5c3951e..5d583fe 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -625,11 +625,14 @@ dependencies = [ "cocoa", "dirs 5.0.1", "env_logger", + "fluent", + "fluent-bundle", "futures", "glob", "libc", "log", "objc", + "once_cell", "regex", "reqwest", "rusqlite", @@ -650,6 +653,7 @@ dependencies = [ "tauri-plugin-updater", "tempfile", "tokio", + "unic-langid", "uuid", "walkdir", "which", @@ -1333,6 +1337,50 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2243,6 +2291,25 @@ dependencies = [ "cfb", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3604,7 +3671,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "socket2", "thiserror 2.0.12", @@ -3624,7 +3691,7 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", @@ -3947,6 +4014,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4129,6 +4202,21 @@ dependencies = [ "thin-slice", ] +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.0", +] + +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + [[package]] name = "semver" version = "1.0.26" @@ -5428,6 +5516,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "typeid" version = "1.0.3" @@ -5472,6 +5569,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unic-ucd-ident" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01efb81..057e020 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -53,6 +53,10 @@ zstd = "0.13" uuid = { version = "1.6", features = ["v4", "serde"] } walkdir = "2" serde_yaml = "0.9" +fluent = "0.16" +fluent-bundle = "0.15" +unic-langid = "0.9" +once_cell = "1.19" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/src-tauri/locales/en/messages.ftl b/src-tauri/locales/en/messages.ftl new file mode 100644 index 0000000..53c8603 --- /dev/null +++ b/src-tauri/locales/en/messages.ftl @@ -0,0 +1,44 @@ +# Error Messages +error-failed-to-create = Failed to create {$item} +error-failed-to-update = Failed to update {$item} +error-failed-to-delete = Failed to delete {$item} +error-failed-to-read = Failed to read {$item} +error-failed-to-write = Failed to write to {$item} +error-file-not-found = File not found: {$path} +error-permission-denied = Permission denied +error-invalid-input = Invalid input provided +error-network-error = Network error occurred +error-database-error = Database error occurred +error-unknown-error = Unknown error occurred + +# Success Messages +success-created = {$item} created successfully +success-updated = {$item} updated successfully +success-deleted = {$item} deleted successfully +success-saved = Saved successfully + +# Agent Messages +agent-not-found = Agent not found +agent-execution-failed = Agent execution failed +agent-invalid-config = Invalid agent configuration + +# Claude Messages +claude-not-installed = Claude Code is not installed +claude-version-check-failed = Failed to check Claude version +claude-execution-failed = Claude execution failed +claude-session-not-found = Claude session not found + +# MCP Messages +mcp-server-not-found = MCP server not found +mcp-connection-failed = Failed to connect to MCP server +mcp-invalid-config = Invalid MCP server configuration + +# Project Messages +project-not-found = Project not found +project-access-denied = Access denied to project +session-not-found = Session not found + +# General Messages +operation-cancelled = Operation cancelled +timeout-error = Operation timed out +validation-error = Validation failed \ No newline at end of file diff --git a/src-tauri/locales/zh/messages.ftl b/src-tauri/locales/zh/messages.ftl new file mode 100644 index 0000000..a0cc707 --- /dev/null +++ b/src-tauri/locales/zh/messages.ftl @@ -0,0 +1,44 @@ +# 错误消息 +error-failed-to-create = 创建{$item}失败 +error-failed-to-update = 更新{$item}失败 +error-failed-to-delete = 删除{$item}失败 +error-failed-to-read = 读取{$item}失败 +error-failed-to-write = 写入{$item}失败 +error-file-not-found = 未找到文件:{$path} +error-permission-denied = 权限被拒绝 +error-invalid-input = 提供的输入无效 +error-network-error = 发生网络错误 +error-database-error = 发生数据库错误 +error-unknown-error = 发生未知错误 + +# 成功消息 +success-created = {$item}创建成功 +success-updated = {$item}更新成功 +success-deleted = {$item}删除成功 +success-saved = 保存成功 + +# 智能体消息 +agent-not-found = 未找到智能体 +agent-execution-failed = 智能体执行失败 +agent-invalid-config = 智能体配置无效 + +# Claude 消息 +claude-not-installed = 未安装 Claude Code +claude-version-check-failed = 检查 Claude 版本失败 +claude-execution-failed = Claude 执行失败 +claude-session-not-found = 未找到 Claude 会话 + +# MCP 消息 +mcp-server-not-found = 未找到 MCP 服务器 +mcp-connection-failed = 连接 MCP 服务器失败 +mcp-invalid-config = MCP 服务器配置无效 + +# 项目消息 +project-not-found = 未找到项目 +project-access-denied = 拒绝访问项目 +session-not-found = 未找到会话 + +# 通用消息 +operation-cancelled = 操作已取消 +timeout-error = 操作超时 +validation-error = 验证失败 \ No newline at end of file diff --git a/src-tauri/src/commands/language.rs b/src-tauri/src/commands/language.rs new file mode 100644 index 0000000..9279664 --- /dev/null +++ b/src-tauri/src/commands/language.rs @@ -0,0 +1,27 @@ +use tauri::command; +use serde::{Deserialize, Serialize}; +use crate::i18n; + +#[derive(Debug, Serialize, Deserialize)] +pub struct LanguageSettings { + pub locale: String, +} + +#[command] +pub async fn get_current_language() -> Result { + Ok(i18n::get_current_locale()) +} + +#[command] +pub async fn set_language(locale: String) -> Result<(), String> { + i18n::set_locale(&locale) + .map_err(|e| format!("Failed to set language: {}", e))?; + + log::info!("Language changed to: {}", locale); + Ok(()) +} + +#[command] +pub async fn get_supported_languages() -> Result, String> { + Ok(i18n::SUPPORTED_LOCALES.iter().map(|&s| s.to_string()).collect()) +} \ No newline at end of file diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index a0fa7e8..0d25fa5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -5,3 +5,4 @@ pub mod usage; pub mod storage; pub mod slash_commands; pub mod proxy; +pub mod language; diff --git a/src-tauri/src/i18n.rs b/src-tauri/src/i18n.rs new file mode 100644 index 0000000..cba2544 --- /dev/null +++ b/src-tauri/src/i18n.rs @@ -0,0 +1,77 @@ +use std::sync::{Arc, Mutex, OnceLock}; + +// 支持的语言 +pub const SUPPORTED_LOCALES: &[&str] = &["en-US", "zh-CN"]; + +// 简化的 I18n 实现,避免线程安全问题 +pub struct SimpleI18n { + current_locale: Arc>, +} + +impl SimpleI18n { + pub fn new() -> Self { + Self { + current_locale: Arc::new(Mutex::new("en-US".to_string())), + } + } + + pub fn set_locale(&self, locale: &str) { + if SUPPORTED_LOCALES.contains(&locale) { + if let Ok(mut current) = self.current_locale.lock() { + *current = locale.to_string(); + } + } + } + + pub fn get_current_locale(&self) -> String { + match self.current_locale.lock() { + Ok(locale) => locale.clone(), + Err(_) => "en-US".to_string(), + } + } + + pub fn t(&self, key: &str) -> String { + let locale = self.get_current_locale(); + + // 简单的翻译映射,避免复杂的 FluentBundle + match (locale.as_str(), key) { + // 英文翻译 + ("en-US", "error-failed-to-create") => "Failed to create".to_string(), + ("en-US", "error-failed-to-update") => "Failed to update".to_string(), + ("en-US", "error-failed-to-delete") => "Failed to delete".to_string(), + ("en-US", "agent-not-found") => "Agent not found".to_string(), + ("en-US", "claude-not-installed") => "Claude Code is not installed".to_string(), + + // 中文翻译 + ("zh-CN", "error-failed-to-create") => "创建失败".to_string(), + ("zh-CN", "error-failed-to-update") => "更新失败".to_string(), + ("zh-CN", "error-failed-to-delete") => "删除失败".to_string(), + ("zh-CN", "agent-not-found") => "未找到智能体".to_string(), + ("zh-CN", "claude-not-installed") => "未安装 Claude Code".to_string(), + + // 默认情况 + _ => key.to_string(), + } + } +} + +// 全局实例 +static GLOBAL_I18N: OnceLock = OnceLock::new(); + +fn get_i18n() -> &'static SimpleI18n { + GLOBAL_I18N.get_or_init(|| SimpleI18n::new()) +} + +// 便捷函数用于全局访问 +pub fn t(key: &str) -> String { + get_i18n().t(key) +} + +pub fn set_locale(locale: &str) -> Result<(), Box> { + get_i18n().set_locale(locale); + Ok(()) +} + +pub fn get_current_locale() -> String { + get_i18n().get_current_locale() +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0a721bc..fb2ac39 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,7 @@ pub mod checkpoint; pub mod claude_binary; pub mod commands; pub mod process; +pub mod i18n; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0589bef..bdd4dee 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,6 +5,7 @@ mod checkpoint; mod claude_binary; mod commands; mod process; +mod i18n; use checkpoint::state::CheckpointState; use commands::agents::{ @@ -43,6 +44,7 @@ use commands::storage::{ storage_insert_row, storage_execute_sql, storage_reset_database, }; use commands::proxy::{get_proxy_settings, save_proxy_settings, apply_proxy_settings}; +use commands::language::{get_current_language, set_language, get_supported_languages}; use process::ProcessRegistryState; use std::sync::Mutex; use tauri::Manager; @@ -249,6 +251,11 @@ fn main() { // Proxy Settings get_proxy_settings, save_proxy_settings, + + // Language Settings + get_current_language, + set_language, + get_supported_languages, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/CreateAgent.tsx b/src/components/CreateAgent.tsx index 3c1ffec..143eb23 100644 --- a/src/components/CreateAgent.tsx +++ b/src/components/CreateAgent.tsx @@ -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 = ({ onAgentCreated, className, }) => { + const { t } = useTranslation(); const [name, setName] = useState(agent?.name || ""); const [selectedIcon, setSelectedIcon] = useState((agent?.icon as AgentIconName) || "bot"); const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || ""); @@ -92,9 +94,9 @@ export const CreateAgent: React.FC = ({ 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 = ({ 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 = ({

- {isEditMode ? "Edit CC Agent" : "Create CC Agent"} + {isEditMode ? t('agents.editAgent') : t('agents.createAgent')}

- {isEditMode ? "Update your Claude Code agent" : "Create a new Claude Code agent"} + {isEditMode ? t('agents.updateAgentDescription') : t('agents.createAgentDescription')}

@@ -153,7 +155,7 @@ export const CreateAgent: React.FC = ({ ) : ( )} - {saving ? "Saving..." : "Save"} + {saving ? t('app.loading') : t('app.save')} diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..f69fa75 --- /dev/null +++ b/src/components/LanguageSwitcher.tsx @@ -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 + * + * + */ +export const LanguageSwitcher: React.FC = ({ + 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 ( + + + + + + {supportedLanguages.map((language) => ( + handleLanguageChange(language.code)} + className="flex items-center justify-between cursor-pointer" + > +
+ {language.nativeName} + {language.name} +
+ {currentLanguage === language.code && ( + + )} +
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index 8917d7d..e7f0a87 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -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 = ({ onAgentsClick, className, }) => { + const { t } = useTranslation(); const [versionStatus, setVersionStatus] = useState(null); const [checking, setChecking] = useState(true); @@ -130,7 +133,7 @@ export const Topbar: React.FC = ({ trigger={statusContent} content={
-

Claude Code not found

+

{t('messages.claudeCodeNotFound')}

                   {versionStatus.output}
@@ -142,7 +145,7 @@ export const Topbar: React.FC = ({
                 className="w-full"
                 onClick={onSettingsClick}
               >
-                Select Claude Installation
+                {t('messages.selectClaudeInstallation')}
               
                = ({
                 rel="noopener noreferrer"
                 className="flex items-center space-x-1 text-xs text-primary hover:underline"
               >
-                Install Claude Code
+                {t('messages.installClaudeCode')}
                 
               
             
@@ -186,7 +189,7 @@ export const Topbar: React.FC = ({ className="text-xs" > - Agents + {t('navigation.agents')} )} @@ -197,7 +200,7 @@ export const Topbar: React.FC = ({ className="text-xs" > - Usage Dashboard + {t('navigation.usage')} + {/* Language Switcher */} + + diff --git a/src/hooks/index.ts b/src/hooks/index.ts index b817e00..ac7189a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -24,3 +24,4 @@ export { useAsyncPerformanceTracker } from './usePerformanceMonitor'; export { TAB_SCREEN_NAMES } from './useAnalytics'; +export { useTranslation, getLanguageDisplayName } from './useTranslation'; diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts new file mode 100644 index 0000000..8f72e2b --- /dev/null +++ b/src/hooks/useTranslation.ts @@ -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; +}; \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 9a78640..e25bf5a 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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 { + try { + return await invoke("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 { + try { + await invoke("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 { + try { + return await invoke("get_supported_languages"); + } catch (error) { + console.error("Failed to get supported languages:", error); + throw error; + } } }; diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts new file mode 100644 index 0000000..e37fffc --- /dev/null +++ b/src/lib/i18n.ts @@ -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; \ No newline at end of file diff --git a/src/locales/en/common.json b/src/locales/en/common.json new file mode 100644 index 0000000..872837d --- /dev/null +++ b/src/locales/en/common.json @@ -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" + } +} \ No newline at end of file diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json new file mode 100644 index 0000000..6579fea --- /dev/null +++ b/src/locales/zh/common.json @@ -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" + } +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index f7d8f21..8a5061b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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";