From 32f5b0b7588ac802656850e7dcbb67307fb8de29 Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:25:34 +0200 Subject: [PATCH 1/7] i18n: add 290+ bilingual translation keys for en_US and zh_CN --- locales/en_US.json | 1374 +++++++++++++++++++++++++++++++----------- locales/zh_CN.json | 1408 ++++++++++++++++++++++++++++++++------------ 2 files changed, 2035 insertions(+), 747 deletions(-) diff --git a/locales/en_US.json b/locales/en_US.json index 92a16d0..c3d43ec 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -3,28 +3,28 @@ "title": "OpenCode Config Manager", "version": "Version" }, - "menu": { - "home": "Home", - "provider": "Provider Management", - "native_provider": "Native Provider", - "model": "Model Management", - "mcp": "MCP Server", - "agent": "Agent Config", - "ohmyagent": "Oh My Agent", - "category": "Category Management", - "permission": "Permission Management", - "skill": "Skill Management", - "rules": "Rules Management", - "compaction": "Context Compaction", - "backup": "Backup Management", - "import": "External Import", - "export": "CLI Tool Export", - "monitor": "Monitor", - "theme": "Toggle Theme", - "help": "Help", - "settings": "Settings", - "language": "Language" - }, + "menu": { + "home": "Home", + "provider": "Provider Management", + "native_provider": "Native Provider", + "model": "Model Management", + "mcp": "MCP Server", + "agent": "Agent Config", + "ohmyagent": "Oh My Agent", + "category": "Category Management", + "permission": "Permission Management", + "skill": "Skill Management", + "rules": "Rules Management", + "compaction": "Context Compaction", + "backup": "Backup Management", + "import": "External Import", + "export": "CLI Tool Export", + "monitor": "Monitor", + "theme": "Toggle Theme", + "help": "Help", + "settings": "Settings", + "language": "Language" + }, "common": { "add": "Add", "edit": "Edit", @@ -75,23 +75,33 @@ "cut": "Cut", "undo": "Undo", "redo": "Redo", - "select_item_first": "Please select an item first", - "added": "Added", - "updated": "Updated", - "add_from_preset": "Add from Preset", - "bulk_model": "Bulk Model", - "keep_all": "- Keep All -", - "confirm_delete_title": "Confirm Delete", - "please_enter_pattern": "Please enter pattern", - "please_enter_source": "Please enter source", - "please_enter_file_path": "Please enter file path", - "cannot_convert_format": "Cannot convert this configuration format", - "config_not_exist_or_empty": "Selected configuration does not exist or is empty", - "no_valid_import_config": "No valid import configuration confirmed", - "sdk": "SDK", - "provider": "Provider", - "no_data": "No data" - }, + "select_item_first": "Please select an item first", + "added": "Added", + "updated": "Updated", + "add_from_preset": "Add from Preset", + "bulk_model": "Bulk Model", + "keep_all": "- Keep All -", + "confirm_delete_title": "Confirm Delete", + "please_enter_pattern": "Please enter pattern", + "please_enter_source": "Please enter source", + "please_enter_file_path": "Please enter file path", + "cannot_convert_format": "Cannot convert this configuration format", + "config_not_exist_or_empty": "Selected configuration does not exist or is empty", + "no_valid_import_config": "No valid import configuration confirmed", + "sdk": "SDK", + "provider": "Provider", + "no_data": "No data", + "failed": "Failed", + "testing": "Testing", + "optional": "Optional", + "unlimited": "Unlimited", + "please_wait": "Please wait", + "local": "Local", + "latest": "Latest", + "has_update": "Update available", + "check_failed": "Check failed", + "unknown": "Unknown" + }, "home": { "title": "Home", "welcome": "Welcome to OpenCode Config Manager", @@ -136,13 +146,15 @@ "reset_to_default_backup": "Reset to default backup directory", "config_reloaded": "Configuration reloaded", "backup_success": "Configuration backed up", - "backup_failed": "Backup failed" + "backup_failed": "Backup failed", + "detect_vendors": "Vendor identification", + "base_preset": "Base" }, - "provider": { - "title": "Provider Management", - "custom_provider": "Custom Provider", - "native_provider": "Native Provider", - "add_provider": "Add Provider", + "provider": { + "title": "Provider Management", + "custom_provider": "Custom Provider", + "native_provider": "Native Provider", + "add_provider": "Add Provider", "edit_provider": "Edit Provider", "delete_provider": "Delete Provider", "fetch_models": "Fetch Models", @@ -166,7 +178,6 @@ "deleted_success": "Provider \"{name}\" deleted", "select_first": "Please select a Provider first", "fetch_models_hint": "Fetching model list from {name}...", - "fetch_failed": "Fetch failed: {error}", "no_models_found": "No models found", "no_models_selected": "No models selected", "models_added": "{count} models added", @@ -244,18 +255,20 @@ "usage_rate": "Usage Rate", "expiry": "Expiry", "never_expire": "Never Expire", - "test_connection": "Test Connection", - "connection_success": "Connection Successful", - "connection_failed": "Connection Failed", - "response_time": "Response Time", - "test_failed": "Test Failed", - "please_configure_provider": "Please configure this Provider first", - "api_key_not_found": "API Key not found", - "cannot_determine_api_address": "Cannot determine API address", - "fetch_failed": "Fetch Failed", - "enter_name": "Please enter Provider name", - "provider_exists": "Provider \"{name}\" already exists" - }, + "test_connection": "Test Connection", + "connection_success": "Connection Successful", + "connection_failed": "Connection Failed", + "response_time": "Response Time", + "test_failed": "Test Failed", + "please_configure_provider": "Please configure this Provider first", + "api_key_not_found": "API Key not found", + "cannot_determine_api_address": "Cannot determine API address", + "fetch_failed": "Fetch Failed", + "enter_name": "Please enter Provider name", + "provider_exists": "Provider \"{name}\" already exists", + "api_key_hint": "API Key not found. Please configure the Provider or set environment variables.", + "no_base_url_skip": ", skipping auto-fetch" + }, "model": { "title": "Model Management", "add_model": "Add Model", @@ -289,16 +302,16 @@ "support_attachment": "Support Attachment (Image/Document)", "input_modality": "Input Modality", "output_modality": "Output Modality", - "configure_provider": "Configure {name}", - "test_failed_not_configured": "Test Failed: Please configure {field} first", - "options_config": "Options Configuration", - "variants_config": "Variants", - "reasoning_config": "Reasoning Configuration", - "one_click_add": "Quick Add", - "fetch_models": "Fetch Models", - "select_models_to_add": "Select Models to Add", - "add_selected": "Add Selected", - "key_value_list": "Key-Value List", + "configure_provider": "Configure {name}", + "test_failed_not_configured": "Test Failed: Please configure {field} first", + "options_config": "Options Configuration", + "variants_config": "Variants", + "reasoning_config": "Reasoning Configuration", + "one_click_add": "Quick Add", + "fetch_models": "Fetch Models", + "select_models_to_add": "Select Models to Add", + "add_selected": "Add Selected", + "key_value_list": "Key-Value List", "key": "Key", "value": "Value", "add": "Add", @@ -370,15 +383,15 @@ "updated_success": "MCP server updated", "deleted_success": "MCP \"{name}\" deleted", "ohmy_updated_success": "Oh My MCP config updated", - "select_first": "Please select an MCP server first", - "additional_info": "Additional Information (Click to Expand/Collapse)", - "tags": "Tags", - "homepage_link": "Homepage Link", - "docs_link": "Documentation Link", - "full_json_preview": "Full JSON Preview", - "full_mcp_config_preview": "Full MCP Configuration Preview", - "include_wrapper": "Include mcpServers Wrapper", - "dialog": { + "select_first": "Please select an MCP server first", + "additional_info": "Additional Information (Click to Expand/Collapse)", + "tags": "Tags", + "homepage_link": "Homepage Link", + "docs_link": "Documentation Link", + "full_json_preview": "Full JSON Preview", + "full_mcp_config_preview": "Full MCP Configuration Preview", + "include_wrapper": "Include mcpServers Wrapper", + "dialog": { "edit_title": "Edit MCP", "add_local_title": "Add Local MCP", "add_remote_title": "Add Remote MCP", @@ -429,7 +442,15 @@ "context7_desc": "Get latest official documentation - Fetch up-to-date official docs for libraries and frameworks", "grep_app_desc": "Ultra-fast code search - Search code across millions of public GitHub repositories via grep.app", "default_desc": "{name} MCP Server" - } + }, + "preset_fetch": "Basic MCP server for fetching web content and resources", + "preset_time": "Lightweight MCP server providing time-related tools", + "preset_memory": "MCP server providing memory graph capabilities", + "preset_thinking": "MCP server for structured reasoning and step-by-step thinking", + "preset_context7": "Context7 MCP providing latest documentation retrieval", + "preset_chrome": "Debug MCP server connecting to Chrome DevTools", + "preset_websearch": "MCP server for open web search and page opening", + "preset_serena": "Serena MCP providing local project understanding and command execution" }, "agent": { "title": "Agent Config", @@ -490,7 +511,10 @@ "add_selected": "Add Selected", "select_button": "Select", "cancel_button": "Cancel" - } + }, + "enter_description": "Please enter Agent description", + "permission_json_error": "Permission config JSON format error: {error}", + "apply_group_failed": "Failed to apply group: {error}" }, "skill": { "title": "Skill Management", @@ -539,18 +563,18 @@ "source_claude_global": "🌐 Claude Global", "source_claude_project": "📁 Claude Project", "source_unknown": "❓ Unknown", - "market_dialog": { - "title": "Skill Market", - "subtitle": "Curated Skills Collection", - "search_placeholder": "Search Skills...", - "category_all": "All Categories", - "category_label": "Category", - "repo_label": "Repository", - "install_button": "Install Selected", - "close_button": "Close", - "no_selection": "Please select Skill(s) to install first", - "browse_more": "🌐 Browse More Community Skills (SkillsMP.com)" - }, + "market_dialog": { + "title": "Skill Market", + "subtitle": "Curated Skills Collection", + "search_placeholder": "Search Skills...", + "category_all": "All Categories", + "category_label": "Category", + "repo_label": "Repository", + "install_button": "Install Selected", + "close_button": "Close", + "no_selection": "Please select Skill(s) to install first", + "browse_more": "🌐 Browse More Community Skills (SkillsMP.com)" + }, "categories": { "dev_tools": "Development Tools", "code_quality": "Code Quality", @@ -782,42 +806,20 @@ "prune_old_output": "Prune Old Output (prune) - Remove old tool outputs to save tokens" }, "monitor": { - "title": "Monitor", - "start_monitoring": "Start Monitoring", - "stop_monitoring": "Stop Monitoring", - "check_now": "Check Now", - "clear_history": "Clear History", - "monitoring_targets": "Monitoring Targets", - "status": "Status", - "latency": "Latency", - "last_check": "Last Check", - "history": "History", - "available": "Available", - "unavailable": "Unavailable", - "checking": "Checking...", - "never_checked": "Never Checked", - "monitoring_started": "Monitoring started", - "monitoring_stopped": "Monitoring stopped", - "history_cleared": "History cleared", - "no_targets": "No Targets", - "availability_rate": "Availability", - "error_count": "Errors", - "chat_latency": "Chat Latency", - "ping": "Ping", - "target_count": "Targets", - "last_checked_short": "Recent", - "check": "Check", - "start": "Start", - "toggle_tooltip": "Start/Stop automatic chat latency detection", - "start_tooltip": "Start automatic chat latency detection (Ping detection is not affected)", - "stop_tooltip": "Stop automatic chat latency detection (Ping detection is not affected)", - "model_provider": "Model/Provider", - "ping_latency": "Ping Latency", - "status_operational": "Operational", - "status_degraded": "Degraded", - "status_failed": "Failed", - "status_error": "Error", - "status_no_config": "Not Configured" + "request_timeout": "Request timeout", + "no_base_url": "baseURL not configured", + "chat_test_paused": "Chat test paused (Ping OK)", + "ping_failed": "Ping failed", + "no_valid_host": "No valid host configured", + "no_api_key": "apiKey not configured", + "base_url_invalid": "baseURL is invalid", + "status_normal": "Normal", + "latency_high": "High latency ({latency_ms}ms)", + "auth_failed": "Authentication failed", + "connection_failed": "Connection failed: {reason}", + "no_base_url_configured": "baseURL not configured", + "no_valid_host_configured": "No valid host configured", + "no_apikey_configured": "apiKey not configured" }, "cli_export": { "title": "CLI Export", @@ -890,38 +892,41 @@ "delete_confirm_msg": "Are you sure you want to delete Category \"{name}\"?", "category_added": "Category added", "preset_category_added": "Preset Category added", - "category_updated": "Category updated" + "category_updated": "Category updated", + "preset_dialog_title": "Add Category from Preset", + "edit_title": "Edit Category", + "already_exists": "Category \"{name}\" already exists" + }, + "ohmyagent": { + "title": "Oh My Agent", + "add_agent": "Add Agent", + "edit_agent": "Edit Agent", + "delete_agent": "Delete Agent", + "agent_name": "Agent Name", + "provider": "Provider", + "model": "Model", + "bulk_model": "Bulk Model", + "select_provider": "Select Provider", + "select_model": "Select Model", + "agent_saved": "Agent saved", + "agent_deleted": "Agent deleted", + "delete_confirm_msg": "Are you sure you want to delete Agent \"{name}\"?", + "agent_added": "Agent added", + "preset_agent_added": "Preset Agent added", + "agent_updated": "Agent updated", + "group_tip": "💡 Tip: Agents can be organized into custom and preset groups, managed centrally in the Agent Configuration menu. Click the 'Manage' button above to create and edit groups.", + "preset_dialog": { + "title": "Add Agent from Preset", + "subtitle": "Select Preset Agent", + "select_preset": "Please select a preset Agent", + "agent_exists": "Agent \"{name}\" already exists" + }, + "dialog": { + "title": "Add/Edit Agent", + "subtitle": "Configure Agent Information", + "model_label": "Bind Model:" + } }, - "ohmyagent": { - "title": "Oh My Agent", - "add_agent": "Add Agent", - "edit_agent": "Edit Agent", - "delete_agent": "Delete Agent", - "agent_name": "Agent Name", - "provider": "Provider", - "model": "Model", - "bulk_model": "Bulk Model", - "select_provider": "Select Provider", - "select_model": "Select Model", - "agent_saved": "Agent saved", - "agent_deleted": "Agent deleted", - "delete_confirm_msg": "Are you sure you want to delete Agent \"{name}\"?", - "agent_added": "Agent added", - "preset_agent_added": "Preset Agent added", - "agent_updated": "Agent updated", - "group_tip": "💡 Tip: Agents can be organized into custom and preset groups, managed centrally in the Agent Configuration menu. Click the 'Manage' button above to create and edit groups.", - "preset_dialog": { - "title": "Add Agent from Preset", - "subtitle": "Select Preset Agent", - "select_preset": "Please select a preset Agent", - "agent_exists": "Agent \"{name}\" already exists" - }, - "dialog": { - "title": "Add/Edit Agent", - "subtitle": "Configure Agent Information", - "model_label": "Bind Model:" - } - }, "rules": { "title": "Rules Management", "instructions": "Instructions", @@ -945,12 +950,12 @@ "file_path_placeholder": "File path, e.g.: CONTRIBUTING.md, docs/*.md", "agents_md_edit": "AGENTS.md Editor", "reload": "Reload", - "use_template": "Use Template", - "agents_md_not_exist": "# AGENTS.md file does not exist\n# Click \"Use Template\" to create a new file", - "path_label": "Path", - "read_failed": "Read failed", - "save_failed": "Save failed: {error}" - }, + "use_template": "Use Template", + "agents_md_not_exist": "# AGENTS.md file does not exist\n# Click \"Use Template\" to create a new file", + "path_label": "Path", + "read_failed": "Read failed", + "save_failed": "Save failed: {error}" + }, "backup": { "title": "Backup Management", "create_backup": "Create Backup", @@ -1009,7 +1014,8 @@ "preview_card_description": "Click 'Preview Conversion' to view side-by-side comparison in a popup.", "select_config_file": "Select Configuration File", "config_files": "Configuration Files (*.json *.jsonc *.toml);;All Files (*.*)", - "select_config_to_convert": "Please select a configuration to convert first" + "select_config_to_convert": "Please select a configuration to convert first", + "manual_confirm": "Manual confirm" }, "native_provider": { "title": "Native Provider", @@ -1028,14 +1034,32 @@ "config_deleted": "Provider configuration deleted", "test_success": "Connection test successful", "test_failed": "Connection test failed", - "select_provider_first": "Please select a provider first", - "detected_env_vars": "Detected environment variables", - "auth_config": "Authentication Configuration", - "import_env_var": "Import {env_var}", - "provider_options": "Provider Options", - "provider_name": "Provider", - "detect_configured": "Detect Configured" - }, + "select_provider_first": "Please select a provider first", + "detected_env_vars": "Detected environment variables", + "auth_config": "Authentication Configuration", + "import_env_var": "Import {env_var}", + "provider_options": "Provider Options", + "provider_name": "Provider", + "detect_configured": "Detect Configured", + "confirm_delete_title": "Confirm Delete", + "confirm_delete_msg": "Are you sure you want to delete {name} configuration?\nThis will delete auth info and options.", + "detect_complete": "Detection Complete", + "no_provider_detected": "No configured Provider detected", + "detected_providers": "Detected configured Providers:\n\n{list}", + "testing_url": "Testing connection: {url}", + "delete_failed": "Delete Failed", + "cannot_delete_auth": "Cannot delete auth config: {error}", + "delete_success": "Delete Success", + "config_deleted_msg": "{name} configuration deleted", + "auth_json_count": "Configured in auth.json ({count}):\n", + "env_var_count": "Environment variables configured ({count}):\n", + "env_var_hint": "\n\nTip: Providers configured via environment variables can be used directly, ", + "env_var_hint2": "but it's recommended to click \"Configure Provider\" to save to auth.json for management", + "detect_result": "Detection Result", + "no_native_detected": "No configured native Provider detected", + "status_auth_and_env": "auth.json + Environment Variables", + "status_env_only": "Environment variables configured but not saved to auth.json" + }, "dialog": { "select_at_least_one_model": "Please select at least one model", "disabled_all_ohmymcp": "All Oh My MCP servers disabled", @@ -1092,190 +1116,822 @@ "reload_failed": "Reload failed:\n{msg}", "new_version_found": "New Version Found", "new_version_available": "v{version} available, click to view", - "confirm_delete_permission": "Are you sure you want to delete permission \"{pattern}\"?" - }, - "agent_group": { - "current_group": "Current Group:", - "apply": "Apply", - "manage": "Manage Groups", - "no_group": "No Group", - "dialog": { - "title": "Agent Group Management", - "my_groups": "My Groups", - "presets": "Preset Templates", - "new_group": "New Group", - "import": "Import Group", - "export": "Export Group", - "use_template": "Use Template", - "usage_count": "Used {count} times", - "last_used_days": "Last used: {days} days ago", - "last_used_hours": "Last used: {hours} hours ago", - "last_used_recent": "Last used: Recently", - "delete_confirm_title": "Confirm Delete", - "delete_confirm_content": "Are you sure you want to delete group \"{name}\"? This action cannot be undone.", - "delete_success": "Group deleted successfully", - "delete_failed": "Failed to delete group", - "apply_info": "Apply Group", - "apply_info_content": "Will apply configuration from group \"{name}\"", - "create_from_preset": "Create Group from Template", - "group_name_placeholder": "Enter group name", - "group_desc_placeholder": "Enter group description", - "name_required": "Group name is required", - "create_success": "Group created successfully", - "create_failed": "Failed to create group", - "import_title": "Import Group", - "import_success": "Group imported successfully", - "import_failed": "Failed to import group", - "select_group_first": "Please select a group first", - "export_not_implemented": "Export feature is under development" - }, - "edit": { - "title_new": "Create Agent Group", - "title_edit": "Edit Agent Group", - "basic_info": "Basic Information", - "name": "Group Name", - "name_placeholder": "Enter group name", - "icon": "Icon", - "description": "Description", - "desc_placeholder": "Enter group description", - "opencode_agents": "OpenCode Agent Configuration", - "omo_agents": "Oh My OpenCode Agent Configuration", - "enabled": "Enabled", - "agent_id": "Agent ID", - "temperature": "Temperature", - "max_steps": "Max Steps", - "provider": "Provider", - "model": "Model", - "name_required": "Group name is required", - "save_success": "Group saved successfully", - "save_failed": "Failed to save group" - } - }, - "web": { - "logout": "Logout", - "please_enter_name": "Please enter a name", - "tools_must_be_json": "Tools must be valid JSON", - "tools_must_be_array": "Tools must be a JSON array", - "confirm_delete_agent": "Confirm delete this Agent?", - "confirm_delete_omo_agent": "Confirm delete this OMO Agent?", - "edit_permission": "Edit Permission", - "delete_skill": "Delete Skill", - "update_status": "Update Status", - "please_enter_github": "Please enter GitHub address", - "github_only": "Only GitHub addresses supported", - "edit_skill_permission": "Edit Skill Permission", - "skill_dir_not_found": "Skill directory not found", - "source_parse_failed": "Source parse failed", - "unknown": "Unknown", - "not_checked": "Not checked", - "repository": "Repository", - "category": "Category", - "market_repo_invalid": "Market repo format invalid", - "backup_create_failed": "Backup creation failed", - "restore_failed": "Restore failed", - "delete_failed": "Delete failed", - "category_exists": "Category already exists", - "edit_target_not_found": "Edit target not found", - "delete_target_not_found": "Delete target not found", - "record_not_exist": "Record does not exist, may have been deleted", - "delete_irreversible": "Deletion is irreversible. Please confirm.", - "please_enter_path": "Please enter a path", - "path_cannot_be_empty": "Path cannot be empty", - "invalid_index": "Invalid index", - "permission_rule": "Permission Rule", - "custom": "Custom", - "command_pattern_optional": "Command pattern (optional)", - "enter_valid_tool": "Please enter a valid tool name", - "config_file_paths": "Config File Paths", - "related_links": "Related Links", - "opencode_docs": "OpenCode Documentation", - "backup_dir": "Backup Directory", - "compaction_strategy": "Compaction Strategy", - "max_tokens": "Max Tokens (maxTokens)", - "refresh_preview": "Refresh Preview", - "thinking_config_optional": "Thinking Config (optional)", - "please_select_provider": "Please select a Provider first", - "please_enter_model_id": "Please enter Model ID", - "provider_not_exist": "Provider does not exist", - "provider_config_error": "Provider config error", - "model_exists": "Model already exists", - "model_not_found": "Model not found", - "options_must_be_json": "options must be valid JSON", - "options_must_be_object": "options must be a JSON object", - "variants_must_be_json": "variants must be valid JSON", - "variants_must_be_object": "variants must be a JSON object", - "monitor_not_started": "Monitor not started", - "refresh_targets": "Refresh Targets", - "target_id": "Target ID", - "latency_ms": "Latency(ms)", - "check_time": "Check Time", - "description": "Description", - "pending_check": "Pending", - "no_monitor_result": "No monitor results yet", - "no_monitor_targets": "No monitor targets. Please configure Provider and Model first.", - "monitor_running": "Monitoring...", - "monitor_stopped": "Monitor stopped", - "no_env_detected": "No configured environment variables detected", - "value_masked": "Value (masked)", - "config_not_found": "Config not found", - "paramiko_required": "Remote management requires paramiko: pip install paramiko", - "host": "Host", - "port": "Port", - "username": "Username", - "auth_type": "Auth Type", - "add_remote_server": "Add Remote Server", - "name_optional": "Name (optional)", - "host_address": "Host Address", - "ssh_port": "SSH Port", - "password": "Password", - "key_path": "Key Path", - "remote_config_path": "Remote Config Path (optional)", - "please_enter_host": "Please enter host address", - "add_server": "Add Server", - "test_connection": "Test Connection", - "confirm_delete_server": "Are you sure to delete the selected server?", - "remote_config_ops": "Remote Config Operations", - "read": "Read", - "read_config_done": "Config loaded", - "read_failed": "Read failed", - "status_fetched": "Status fetched", - "status_fetch_failed": "Failed to fetch status", - "view_remote_status": "View Remote Status", - "create_remote_backup": "Create Remote Backup", - "remote_backup_created": "Remote backup created", - "backup_failed": "Backup failed", - "server_not_found": "Server not found", - "select_server_hint": "Select a server and click Read to view config", - "version": "Version", - "source": "Source", - "uninstall_selected": "Uninstall Selected", - "please_enter_github_url": "Please enter GitHub URL", - "github_url_invalid": "Invalid GitHub URL format", - "install_failed": "Install failed", - "plugin_not_found": "Selected plugin not found", - "uninstall_failed": "Uninstall failed", - "no_provider_detected": "No Provider detected. Please configure in Provider page first.", - "select_provider": "Select Provider", - "select_model": "Select Model", - "export_preview": "Export Preview", - "please_select_valid_provider": "Please select a valid Provider", - "please_select_model": "Please enter or select a model", - "copied_to_clipboard": "Copied to clipboard", - "copy_failed": "Copy failed. Please check browser permissions.", - "copy_preview": "Copy Preview", - "do_export": "Export", - "scan_detect": "Scan & Detect", - "preview_hint": "Preview: Please scan and select a source first", - "scan_done_select": "Scan complete. Select a source and click Preview.", - "source_not_detected": "Source config not detected", - "convert_failed": "Conversion failed or no importable content", - "please_preview_first": "Please run preview conversion first", - "command_json_array": "Command (JSON array)", - "convert_preview": "Conversion Preview", - "please_select_skill": "Please select a Skill first", - "server_added": "Added {name}", - "connection_success": "Connection successful", - "connection_failed": "Connection failed", - "github_input_label": "GitHub (user/repo or URL)" - } + "confirm_delete_permission": "Are you sure you want to delete permission \"{pattern}\"?", + "json_format_error": "JSON format error: {error}", + "enter_model_id": "Please enter Model ID", + "model_exists": "Model \"{id}\" already exists", + "config_validation_failed": "Configuration validation failed:\n{msg}", + "category_exists": "Category \"{name}\" already exists", + "delete_failed_error": "Delete failed: {error}", + "save_failed_error": "Save failed: {error}", + "install_failed_error": "Install failed: {error}", + "scan_failed_error": "Scan failed: {error}", + "check_update_failed": "Check update failed: {error}", + "cannot_read_backup": "Cannot read backup content: {error}", + "select_provider_first": "Please select a Provider first", + "select_model_first": "Please select a model first", + "enter_mcp_name": "Please enter MCP name", + "mcp_exists": "MCP \"{name}\" already exists", + "enter_agent_name": "Please enter Agent name", + "agent_exists": "Agent \"{name}\" already exists", + "select_preset_agent": "Please select a preset Agent", + "no_skill_selected": "Please select a Skill first", + "skill_deleted": "Skill deleted", + "skill_delete_failed": "Failed to delete Skill: {error}", + "permission_config_saved": "Permission configuration saved", + "agent_config_saved": "Agent configuration saved" + }, + "agent_group": { + "current_group": "Current Group:", + "apply": "Apply", + "manage": "Manage Groups", + "no_group": "No Group", + "dialog": { + "title": "Agent Group Management", + "my_groups": "My Groups", + "presets": "Preset Templates", + "new_group": "New Group", + "import": "Import Group", + "export": "Export Group", + "use_template": "Use Template", + "usage_count": "Used {count} times", + "last_used_days": "Last used: {days} days ago", + "last_used_hours": "Last used: {hours} hours ago", + "last_used_recent": "Last used: Recently", + "delete_confirm_title": "Confirm Delete", + "delete_confirm_content": "Are you sure you want to delete group \"{name}\"? This action cannot be undone.", + "delete_success": "Group deleted successfully", + "delete_failed": "Failed to delete group", + "apply_info": "Apply Group", + "apply_info_content": "Will apply configuration from group \"{name}\"", + "create_from_preset": "Create Group from Template", + "group_name_placeholder": "Enter group name", + "group_desc_placeholder": "Enter group description", + "name_required": "Group name is required", + "create_success": "Group created successfully", + "create_failed": "Failed to create group", + "import_title": "Import Group", + "import_success": "Group imported successfully", + "import_failed": "Failed to import group", + "select_group_first": "Please select a group first", + "export_not_implemented": "Export feature is under development" + }, + "edit": { + "title_new": "Create Agent Group", + "title_edit": "Edit Agent Group", + "basic_info": "Basic Information", + "name": "Group Name", + "name_placeholder": "Enter group name", + "icon": "Icon", + "description": "Description", + "desc_placeholder": "Enter group description", + "opencode_agents": "OpenCode Agent Configuration", + "omo_agents": "Oh My OpenCode Agent Configuration", + "enabled": "Enabled", + "agent_id": "Agent ID", + "temperature": "Temperature", + "max_steps": "Max Steps", + "provider": "Provider", + "model": "Model", + "name_required": "Group name is required", + "save_success": "Group saved successfully", + "save_failed": "Failed to save group" + }, + "apply_failed": "Failed to apply group: {error}" + }, + "web": { + "logout": "Logout", + "please_enter_name": "Please enter a name", + "tools_must_be_json": "Tools must be valid JSON", + "tools_must_be_array": "Tools must be a JSON array", + "confirm_delete_agent": "Confirm delete this Agent?", + "confirm_delete_omo_agent": "Confirm delete this OMO Agent?", + "edit_permission": "Edit Permission", + "delete_skill": "Delete Skill", + "update_status": "Update Status", + "please_enter_github": "Please enter GitHub address", + "github_only": "Only GitHub addresses supported", + "edit_skill_permission": "Edit Skill Permission", + "skill_dir_not_found": "Skill directory not found", + "source_parse_failed": "Source parse failed", + "unknown": "Unknown", + "not_checked": "Not checked", + "repository": "Repository", + "category": "Category", + "market_repo_invalid": "Market repo format invalid", + "backup_create_failed": "Backup creation failed", + "restore_failed": "Restore failed", + "delete_failed": "Delete failed", + "category_exists": "Category already exists", + "edit_target_not_found": "Edit target not found", + "delete_target_not_found": "Delete target not found", + "record_not_exist": "Record does not exist, may have been deleted", + "delete_irreversible": "Deletion is irreversible. Please confirm.", + "please_enter_path": "Please enter a path", + "path_cannot_be_empty": "Path cannot be empty", + "invalid_index": "Invalid index", + "permission_rule": "Permission Rule", + "custom": "Custom", + "command_pattern_optional": "Command pattern (optional)", + "enter_valid_tool": "Please enter a valid tool name", + "config_file_paths": "Config File Paths", + "related_links": "Related Links", + "opencode_docs": "OpenCode Documentation", + "backup_dir": "Backup Directory", + "compaction_strategy": "Compaction Strategy", + "max_tokens": "Max Tokens (maxTokens)", + "refresh_preview": "Refresh Preview", + "thinking_config_optional": "Thinking Config (optional)", + "please_select_provider": "Please select a Provider first", + "please_enter_model_id": "Please enter Model ID", + "provider_not_exist": "Provider does not exist", + "provider_config_error": "Provider config error", + "model_exists": "Model already exists", + "model_not_found": "Model not found", + "options_must_be_json": "options must be valid JSON", + "options_must_be_object": "options must be a JSON object", + "variants_must_be_json": "variants must be valid JSON", + "variants_must_be_object": "variants must be a JSON object", + "monitor_not_started": "Monitor not started", + "refresh_targets": "Refresh Targets", + "target_id": "Target ID", + "latency_ms": "Latency(ms)", + "check_time": "Check Time", + "description": "Description", + "pending_check": "Pending", + "no_monitor_result": "No monitor results yet", + "no_monitor_targets": "No monitor targets. Please configure Provider and Model first.", + "monitor_running": "Monitoring...", + "monitor_stopped": "Monitor stopped", + "no_env_detected": "No configured environment variables detected", + "value_masked": "Value (masked)", + "config_not_found": "Config not found", + "paramiko_required": "Remote management requires paramiko: pip install paramiko", + "host": "Host", + "port": "Port", + "username": "Username", + "auth_type": "Auth Type", + "add_remote_server": "Add Remote Server", + "name_optional": "Name (optional)", + "host_address": "Host Address", + "ssh_port": "SSH Port", + "password": "Password", + "key_path": "Key Path", + "remote_config_path": "Remote Config Path (optional)", + "please_enter_host": "Please enter host address", + "add_server": "Add Server", + "test_connection": "Test Connection", + "confirm_delete_server": "Are you sure to delete the selected server?", + "remote_config_ops": "Remote Config Operations", + "read": "Read", + "read_config_done": "Config loaded", + "read_failed": "Read failed", + "status_fetched": "Status fetched", + "status_fetch_failed": "Failed to fetch status", + "view_remote_status": "View Remote Status", + "create_remote_backup": "Create Remote Backup", + "remote_backup_created": "Remote backup created", + "backup_failed": "Backup failed", + "server_not_found": "Server not found", + "select_server_hint": "Select a server and click Read to view config", + "version": "Version", + "source": "Source", + "uninstall_selected": "Uninstall Selected", + "please_enter_github_url": "Please enter GitHub URL", + "github_url_invalid": "Invalid GitHub URL format", + "install_failed": "Install failed", + "plugin_not_found": "Selected plugin not found", + "uninstall_failed": "Uninstall failed", + "no_provider_detected": "No Provider detected. Please configure in Provider page first.", + "select_provider": "Select Provider", + "select_model": "Select Model", + "export_preview": "Export Preview", + "please_select_valid_provider": "Please select a valid Provider", + "please_select_model": "Please enter or select a model", + "copied_to_clipboard": "Copied to clipboard", + "copy_failed": "Copy failed. Please check browser permissions.", + "copy_preview": "Copy Preview", + "do_export": "Export", + "scan_detect": "Scan & Detect", + "preview_hint": "Preview: Please scan and select a source first", + "scan_done_select": "Scan complete. Select a source and click Preview.", + "source_not_detected": "Source config not detected", + "convert_failed": "Conversion failed or no importable content", + "please_preview_first": "Please run preview conversion first", + "command_json_array": "Command (JSON array)", + "convert_preview": "Conversion Preview", + "please_select_skill": "Please select a Skill first", + "server_added": "Added {name}", + "connection_success": "Connection successful", + "connection_failed": "Connection failed", + "github_input_label": "GitHub (user/repo or URL)" + }, + "validator": { + "config_parse_failed": "Config file cannot be parsed or read", + "root_must_be_object": "Config root must be an object type", + "config_empty": "Config is empty, no Provider has been added yet", + "schema_recommend": "Recommend setting $schema to https://opencode.ai/config.json", + "no_providers": "No Provider configured", + "provider_must_be_object": "provider must be an object type", + "provider_value_not_object": "Provider '{name}' value must be an object, currently is {type}", + "provider_missing_field": "Provider '{name}' is missing required field '{field}'", + "provider_field_empty": "Provider '{name}' field '{field}' is empty", + "provider_unknown_npm": "Provider '{name}' npm package '{npm}' is not in the known list", + "provider_options_not_object": "Provider '{name}' options must be an object", + "provider_options_missing": "Provider '{name}' options is missing '{field}'", + "provider_options_empty": "Provider '{name}' options.{field} is empty", + "provider_models_not_object": "Provider '{name}' models must be an object", + "provider_no_models": "Provider '{name}' has no models configured", + "provider_empty_model_id": "Provider '{name}' has an empty model ID", + "model_value_not_object": "Model '{model}' value must be an object", + "model_limit_should_be_object": "Model '{model}' limit should be an object", + "model_context_should_be_int": "Model '{model}' context should be an integer", + "model_output_should_be_int": "Model '{model}' output should be an integer", + "mcp_must_be_object": "mcp must be an object type", + "mcp_value_not_object": "MCP '{name}' value must be an object", + "mcp_local_missing_command": "Local MCP '{name}' is missing command field", + "mcp_remote_missing_url": "Remote MCP '{name}' is missing url field", + "agent_must_be_object": "agent must be an object type", + "config_empty_or_invalid": "Config file is empty or cannot be parsed", + "agents_must_be_object": "agents must be an object type", + "no_agents": "No Agent configured", + "agent_name_empty": "Agent name is empty", + "agent_value_not_object": "Agent '{name}' value must be an object", + "agent_missing_field": "Agent '{name}' is missing required field '{field}'", + "agent_field_empty": "Agent '{name}' field '{field}' is empty", + "agent_description_empty": "Agent '{name}' description is empty", + "no_categories": "No Category configured", + "categories_must_be_object": "categories must be an object type", + "category_name_empty": "Category name is empty", + "category_value_not_object": "Category '{name}' value must be an object", + "category_missing_field": "Category '{name}' is missing required field '{field}'", + "category_field_empty": "Category '{name}' field '{field}' is empty", + "category_temperature_should_be_number": "Category '{name}' temperature should be a number", + "category_description_empty": "Category '{name}' description is empty", + "fix_skip_invalid_provider": "Skipped invalid Provider '{name}' (value is not an object)", + "fix_add_default_npm": "Provider '{name}': Added default npm field", + "fix_options_field": "Provider '{name}': Fixed options field", + "fix_add_empty_baseurl": "Provider '{name}': Added empty baseURL", + "fix_add_empty_apikey": "Provider '{name}': Added empty apiKey", + "fix_add_empty_models": "Provider '{name}': Added empty models field", + "fix_models_to_object": "Provider '{name}': Fixed models field to object", + "fix_remove_invalid_limit": "Provider '{name}' Model '{model}': Removed invalid limit", + "fix_remove_empty_limit": "Provider '{name}' Model '{model}': Removed empty limit", + "error_count": "❌ {count} error(s):", + "error_more": " ... and {count} more error(s)", + "warning_count": "⚠️ {count} warning(s):", + "warning_more": " ... and {count} more warning(s)", + "config_valid": "✅ Config format is correct", + "config_empty_or_unparseable": "Config file is empty or cannot be parsed", + "omo_root_must_be_object": "Config root must be object type", + "agent_desc_empty": "Agent '{name}' description is empty", + "category_temp_not_number": "Category '{name}' temperature should be a number", + "category_desc_empty": "Category '{name}' description is empty", + "errors_count": "❌ {count} error(s):", + "more_errors": " ... and {count} more error(s)", + "warnings_count": "⚠️ {count} warning(s):", + "more_warnings": " ... and {count} more warning(s)", + "config_ok": "✅ Config format is correct" + }, + "auth": { + "login_locked": "Too many failed attempts, please try again in {remain} minute(s)", + "login_failed_locked": "Login failed 5 times, locked for 15 minutes", + "password_wrong": "Wrong password, {remain} attempt(s) remaining", + "login_success": "Login successful", + "new_password_min_length": "New password must be at least 8 characters", + "old_password_wrong": "Old password is incorrect", + "password_changed": "Password changed successfully", + "logged_out": "Logged out", + "not_logged_in": "Not logged in", + "web_login_subtitle": "OCCM Web Login", + "admin_password_label": "Admin Password", + "login_failed_fallback": "Login failed", + "login_button": "Login", + "old_password_label": "Old Password", + "new_password_label": "New Password", + "password_updated_fallback": "Password updated", + "change_failed_fallback": "Change failed", + "submit_button": "Submit" + }, + "skill_manager": { + "name_empty": "Name cannot be empty", + "name_too_long": "Name cannot exceed 64 characters", + "name_format_error": "Invalid name format: only lowercase letters, digits, and single hyphens allowed", + "desc_empty": "Description cannot be empty", + "desc_too_long": "Description cannot exceed 1024 characters", + "parse_failed": "Failed to parse skill {name}: {error}", + "scan_dir_failed": "Failed to scan directory {path}: {error}", + "downloading": "Downloading...", + "detecting_branch": "Detecting branch...", + "using_branch": "Using branch: {branch}", + "extracting": "Extracting...", + "subdir_not_found": "Subdirectory does not exist: {subdir}", + "skill_file_not_found": "SKILL.md or SKILL.txt file not found", + "skill_file_not_found_in_subdir": "SKILL.md or SKILL.txt file not found (in {subdir})", + "skill_file_format_error": "SKILL file format error", + "installing": "Installing...", + "install_complete": "Installation complete!", + "install_success": "Skill '{name}' installed successfully", + "network_error": "Network error: {error}", + "install_failed": "Installation failed: {error}", + "path_not_found": "Path does not exist: {path}", + "skill_md_not_found": "SKILL.md file not found", + "skill_md_format_error": "SKILL.md format error", + "copying": "Copying...", + "status_local": "Local", + "status_unknown": "Unknown", + "status_has_update": "Update available", + "status_latest": "Up to date", + "status_check_failed": "Check failed", + "check_update_failed": "Failed to check updates for {name}: {error}", + "update_github_only": "Only Skills installed from GitHub can be updated", + "update_failed": "Update failed: {error}", + "scan_failed": "Scan failed: {error}", + "unrecognized_source": "Unrecognized source format: {source}" + }, + "cli_export_msg": { + "provider_config_incomplete": "Provider configuration incomplete: missing {fields}", + "write_config_failed": "Failed to write config ({path}): {reason}", + "parse_config_failed": "Failed to parse {format} config ({path}): {reason}", + "backup_failed": "Failed to backup {cli_type} config: {reason}", + "restore_failed": "Failed to restore backup ({path}): {reason}", + "json_validation_failed": "JSON format validation failed: {error}", + "backup_dir_not_found": "Backup directory does not exist", + "list_backups_failed": "Failed to list backups: {error}", + "delete_old_backup_failed": "Failed to delete old backup ({path}): {error}", + "set_permissions_failed": "Failed to set file permissions ({path}): {error}", + "missing_base_url": "Missing API address (baseURL)", + "missing_api_key": "Missing API key (apiKey)", + "no_models_configured": "No models configured", + "export_failed": "Export failed: {error}", + "export_exception": "Export exception: {error}", + "unknown_cli_type": "Unknown CLI type: {cli_type}", + "settings_json_not_found": "settings.json file does not exist", + "settings_json_missing_env": "settings.json missing env field", + "missing_anthropic_base_url": "Missing ANTHROPIC_BASE_URL", + "missing_anthropic_auth_token": "Missing ANTHROPIC_AUTH_TOKEN", + "settings_json_format_error": "settings.json format error: {error}", + "read_settings_json_failed": "Failed to read settings.json: {error}", + "auth_json_not_found": "auth.json file does not exist", + "auth_json_missing_key": "auth.json missing OPENAI_API_KEY", + "auth_json_format_error": "auth.json format error: {error}", + "read_auth_json_failed": "Failed to read auth.json: {error}", + "config_toml_not_found": "config.toml file does not exist", + "config_toml_missing_provider": "config.toml missing model_provider", + "config_toml_missing_model": "config.toml missing model", + "read_config_toml_failed": "Failed to read config.toml: {error}", + "env_file_not_found": ".env file does not exist", + "env_missing_api_key": ".env missing GEMINI_API_KEY", + "env_missing_base_url": ".env missing GOOGLE_GEMINI_BASE_URL", + "read_env_failed": "Failed to read .env: {error}", + "settings_json_not_found_warn": "settings.json file does not exist", + "settings_json_missing_security": "settings.json missing security field", + "settings_json_format_error_gemini": "settings.json format error: {error}", + "read_settings_json_failed_gemini": "Failed to read settings.json: {error}", + "file_size_bytes": "{size} bytes", + "restore_backup_dir_missing": "Backup directory does not exist", + "dot_env_not_found": ".env file not found", + "dot_env_missing_gemini_key": ".env missing GEMINI_API_KEY", + "dot_env_missing_gemini_url": ".env missing GOOGLE_GEMINI_BASE_URL", + "read_dot_env_failed": "Failed to read .env: {error}", + "settings_json_warning_not_found": "settings.json file not found" + }, + "remote_mgr": { + "paramiko_not_installed": "paramiko is not installed, remote management is unavailable. Please run: pip install paramiko", + "unsupported_config_type": "Unsupported config type: {config_type}. Only opencode / oh-my-opencode / auth are supported", + "key_path_required": "key_path is required when using key authentication", + "key_file_not_found": "Private key file does not exist: {path}", + "password_required": "password is required when using password authentication", + "unsupported_auth_type": "Unsupported auth_type: {auth_type}", + "already_connected": "Already connected (reusing existing connection)", + "connect_success": "Connection successful", + "connect_failed": "Connection failed: {error}", + "test_success": "Connection test successful", + "test_failed": "Connection test failed: {error}", + "test_exception": "Connection test exception: {error}", + "expand_path_failed": "Failed to expand remote path: {error}", + "remote_config_not_found": "Remote config file does not exist: {config_type}", + "remote_config_parse_failed": "Remote config file JSON parse failed: {error}", + "read_remote_config_failed": "Failed to read remote config: {error}", + "write_data_must_be_dict": "Write failed: data must be a dict", + "create_remote_dir_failed": "Failed to create remote directory: {error}", + "write_remote_config_failed": "Failed to write remote config: {error}", + "create_remote_backup_failed": "Failed to create remote backup: {error}", + "list_remote_backups_failed": "Failed to list remote backups: {error}", + "check_remote_status_failed": "Failed to check remote status: {error}", + "unknown_error": "Unknown error" + }, + "plugin_mgr": { + "install_failed": "Failed to install plugin: {error}", + "uninstall_failed": "Failed to uninstall plugin: {error}", + "check_version_failed": "Failed to check version: {error}" + }, + "crash": { + "title": "OCCM Crash", + "message": "The application encountered an error and needs to close.\n\nError log saved to:\n{log_file}\n\nPlease send this log file to the developer to help fix the issue.\n\nGitHub: https://github.com/icysaintdx/OpenCode-Config-Manager/issues", + "console_msg": "OCCM Crash - Log saved to: {log_file}" + }, + "cli_exception": { + "provider_incomplete": "Provider config incomplete: missing {fields}", + "write_failed": "Failed to write config ({path}): {reason}", + "parse_failed": "Failed to parse {format_type} config ({path}): {reason}", + "backup_failed": "Backup {cli_type} config failed: {reason}", + "restore_failed": "Failed to restore backup ({backup_path}): {reason}" + }, + "agent_group_mgr": { + "load_failed": "Failed to load group config: {error}", + "save_failed": "Failed to save group config: {error}", + "backup_failed": "Failed to backup group config: {error}", + "cleanup_failed": "Failed to clean up old backups: {error}", + "export_failed": "Failed to export group: {error}", + "import_failed": "Failed to import group: {error}", + "import_format_error": "Import file format error: missing group field", + "group_exists": "Group '{name}' already exists" + }, + "tooltip": { + "provider_name": "Provider Name - Unique identifier for this provider\nFormat: lowercase letters and hyphens, e.g. anthropic, openai, my-proxy", + "provider_display": "Display Name - Friendly name shown in the UI\nExample: Anthropic (Claude), OpenAI Official", + "provider_sdk": "SDK Package - Specifies which AI SDK to use for API calls\n• Claude series -> @ai-sdk/anthropic\n• GPT/OpenAI series -> @ai-sdk/openai\n• Gemini series -> @ai-sdk/google", + "provider_url": "API URL (baseURL) - API service access address\n• Official API -> Leave empty (auto-uses default)\n• Proxy -> Fill in proxy address", + "provider_apikey": "API Key - Authentication key\nSupports env variable: {env:ANTHROPIC_API_KEY}", + "provider_timeout": "Request Timeout - Unit: milliseconds (ms)\nDefault: 300000 (5 minutes)", + "model_id": "Model ID - Unique identifier, must match API provider\nExample: claude-sonnet-4-5-20250929, gpt-5", + "model_name": "Display Name - Friendly name shown in the UI", + "model_attachment": "Attachment Support - Whether file uploads (images, documents, etc.) are supported", + "model_context": "Context Window - Maximum input length the model can process (tokens)", + "model_output": "Max Output - Maximum reply length per response (tokens)", + "model_options": "Model Default Config (Options) - Parameters automatically applied to every model call\n• Claude thinking: thinking.type, thinking.budgetTokens\n• OpenAI: reasoningEffort, textVerbosity\n• Gemini: thinkingConfig.thinkingBudget", + "model_variants": "Model Variants - Preset config combinations switchable via hotkey\nUsed for different configurations of the same model, e.g. different thinking budgets", + "agent_name": "Agent Name - Unique identifier\nPreset Agents: oracle, librarian, explore, code-reviewer", + "agent_model": "Bound Model - Format: provider/model-id\nExample: anthropic/claude-sonnet-4-5-20250929", + "agent_description": "Agent Description - Describes the agent's function and use cases", + "opencode_agent_mode": "Agent Mode\n• primary - Main Agent, switchable via Tab\n• subagent - Sub Agent, invoked via @mention\n• all - Both modes supported", + "opencode_agent_temperature": "Generation Temperature - Range: 0.0 - 2.0\n• 0.0-0.2: Suited for code/analysis\n• 0.3-0.5: Balances creativity and accuracy", + "opencode_agent_maxSteps": "Max Steps - Limits the number of tool calls an Agent can make\nEmpty = unlimited", + "opencode_agent_prompt": "System Prompt - Defines Agent behavior and expertise\nSupports file reference: {file:./prompts/agent.txt}", + "opencode_agent_tools": "Tool Config - JSON object format\n• true - Enable tool\n• false - Disable tool", + "opencode_agent_permission": "Permission Config\n• allow - Allow without confirmation\n• ask - Ask user each time\n• deny - Deny usage", + "opencode_agent_hidden": "Hidden - Whether to hide this Agent in @ autocomplete\nOnly effective for subagent", + "category_name": "Category Name\nPreset categories: visual, business-logic, documentation, code-analysis", + "category_model": "Bound Model - Format: provider/model-id", + "category_temperature": "Temperature - Recommended:\n• visual (frontend): 0.7\n• business-logic (backend): 0.1\n• documentation (docs): 0.3", + "category_description": "Category Description - Explains the category's purpose and use cases", + "permission_tool": "Tool Name\nBuilt-in: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Task\nMCP format: mcp_servername_toolname", + "permission_level": "Permission Level\n• allow - Use directly without confirmation\n• ask - Ask user before each use\n• deny - Deny usage", + "permission_bash_pattern": "Bash Command Pattern - Supports wildcards\n• * - Match all commands\n• git * - Match all git commands", + "mcp_name": "MCP Name - Unique identifier for MCP server\nExample: context7, sentry, gh_grep", + "mcp_type": "MCP Type\n• local - Local process, started via command\n• remote - Remote service, connected via URL", + "mcp_enabled": "Enabled - Whether this MCP server is enabled\nDisabled preserves config but doesn't load", + "mcp_command": "Start Command (Local type) - JSON array format\nExample: [\"npx\", \"-y\", \"@mcp/server\"]", + "mcp_url": "Server URL (Remote type) - Full HTTP/HTTPS URL", + "mcp_headers": "Request Headers (Remote type) - JSON object format\nExample: {\"Authorization\": \"Bearer your-api-key\"}", + "mcp_environment": "Environment Variables (Local type) - JSON object format\nExample: {\"API_KEY\": \"xxx\"}", + "mcp_timeout": "Timeout - Unit: milliseconds (ms)\nDefault: 5000 (5 seconds)", + "skill_name": "Skill Name - 1-64 chars, lowercase letters, numbers, hyphens\nExample: git-release, pr-review", + "skill_permission": "Skill Permission\n• allow - Load immediately without confirmation\n• deny - Hide and deny access\n• ask - Ask user before loading", + "skill_pattern": "Permission Pattern - Supports wildcards\n• * - Match all Skills\n• internal-* - Match Skills starting with internal-", + "skill_description": "Skill Description - Describes the Skill's function, helps Agent choose", + "instructions_path": "Instructions File Path - Supports relative/absolute paths, Glob patterns, remote URLs", + "rules_agents_md": "AGENTS.md File - Project-level or global rules file\nSuggested content: project structure, code standards", + "compaction_auto": "Auto Compaction - Automatically compresses session when context is nearly full\nDefault: true (enabled)", + "compaction_prune": "Prune Old Output - Removes old tool output to save tokens\nDefault: true (enabled)" + }, + "preset_desc": { + "omo_agents": { + "oracle": "Architecture design, code review, strategic planning expert - for complex decisions and deep analysis", + "librarian": "Multi-repo analysis, documentation lookup, implementation examples expert - for finding external resources and docs", + "explore": "Fast codebase exploration and pattern matching expert - for code search and pattern discovery", + "frontend-ui-ux-engineer": "UI/UX design and frontend development expert - for frontend visual tasks", + "document-writer": "Technical documentation writing expert - for generating README, API docs, etc.", + "multimodal-looker": "Visual content analysis expert - for analyzing images, PDFs, and other media", + "code-reviewer": "Code quality review, security analysis expert - for code review tasks", + "debugger": "Problem diagnosis, bug fixing expert - for debugging and troubleshooting" + }, + "opencode_agents": { + "build": "Default main Agent with full tool permissions, for development work", + "plan": "Planning/analysis Agent with limited write permissions, for code analysis and planning", + "general": "General sub-Agent for researching complex problems and multi-step tasks", + "explore": "Fast exploration Agent for codebase search and pattern discovery", + "code-reviewer": "Code review Agent, read-only permissions, focused on code quality analysis", + "docs-writer": "Documentation Agent, focused on technical writing", + "security-auditor": "Security audit Agent, read-only permissions, focused on security vulnerability analysis" + }, + "categories": { + "visual": "Frontend, UI/UX, design-related tasks", + "business-logic": "Backend logic, architecture design, strategic reasoning", + "documentation": "Documentation writing, technical writing tasks", + "code-analysis": "Code review, refactoring analysis tasks" + }, + "model_categories": { + "claude_series": "Claude Series", + "openai_codex_series": "OpenAI/Codex Series", + "gemini_series": "Gemini Series", + "other_models": "Other Models" + }, + "model_descriptions": { + "claude_opus_45": "The most powerful Claude model, supports extended thinking mode\noptions.thinking.budgetTokens controls thinking budget", + "claude_sonnet_45": "Balanced performance and cost Claude model, supports thinking mode", + "claude_sonnet_4": "Claude Sonnet 4 base, does not support thinking", + "claude_haiku_45": "Fast response lightweight Claude model", + "gpt5": "OpenAI's latest flagship model\noptions.reasoningEffort: high/medium/low/xhigh", + "gpt51_codex": "OpenAI code-specific model, optimized for programming tasks", + "gpt4o": "OpenAI multimodal model", + "o1_preview": "OpenAI reasoning model, supports reasoningEffort parameter", + "o3_mini": "OpenAI's latest reasoning model", + "gemini_3_pro": "Google's latest Pro model, supports thinking mode", + "gemini_20_flash": "Google Flash model, supports thinking mode", + "gemini_20_flash_thinking": "Gemini dedicated thinking experimental model", + "gemini_15_pro": "Ultra-long context Gemini Pro model", + "minimax_m21": "Minimax M2.1 model", + "deepseek_chat": "DeepSeek chat model", + "deepseek_reasoner": "DeepSeek reasoning model", + "qwen_max": "Alibaba Qwen Max model", + "moonshot": "Moonshot AI (Kimi) model" + }, + "model_pack_names": { + "default": "Default", + "high_thinking": "High Thinking", + "max_thinking": "Max Thinking", + "lightweight": "Lightweight", + "basic": "Basic", + "high": "High" + } + }, + "agent_groups": { + "preset_minimal": "Minimal", + "preset_minimal_desc": "Core agents only, for simple tasks", + "preset_standard": "Standard", + "preset_standard_desc": "Balanced agent combination for most tasks", + "preset_common": "Common", + "preset_common_desc": "Common agent combination for most complex projects", + "preset_complete": "Complete", + "preset_complete_desc": "All agents enabled, maximum functionality", + "preset_frontend": "Frontend", + "preset_frontend_desc": "Optimized for frontend UI/UX development", + "preset_backend": "Backend", + "preset_backend_desc": "Optimized for backend API/database development", + "load_failed": "Failed to load group config: {error}", + "save_failed": "Failed to save group config: {error}", + "backup_failed": "Failed to backup group config: {error}", + "cleanup_failed": "Failed to clean up old backups: {error}", + "export_failed": "Failed to export group: {error}", + "import_format_error": "Import file format error: missing group field", + "import_exists": "Group '{name}' already exists", + "import_failed": "Failed to import group: {error}" + }, + "config_mgr": { + "json_parse_failed": "Standard JSON parse failed: {error}", + "jsonc_parse_failed": "JSONC parse failed: {error}", + "file_size": "File size: {size} bytes", + "file_preview": "File preview: {preview}..." + }, + "version_checker": { + "rate_limited": "GitHub API rate limited (403), will retry in 6 hours", + "network_error": "Network error - {reason}" + }, + "config_viewer": { + "view_config": "View Config File", + "view_auth": "View Auth File", + "cannot_read": "Cannot read config file: {error}", + "backed_up": "Config file backed up", + "backup_failed": "Cannot backup config file: {error}", + "json_format_error": "JSON Format Error", + "json_invalid": "Config file format is invalid:\n{error}", + "save_success": "Save Success", + "config_saved": "Config file saved", + "save_failed": "Save Failed", + "cannot_save": "Cannot save config file: {error}" + }, + "balance": { + "query_failed_both": "Balance query failed. NewAPI: {newapi}... OpenAI API: {openai}...", + "newapi_query_failed": "NewAPI query failed: {code} - {body}", + "newapi_request_failed": "NewAPI request failed: {error}", + "newapi_invalid_response": "NewAPI response format is invalid", + "subscription_query_failed": "Subscription info query failed: {code} - {body}", + "subscription_request_failed": "Subscription info request failed: {error}", + "usage_query_failed": "Usage query failed: {code} - {body}", + "usage_request_failed": "Usage request failed: {error}", + "testing_connection": "Testing connection...", + "not_supported": "This Provider does not support balance query via API (only NewAPI/OpenAI billing API compatible services).", + "no_base_url": "No baseURL configured, cannot query balance.", + "not_available": "Balance Query Not Available", + "api_type_newapi": "API Type: NewAPI / One-API", + "token_name": "Token Name: {name}" + }, + "model_fetch": { + "no_model_list_url": "Model list URL not configured", + "no_models_returned": "No models returned from API", + "fetch_failed": "Fetch failed", + "not_supported": "{name} does not support fetching model list via API.\nPlease add models manually or refer to official docs.", + "cannot_determine_url": "Cannot determine API URL. Please configure Provider's baseURL first.", + "fetching": "Fetching model list from {url}...", + "empty_list": "API returned an empty model list", + "models_added": "{count} models added", + "auth_required": "HTTP {code}: {reason}\n\nThis API requires authentication. Please configure Provider's API Key first.", + "key_invalid": "HTTP {code}: {reason}\n\nAPI Key may be invalid or expired.", + "fetched_count": "Fetched {count} models from {provider}", + "unsupported_fetch": "{name} does not support fetching model list via API.\nPlease add models manually or refer to the official documentation." + }, + "model_dialog": { + "placeholder_model_id": "e.g. claude-sonnet-4-5-20250929", + "thinking_added": "Claude Thinking config added", + "json_error": "JSON format error: {error}", + "enter_model_id": "Please enter model ID", + "model_exists": "Model \"{id}\" already exists", + "validation_more_errors": "\n... and {count} more error(s)", + "validation_failed": "Config validation failed:\n{msg}", + "select_header": "Select models to add", + "select_column_check": "Select", + "select_column_id": "Model ID", + "select_column_time": "Created", + "select_at_least_one": "Please select at least one model", + "provider_not_found": "Provider \"{name}\" does not exist. Please create it in the Provider management page first.", + "provider_incomplete": "Provider \"{name}\" config is incomplete. Please complete it in the Provider management page first." + }, + "provider_dialog": { + "config_conflict": "Config Conflict", + "conflict_msg": "A custom Provider '{id}' with the same name already exists.\nContinue saving?", + "validation_required": "Validation Failed", + "field_required": "{label} is required", + "save_failed": "Save Failed", + "cannot_save_auth": "Cannot save auth config: {error}" + }, + "match_mode": { + "regex": "Regex", + "contains": "Contains", + "prefix": "Prefix" + }, + "config_reload": { + "jsonc_comments_lost_title": "JSONC Comments Lost", + "jsonc_comments_lost_msg": "Original config file contained comments. Comments were lost after saving. Original file has been auto-backed up.", + "external_change_msg": "Detected {name} config file was modified externally.\n\nPlease choose how to handle:\n• Click [OK] to reload file content (may overwrite current UI data)\n• Click [Cancel] to keep current UI data (file keeps external changes)", + "reloaded_title": "Reloaded", + "reloaded_msg": "Loaded latest {name} config", + "keep_current_title": "Keeping Current Data", + "keep_current_msg": "Did not reload {name}, current UI data unchanged", + "size_label": "Size: {size}", + "mtime_label": "Modified: {mtime}", + "switched_title": "Config Switched", + "switched_msg": "Deleted {jsonc}, will use {json}", + "delete_failed_title": "Delete Failed", + "delete_failed_msg": "Cannot delete {file}: {error}", + "keep_status_title": "Keeping Current", + "keep_status_msg": "Will continue using {file}", + "fix_complete": "Config Fixed", + "fix_msg": "Completed {count} fix(es):\n{fixes}", + "fix_more": "\n... and {count} more", + "no_fix_needed_title": "No Fix Needed", + "no_fix_needed_msg": "Config structure is already correct", + "more_errors_summary": "\n... and {count} more error(s)", + "dual_config_detected": "Detected {name} has two config files:", + "dual_config_json_info": "\n{json_name}\n Size: {json_size}\n Modified: {json_mtime}", + "dual_config_jsonc_info": "\n{jsonc_name}\n Size: {jsonc_size}\n Modified: {jsonc_mtime}", + "dual_config_warning": "\nThe program will prioritize loading the .jsonc file.", + "dual_config_prompt": "\nPlease select which config file to use:\n- Click [OK] to use .json file (delete .jsonc)\n- Click [Cancel] to use .jsonc file (keep current)" + }, + "skill_ui": { + "name_empty": "Name cannot be empty", + "name_too_long": "Name cannot exceed 64 characters", + "name_format_error": "Name format error: only lowercase letters, numbers, single hyphens", + "desc_empty": "Description cannot be empty", + "desc_too_long": "Description cannot exceed 1024 characters", + "parse_failed": "Failed to parse skill {name}: {error}", + "scan_dir_failed": "Failed to scan directory {path}: {error}", + "visit_skillsmp": "Visit SkillsMP.com to browse more community skills", + "visit_composio": "Visit ComposioHQ to browse more community skills", + "scan_failed": "Scan failed: {error}", + "unrecognized_source": "Unrecognized source format: {source}", + "downloading": "Downloading...", + "detecting_branch": "Detecting branch...", + "using_branch": "Using branch: {branch}", + "extracting": "Extracting...", + "subdir_not_found": "Subdirectory does not exist: {subdir}", + "skill_file_not_found": "SKILL.md or SKILL.txt file not found", + "skill_file_not_found_in": "SKILL.md or SKILL.txt file not found (in {subdir})", + "skill_format_error": "SKILL file format error", + "installing": "Installing...", + "install_complete": "Installation complete!", + "install_success": "Skill '{name}' installed successfully", + "network_error": "Network error: {error}", + "install_failed": "Installation failed: {error}", + "path_not_found": "Path does not exist: {path}", + "skill_md_not_found": "SKILL.md file not found", + "skill_md_format_error": "SKILL.md format error", + "copying": "Copying...", + "check_update_failed": "Failed to check update {name}: {error}", + "github_only_update": "Only supports updating Skills installed from GitHub", + "update_failed": "Update failed: {error}", + "compatibility": "Compatibility: {value}", + "claude_global_loc": "Claude Global (~/.claude/skills/)", + "claude_project_loc": "Claude Project (.claude/skills/)", + "opencode_global_loc": "OpenCode Global (~/.config/opencode/skills/)", + "opencode_project_loc": "OpenCode Project (.opencode/skills/)", + "name_error": "Name Error", + "desc_error": "Description Error", + "default_content": "## What I do\n\n- Describe features\n\n## Instructions\n\n- Specific instructions", + "save_failed": "Save failed: {error}", + "installing_skill": "Installing {name}...", + "no_skills_found": "No Skills found", + "checking_updates": "Checking for updates...", + "no_skills_selected": "No Skills selected", + "check_updates_failed": "Failed to check updates: {error}", + "updating_skills": "Updating Skills (0/{total})...", + "updating_skill": "Updating {name} ({current}/{total})...", + "update_success": "Successfully updated {count} Skills", + "partial_success": "Partial Success", + "partial_success_msg": "Successfully updated {success}, failed {failed}\n\nFailed details:\n{details}", + "all_failed": "All updates failed\n\nDetails:\n{details}", + "delete_failed": "Delete failed: {error}", + "scan_failed_desc": "Scan failed: {error}" + }, + "export_ui": { + "export_failed": "Export Failed", + "restore_backup_title": "Restore Backup", + "restore_failed": "Restore Failed", + "cannot_restore": "Cannot restore backup", + "edit_config_title": "Edit {type} Common Config", + "edit_codex_hint": "Edit Codex common config (TOML format), will be merged into config.toml", + "edit_gemini_hint": "Edit Gemini common config (ENV format), will be merged into .env file", + "click_preview": "Click \"Preview Convert\" to view side-by-side comparison.", + "not_found": "Not found", + "preview_title": "Config Conversion Preview", + "original_config": "Original Config", + "select_config_first": "Please select a config to import first", + "confirm_import": "Confirm Import", + "import_summary": "Will import the following config:\n• Provider: {providers}\n• Permission: {perms}\n\nContinue?", + "conflict_title": "Conflict", + "conflict_msg": "Provider \"{name}\" already exists, overwrite?", + "preview_first": "Please preview conversion results first", + "confirm_mapping_title": "Confirm Import Mapping", + "confirm_fields": "Please confirm required fields", + "confirm_import_btn": "Confirm Import", + "cannot_read_backup": "Cannot read backup content: {error}", + "backup_preview_title": "Backup Content Preview", + "confirm_restore": "Confirm Restore", + "confirm_restore_msg": "Are you sure you want to restore this backup?\nCurrent config will be overwritten (auto-backed up first).", + "example_general_config": "Example general configuration" + }, + "plugin_ui": { + "install_plugin_failed": "Failed to install plugin: {error}", + "uninstall_plugin_failed": "Failed to uninstall plugin: {error}", + "check_version_failed": "Failed to check version: {error}", + "plugin_skill_discovery": "Auto-discover and register Skills as dynamic tools, supporting Anthropic Agent Skills specification", + "plugin_multi_agent": "Multi-Agent collaboration and workflow orchestration, supporting round-based discussion and parallel exploration", + "plugin_helicone": "Auto-inject Helicone session ID and name for LLM request grouping and tracking", + "plugin_wakatime": "Code time tracking, auto-record coding time and project statistics", + "plugin_category_tools": "Tool Enhancement", + "plugin_category_collab": "Collaboration Enhancement", + "plugin_category_monitor": "Monitoring & Tracking", + "page_title": "Plugin Management", + "tab_plugins": "Plugin Management", + "search_placeholder": "Search plugins...", + "install_plugin_btn": "Install Plugin", + "check_update_btn": "Check Updates", + "market_btn": "Plugin Market", + "table_headers": "Plugin Name,Version,Type,Status,Description,Action", + "detecting": "Detecting...", + "enable_plugin": "Enable Plugin", + "refresh_status": "Refresh Status", + "open_config": "Open Config File", + "disabled_success": "Oh My OpenCode disabled", + "enabled_success": "Oh My OpenCode enabled", + "npm_install_hint": "Please install oh-my-opencode plugin via npm first:\nnpm install -g oh-my-opencode", + "status_refreshed": "Status refreshed", + "status_installed_enabled": "Installed and enabled", + "disable_plugin": "Disable Plugin", + "status_installed_disabled": "Installed but not enabled", + "status_config_error": "Config error: enabled but config file missing", + "status_not_installed": "Not installed", + "install_plugin_label": "Install Plugin", + "config_not_exist": "Config file does not exist, please install Oh My OpenCode plugin first", + "type_npm": "npm", + "type_local": "local", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "uninstall_tooltip": "Uninstall plugin", + "confirm_uninstall": "Confirm Uninstall", + "confirm_uninstall_msg": "Are you sure you want to uninstall plugin {name}?\n\nNote: OpenCode needs to restart to take effect.", + "uninstall_success": "Plugin {name} uninstalled", + "uninstall_failed": "Uninstall plugin failed", + "checking_updates": "Checking for updates...", + "install_dialog_title": "Install Plugin", + "install_method": "Install method:", + "from_npm": "From npm", + "from_local": "From local file", + "npm_package_name": "npm package name:", + "npm_placeholder": "e.g. opencode-skills or opencode-skills@0.1.0", + "npm_hint": "Supports regular and scoped packages (e.g. @my-org/plugin)", + "local_file": "Local file:", + "local_placeholder": "Select .js or .ts file", + "browse_btn": "Browse...", + "select_plugin_file": "Select Plugin File", + "enter_npm_name": "Please enter npm package name", + "plugin_added": "Plugin {name} added to config\n\nOpenCode will auto-install on next startup", + "install_failed": "Install plugin failed", + "select_plugin_file_first": "Please select a plugin file", + "local_not_implemented": "Local plugin installation not yet implemented", + "market_title": "Plugin Market", + "preset_plugins": "Preset Plugins", + "market_headers": "Plugin Name,Category,Description,Action" + } } \ No newline at end of file diff --git a/locales/zh_CN.json b/locales/zh_CN.json index d70af50..39f8b9a 100644 --- a/locales/zh_CN.json +++ b/locales/zh_CN.json @@ -3,28 +3,28 @@ "title": "OpenCode Config Manager", "version": "版本" }, - "menu": { - "home": "首页", - "provider": "Provider 管理", - "native_provider": "原生 Provider", - "model": "Model 管理", - "mcp": "MCP 服务器", - "agent": "Agent 配置", - "ohmyagent": "Oh My Agent", - "category": "Category 管理", - "permission": "权限管理", - "skill": "Skill 管理", - "rules": "Rules 管理", - "compaction": "上下文压缩", - "backup": "备份管理", - "import": "外部导入", - "export": "CLI 工具导出", - "monitor": "监控", - "theme": "切换主题", - "help": "帮助说明", - "settings": "设置", - "language": "语言切换" - }, + "menu": { + "home": "首页", + "provider": "Provider 管理", + "native_provider": "原生 Provider", + "model": "Model 管理", + "mcp": "MCP 服务器", + "agent": "Agent 配置", + "ohmyagent": "Oh My Agent", + "category": "Category 管理", + "permission": "权限管理", + "skill": "Skill 管理", + "rules": "Rules 管理", + "compaction": "上下文压缩", + "backup": "备份管理", + "import": "外部导入", + "export": "CLI 工具导出", + "monitor": "监控", + "theme": "切换主题", + "help": "帮助说明", + "settings": "设置", + "language": "语言切换" + }, "common": { "add": "添加", "edit": "编辑", @@ -75,23 +75,33 @@ "cut": "剪切", "undo": "撤销", "redo": "重做", - "select_item_first": "请先选择一项", - "added": "已添加", - "updated": "已更新", - "add_from_preset": "从预设添加", - "bulk_model": "批量模型", - "keep_all": "- 全部保持 -", - "confirm_delete_title": "确认删除", - "please_enter_pattern": "请输入模式", - "please_enter_source": "请输入来源", - "please_enter_file_path": "请输入文件路径", - "cannot_convert_format": "无法转换此配置格式", - "config_not_exist_or_empty": "所选配置不存在或为空", - "no_valid_import_config": "未确认任何有效的导入配置", - "sdk": "SDK", - "provider": "Provider", - "no_data": "暂无数据" - }, + "select_item_first": "请先选择一项", + "added": "已添加", + "updated": "已更新", + "add_from_preset": "从预设添加", + "bulk_model": "批量模型", + "keep_all": "- 全部保持 -", + "confirm_delete_title": "确认删除", + "please_enter_pattern": "请输入模式", + "please_enter_source": "请输入来源", + "please_enter_file_path": "请输入文件路径", + "cannot_convert_format": "无法转换此配置格式", + "config_not_exist_or_empty": "所选配置不存在或为空", + "no_valid_import_config": "未确认任何有效的导入配置", + "sdk": "SDK", + "provider": "Provider", + "no_data": "暂无数据", + "failed": "失败", + "testing": "测试中", + "optional": "可选", + "unlimited": "无限", + "please_wait": "请稍候", + "local": "本地", + "latest": "最新", + "has_update": "有更新", + "check_failed": "检查失败", + "unknown": "未知" + }, "home": { "title": "首页", "welcome": "欢迎使用 OpenCode Config Manager", @@ -136,13 +146,15 @@ "reset_to_default_backup": "已重置为默认备份目录", "config_reloaded": "配置已重新加载", "backup_success": "配置已备份", - "backup_failed": "备份失败" + "backup_failed": "备份失败", + "detect_vendors": "厂商识别", + "base_preset": "基础" }, - "provider": { - "title": "Provider 管理", - "custom_provider": "自定义 Provider", - "native_provider": "原生 Provider", - "add_provider": "添加 Provider", + "provider": { + "title": "Provider 管理", + "custom_provider": "自定义 Provider", + "native_provider": "原生 Provider", + "add_provider": "添加 Provider", "edit_provider": "编辑 Provider", "delete_provider": "删除 Provider", "fetch_models": "拉取模型", @@ -166,7 +178,6 @@ "deleted_success": "Provider \"{name}\" 已删除", "select_first": "请先选择一个 Provider", "fetch_models_hint": "正在获取 {name} 模型列表...", - "fetch_failed": "获取失败: {error}", "no_models_found": "未获取到任何模型", "no_models_selected": "未选择任何模型", "models_added": "已添加 {count} 个模型", @@ -244,18 +255,20 @@ "usage_rate": "使用率", "expiry": "有效期", "never_expire": "永不过期", - "test_connection": "测试连接", - "connection_success": "连接成功", - "connection_failed": "连接失败", - "response_time": "响应时间", - "test_failed": "测试失败", - "please_configure_provider": "请先配置此 Provider", - "api_key_not_found": "未找到 API Key", - "cannot_determine_api_address": "无法确定 API 地址", - "fetch_failed": "获取失败", - "enter_name": "请输入 Provider 名称", - "provider_exists": "Provider \"{name}\" 已存在" - }, + "test_connection": "测试连接", + "connection_success": "连接成功", + "connection_failed": "连接失败", + "response_time": "响应时间", + "test_failed": "测试失败", + "please_configure_provider": "请先配置此 Provider", + "api_key_not_found": "未找到 API Key", + "cannot_determine_api_address": "无法确定 API 地址", + "fetch_failed": "获取失败", + "enter_name": "请输入 Provider 名称", + "provider_exists": "Provider \"{name}\" 已存在", + "api_key_hint": "API Key 未找到。请先配置Provider或设置环境变量。", + "no_base_url_skip": ",跳过自动拉取" + }, "model": { "title": "Model 管理", "add_model": "添加模型", @@ -289,16 +302,16 @@ "support_attachment": "支持附件 (图片/文档)", "input_modality": "输入模态", "output_modality": "输出模态", - "configure_provider": "配置 {name}", - "test_failed_not_configured": "测试失败:请先配置 {field}", - "options_config": "Options 配置", - "variants_config": "Variants 变体", - "reasoning_config": "推理配置", - "one_click_add": "一键添加", - "fetch_models": "获取模型", - "select_models_to_add": "选择要添加的模型", - "add_selected": "添加选中", - "key_value_list": "键值对列表", + "configure_provider": "配置 {name}", + "test_failed_not_configured": "测试失败:请先配置 {field}", + "options_config": "Options 配置", + "variants_config": "Variants 变体", + "reasoning_config": "推理配置", + "one_click_add": "一键添加", + "fetch_models": "获取模型", + "select_models_to_add": "选择要添加的模型", + "add_selected": "添加选中", + "key_value_list": "键值对列表", "key": "键", "value": "值", "add": "添加", @@ -371,43 +384,43 @@ "deleted_success": "MCP \"{name}\" 已删除", "ohmy_updated_success": "Oh My MCP 配置已更新", "select_first": "请先选择一个 MCP 服务器", - "additional_info": "附加信息(点击标题展开/收起)", - "tags": "标签", - "homepage_link": "主页链接", - "docs_link": "文档链接", - "full_json_preview": "完整 JSON 预览", - "full_mcp_config_preview": "完整 MCP 配置预览", - "include_wrapper": "包含 mcpServers 包装", - "dialog": { - "edit_title": "编辑 MCP", - "add_local_title": "添加 Local MCP", - "add_remote_title": "添加 Remote MCP", - "preset_label": "常用 MCP 预设:", - "preset_tooltip": "点击应用预设", - "preset_disabled_tooltip": "当前为{current}MCP,此预设为{preset}类型", - "mcp_name_label": "MCP 名称:", - "mcp_name_placeholder": "如: context7, filesystem", - "enable_checkbox": "启用此 MCP 服务器", - "command_label": "启动命令 (JSON数组):", - "command_placeholder": "[\"npx\", \"-y\", \"@mcp/server\"]", - "env_label": "环境变量 (JSON对象):", - "env_placeholder": "{\"API_KEY\": \"xxx\"}", - "url_label": "服务器 URL:", - "url_placeholder": "https://mcp.example.com/mcp", - "headers_label": "请求头 (JSON对象):", - "headers_placeholder": "{\"Authorization\": \"Bearer xxx\"}", - "timeout_label": "超时 (ms):", - "timeout_placeholder": "30000", - "preview_label": "配置预览:", - "name_required": "请输入 MCP 名称", - "name_exists": "MCP 名称已存在", - "command_required": "请输入启动命令", - "command_invalid": "命令格式错误: {error}", - "env_invalid": "环境变量格式错误: {error}", - "url_required": "请输入服务器 URL", - "headers_invalid": "请求头格式错误: {error}", - "timeout_invalid": "超时必须是正整数" - }, + "additional_info": "附加信息(点击标题展开/收起)", + "tags": "标签", + "homepage_link": "主页链接", + "docs_link": "文档链接", + "full_json_preview": "完整 JSON 预览", + "full_mcp_config_preview": "完整 MCP 配置预览", + "include_wrapper": "包含 mcpServers 包装", + "dialog": { + "edit_title": "编辑 MCP", + "add_local_title": "添加 Local MCP", + "add_remote_title": "添加 Remote MCP", + "preset_label": "常用 MCP 预设:", + "preset_tooltip": "点击应用预设", + "preset_disabled_tooltip": "当前为{current}MCP,此预设为{preset}类型", + "mcp_name_label": "MCP 名称:", + "mcp_name_placeholder": "如: context7, filesystem", + "enable_checkbox": "启用此 MCP 服务器", + "command_label": "启动命令 (JSON数组):", + "command_placeholder": "[\"npx\", \"-y\", \"@mcp/server\"]", + "env_label": "环境变量 (JSON对象):", + "env_placeholder": "{\"API_KEY\": \"xxx\"}", + "url_label": "服务器 URL:", + "url_placeholder": "https://mcp.example.com/mcp", + "headers_label": "请求头 (JSON对象):", + "headers_placeholder": "{\"Authorization\": \"Bearer xxx\"}", + "timeout_label": "超时 (ms):", + "timeout_placeholder": "30000", + "preview_label": "配置预览:", + "name_required": "请输入 MCP 名称", + "name_exists": "MCP 名称已存在", + "command_required": "请输入启动命令", + "command_invalid": "命令格式错误: {error}", + "env_invalid": "环境变量格式错误: {error}", + "url_required": "请输入服务器 URL", + "headers_invalid": "请求头格式错误: {error}", + "timeout_invalid": "超时必须是正整数" + }, "ohmy_dialog": { "title": "Oh My OpenCode MCP 管理", "info_text": "Oh My OpenCode 默认启用以下 MCP 服务器。您可以选择禁用不需要的服务器。", @@ -429,7 +442,15 @@ "context7_desc": "获取最新官方文档 - 为库和框架获取最新的官方文档", "grep_app_desc": "超快代码搜索 - 通过 grep.app 在数百万公共 GitHub 仓库中搜索代码", "default_desc": "{name} MCP 服务器" - } + }, + "preset_fetch": "抓取网页内容与资源的基础 MCP 服务器", + "preset_time": "提供时间相关工具的轻量 MCP 服务器", + "preset_memory": "提供记忆图谱能力的 MCP 服务器", + "preset_thinking": "结构化推理与分步思考的 MCP 服务器", + "preset_context7": "提供最新文档检索的 Context7 MCP", + "preset_chrome": "连接 Chrome DevTools 的调试 MCP 服务器", + "preset_websearch": "开放网页搜索与打开页面的 MCP 服务器", + "preset_serena": "提供本地项目理解与指令执行的 Serena MCP" }, "agent": { "title": "Agent 配置", @@ -490,7 +511,10 @@ "add_selected": "添加选中", "select_button": "选择", "cancel_button": "取消" - } + }, + "enter_description": "请输入 Agent 描述", + "permission_json_error": "权限配置 JSON 格式错误: {error}", + "apply_group_failed": "应用分组失败: {error}" }, "skill": { "title": "Skill 管理", @@ -539,18 +563,18 @@ "source_claude_global": "🌐 Claude 全局", "source_claude_project": "📁 Claude 项目", "source_unknown": "❓ 未知", - "market_dialog": { - "title": "Skill 市场", - "subtitle": "精选 Skills 集合", - "search_placeholder": "搜索 Skills...", - "category_all": "全部分类", - "category_label": "分类", - "repo_label": "仓库", - "install_button": "安装选中", - "close_button": "关闭", - "no_selection": "请先选择要安装的 Skill", - "browse_more": "🌐 浏览更多社区技能 (SkillsMP.com)" - }, + "market_dialog": { + "title": "Skill 市场", + "subtitle": "精选 Skills 集合", + "search_placeholder": "搜索 Skills...", + "category_all": "全部分类", + "category_label": "分类", + "repo_label": "仓库", + "install_button": "安装选中", + "close_button": "关闭", + "no_selection": "请先选择要安装的 Skill", + "browse_more": "🌐 浏览更多社区技能 (SkillsMP.com)" + }, "categories": { "dev_tools": "开发工具", "code_quality": "代码质量", @@ -723,42 +747,20 @@ } }, "monitor": { - "title": "监控", - "start_monitoring": "启动监控", - "stop_monitoring": "停止监控", - "check_now": "立即检测", - "clear_history": "清空历史", - "monitoring_targets": "监控目标", - "status": "状态", - "latency": "延迟", - "last_check": "最后检测", - "history": "历史记录", - "available": "可用", - "unavailable": "不可用", - "checking": "检测中...", - "never_checked": "未检测", - "monitoring_started": "监控已启动", - "monitoring_stopped": "监控已停止", - "history_cleared": "历史记录已清空", - "no_targets": "无目标", - "availability_rate": "可用率", - "error_count": "异常", - "chat_latency": "对话延迟", - "ping": "Ping", - "target_count": "目标", - "last_checked_short": "最近", - "check": "检测", - "start": "启动", - "toggle_tooltip": "启动/停止对话延迟自动检测", - "start_tooltip": "启动对话延迟自动检测(Ping 检测不受影响)", - "stop_tooltip": "停止对话延迟自动检测(Ping 检测不受影响)", - "model_provider": "模型/提供商", - "ping_latency": "Ping延迟", - "status_operational": "正常", - "status_degraded": "延迟", - "status_failed": "异常", - "status_error": "错误", - "status_no_config": "未配置" + "request_timeout": "请求超时", + "no_base_url": "未配置 baseURL", + "chat_test_paused": "对话测试已暂停 (Ping 正常)", + "ping_failed": "Ping 失败", + "no_valid_host": "未配置有效的主机", + "no_api_key": "未配置 apiKey", + "base_url_invalid": "baseURL 无效", + "status_normal": "正常", + "latency_high": "延迟较高 ({latency_ms}ms)", + "auth_failed": "鉴权失败", + "connection_failed": "连接失败: {reason}", + "no_base_url_configured": "未配置 baseURL", + "no_valid_host_configured": "未配置有效的主机", + "no_apikey_configured": "未配置 apiKey" }, "cli_export": { "title": "CLI 工具导出", @@ -837,7 +839,8 @@ "preview_card_description": "点击「预览转换」在弹窗中查看左右对照。", "select_config_file": "选择配置文件", "config_files": "配置文件 (*.json *.jsonc *.toml);;所有文件 (*.*)", - "select_config_to_convert": "请先选择要转换的配置" + "select_config_to_convert": "请先选择要转换的配置", + "manual_confirm": "手动确认" }, "permission": { "title": "权限管理", @@ -874,38 +877,40 @@ "category_added": "Category 已添加", "preset_category_added": "预设 Category 已添加", "category_updated": "Category 已更新", - "preset_dialog_title": "从预设添加 Category" + "preset_dialog_title": "从预设添加 Category", + "edit_title": "编辑 Category", + "already_exists": "Category \"{name}\" 已存在" + }, + "ohmyagent": { + "title": "Oh My Agent", + "add_agent": "添加 Agent", + "edit_agent": "编辑 Agent", + "delete_agent": "删除 Agent", + "agent_name": "Agent 名称", + "provider": "Provider", + "model": "模型", + "bulk_model": "批量模型", + "select_provider": "选择 Provider", + "select_model": "选择模型", + "agent_saved": "Agent 已保存", + "agent_deleted": "Agent 已删除", + "delete_confirm_msg": "确定要删除 Agent \"{name}\" 吗?", + "agent_added": "Agent 已添加", + "preset_agent_added": "预设 Agent 已添加", + "agent_updated": "Agent 已更新", + "group_tip": "💡 提示:Agent 可以设置自定义分组和预设分组,在 Agent 配置菜单中统一管理。点击上方的【管理】按钮即可创建和编辑分组。", + "preset_dialog": { + "title": "从预设添加 Agent", + "subtitle": "选择预设 Agent", + "select_preset": "请选择一个预设 Agent", + "agent_exists": "Agent \"{name}\" 已存在" + }, + "dialog": { + "title": "添加/编辑 Agent", + "subtitle": "配置 Agent 信息", + "model_label": "绑定模型:" + } }, - "ohmyagent": { - "title": "Oh My Agent", - "add_agent": "添加 Agent", - "edit_agent": "编辑 Agent", - "delete_agent": "删除 Agent", - "agent_name": "Agent 名称", - "provider": "Provider", - "model": "模型", - "bulk_model": "批量模型", - "select_provider": "选择 Provider", - "select_model": "选择模型", - "agent_saved": "Agent 已保存", - "agent_deleted": "Agent 已删除", - "delete_confirm_msg": "确定要删除 Agent \"{name}\" 吗?", - "agent_added": "Agent 已添加", - "preset_agent_added": "预设 Agent 已添加", - "agent_updated": "Agent 已更新", - "group_tip": "💡 提示:Agent 可以设置自定义分组和预设分组,在 Agent 配置菜单中统一管理。点击上方的【管理】按钮即可创建和编辑分组。", - "preset_dialog": { - "title": "从预设添加 Agent", - "subtitle": "选择预设 Agent", - "select_preset": "请选择一个预设 Agent", - "agent_exists": "Agent \"{name}\" 已存在" - }, - "dialog": { - "title": "添加/编辑 Agent", - "subtitle": "配置 Agent 信息", - "model_label": "绑定模型:" - } - }, "help": { "title": "帮助", "about_description": "一个可视化的GUI工具,用于管理OpenCode和Oh My OpenCode的配置文件", @@ -979,12 +984,12 @@ "file_path_placeholder": "文件路径,如: CONTRIBUTING.md, docs/*.md", "agents_md_edit": "AGENTS.md 编辑", "reload": "重新加载", - "use_template": "使用模板", - "agents_md_not_exist": "# AGENTS.md 文件不存在\n# 点击\"使用模板\"创建新文件", - "path_label": "路径", - "read_failed": "读取失败", - "save_failed": "保存失败: {error}" - }, + "use_template": "使用模板", + "agents_md_not_exist": "# AGENTS.md 文件不存在\n# 点击\"使用模板\"创建新文件", + "path_label": "路径", + "read_failed": "读取失败", + "save_failed": "保存失败: {error}" + }, "compaction": { "title": "上下文压缩", "enable_compaction": "启用上下文压缩", @@ -1029,14 +1034,32 @@ "config_deleted": "Provider 配置已删除", "test_success": "连接测试成功", "test_failed": "连接测试失败", - "select_provider_first": "请先选择一个 Provider", - "detected_env_vars": "检测到环境变量", - "auth_config": "认证配置", - "import_env_var": "导入 {env_var}", - "provider_options": "Provider 选项", - "provider_name": "Provider", - "detect_configured": "检测已配置" - }, + "select_provider_first": "请先选择一个 Provider", + "detected_env_vars": "检测到环境变量", + "auth_config": "认证配置", + "import_env_var": "导入 {env_var}", + "provider_options": "Provider 选项", + "provider_name": "Provider", + "detect_configured": "检测已配置", + "confirm_delete_title": "确认删除", + "confirm_delete_msg": "确定要删除 {name} 的配置吗?\n这将删除认证信息和选项配置。", + "detect_complete": "检测完成", + "no_provider_detected": "未检测到已配置的 Provider", + "detected_providers": "检测到以下已配置的 Provider:\n\n{list}", + "testing_url": "正在测试连接: {url}", + "delete_failed": "删除失败", + "cannot_delete_auth": "无法删除认证配置: {error}", + "delete_success": "删除成功", + "config_deleted_msg": "{name} 配置已删除", + "auth_json_count": "auth.json中已配置 ({count}个):\n", + "env_var_count": "环境变量已配置 ({count}个):\n", + "env_var_hint": "\n\n💡 提示: 环境变量配置的Provider可以直接使用,", + "env_var_hint2": "但建议点击\"配置Provider\"保存到auth.json以便管理", + "detect_result": "检测结果", + "no_native_detected": "未检测到已配置的原生Provider", + "status_auth_and_env": "auth.json + 环境变量", + "status_env_only": "环境变量已配置,但未保存到auth.json" + }, "dialog": { "select_at_least_one_model": "请选择至少一个模型", "disabled_all_ohmymcp": "已禁用所有 Oh My MCP 服务器", @@ -1115,191 +1138,800 @@ "no_skill_selected": "请先选择一个 Skill", "skill_deleted": "Skill 已删除", "skill_delete_failed": "删除 Skill 失败: {error}", - "permission_config_saved": "权限配置已保存", - "agent_config_saved": "Agent 配置已保存" - }, - "agent_group": { - "current_group": "当前分组:", - "apply": "应用", - "manage": "分组管理", - "no_group": "无分组", - "dialog": { - "title": "Agent 分组管理", - "my_groups": "我的分组", - "presets": "预设模板", - "new_group": "新建分组", - "import": "导入分组", - "export": "导出分组", - "use_template": "使用模板", - "usage_count": "使用次数: {count}次", - "last_used_days": "最后使用: {days}天前", - "last_used_hours": "最后使用: {hours}小时前", - "last_used_recent": "最后使用: 最近", - "delete_confirm_title": "确认删除", - "delete_confirm_content": "确定要删除分组 \"{name}\" 吗?此操作不可恢复。", - "delete_success": "分组已删除", - "delete_failed": "删除分组失败", - "apply_info": "应用分组", - "apply_info_content": "将应用分组 \"{name}\" 的配置", - "create_from_preset": "从模板创建分组", - "group_name_placeholder": "输入分组名称", - "group_desc_placeholder": "输入分组描述", - "name_required": "分组名称不能为空", - "create_success": "分组创建成功", - "create_failed": "创建分组失败", - "import_title": "导入分组", - "import_success": "分组导入成功", - "import_failed": "导入分组失败", - "select_group_first": "请先选择一个分组", - "export_not_implemented": "导出功能开发中" - }, - "edit": { - "title_new": "创建 Agent 分组", - "title_edit": "编辑 Agent 分组", - "basic_info": "基本信息", - "name": "分组名称", - "name_placeholder": "输入分组名称", - "icon": "图标", - "description": "描述", - "desc_placeholder": "输入分组描述", - "opencode_agents": "OpenCode Agent 配置", - "omo_agents": "Oh My OpenCode Agent 配置", - "enabled": "启用", - "agent_id": "Agent ID", - "temperature": "Temperature", - "max_steps": "Max Steps", - "provider": "Provider", - "model": "Model", - "name_required": "分组名称不能为空", - "save_success": "分组保存成功", - "save_failed": "保存分组失败" - } - }, - "web": { - "logout": "退出登录", - "please_enter_name": "请输入名称", - "tools_must_be_json": "Tools 必须是合法 JSON", - "tools_must_be_array": "Tools 必须是 JSON 数组", - "confirm_delete_agent": "确认删除该 Agent 吗?", - "confirm_delete_omo_agent": "确认删除该 OMO Agent 吗?", - "edit_permission": "编辑权限", - "delete_skill": "删除 Skill", - "update_status": "更新状态", - "please_enter_github": "请输入 GitHub 地址", - "github_only": "仅支持 GitHub 地址", - "edit_skill_permission": "编辑 Skill 权限", - "skill_dir_not_found": "未找到 Skill 文件目录", - "source_parse_failed": "来源解析失败", - "unknown": "未知", - "not_checked": "未检查", - "repository": "仓库", - "category": "分类", - "market_repo_invalid": "市场仓库格式无效", - "backup_create_failed": "创建备份失败", - "restore_failed": "恢复失败", - "delete_failed": "删除失败", - "category_exists": "该分类已存在", - "edit_target_not_found": "未找到要编辑的记录", - "delete_target_not_found": "未找到要删除的记录", - "record_not_exist": "记录不存在,可能已被删除", - "delete_irreversible": "删除后不可恢复,请确认操作。", - "please_enter_path": "请输入路径", - "path_cannot_be_empty": "路径不能为空", - "invalid_index": "无效索引", - "permission_rule": "权限规则", - "custom": "自定义", - "command_pattern_optional": "命令模式(可选)", - "enter_valid_tool": "请输入有效工具名", - "config_file_paths": "配置文件路径", - "related_links": "相关链接", - "opencode_docs": "OpenCode 官方文档", - "backup_dir": "备份目录", - "compaction_strategy": "压缩策略 (strategy)", - "max_tokens": "最大 Token (maxTokens)", - "refresh_preview": "刷新预览", - "thinking_config_optional": "Thinking 配置 (可选)", - "please_select_provider": "请先选择 Provider", - "please_enter_model_id": "请输入 Model ID", - "provider_not_exist": "Provider 不存在", - "provider_config_error": "Provider 配置异常", - "model_exists": "该 Model 已存在", - "model_not_found": "未找到该模型", - "options_must_be_json": "options 必须是合法 JSON", - "options_must_be_object": "options 必须是 JSON 对象", - "variants_must_be_json": "variants 必须是合法 JSON", - "variants_must_be_object": "variants 必须是 JSON 对象", - "monitor_not_started": "监控未启动", - "refresh_targets": "刷新目标", - "target_id": "目标ID", - "latency_ms": "延迟(ms)", - "check_time": "检查时间", - "description": "说明", - "pending_check": "待检测", - "no_monitor_result": "尚未产生监控结果", - "no_monitor_targets": "没有可监控目标,请先配置 Provider 与模型", - "monitor_running": "监控运行中...", - "monitor_stopped": "监控已停止", - "no_env_detected": "未检测到已配置的环境变量", - "value_masked": "值(已遮蔽)", - "config_not_found": "未找到配置", - "paramiko_required": "远程管理功能需要安装 paramiko:pip install paramiko", - "host": "主机", - "port": "端口", - "username": "用户名", - "auth_type": "认证方式", - "add_remote_server": "添加远程服务器", - "name_optional": "名称(可选)", - "host_address": "主机地址", - "ssh_port": "SSH端口", - "password": "密码", - "key_path": "密钥路径", - "remote_config_path": "远程配置路径(可选)", - "please_enter_host": "请输入主机地址", - "add_server": "添加服务器", - "test_connection": "测试连接", - "confirm_delete_server": "确定要删除选中的服务器吗?", - "remote_config_ops": "远程配置操作", - "read": "读取", - "read_config_done": "已读取", - "read_failed": "读取失败", - "status_fetched": "已获取状态", - "status_fetch_failed": "获取状态失败", - "view_remote_status": "查看远程状态", - "create_remote_backup": "创建远程备份", - "remote_backup_created": "远程备份已创建", - "backup_failed": "备份失败", - "server_not_found": "未找到服务器", - "select_server_hint": "选择服务器后点击读取按钮查看配置", - "version": "版本", - "source": "来源", - "uninstall_selected": "卸载选中插件", - "please_enter_github_url": "请输入 GitHub URL", - "github_url_invalid": "GitHub URL 格式无效", - "install_failed": "安装失败", - "plugin_not_found": "未找到选中插件", - "uninstall_failed": "卸载失败", - "no_provider_detected": "未检测到 Provider,请先在 Provider 页面配置", - "select_provider": "选择 Provider", - "select_model": "选择 Model", - "export_preview": "导出预览", - "please_select_valid_provider": "请选择有效 Provider", - "please_select_model": "请输入或选择模型", - "copied_to_clipboard": "已复制到剪贴板", - "copy_failed": "复制失败,请检查浏览器权限", - "copy_preview": "复制预览", - "do_export": "执行导出", - "scan_detect": "扫描检测", - "preview_hint": "预览区域:请先扫描并选择来源", - "scan_done_select": "扫描完成,请选择来源后点击预览转换", - "source_not_detected": "未检测到该来源配置", - "convert_failed": "转换失败或无可导入内容", - "please_preview_first": "请先执行预览转换", - "command_json_array": "Command (JSON数组)", - "convert_preview": "转换预览", - "please_select_skill": "请先选择一行 Skill", - "server_added": "已添加 {name}", - "connection_success": "连接成功", - "connection_failed": "连接失败", - "github_input_label": "GitHub (user/repo 或 URL)" - } + "permission_config_saved": "权限配置已保存", + "agent_config_saved": "Agent 配置已保存" + }, + "agent_group": { + "current_group": "当前分组:", + "apply": "应用", + "manage": "分组管理", + "no_group": "无分组", + "dialog": { + "title": "Agent 分组管理", + "my_groups": "我的分组", + "presets": "预设模板", + "new_group": "新建分组", + "import": "导入分组", + "export": "导出分组", + "use_template": "使用模板", + "usage_count": "使用次数: {count}次", + "last_used_days": "最后使用: {days}天前", + "last_used_hours": "最后使用: {hours}小时前", + "last_used_recent": "最后使用: 最近", + "delete_confirm_title": "确认删除", + "delete_confirm_content": "确定要删除分组 \"{name}\" 吗?此操作不可恢复。", + "delete_success": "分组已删除", + "delete_failed": "删除分组失败", + "apply_info": "应用分组", + "apply_info_content": "将应用分组 \"{name}\" 的配置", + "create_from_preset": "从模板创建分组", + "group_name_placeholder": "输入分组名称", + "group_desc_placeholder": "输入分组描述", + "name_required": "分组名称不能为空", + "create_success": "分组创建成功", + "create_failed": "创建分组失败", + "import_title": "导入分组", + "import_success": "分组导入成功", + "import_failed": "导入分组失败", + "select_group_first": "请先选择一个分组", + "export_not_implemented": "导出功能开发中" + }, + "edit": { + "title_new": "创建 Agent 分组", + "title_edit": "编辑 Agent 分组", + "basic_info": "基本信息", + "name": "分组名称", + "name_placeholder": "输入分组名称", + "icon": "图标", + "description": "描述", + "desc_placeholder": "输入分组描述", + "opencode_agents": "OpenCode Agent 配置", + "omo_agents": "Oh My OpenCode Agent 配置", + "enabled": "启用", + "agent_id": "Agent ID", + "temperature": "Temperature", + "max_steps": "Max Steps", + "provider": "Provider", + "model": "Model", + "name_required": "分组名称不能为空", + "save_success": "分组保存成功", + "save_failed": "保存分组失败" + }, + "apply_failed": "应用分组失败: {error}" + }, + "web": { + "logout": "退出登录", + "please_enter_name": "请输入名称", + "tools_must_be_json": "Tools 必须是合法 JSON", + "tools_must_be_array": "Tools 必须是 JSON 数组", + "confirm_delete_agent": "确认删除该 Agent 吗?", + "confirm_delete_omo_agent": "确认删除该 OMO Agent 吗?", + "edit_permission": "编辑权限", + "delete_skill": "删除 Skill", + "update_status": "更新状态", + "please_enter_github": "请输入 GitHub 地址", + "github_only": "仅支持 GitHub 地址", + "edit_skill_permission": "编辑 Skill 权限", + "skill_dir_not_found": "未找到 Skill 文件目录", + "source_parse_failed": "来源解析失败", + "unknown": "未知", + "not_checked": "未检查", + "repository": "仓库", + "category": "分类", + "market_repo_invalid": "市场仓库格式无效", + "backup_create_failed": "创建备份失败", + "restore_failed": "恢复失败", + "delete_failed": "删除失败", + "category_exists": "该分类已存在", + "edit_target_not_found": "未找到要编辑的记录", + "delete_target_not_found": "未找到要删除的记录", + "record_not_exist": "记录不存在,可能已被删除", + "delete_irreversible": "删除后不可恢复,请确认操作。", + "please_enter_path": "请输入路径", + "path_cannot_be_empty": "路径不能为空", + "invalid_index": "无效索引", + "permission_rule": "权限规则", + "custom": "自定义", + "command_pattern_optional": "命令模式(可选)", + "enter_valid_tool": "请输入有效工具名", + "config_file_paths": "配置文件路径", + "related_links": "相关链接", + "opencode_docs": "OpenCode 官方文档", + "backup_dir": "备份目录", + "compaction_strategy": "压缩策略 (strategy)", + "max_tokens": "最大 Token (maxTokens)", + "refresh_preview": "刷新预览", + "thinking_config_optional": "Thinking 配置 (可选)", + "please_select_provider": "请先选择 Provider", + "please_enter_model_id": "请输入 Model ID", + "provider_not_exist": "Provider 不存在", + "provider_config_error": "Provider 配置异常", + "model_exists": "该 Model 已存在", + "model_not_found": "未找到该模型", + "options_must_be_json": "options 必须是合法 JSON", + "options_must_be_object": "options 必须是 JSON 对象", + "variants_must_be_json": "variants 必须是合法 JSON", + "variants_must_be_object": "variants 必须是 JSON 对象", + "monitor_not_started": "监控未启动", + "refresh_targets": "刷新目标", + "target_id": "目标ID", + "latency_ms": "延迟(ms)", + "check_time": "检查时间", + "description": "说明", + "pending_check": "待检测", + "no_monitor_result": "尚未产生监控结果", + "no_monitor_targets": "没有可监控目标,请先配置 Provider 与模型", + "monitor_running": "监控运行中...", + "monitor_stopped": "监控已停止", + "no_env_detected": "未检测到已配置的环境变量", + "value_masked": "值(已遮蔽)", + "config_not_found": "未找到配置", + "paramiko_required": "远程管理功能需要安装 paramiko:pip install paramiko", + "host": "主机", + "port": "端口", + "username": "用户名", + "auth_type": "认证方式", + "add_remote_server": "添加远程服务器", + "name_optional": "名称(可选)", + "host_address": "主机地址", + "ssh_port": "SSH端口", + "password": "密码", + "key_path": "密钥路径", + "remote_config_path": "远程配置路径(可选)", + "please_enter_host": "请输入主机地址", + "add_server": "添加服务器", + "test_connection": "测试连接", + "confirm_delete_server": "确定要删除选中的服务器吗?", + "remote_config_ops": "远程配置操作", + "read": "读取", + "read_config_done": "已读取", + "read_failed": "读取失败", + "status_fetched": "已获取状态", + "status_fetch_failed": "获取状态失败", + "view_remote_status": "查看远程状态", + "create_remote_backup": "创建远程备份", + "remote_backup_created": "远程备份已创建", + "backup_failed": "备份失败", + "server_not_found": "未找到服务器", + "select_server_hint": "选择服务器后点击读取按钮查看配置", + "version": "版本", + "source": "来源", + "uninstall_selected": "卸载选中插件", + "please_enter_github_url": "请输入 GitHub URL", + "github_url_invalid": "GitHub URL 格式无效", + "install_failed": "安装失败", + "plugin_not_found": "未找到选中插件", + "uninstall_failed": "卸载失败", + "no_provider_detected": "未检测到 Provider,请先在 Provider 页面配置", + "select_provider": "选择 Provider", + "select_model": "选择 Model", + "export_preview": "导出预览", + "please_select_valid_provider": "请选择有效 Provider", + "please_select_model": "请输入或选择模型", + "copied_to_clipboard": "已复制到剪贴板", + "copy_failed": "复制失败,请检查浏览器权限", + "copy_preview": "复制预览", + "do_export": "执行导出", + "scan_detect": "扫描检测", + "preview_hint": "预览区域:请先扫描并选择来源", + "scan_done_select": "扫描完成,请选择来源后点击预览转换", + "source_not_detected": "未检测到该来源配置", + "convert_failed": "转换失败或无可导入内容", + "please_preview_first": "请先执行预览转换", + "command_json_array": "Command (JSON数组)", + "convert_preview": "转换预览", + "please_select_skill": "请先选择一行 Skill", + "server_added": "已添加 {name}", + "connection_success": "连接成功", + "connection_failed": "连接失败", + "github_input_label": "GitHub (user/repo 或 URL)" + }, + "validator": { + "config_parse_failed": "配置文件无法解析或读取失败", + "root_must_be_object": "配置根必须是对象类型", + "config_empty": "配置为空,尚未添加任何Provider", + "schema_recommend": "建议设置 $schema 为 https://opencode.ai/config.json", + "no_providers": "未配置任何 Provider", + "provider_must_be_object": "provider 必须是对象类型", + "provider_value_not_object": "Provider '{name}' 的值必须是对象,当前是 {type}", + "provider_missing_field": "Provider '{name}' 缺少必需字段 '{field}'", + "provider_field_empty": "Provider '{name}' 的 '{field}' 为空", + "provider_unknown_npm": "Provider '{name}' 的 npm 包 '{npm}' 不在已知列表中", + "provider_options_not_object": "Provider '{name}' 的 options 必须是对象", + "provider_options_missing": "Provider '{name}' 的 options 缺少 '{field}'", + "provider_options_empty": "Provider '{name}' 的 options.{field} 为空", + "provider_models_not_object": "Provider '{name}' 的 models 必须是对象", + "provider_no_models": "Provider '{name}' 没有配置任何模型", + "provider_empty_model_id": "Provider '{name}' 存在空模型ID", + "model_value_not_object": "Model '{model}' 的值必须是对象", + "model_limit_should_be_object": "Model '{model}' 的 limit 应该是对象", + "model_context_should_be_int": "Model '{model}' 的 context 应该是整数", + "model_output_should_be_int": "Model '{model}' 的 output 应该是整数", + "mcp_must_be_object": "mcp 必须是对象类型", + "mcp_value_not_object": "MCP '{name}' 的值必须是对象", + "mcp_local_missing_command": "Local MCP '{name}' 缺少 command 字段", + "mcp_remote_missing_url": "Remote MCP '{name}' 缺少 url 字段", + "agent_must_be_object": "agent 必须是对象类型", + "config_empty_or_invalid": "配置文件为空或无法解析", + "agents_must_be_object": "agents 必须是对象类型", + "no_agents": "未配置任何 Agent", + "agent_name_empty": "Agent 名称为空", + "agent_value_not_object": "Agent '{name}' 的值必须是对象", + "agent_missing_field": "Agent '{name}' 缺少必需字段 '{field}'", + "agent_field_empty": "Agent '{name}' 的 '{field}' 为空", + "agent_description_empty": "Agent '{name}' 的 description 为空", + "no_categories": "未配置任何 Category", + "categories_must_be_object": "categories 必须是对象类型", + "category_name_empty": "Category 名称为空", + "category_value_not_object": "Category '{name}' 的值必须是对象", + "category_missing_field": "Category '{name}' 缺少必需字段 '{field}'", + "category_field_empty": "Category '{name}' 的 '{field}' 为空", + "category_temperature_should_be_number": "Category '{name}' 的 temperature 应该是数字", + "category_description_empty": "Category '{name}' 的 description 为空", + "fix_skip_invalid_provider": "跳过无效 Provider '{name}' (值不是对象)", + "fix_add_default_npm": "Provider '{name}': 添加默认 npm 字段", + "fix_options_field": "Provider '{name}': 修复 options 字段", + "fix_add_empty_baseurl": "Provider '{name}': 添加空 baseURL", + "fix_add_empty_apikey": "Provider '{name}': 添加空 apiKey", + "fix_add_empty_models": "Provider '{name}': 添加空 models 字段", + "fix_models_to_object": "Provider '{name}': 修复 models 字段为对象", + "fix_remove_invalid_limit": "Provider '{name}' Model '{model}': 移除无效 limit", + "fix_remove_empty_limit": "Provider '{name}' Model '{model}': 移除空 limit", + "error_count": "❌ {count} 个错误:", + "error_more": " ... 还有 {count} 个错误", + "warning_count": "⚠️ {count} 个警告:", + "warning_more": " ... 还有 {count} 个警告", + "config_valid": "✅ 配置格式正确", + "config_empty_or_unparseable": "配置文件为空或无法解析", + "omo_root_must_be_object": "配置根必须是对象类型", + "agent_desc_empty": "Agent '{name}' 的 description 为空", + "category_temp_not_number": "Category '{name}' 的 temperature 应该是数字", + "category_desc_empty": "Category '{name}' 的 description 为空", + "errors_count": "❌ {count} 个错误:", + "more_errors": " ... 还有 {count} 个错误", + "warnings_count": "⚠️ {count} 个警告:", + "more_warnings": " ... 还有 {count} 个警告", + "config_ok": "✅ 配置格式正确" + }, + "auth": { + "login_locked": "登录失败过多,请 {remain} 分钟后再试", + "login_failed_locked": "登录失败 5 次,已锁定 15 分钟", + "password_wrong": "密码错误,还可尝试 {remain} 次", + "login_success": "登录成功", + "new_password_min_length": "新密码至少 8 位", + "old_password_wrong": "原密码错误", + "password_changed": "密码修改成功", + "logged_out": "已退出登录", + "not_logged_in": "未登录", + "web_login_subtitle": "OCCM Web 登录", + "admin_password_label": "管理密码", + "login_failed_fallback": "登录失败", + "login_button": "登录", + "old_password_label": "原密码", + "new_password_label": "新密码", + "password_updated_fallback": "密码已更新", + "change_failed_fallback": "修改失败", + "submit_button": "提交" + }, + "skill_manager": { + "name_empty": "名称不能为空", + "name_too_long": "名称不能超过 64 字符", + "name_format_error": "名称格式错误:只能使用小写字母、数字、单连字符分隔", + "desc_empty": "描述不能为空", + "desc_too_long": "描述不能超过 1024 字符", + "parse_failed": "解析 skill 失败 {name}: {error}", + "scan_dir_failed": "遍历目录失败 {path}: {error}", + "downloading": "正在下载...", + "detecting_branch": "检测分支...", + "using_branch": "使用分支: {branch}", + "extracting": "正在解压...", + "subdir_not_found": "子目录不存在: {subdir}", + "skill_file_not_found": "未找到 SKILL.md 或 SKILL.txt 文件", + "skill_file_not_found_in_subdir": "未找到 SKILL.md 或 SKILL.txt 文件 (在 {subdir} 中)", + "skill_file_format_error": "SKILL 文件格式错误", + "installing": "正在安装...", + "install_complete": "安装完成!", + "install_success": "Skill '{name}' 安装成功", + "network_error": "网络错误: {error}", + "install_failed": "安装失败: {error}", + "path_not_found": "路径不存在: {path}", + "skill_md_not_found": "未找到 SKILL.md 文件", + "skill_md_format_error": "SKILL.md 格式错误", + "copying": "正在复制...", + "status_local": "本地", + "status_unknown": "未知", + "status_has_update": "有更新", + "status_latest": "最新", + "status_check_failed": "检查失败", + "check_update_failed": "检查更新失败 {name}: {error}", + "update_github_only": "仅支持更新从 GitHub 安装的 Skills", + "update_failed": "更新失败: {error}", + "scan_failed": "扫描失败: {error}", + "unrecognized_source": "无法识别的来源格式: {source}" + }, + "cli_export_msg": { + "provider_config_incomplete": "Provider 配置不完整: 缺少 {fields}", + "write_config_failed": "写入配置失败 ({path}): {reason}", + "parse_config_failed": "解析 {format} 配置失败 ({path}): {reason}", + "backup_failed": "备份 {cli_type} 配置失败: {reason}", + "restore_failed": "恢复备份失败 ({path}): {reason}", + "json_validation_failed": "JSON 格式验证失败: {error}", + "backup_dir_not_found": "备份目录不存在", + "list_backups_failed": "列出备份失败: {error}", + "delete_old_backup_failed": "删除旧备份失败 ({path}): {error}", + "set_permissions_failed": "设置文件权限失败 ({path}): {error}", + "missing_base_url": "缺少 API 地址 (baseURL)", + "missing_api_key": "缺少 API 密钥 (apiKey)", + "no_models_configured": "未配置任何模型", + "export_failed": "导出失败: {error}", + "export_exception": "导出异常: {error}", + "unknown_cli_type": "未知的 CLI 类型: {cli_type}", + "settings_json_not_found": "settings.json 文件不存在", + "settings_json_missing_env": "settings.json 缺少 env 字段", + "missing_anthropic_base_url": "缺少 ANTHROPIC_BASE_URL", + "missing_anthropic_auth_token": "缺少 ANTHROPIC_AUTH_TOKEN", + "settings_json_format_error": "settings.json 格式错误: {error}", + "read_settings_json_failed": "读取 settings.json 失败: {error}", + "auth_json_not_found": "auth.json 文件不存在", + "auth_json_missing_key": "auth.json 缺少 OPENAI_API_KEY", + "auth_json_format_error": "auth.json 格式错误: {error}", + "read_auth_json_failed": "读取 auth.json 失败: {error}", + "config_toml_not_found": "config.toml 文件不存在", + "config_toml_missing_provider": "config.toml 缺少 model_provider", + "config_toml_missing_model": "config.toml 缺少 model", + "read_config_toml_failed": "读取 config.toml 失败: {error}", + "env_file_not_found": ".env 文件不存在", + "env_missing_api_key": ".env 缺少 GEMINI_API_KEY", + "env_missing_base_url": ".env 缺少 GOOGLE_GEMINI_BASE_URL", + "read_env_failed": "读取 .env 失败: {error}", + "settings_json_not_found_warn": "settings.json 文件不存在", + "settings_json_missing_security": "settings.json 缺少 security 字段", + "settings_json_format_error_gemini": "settings.json 格式错误: {error}", + "read_settings_json_failed_gemini": "读取 settings.json 失败: {error}", + "file_size_bytes": "{size} 字节", + "restore_backup_dir_missing": "备份目录不存在", + "dot_env_not_found": ".env 文件不存在", + "dot_env_missing_gemini_key": ".env 缺少 GEMINI_API_KEY", + "dot_env_missing_gemini_url": ".env 缺少 GOOGLE_GEMINI_BASE_URL", + "read_dot_env_failed": "读取 .env 失败: {error}", + "settings_json_warning_not_found": "settings.json 文件不存在" + }, + "remote_mgr": { + "paramiko_not_installed": "未安装 paramiko,无法使用远程管理功能。请先执行: pip install paramiko", + "unsupported_config_type": "不支持的配置类型: {config_type}。仅支持 opencode / oh-my-opencode / auth", + "key_path_required": "使用密钥登录时必须提供 key_path", + "key_file_not_found": "私钥文件不存在: {path}", + "password_required": "使用密码登录时必须提供 password", + "unsupported_auth_type": "不支持的 auth_type: {auth_type}", + "already_connected": "已连接(复用现有连接)", + "connect_success": "连接成功", + "connect_failed": "连接失败: {error}", + "test_success": "连接测试成功", + "test_failed": "连接测试失败: {error}", + "test_exception": "连接测试异常: {error}", + "expand_path_failed": "远程路径展开失败: {error}", + "remote_config_not_found": "远程配置文件不存在: {config_type}", + "remote_config_parse_failed": "远程配置文件 JSON 解析失败: {error}", + "read_remote_config_failed": "读取远程配置失败: {error}", + "write_data_must_be_dict": "写入失败:data 必须为 dict", + "create_remote_dir_failed": "创建远程目录失败: {error}", + "write_remote_config_failed": "写入远程配置失败: {error}", + "create_remote_backup_failed": "创建远程备份失败: {error}", + "list_remote_backups_failed": "列出远程备份失败: {error}", + "check_remote_status_failed": "检查远程状态失败: {error}", + "unknown_error": "未知错误" + }, + "plugin_mgr": { + "install_failed": "安装插件失败: {error}", + "uninstall_failed": "卸载插件失败: {error}", + "check_version_failed": "检查版本失败: {error}" + }, + "crash": { + "title": "OCCM 崩溃", + "message": "应用程序遇到错误并需要关闭。\n\n错误日志已保存到:\n{log_file}\n\n请将此日志文件发送给开发者以帮助修复问题。\n\nGitHub: https://github.com/icysaintdx/OpenCode-Config-Manager/issues", + "console_msg": "OCCM 崩溃 - 日志已保存到: {log_file}" + }, + "cli_exception": { + "provider_incomplete": "Provider 配置不完整: 缺少 {fields}", + "write_failed": "写入配置失败 ({path}): {reason}", + "parse_failed": "解析 {format_type} 配置失败 ({path}): {reason}", + "backup_failed": "备份 {cli_type} 配置失败: {reason}", + "restore_failed": "恢复备份失败 ({backup_path}): {reason}" + }, + "agent_group_mgr": { + "load_failed": "加载分组配置失败: {error}", + "save_failed": "保存分组配置失败: {error}", + "backup_failed": "备份分组配置失败: {error}", + "cleanup_failed": "清理旧备份失败: {error}", + "export_failed": "导出分组失败: {error}", + "import_failed": "导入分组失败: {error}", + "import_format_error": "导入文件格式错误:缺少group字段", + "group_exists": "分组 '{name}' 已存在" + }, + "tooltip": { + "provider_name": "Provider 名称 - Provider的唯一标识符,用于在配置中引用\n格式:小写字母和连字符,如 anthropic, openai, my-proxy", + "provider_display": "显示名称 - 在界面中显示的友好名称\n示例:Anthropic (Claude)、OpenAI 官方", + "provider_sdk": "SDK 包名 - 指定使用哪个AI SDK来调用API\n• Claude系列 → @ai-sdk/anthropic\n• GPT/OpenAI系列 → @ai-sdk/openai\n• Gemini系列 → @ai-sdk/google", + "provider_url": "API 地址 (baseURL) - API服务的访问地址\n• 官方API → 留空(自动使用默认地址)\n• 中转站 → 填写中转站地址", + "provider_apikey": "API 密钥 - 用于身份验证的密钥\n支持环境变量: {env:ANTHROPIC_API_KEY}", + "provider_timeout": "请求超时 - 单位:毫秒 (ms)\n默认:300000 (5分钟)", + "model_id": "模型 ID - 模型的唯一标识符,必须与API提供商一致\n示例:claude-sonnet-4-5-20250929, gpt-5", + "model_name": "显示名称 - 在界面中显示的友好名称", + "model_attachment": "支持附件 - 是否支持上传文件(图片、文档等)", + "model_context": "上下文窗口 - 模型能处理的最大输入长度(tokens)", + "model_output": "最大输出 - 模型单次回复的最大长度(tokens)", + "model_options": "模型默认配置 (Options) - 每次调用模型时自动使用的参数\n• Claude thinking: thinking.type, thinking.budgetTokens\n• OpenAI: reasoningEffort, textVerbosity\n• Gemini: thinkingConfig.thinkingBudget", + "model_variants": "模型变体 (Variants) - 可通过快捷键切换的预设配置组合\n用于同一模型的不同配置,如不同的thinking预算", + "agent_name": "Agent 名称 - Agent的唯一标识符\n预设Agent:oracle, librarian, explore, code-reviewer", + "agent_model": "绑定模型 - 格式:provider/model-id\n示例:anthropic/claude-sonnet-4-5-20250929", + "agent_description": "Agent 描述 - 描述Agent的功能和适用场景", + "opencode_agent_mode": "Agent 模式\n• primary - 主Agent,可通过Tab键切换\n• subagent - 子Agent,通过@提及调用\n• all - 两种模式都支持", + "opencode_agent_temperature": "生成温度 - 取值范围:0.0 - 2.0\n• 0.0-0.2: 适合代码/分析\n• 0.3-0.5: 平衡创造性和准确性", + "opencode_agent_maxSteps": "最大步数 - 限制Agent执行的工具调用次数\n留空 = 无限制", + "opencode_agent_prompt": "系统提示词 - 定义Agent的行为和专长\n支持文件引用: {file:./prompts/agent.txt}", + "opencode_agent_tools": "工具配置 - JSON对象格式\n• true - 启用工具\n• false - 禁用工具", + "opencode_agent_permission": "权限配置\n• allow - 允许,无需确认\n• ask - 每次询问用户\n• deny - 禁止使用", + "opencode_agent_hidden": "隐藏 - 是否在@自动完成中隐藏此Agent\n仅对subagent有效", + "category_name": "Category 名称\n预设分类:visual, business-logic, documentation, code-analysis", + "category_model": "绑定模型 - 格式:provider/model-id", + "category_temperature": "Temperature - 推荐设置:\n• visual (前端): 0.7\n• business-logic (后端): 0.1\n• documentation (文档): 0.3", + "category_description": "分类描述 - 说明该分类的用途和适用场景", + "permission_tool": "工具名称\n内置工具:Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Task\nMCP工具格式:mcp_servername_toolname", + "permission_level": "权限级别\n• allow - 直接使用,无需确认\n• ask - 每次使用前询问用户\n• deny - 禁止使用", + "permission_bash_pattern": "Bash 命令模式 - 支持通配符\n• * - 匹配所有命令\n• git * - 匹配所有git命令", + "mcp_name": "MCP 名称 - MCP服务器的唯一标识符\n示例:context7, sentry, gh_grep", + "mcp_type": "MCP 类型\n• local - 本地进程,通过命令启动\n• remote - 远程服务,通过URL连接", + "mcp_enabled": "启用状态 - 是否启用此MCP服务器\n禁用后保留配置但不加载", + "mcp_command": "启动命令 (Local类型) - JSON数组格式\n示例:[\"npx\", \"-y\", \"@mcp/server\"]", + "mcp_url": "服务器 URL (Remote类型) - 完整的HTTP/HTTPS URL", + "mcp_headers": "请求头 (Remote类型) - JSON对象格式\n示例:{\"Authorization\": \"Bearer your-api-key\"}", + "mcp_environment": "环境变量 (Local类型) - JSON对象格式\n示例:{\"API_KEY\": \"xxx\"}", + "mcp_timeout": "超时时间 - 单位:毫秒 (ms)\n默认值:5000 (5秒)", + "skill_name": "Skill 名称 - 1-64字符,小写字母、数字、连字符\n示例:git-release, pr-review", + "skill_permission": "Skill 权限\n• allow - 立即加载,无需确认\n• deny - 隐藏并拒绝访问\n• ask - 加载前询问用户", + "skill_pattern": "权限模式 - 支持通配符\n• * - 匹配所有Skill\n• internal-* - 匹配internal-开头的Skill", + "skill_description": "Skill 描述 - 描述Skill的功能,帮助Agent选择", + "instructions_path": "指令文件路径 - 支持相对路径、绝对路径、Glob模式、远程URL", + "rules_agents_md": "AGENTS.md 文件 - 项目级或全局级的规则文件\n内容建议:项目结构说明、代码规范要求", + "compaction_auto": "自动压缩 - 当上下文接近满时自动压缩会话\n默认值:true (启用)", + "compaction_prune": "修剪旧输出 - 删除旧的工具输出以节省tokens\n默认值:true (启用)" + }, + "preset_desc": { + "omo_agents": { + "oracle": "架构设计、代码审查、策略规划专家 - 用于复杂决策和深度分析", + "librarian": "多仓库分析、文档查找、实现示例专家 - 用于查找外部资源和文档", + "explore": "快速代码库探索和模式匹配专家 - 用于代码搜索和模式发现", + "frontend-ui-ux-engineer": "UI/UX 设计和前端开发专家 - 用于前端视觉相关任务", + "document-writer": "技术文档写作专家 - 用于生成README、API文档等", + "multimodal-looker": "视觉内容分析专家 - 用于分析图片、PDF等媒体文件", + "code-reviewer": "代码质量审查、安全分析专家 - 用于代码审查任务", + "debugger": "问题诊断、Bug 修复专家 - 用于调试和问题排查" + }, + "opencode_agents": { + "build": "默认主Agent,拥有所有工具权限,用于开发工作", + "plan": "规划分析Agent,限制写入权限,用于代码分析和规划", + "general": "通用子Agent,用于研究复杂问题和执行多步骤任务", + "explore": "快速探索Agent,用于代码库搜索和模式发现", + "code-reviewer": "代码审查Agent,只读权限,专注于代码质量分析", + "docs-writer": "文档编写Agent,专注于技术文档创作", + "security-auditor": "安全审计Agent,只读权限,专注于安全漏洞分析" + }, + "categories": { + "visual": "前端、UI/UX、设计相关任务", + "business-logic": "后端逻辑、架构设计、战略推理", + "documentation": "文档编写、技术写作任务", + "code-analysis": "代码审查、重构分析任务" + }, + "model_categories": { + "claude_series": "Claude 系列", + "openai_codex_series": "OpenAI/Codex 系列", + "gemini_series": "Gemini 系列", + "other_models": "其他模型" + }, + "model_descriptions": { + "claude_opus_45": "最强大的Claude模型,支持extended thinking模式\noptions.thinking.budgetTokens 控制思考预算", + "claude_sonnet_45": "平衡性能与成本的Claude模型,支持thinking模式", + "claude_sonnet_4": "Claude Sonnet 4基础版,不支持thinking", + "claude_haiku_45": "快速响应的轻量级Claude模型", + "gpt5": "OpenAI最新旗舰模型\noptions.reasoningEffort: high/medium/low/xhigh", + "gpt51_codex": "OpenAI代码专用模型,针对编程任务优化", + "gpt4o": "OpenAI多模态模型", + "o1_preview": "OpenAI推理模型,支持reasoningEffort参数", + "o3_mini": "OpenAI最新推理模型", + "gemini_3_pro": "Google最新Pro模型,支持thinking模式", + "gemini_20_flash": "Google Flash模型,支持thinking模式", + "gemini_20_flash_thinking": "Gemini专用thinking实验模型", + "gemini_15_pro": "超长上下文的Gemini Pro模型", + "minimax_m21": "Minimax M2.1模型", + "deepseek_chat": "DeepSeek对话模型", + "deepseek_reasoner": "DeepSeek推理模型", + "qwen_max": "阿里通义千问Max模型", + "moonshot": "月之暗面 (Kimi) 模型" + }, + "model_pack_names": { + "default": "默认", + "high_thinking": "高思考", + "max_thinking": "最大思考", + "lightweight": "轻量", + "basic": "基础", + "high": "高" + } + }, + "agent_groups": { + "preset_minimal": "最小化配置", + "preset_minimal_desc": "仅启用核心Agent,适合简单任务", + "preset_standard": "标准配置", + "preset_standard_desc": "平衡的Agent组合,适合大多数任务", + "preset_common": "常用配置", + "preset_common_desc": "常用Agent组合,适合大多数复杂项目", + "preset_complete": "完整配置", + "preset_complete_desc": "启用所有Agent,最大化功能", + "preset_frontend": "前端开发", + "preset_frontend_desc": "针对前端UI/UX开发优化", + "preset_backend": "后端开发", + "preset_backend_desc": "针对后端API/数据库开发优化", + "load_failed": "加载分组配置失败: {error}", + "save_failed": "保存分组配置失败: {error}", + "backup_failed": "备份分组配置失败: {error}", + "cleanup_failed": "清理旧备份失败: {error}", + "export_failed": "导出分组失败: {error}", + "import_format_error": "导入文件格式错误:缺少group字段", + "import_exists": "分组 '{name}' 已存在", + "import_failed": "导入分组失败: {error}" + }, + "config_mgr": { + "json_parse_failed": "标准JSON解析失败: {error}", + "jsonc_parse_failed": "JSONC解析失败: {error}", + "file_size": "文件大小: {size} 字节", + "file_preview": "文件预览: {preview}..." + }, + "version_checker": { + "rate_limited": "GitHub API速率限制(403),将在6小时后重试", + "network_error": "网络错误 - {reason}" + }, + "config_viewer": { + "view_config": "查看配置文件", + "view_auth": "查看认证文件", + "cannot_read": "无法读取配置文件: {error}", + "backed_up": "已自动备份配置文件", + "backup_failed": "无法备份配置文件: {error}", + "json_format_error": "JSON格式错误", + "json_invalid": "配置文件格式不正确:\n{error}", + "save_success": "保存成功", + "config_saved": "配置文件已保存", + "save_failed": "保存失败", + "cannot_save": "无法保存配置文件: {error}" + }, + "balance": { + "query_failed_both": "余额查询失败。NewAPI: {newapi}... OpenAI API: {openai}...", + "newapi_query_failed": "NewAPI 查询失败: {code} - {body}", + "newapi_request_failed": "NewAPI 请求失败: {error}", + "newapi_invalid_response": "NewAPI 响应格式不正确", + "subscription_query_failed": "订阅信息查询失败: {code} - {body}", + "subscription_request_failed": "请求订阅信息失败: {error}", + "usage_query_failed": "使用情况查询失败: {code} - {body}", + "usage_request_failed": "请求使用情况失败: {error}", + "testing_connection": "正在测试连接...", + "not_supported": "当前 Provider 暂不支持通用余额接口查询(仅支持兼容 NewAPI/OpenAI 计费接口的服务)。", + "no_base_url": "未配置可用的 baseURL,无法查询余额。", + "not_available": "余额查询不可用", + "api_type_newapi": "API 类型: NewAPI / One-API", + "token_name": "Token 名称: {name}" + }, + "model_fetch": { + "no_model_list_url": "未配置模型列表地址", + "no_models_returned": "未返回可用模型列表", + "fetch_failed": "获取失败", + "not_supported": "{name} 不支持通过API获取模型列表。\n请手动添加模型或参考官方文档。", + "cannot_determine_url": "无法确定API地址。请先配置Provider的baseURL。", + "fetching": "正在从 {url} 获取模型列表...", + "empty_list": "API返回的模型列表为空", + "models_added": "已添加 {count} 个模型", + "auth_required": "HTTP {code}: {reason}\n\n该API需要认证。请先配置Provider的API Key。", + "key_invalid": "HTTP {code}: {reason}\n\nAPI Key可能无效或已过期。", + "fetched_count": "从 {provider} 获取到 {count} 个模型", + "unsupported_fetch": "{name} 不支持通过API获取模型列表。\n请手动添加模型或参考官方文档。" + }, + "model_dialog": { + "placeholder_model_id": "如: claude-sonnet-4-5-20250929", + "thinking_added": "已添加 Claude Thinking 配置", + "json_error": "JSON 格式错误: {error}", + "enter_model_id": "请输入模型 ID", + "model_exists": "模型 \"{id}\" 已存在", + "validation_more_errors": "\n... 还有 {count} 个错误", + "validation_failed": "配置校验失败:\n{msg}", + "select_header": "选择要添加的模型", + "select_column_check": "选择", + "select_column_id": "模型ID", + "select_column_time": "创建时间", + "select_at_least_one": "请至少选择一个模型", + "provider_not_found": "Provider \"{name}\" 不存在,请先在 Provider 管理页面创建", + "provider_incomplete": "Provider \"{name}\" 配置不完整,请先在 Provider 管理页面完善配置" + }, + "provider_dialog": { + "config_conflict": "配置冲突", + "conflict_msg": "已存在同名的自定义 Provider '{id}'。\n继续保存?", + "validation_required": "验证失败", + "field_required": "{label} 是必填项", + "save_failed": "保存失败", + "cannot_save_auth": "无法保存认证配置: {error}" + }, + "match_mode": { + "regex": "正则", + "contains": "包含", + "prefix": "前缀" + }, + "config_reload": { + "jsonc_comments_lost_title": "JSONC 注释已丢失", + "jsonc_comments_lost_msg": "原配置文件包含注释,保存后注释已丢失。已自动备份原文件。", + "external_change_msg": "检测到 {name} 配置文件已被外部修改。\n\n请选择如何处理:\n• 点击【确定】重新加载文件内容(可能覆盖当前界面数据)\n• 点击【取消】保留当前界面数据(文件保持外部修改)", + "reloaded_title": "已重新加载", + "reloaded_msg": "已加载 {name} 最新配置", + "keep_current_title": "保持当前数据", + "keep_current_msg": "未重新加载 {name},当前界面数据保持不变", + "size_label": "大小: {size}", + "mtime_label": "修改时间: {mtime}", + "switched_title": "已切换配置", + "switched_msg": "已删除 {jsonc},将使用 {json}", + "delete_failed_title": "删除失败", + "delete_failed_msg": "无法删除 {file}: {error}", + "keep_status_title": "保持现状", + "keep_status_msg": "将继续使用 {file}", + "fix_complete": "配置已修复", + "fix_msg": "已完成 {count} 项修复:\n{fixes}", + "fix_more": "\n... 还有 {count} 项", + "no_fix_needed_title": "无需修复", + "no_fix_needed_msg": "配置结构已经正确", + "more_errors_summary": "\n... 还有 {count} 个错误", + "dual_config_detected": "检测到 {name} 同时存在两个配置文件:", + "dual_config_json_info": "\n{json_name}\n 大小: {json_size}\n 修改时间: {json_mtime}", + "dual_config_jsonc_info": "\n{jsonc_name}\n 大小: {jsonc_size}\n 修改时间: {jsonc_mtime}", + "dual_config_warning": "\n⚠️ 当前程序会优先加载 .jsonc 文件。", + "dual_config_prompt": "\n请选择要使用的配置文件:\n• 点击「确定」使用 .json 文件(删除 .jsonc)\n• 点击「取消」使用 .jsonc 文件(保持现状)" + }, + "skill_ui": { + "name_empty": "名称不能为空", + "name_too_long": "名称不能超过 64 字符", + "name_format_error": "名称格式错误:只能使用小写字母、数字、单连字符分隔", + "desc_empty": "描述不能为空", + "desc_too_long": "描述不能超过 1024 字符", + "parse_failed": "解析 skill 失败 {name}: {error}", + "scan_dir_failed": "遍历目录失败 {path}: {error}", + "visit_skillsmp": "访问 SkillsMP.com 浏览更多社区技能", + "visit_composio": "访问 ComposioHQ 浏览更多社区技能", + "scan_failed": "扫描失败: {error}", + "unrecognized_source": "无法识别的来源格式: {source}", + "downloading": "正在下载...", + "detecting_branch": "检测分支...", + "using_branch": "使用分支: {branch}", + "extracting": "正在解压...", + "subdir_not_found": "子目录不存在: {subdir}", + "skill_file_not_found": "未找到 SKILL.md 或 SKILL.txt 文件", + "skill_file_not_found_in": "未找到 SKILL.md 或 SKILL.txt 文件 (在 {subdir} 中)", + "skill_format_error": "SKILL 文件格式错误", + "installing": "正在安装...", + "install_complete": "安装完成!", + "install_success": "Skill '{name}' 安装成功", + "network_error": "网络错误: {error}", + "install_failed": "安装失败: {error}", + "path_not_found": "路径不存在: {path}", + "skill_md_not_found": "未找到 SKILL.md 文件", + "skill_md_format_error": "SKILL.md 格式错误", + "copying": "正在复制...", + "check_update_failed": "检查更新失败 {name}: {error}", + "github_only_update": "仅支持更新从 GitHub 安装的 Skills", + "update_failed": "更新失败: {error}", + "compatibility": "兼容: {value}", + "claude_global_loc": "Claude 全局 (~/.claude/skills/)", + "claude_project_loc": "Claude 项目 (.claude/skills/)", + "opencode_global_loc": "OpenCode 全局 (~/.config/opencode/skills/)", + "opencode_project_loc": "OpenCode 项目 (.opencode/skills/)", + "name_error": "名称错误", + "desc_error": "描述错误", + "default_content": "## What I do\n\n- 描述功能\n\n## Instructions\n\n- 具体指令", + "save_failed": "保存失败: {error}", + "installing_skill": "正在安装 {name}...", + "no_skills_found": "未发现任何 Skills", + "checking_updates": "正在检查更新...", + "no_skills_selected": "未选择任何 Skills", + "check_updates_failed": "检查更新失败: {error}", + "updating_skills": "正在更新 Skills (0/{total})...", + "updating_skill": "正在更新 {name} ({current}/{total})...", + "update_success": "成功更新 {count} 个 Skills", + "partial_success": "部分成功", + "partial_success_msg": "成功更新 {success} 个,失败 {failed} 个\n\n失败详情:\n{details}", + "all_failed": "所有更新均失败\n\n详情:\n{details}", + "delete_failed": "删除失败: {error}", + "scan_failed_desc": "扫描失败: {error}" + }, + "export_ui": { + "export_failed": "导出失败", + "restore_backup_title": "恢复备份", + "restore_failed": "恢复失败", + "cannot_restore": "无法恢复备份", + "edit_config_title": "编辑 {type} 通用配置", + "edit_codex_hint": "编辑 Codex 通用配置 (TOML 格式),这些配置会合并到 config.toml 中", + "edit_gemini_hint": "编辑 Gemini 通用配置 (ENV 格式),这些配置会合并到 .env 文件中", + "click_preview": "点击\"预览转换\"在弹窗中查看左右对照。", + "not_found": "未找到", + "preview_title": "配置转换预览", + "original_config": "原始配置", + "select_config_first": "请先选择要导入的配置", + "confirm_import": "确认导入", + "import_summary": "将导入以下配置:\n• Provider: {providers} 个\n• 权限: {perms} 个\n\n是否继续?", + "conflict_title": "冲突", + "conflict_msg": "Provider \"{name}\" 已存在,是否覆盖?", + "preview_first": "请先预览转换结果", + "confirm_mapping_title": "确认导入映射", + "confirm_fields": "请确认必要字段", + "confirm_import_btn": "确认导入", + "cannot_read_backup": "无法读取备份内容: {error}", + "backup_preview_title": "备份内容预览", + "confirm_restore": "确认恢复", + "confirm_restore_msg": "确定要恢复此备份吗?\n当前配置将被覆盖(会先自动备份)。", + "example_general_config": "示例通用配置" + }, + "plugin_ui": { + "install_plugin_failed": "安装插件失败: {error}", + "uninstall_plugin_failed": "卸载插件失败: {error}", + "check_version_failed": "检查版本失败: {error}", + "plugin_skill_discovery": "自动发现和注册Skills为动态工具,支持Anthropic Agent Skills规范", + "plugin_multi_agent": "多Agent协作和工作流编排,支持回合制讨论和并行探索", + "plugin_helicone": "自动注入Helicone会话ID和名称,用于LLM请求分组和追踪", + "plugin_wakatime": "代码时间追踪,自动记录编码时间和项目统计", + "plugin_category_tools": "工具增强", + "plugin_category_collab": "协作增强", + "plugin_category_monitor": "监控追踪", + "page_title": "Plugin 插件管理", + "tab_plugins": "插件管理", + "search_placeholder": "搜索插件...", + "install_plugin_btn": "安装插件", + "check_update_btn": "检查更新", + "market_btn": "插件市场", + "table_headers": "插件名称,版本,类型,状态,描述,操作", + "detecting": "检测中...", + "enable_plugin": "启用插件", + "refresh_status": "刷新状态", + "open_config": "打开配置文件", + "disabled_success": "Oh My OpenCode 已禁用", + "enabled_success": "Oh My OpenCode 已启用", + "npm_install_hint": "请先通过npm安装oh-my-opencode插件:\nnpm install -g oh-my-opencode", + "status_refreshed": "状态已刷新", + "status_installed_enabled": "已安装且已启用", + "disable_plugin": "禁用插件", + "status_installed_disabled": "已安装但未启用", + "status_config_error": "配置异常:已启用但配置文件不存在", + "status_not_installed": "未安装", + "install_plugin_label": "安装插件", + "config_not_exist": "配置文件不存在,请先安装Oh My OpenCode插件", + "type_npm": "npm", + "type_local": "本地", + "status_enabled": "已启用", + "status_disabled": "已禁用", + "uninstall_tooltip": "卸载插件", + "confirm_uninstall": "确认卸载", + "confirm_uninstall_msg": "确定要卸载插件 {name} 吗?\n\n注意:OpenCode需要重启后才会生效。", + "uninstall_success": "插件 {name} 已卸载", + "uninstall_failed": "卸载插件失败", + "checking_updates": "正在检查更新...", + "install_dialog_title": "安装插件", + "install_method": "安装方式:", + "from_npm": "从npm安装", + "from_local": "从本地文件安装", + "npm_package_name": "npm包名:", + "npm_placeholder": "例如: opencode-skills 或 opencode-skills@0.1.0", + "npm_hint": "支持普通包和scoped包(如@my-org/plugin)", + "local_file": "本地文件:", + "local_placeholder": "选择.js或.ts文件", + "browse_btn": "浏览...", + "select_plugin_file": "选择插件文件", + "enter_npm_name": "请输入npm包名", + "plugin_added": "插件 {name} 已添加到配置\n\nOpenCode将在下次启动时自动安装", + "install_failed": "安装插件失败", + "select_plugin_file_first": "请选择插件文件", + "local_not_implemented": "本地插件安装功能暂未实现", + "market_title": "插件市场", + "preset_plugins": "预设插件", + "market_headers": "插件名称,分类,描述,操作" + } } \ No newline at end of file From 1ca9a313d9a261d0f8687a20be257ba6379a582b Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:25:40 +0200 Subject: [PATCH 2/7] i18n(occm_core): localize config manager and validator UI text --- occm_core/config_manager.py | 11 ++-- occm_core/config_validator.py | 113 +++++++++++++++++----------------- 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/occm_core/config_manager.py b/occm_core/config_manager.py index 241950c..1d82c07 100644 --- a/occm_core/config_manager.py +++ b/occm_core/config_manager.py @@ -3,6 +3,7 @@ import json from pathlib import Path from typing import Dict, Optional, Tuple +from .i18n import tr class ConfigManager: @@ -92,12 +93,12 @@ def load_json(path: Path) -> Optional[Dict]: except json.JSONDecodeError as e2: # 详细记录解析失败原因 print(f"Load failed {path}:") - print(f" - 标准JSON解析失败: {e1}") - print(f" - JSONC解析失败: {e2}") - print(f" - 文件大小: {len(content)} 字节") - # 打印前200个字符用于调试 + print(f" - {tr('config_mgr.json_parse_failed', error=str(e1))}") + print(f" - {tr('config_mgr.jsonc_parse_failed', error=str(e2))}") + print(f" - {tr('config_mgr.file_size', size=len(content))}") + # Print first 200 chars for debugging preview = content[:200].replace("\n", "\\n") - print(f" - 文件预览: {preview}...") + print(f" - {tr('config_mgr.file_preview', preview=preview)}") return None except Exception as e: print(f"Load failed {path}: {e}") diff --git a/occm_core/config_validator.py b/occm_core/config_validator.py index 2c7362c..53d6660 100644 --- a/occm_core/config_validator.py +++ b/occm_core/config_validator.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any, Dict, List, Tuple +from .i18n import tr class ConfigValidator: @@ -43,14 +44,14 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": "root", - "message": "配置文件无法解析或读取失败", + "message": tr("validator.config_parse_failed"), } ) return issues if not isinstance(config, dict): issues.append( - {"level": "error", "path": "root", "message": "配置根必须是对象类型"} + {"level": "error", "path": "root", "message": tr("validator.root_must_be_object")} ) return issues @@ -59,7 +60,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "root", - "message": "配置为空,尚未添加任何Provider", + "message": tr("validator.config_empty"), } ) return issues @@ -70,7 +71,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "$schema", - "message": "建议设置 $schema 为 https://opencode.ai/config.json", + "message": tr("validator.schema_recommend"), } ) @@ -80,7 +81,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "provider", - "message": "未配置任何 Provider", + "message": tr("validator.no_providers"), } ) if not isinstance(providers, dict): @@ -88,7 +89,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": "provider", - "message": "provider 必须是对象类型", + "message": tr("validator.provider_must_be_object"), } ) return issues @@ -101,7 +102,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": provider_path, - "message": f"Provider '{provider_name}' 的值必须是对象,当前是 {type(provider_data).__name__}", + "message": tr("validator.provider_value_not_object", name=provider_name, type=type(provider_data).__name__), } ) continue @@ -112,7 +113,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.{field}", - "message": f"Provider '{provider_name}' 缺少必需字段 '{field}'", + "message": tr("validator.provider_missing_field", name=provider_name, field=field), } ) elif ConfigValidator._is_blank(provider_data.get(field)): @@ -120,7 +121,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.{field}", - "message": f"Provider '{provider_name}' 的 '{field}' 为空", + "message": tr("validator.provider_field_empty", name=provider_name, field=field), } ) @@ -130,7 +131,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.npm", - "message": f"Provider '{provider_name}' 的 npm 包 '{npm}' 不在已知列表中", + "message": tr("validator.provider_unknown_npm", name=provider_name, npm=npm), } ) @@ -140,7 +141,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.options", - "message": f"Provider '{provider_name}' 的 options 必须是对象", + "message": tr("validator.provider_options_not_object", name=provider_name), } ) else: @@ -150,7 +151,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.options.{opt_field}", - "message": f"Provider '{provider_name}' 的 options 缺少 '{opt_field}'", + "message": tr("validator.provider_options_missing", name=provider_name, field=opt_field), } ) elif ConfigValidator._is_blank(options.get(opt_field)): @@ -158,7 +159,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.options.{opt_field}", - "message": f"Provider '{provider_name}' 的 options.{opt_field} 为空", + "message": tr("validator.provider_options_empty", name=provider_name, field=opt_field), } ) @@ -168,7 +169,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.models", - "message": f"Provider '{provider_name}' 的 models 必须是对象", + "message": tr("validator.provider_models_not_object", name=provider_name), } ) else: @@ -177,7 +178,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.models", - "message": f"Provider '{provider_name}' 没有配置任何模型", + "message": tr("validator.provider_no_models", name=provider_name), } ) for model_id, model_data in models.items(): @@ -187,7 +188,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": model_path, - "message": f"Provider '{provider_name}' 存在空模型ID", + "message": tr("validator.provider_empty_model_id", name=provider_name), } ) continue @@ -196,7 +197,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": model_path, - "message": f"Model '{model_id}' 的值必须是对象", + "message": tr("validator.model_value_not_object", model=model_id), } ) continue @@ -207,7 +208,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{model_path}.limit", - "message": f"Model '{model_id}' 的 limit 应该是对象", + "message": tr("validator.model_limit_should_be_object", model=model_id), } ) elif limit: @@ -218,7 +219,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{model_path}.limit.context", - "message": f"Model '{model_id}' 的 context 应该是整数", + "message": tr("validator.model_context_should_be_int", model=model_id), } ) if output is not None and not isinstance(output, int): @@ -226,14 +227,14 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{model_path}.limit.output", - "message": f"Model '{model_id}' 的 output 应该是整数", + "message": tr("validator.model_output_should_be_int", model=model_id), } ) mcp = config.get("mcp", {}) if mcp and not isinstance(mcp, dict): issues.append( - {"level": "error", "path": "mcp", "message": "mcp 必须是对象类型"} + {"level": "error", "path": "mcp", "message": tr("validator.mcp_must_be_object")} ) elif isinstance(mcp, dict): for mcp_name, mcp_data in mcp.items(): @@ -243,7 +244,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": mcp_path, - "message": f"MCP '{mcp_name}' 的值必须是对象", + "message": tr("validator.mcp_value_not_object", name=mcp_name), } ) continue @@ -254,7 +255,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{mcp_path}.command", - "message": f"Local MCP '{mcp_name}' 缺少 command 字段", + "message": tr("validator.mcp_local_missing_command", name=mcp_name), } ) elif mcp_type == "remote" and "url" not in mcp_data: @@ -262,14 +263,14 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{mcp_path}.url", - "message": f"Remote MCP '{mcp_name}' 缺少 url 字段", + "message": tr("validator.mcp_remote_missing_url", name=mcp_name), } ) agent = config.get("agent", {}) if agent and not isinstance(agent, dict): issues.append( - {"level": "error", "path": "agent", "message": "agent 必须是对象类型"} + {"level": "error", "path": "agent", "message": tr("validator.agent_must_be_object")} ) return issues @@ -279,23 +280,23 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: issues = [] if not config: issues.append( - {"level": "error", "path": "root", "message": "配置文件为空或无法解析"} + {"level": "error", "path": "root", "message": tr("validator.config_empty_or_invalid")} ) return issues if not isinstance(config, dict): issues.append( - {"level": "error", "path": "root", "message": "配置根必须是对象类型"} + {"level": "error", "path": "root", "message": tr("validator.root_must_be_object")} ) return issues agents = config.get("agents", {}) if not agents: issues.append( - {"level": "warning", "path": "agents", "message": "未配置任何 Agent"} + {"level": "warning", "path": "agents", "message": tr("validator.no_agents")} ) if agents and not isinstance(agents, dict): issues.append( - {"level": "error", "path": "agents", "message": "agents 必须是对象类型"} + {"level": "error", "path": "agents", "message": tr("validator.agents_must_be_object")} ) return issues @@ -307,7 +308,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": agent_path, - "message": "Agent 名称为空", + "message": tr("validator.agent_name_empty"), } ) continue @@ -316,7 +317,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": agent_path, - "message": f"Agent '{agent_name}' 的值必须是对象", + "message": tr("validator.agent_value_not_object", name=agent_name), } ) continue @@ -326,7 +327,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{agent_path}.{field}", - "message": f"Agent '{agent_name}' 缺少必需字段 '{field}'", + "message": tr("validator.agent_missing_field", name=agent_name, field=field), } ) elif ConfigValidator._is_blank(agent_data.get(field)): @@ -334,7 +335,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{agent_path}.{field}", - "message": f"Agent '{agent_name}' 的 '{field}' 为空", + "message": tr("validator.agent_field_empty", name=agent_name, field=field), } ) if "description" in agent_data and ConfigValidator._is_blank( @@ -344,7 +345,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{agent_path}.description", - "message": f"Agent '{agent_name}' 的 description 为空", + "message": tr("validator.agent_description_empty", name=agent_name), } ) @@ -354,7 +355,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "categories", - "message": "未配置任何 Category", + "message": tr("validator.no_categories"), } ) if categories and not isinstance(categories, dict): @@ -362,7 +363,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": "categories", - "message": "categories 必须是对象类型", + "message": tr("validator.categories_must_be_object"), } ) return issues @@ -375,7 +376,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": category_path, - "message": "Category 名称为空", + "message": tr("validator.category_name_empty"), } ) continue @@ -384,7 +385,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": category_path, - "message": f"Category '{category_name}' 的值必须是对象", + "message": tr("validator.category_value_not_object", name=category_name), } ) continue @@ -394,7 +395,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{category_path}.{field}", - "message": f"Category '{category_name}' 缺少必需字段 '{field}'", + "message": tr("validator.category_missing_field", name=category_name, field=field), } ) elif ConfigValidator._is_blank(category_data.get(field)): @@ -402,7 +403,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{category_path}.{field}", - "message": f"Category '{category_name}' 的 '{field}' 为空", + "message": tr("validator.category_field_empty", name=category_name, field=field), } ) @@ -414,7 +415,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{category_path}.temperature", - "message": f"Category '{category_name}' 的 temperature 应该是数字", + "message": tr("validator.category_temperature_should_be_number", name=category_name), } ) if "description" in category_data and ConfigValidator._is_blank( @@ -424,7 +425,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{category_path}.description", - "message": f"Category '{category_name}' 的 description 为空", + "message": tr("validator.category_description_empty", name=category_name), } ) @@ -443,14 +444,14 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: fixed_providers = {} for provider_name, provider_data in providers.items(): if not isinstance(provider_data, dict): - fixes.append(f"跳过无效 Provider '{provider_name}' (值不是对象)") + fixes.append(tr("validator.fix_skip_invalid_provider", name=provider_name)) continue fixed_provider = dict(provider_data) if "npm" not in fixed_provider: fixed_provider["npm"] = "@ai-sdk/openai" - fixes.append(f"Provider '{provider_name}': 添加默认 npm 字段") + fixes.append(tr("validator.fix_add_default_npm", name=provider_name)) if "options" not in fixed_provider or not isinstance( fixed_provider.get("options"), dict @@ -458,21 +459,21 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: fixed_provider["options"] = fixed_provider.get("options", {}) if not isinstance(fixed_provider["options"], dict): fixed_provider["options"] = {} - fixes.append(f"Provider '{provider_name}': 修复 options 字段") + fixes.append(tr("validator.fix_options_field", name=provider_name)) if "baseURL" not in fixed_provider["options"]: fixed_provider["options"]["baseURL"] = "" - fixes.append(f"Provider '{provider_name}': 添加空 baseURL") + fixes.append(tr("validator.fix_add_empty_baseurl", name=provider_name)) if "apiKey" not in fixed_provider["options"]: fixed_provider["options"]["apiKey"] = "" - fixes.append(f"Provider '{provider_name}': 添加空 apiKey") + fixes.append(tr("validator.fix_add_empty_apikey", name=provider_name)) if "models" not in fixed_provider: fixed_provider["models"] = {} - fixes.append(f"Provider '{provider_name}': 添加空 models 字段") + fixes.append(tr("validator.fix_add_empty_models", name=provider_name)) elif not isinstance(fixed_provider.get("models"), dict): fixed_provider["models"] = {} - fixes.append(f"Provider '{provider_name}': 修复 models 字段为对象") + fixes.append(tr("validator.fix_models_to_object", name=provider_name)) for model_id, model_cfg in list(fixed_provider.get("models", {}).items()): if not isinstance(model_cfg, dict): @@ -484,7 +485,7 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: if not isinstance(limit, dict): model_cfg.pop("limit", None) fixes.append( - f"Provider '{provider_name}' Model '{model_id}': 移除无效 limit" + tr("validator.fix_remove_invalid_limit", name=provider_name, model=model_id) ) continue @@ -499,7 +500,7 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: else: model_cfg.pop("limit", None) fixes.append( - f"Provider '{provider_name}' Model '{model_id}': 移除空 limit" + tr("validator.fix_remove_empty_limit", name=provider_name, model=model_id) ) ordered_provider = {} @@ -528,17 +529,17 @@ def get_issues_summary(issues: List[Dict]) -> str: lines = [] if errors: - lines.append(f"❌ {len(errors)} 个错误:") + lines.append(tr("validator.error_count", count=len(errors))) for e in errors[:5]: lines.append(f" • {e['message']}") if len(errors) > 5: - lines.append(f" ... 还有 {len(errors) - 5} 个错误") + lines.append(tr("validator.error_more", count=len(errors) - 5)) if warnings: - lines.append(f"⚠️ {len(warnings)} 个警告:") + lines.append(tr("validator.warning_count", count=len(warnings))) for w in warnings[:5]: lines.append(f" • {w['message']}") if len(warnings) > 5: - lines.append(f" ... 还有 {len(warnings) - 5} 个警告") + lines.append(tr("validator.warning_more", count=len(warnings) - 5)) - return "\n".join(lines) if lines else "✅ 配置格式正确" + return "\n".join(lines) if lines else tr("validator.config_valid") From 8d50a77d2d0a53191cac52093eee780f1157e34a Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:25:46 +0200 Subject: [PATCH 3/7] i18n(occm_core): localize network service modules UI text --- occm_core/monitor_service.py | 25 +++++++++--------- occm_core/remote_manager.py | 50 +++++++++++++++++++----------------- occm_core/version_checker.py | 5 ++-- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/occm_core/monitor_service.py b/occm_core/monitor_service.py index 3ca06d9..b1e36a1 100644 --- a/occm_core/monitor_service.py +++ b/occm_core/monitor_service.py @@ -14,6 +14,7 @@ from urllib.parse import urlparse from .native_providers import _resolve_env_value, _safe_base_url +from .i18n import tr MONITOR_POLL_INTERVAL_MS = 60000 @@ -237,7 +238,7 @@ def _do_poll(self) -> None: latency_ms=None, ping_ms=None, checked_at=datetime.now(), - message="请求超时", + message=tr("monitor.request_timeout"), ) self._record_result(timeout_result) futures.pop(future, None) @@ -260,25 +261,25 @@ def check_target(self, target: MonitorTarget) -> MonitorResult: if not self._chat_test_enabled: if not target.base_url: - message = "未配置 baseURL" + message = tr("monitor.no_base_url") elif ping_ms is not None: status = "operational" - message = "对话测试已暂停 (Ping 正常)" + message = tr("monitor.chat_test_paused") elif origin: status = "error" - message = "Ping 失败" + message = tr("monitor.ping_failed") else: status = "no_config" - message = "未配置有效的主机" + message = tr("monitor.no_valid_host") elif not target.base_url: - message = "未配置 baseURL" + message = tr("monitor.no_base_url") elif not target.api_key: - message = "未配置 apiKey" + message = tr("monitor.no_api_key") else: try: url = _build_chat_url(target.base_url) if not url: - raise ValueError("baseURL 无效") + raise ValueError(tr("monitor.base_url_invalid")) payload = json.dumps( { "model": target.model_id, @@ -301,16 +302,16 @@ def check_target(self, target: MonitorTarget) -> MonitorResult: latency_ms = int((time.time() - start) * 1000) if latency_ms <= DEGRADED_THRESHOLD_MS: status = "operational" - message = "正常" + message = tr("monitor.status_normal") else: status = "degraded" - message = f"延迟较高 ({latency_ms}ms)" + message = tr("monitor.latency_high", latency_ms=latency_ms) except urllib.error.HTTPError as e: status = "failed" - message = "鉴权失败" if e.code in (401, 403) else f"HTTP {e.code}" + message = tr("monitor.auth_failed") if e.code in (401, 403) else f"HTTP {e.code}" except urllib.error.URLError as e: status = "error" - message = f"连接失败: {e.reason}" + message = tr("monitor.connection_failed", reason=e.reason) except Exception as e: status = "error" message = str(e)[:50] diff --git a/occm_core/remote_manager.py b/occm_core/remote_manager.py index 4723761..30ede7f 100644 --- a/occm_core/remote_manager.py +++ b/occm_core/remote_manager.py @@ -9,6 +9,8 @@ from pathlib import Path from typing import Any, Dict, List, Literal, Optional, Tuple +from .i18n import tr + # paramiko 为可选依赖:如果未安装,远程功能将不可用 try: import paramiko # pyright: ignore[reportMissingModuleSource] @@ -107,7 +109,7 @@ def _ensure_paramiko() -> None: """确保 paramiko 可用。""" if paramiko is None: raise RuntimeError( - "未安装 paramiko,无法使用远程管理功能。请先执行: pip install paramiko" + tr("remote_mgr.paramiko_not_installed") ) @staticmethod @@ -116,7 +118,7 @@ def _normalize_config_type(config_type: str) -> str: key = (config_type or "").strip().lower() if key not in RemoteManager._CONFIG_FILENAME_MAP: raise ValueError( - f"不支持的配置类型: {config_type}。仅支持 opencode / oh-my-opencode / auth" + f"{tr('remote_mgr.unsupported_config_type', config_type=config_type)}" ) return RemoteManager._CONFIG_FILENAME_MAP[key] @@ -124,11 +126,11 @@ def _connect_auth(self, client: Any, server: RemoteServer) -> None: """按鉴权方式建立 SSH 连接。""" if server.auth_type == "key": if not server.key_path: - raise ValueError("使用密钥登录时必须提供 key_path") + raise ValueError(tr("remote_mgr.key_path_required")) key_path = Path(server.key_path).expanduser() if not key_path.exists(): - raise FileNotFoundError(f"私钥文件不存在: {key_path}") + raise FileNotFoundError(tr("remote_mgr.key_file_not_found", path=key_path)) client.connect( hostname=server.host, @@ -143,7 +145,7 @@ def _connect_auth(self, client: Any, server: RemoteServer) -> None: if server.auth_type == "password": if not server.password: - raise ValueError("使用密码登录时必须提供 password") + raise ValueError(tr("remote_mgr.password_required")) client.connect( hostname=server.host, @@ -156,7 +158,7 @@ def _connect_auth(self, client: Any, server: RemoteServer) -> None: ) return - raise ValueError(f"不支持的 auth_type: {server.auth_type}") + raise ValueError(tr("remote_mgr.unsupported_auth_type", auth_type=server.auth_type)) def connect(self, server: RemoteServer) -> Tuple[bool, str]: """连接远程服务器。""" @@ -168,20 +170,20 @@ def connect(self, server: RemoteServer) -> Tuple[bool, str]: if key in self._clients: transport = self._clients[key].get_transport() if transport and transport.is_active(): - return True, "已连接(复用现有连接)" + return True, tr("remote_mgr.already_connected") self.disconnect(server) pm = paramiko if pm is None: - raise RuntimeError("paramiko 不可用") + raise RuntimeError(tr("remote_mgr.paramiko_not_installed")) client = pm.SSHClient() client.set_missing_host_key_policy(pm.AutoAddPolicy()) self._connect_auth(client, server) self._clients[key] = client - return True, "连接成功" + return True, tr("remote_mgr.connect_success") except Exception as e: - return False, f"连接失败: {e}" + return False, tr("remote_mgr.connect_failed", error=e) def disconnect(self, server: RemoteServer) -> None: """断开单个服务器连接。""" @@ -211,10 +213,10 @@ def test_connection(self, server: RemoteServer) -> Tuple[bool, str]: try: code, out, err = self._exec(server, "echo OCCM_REMOTE_OK") if code == 0 and out.strip() == "OCCM_REMOTE_OK": - return True, "连接测试成功" - return False, f"连接测试失败: {err or out}" + return True, tr("remote_mgr.test_success") + return False, tr("remote_mgr.test_failed", error=err or out) except Exception as e: - return False, f"连接测试异常: {e}" + return False, tr("remote_mgr.test_exception", error=e) def _get_client(self, server: RemoteServer) -> Any: """获取可用客户端,不可用时自动重连。""" @@ -262,7 +264,7 @@ def _expand_remote_path(self, server: RemoteServer, path: str) -> str: if code2 == 0 and out2.strip(): return out2.strip() - raise RuntimeError(f"远程路径展开失败: {err or err2 or out or out2}") + raise RuntimeError(tr("remote_mgr.expand_path_failed", error=err or err2 or out or out2)) def _get_remote_config_dir(self, server: RemoteServer) -> str: """获取远程配置目录。 @@ -305,18 +307,18 @@ def read_remote_config( return json.loads(content) except FileNotFoundError: - raise FileNotFoundError(f"远程配置文件不存在: {config_type}") + raise FileNotFoundError(tr("remote_mgr.remote_config_not_found", config_type=config_type)) except json.JSONDecodeError as e: - raise ValueError(f"远程配置文件 JSON 解析失败: {e}") + raise ValueError(tr("remote_mgr.remote_config_parse_failed", error=e)) except Exception as e: - raise RuntimeError(f"读取远程配置失败: {e}") + raise RuntimeError(tr("remote_mgr.read_remote_config_failed", error=e)) def write_remote_config( self, server: RemoteServer, config_type: str, data: Dict[str, Any] ) -> bool: """写入远程配置文件。""" if not isinstance(data, dict): - raise ValueError("写入失败:data 必须为 dict") + raise ValueError(tr("remote_mgr.write_data_must_be_dict")) try: remote_path = self._get_remote_config_path(server, config_type) @@ -326,7 +328,7 @@ def write_remote_config( mk_cmd = f"mkdir -p {shlex.quote(remote_dir)}" code, _, err = self._exec(server, mk_cmd) if code != 0: - raise RuntimeError(f"创建远程目录失败: {err}") + raise RuntimeError(tr("remote_mgr.create_remote_dir_failed", error=err)) payload = json.dumps(data, indent=2, ensure_ascii=False) client = self._get_client(server) @@ -339,7 +341,7 @@ def write_remote_config( return True except Exception as e: - raise RuntimeError(f"写入远程配置失败: {e}") + raise RuntimeError(tr("remote_mgr.write_remote_config_failed", error=e)) def create_remote_backup(self, server: RemoteServer) -> str: """创建远程备份(按时间戳打包当前配置文件)。""" @@ -359,11 +361,11 @@ def create_remote_backup(self, server: RemoteServer) -> str: ) code, _, err = self._exec(server, cmd) if code != 0: - raise RuntimeError(err or "未知错误") + raise RuntimeError(err or tr("remote_mgr.unknown_error")) return backup_dir except Exception as e: - raise RuntimeError(f"创建远程备份失败: {e}") + raise RuntimeError(tr("remote_mgr.create_remote_backup_failed", error=e)) def list_remote_backups(self, server: RemoteServer) -> List[Dict[str, Any]]: """列出远程备份文件。""" @@ -400,7 +402,7 @@ def list_remote_backups(self, server: RemoteServer) -> List[Dict[str, Any]]: finally: sftp.close() except Exception as e: - raise RuntimeError(f"列出远程备份失败: {e}") + raise RuntimeError(tr("remote_mgr.list_remote_backups_failed", error=e)) def get_remote_opencode_status(self, server: RemoteServer) -> Dict[str, Any]: """检查远程 OpenCode 运行状态。""" @@ -449,7 +451,7 @@ def get_remote_opencode_status(self, server: RemoteServer) -> Dict[str, Any]: result["process_running"] = len(process_lines) > 0 return result except Exception as e: - result["error"] = f"检查远程状态失败: {e}" + result["error"] = tr("remote_mgr.check_remote_status_failed", error=e) return result diff --git a/occm_core/version_checker.py b/occm_core/version_checker.py index c28f0d4..ce5be07 100644 --- a/occm_core/version_checker.py +++ b/occm_core/version_checker.py @@ -7,6 +7,7 @@ import urllib.error import urllib.request from typing import Callable, List, Optional +from .i18n import tr GITHUB_REPO = "icysaintdx/OpenCode-Config-Manager" @@ -88,7 +89,7 @@ def _check_update(self): except urllib.error.HTTPError as e: error_msg = "" if e.code == 403: - error_msg = "GitHub API速率限制(403),将在6小时后重试" + error_msg = tr("version_checker.rate_limited") print(f"Version check failed: {error_msg}") self.check_interval = 21600 else: @@ -96,7 +97,7 @@ def _check_update(self): print(f"Version check failed: {error_msg}") self._notify_error(error_msg) except urllib.error.URLError as e: - error_msg = f"网络错误 - {e.reason}" + error_msg = tr("version_checker.network_error", reason=str(e.reason)) print(f"Version check failed: {error_msg}") self._notify_error(error_msg) except Exception as e: From da33bb0924e71764b3fc3f46e50db4f9cfcaa08c Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:25:52 +0200 Subject: [PATCH 4/7] i18n(occm_core): localize agent groups and CLI export UI text --- occm_core/agent_groups.py | 46 ++++++++++++++++------ occm_core/cli_export.py | 81 ++++++++++++++++++++------------------- 2 files changed, 76 insertions(+), 51 deletions(-) diff --git a/occm_core/agent_groups.py b/occm_core/agent_groups.py index 0c53e5c..3a663ae 100644 --- a/occm_core/agent_groups.py +++ b/occm_core/agent_groups.py @@ -4,6 +4,7 @@ from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Tuple +from .i18n import tr class AgentGroupManager: @@ -184,7 +185,7 @@ def load_groups(self) -> None: "default_group_id": None, } except Exception as e: - print(f"加载分组配置失败: {e}") + print(tr("agent_groups.load_failed", error=str(e))) self.groups_data = { "version": "1.0.0", "groups": [], @@ -210,7 +211,7 @@ def save_groups(self) -> None: with open(self.groups_file, "w", encoding="utf-8") as f: json.dump(self.groups_data, f, indent=2, ensure_ascii=False) except Exception as e: - print(f"保存分组配置失败: {e}") + print(tr("agent_groups.save_failed", error=str(e))) raise def backup_groups(self) -> Optional[Path]: @@ -238,7 +239,7 @@ def backup_groups(self) -> Optional[Path]: return backup_file except Exception as e: - print(f"备份分组配置失败: {e}") + print(tr("agent_groups.backup_failed", error=str(e))) return None def _cleanup_old_backups(self, keep_count: int = 10) -> None: @@ -259,7 +260,7 @@ def _cleanup_old_backups(self, keep_count: int = 10) -> None: for backup_file in backup_files[keep_count:]: backup_file.unlink() except Exception as e: - print(f"清理旧备份失败: {e}") + print(tr("agent_groups.cleanup_failed", error=str(e))) # ========== 分组CRUD操作 ========== @@ -372,10 +373,9 @@ def list_groups(self, include_presets: bool = False) -> List[Dict]: if include_presets: # 添加预设模板(标记为preset类型) for preset in self.PRESETS: - preset_copy = preset.copy() + preset_copy = self._localize_preset(preset) preset_copy["type"] = "preset" groups.append(preset_copy) - return groups # ========== 分组应用 ========== @@ -509,13 +509,37 @@ def get_current_group_match( # ========== 预设模板 ========== + # Mapping from preset id to locale key prefix + _PRESET_LOCALE_MAP = { + "preset-minimal": "preset_minimal", + "preset-standard": "preset_standard", + "preset-full": "preset_common", + "preset-complete": "preset_complete", + "preset-frontend": "preset_frontend", + "preset-backend": "preset_backend", + } + + @staticmethod + def _localize_preset(preset: Dict) -> Dict: + """Return a copy of the preset with localized name/description.""" + p = preset.copy() + key = AgentGroupManager._PRESET_LOCALE_MAP.get(preset["id"]) + if key: + localized_name = tr(f"agent_groups.{key}") + if localized_name != f"agent_groups.{key}": + p["name"] = localized_name + localized_desc = tr(f"agent_groups.{key}_desc") + if localized_desc != f"agent_groups.{key}_desc": + p["description"] = localized_desc + return p + def get_presets(self) -> List[Dict]: """获取所有预设模板 Returns: List[Dict]: 预设模板列表 """ - return self.PRESETS.copy() + return [self._localize_preset(p) for p in self.PRESETS] def create_from_preset( self, preset_id: str, name: str, description: Optional[str] = None @@ -582,7 +606,7 @@ def export_group(self, group_id: str, file_path: Path) -> bool: return True except Exception as e: - print(f"导出分组失败: {e}") + print(tr("agent_groups.export_failed", error=str(e))) return False def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str]: @@ -602,7 +626,7 @@ def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str # 验证格式 if "group" not in import_data: - print("导入文件格式错误:缺少group字段") + print(tr("agent_groups.import_format_error")) return None group = import_data["group"] @@ -615,7 +639,7 @@ def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str break if existing_group and not overwrite: - print(f"分组 '{group['name']}' 已存在") + print(tr("agent_groups.import_exists", name=group['name'])) return None if existing_group and overwrite: @@ -637,7 +661,7 @@ def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str icon=group.get("icon", "📁"), ) except Exception as e: - print(f"导入分组失败: {e}") + print(tr("agent_groups.import_failed", error=str(e))) return None # ========== 统计信息 ========== diff --git a/occm_core/cli_export.py b/occm_core/cli_export.py index 5d0216f..71aad69 100644 --- a/occm_core/cli_export.py +++ b/occm_core/cli_export.py @@ -14,6 +14,7 @@ ExportResult, ValidationResult, ) +from .i18n import tr # ==================== CLI 导出模块异常类 ==================== @@ -28,7 +29,7 @@ class ProviderValidationError(CLIExportError): def __init__(self, missing_fields: List[str]): self.missing_fields = missing_fields - super().__init__(f"Provider 配置不完整: 缺少 {', '.join(missing_fields)}") + super().__init__(tr("cli_export_msg.provider_config_incomplete", fields=", ".join(missing_fields))) class ConfigWriteError(CLIExportError): @@ -37,7 +38,7 @@ class ConfigWriteError(CLIExportError): def __init__(self, path: Path, reason: str): self.path = path self.reason = reason - super().__init__(f"写入配置失败 ({path}): {reason}") + super().__init__(tr("cli_export_msg.write_config_failed", path=path, reason=reason)) class ConfigParseError(CLIExportError): @@ -47,7 +48,7 @@ def __init__(self, path: Path, format_type: str, reason: str): self.path = path self.format_type = format_type self.reason = reason - super().__init__(f"解析 {format_type} 配置失败 ({path}): {reason}") + super().__init__(tr("cli_export_msg.parse_config_failed", format=format_type, path=path, reason=reason)) class BackupError(CLIExportError): @@ -56,7 +57,7 @@ class BackupError(CLIExportError): def __init__(self, cli_type: str, reason: str): self.cli_type = cli_type self.reason = reason - super().__init__(f"备份 {cli_type} 配置失败: {reason}") + super().__init__(tr("cli_export_msg.backup_failed", cli_type=cli_type, reason=reason)) class RestoreError(CLIExportError): @@ -65,7 +66,7 @@ class RestoreError(CLIExportError): def __init__(self, backup_path: Path, reason: str): self.backup_path = backup_path self.reason = reason - super().__init__(f"恢复备份失败 ({backup_path}): {reason}") + super().__init__(tr("cli_export_msg.restore_failed", path=backup_path, reason=reason)) class CLIConfigWriter: @@ -132,7 +133,7 @@ def atomic_write_json(self, path: Path, data: Dict) -> None: except json.JSONDecodeError as e: if temp_path.exists(): temp_path.unlink() - raise ConfigWriteError(path, f"JSON 格式验证失败: {e}") + raise ConfigWriteError(path, tr("cli_export_msg.json_validation_failed", error=e)) except Exception as e: if temp_path.exists(): temp_path.unlink() @@ -177,7 +178,7 @@ def set_file_permissions(self, path: Path, mode: int = 0o600) -> None: try: path.chmod(mode) except Exception as e: - print(f"设置文件权限失败 ({path}): {e}") + print(tr("cli_export_msg.set_permissions_failed", path=path, error=e)) def write_claude_settings(self, config: Dict, merge: bool = True) -> None: """写入 Claude settings.json @@ -339,7 +340,7 @@ def restore_backup(self, backup_path: Path, cli_type: str) -> bool: """从备份恢复配置""" try: if not backup_path.exists(): - raise RestoreError(backup_path, "备份目录不存在") + raise RestoreError(backup_path, tr("cli_export_msg.backup_dir_not_found")) cli_dir = CLIConfigWriter.get_cli_dir(cli_type) cli_dir.mkdir(parents=True, exist_ok=True) @@ -386,7 +387,7 @@ def list_backups(self, cli_type: str) -> List[BackupInfo]: backups.sort(key=lambda x: x.created_at, reverse=True) except Exception as e: - print(f"列出备份失败: {e}") + print(tr("cli_export_msg.list_backups_failed", error=e)) return backups @@ -397,7 +398,7 @@ def cleanup_old_backups(self, cli_type: str) -> None: try: shutil.rmtree(backup.path) except Exception as e: - print(f"删除旧备份失败 ({backup.path}): {e}") + print(tr("cli_export_msg.delete_old_backup_failed", path=backup.path, error=e)) class CLIConfigGenerator: @@ -531,17 +532,17 @@ def validate_provider(self, provider: Dict) -> ValidationResult: "baseURL", "" ) if not base_url or not base_url.strip(): - errors.append("缺少 API 地址 (baseURL)") + errors.append(tr("cli_export_msg.missing_base_url")) api_key = provider.get("apiKey", "") or provider.get("options", {}).get( "apiKey", "" ) if not api_key or not api_key.strip(): - errors.append("缺少 API 密钥 (apiKey)") + errors.append(tr("cli_export_msg.missing_api_key")) models = provider.get("models", {}) if not models: - warnings.append("未配置任何模型") + warnings.append(tr("cli_export_msg.no_models_configured")) if errors: return ValidationResult.failure(errors, warnings) @@ -566,7 +567,7 @@ def export_to_claude(self, provider: Dict, model: str) -> ExportResult: except CLIExportError as e: return ExportResult.fail(cli_type, str(e), backup_path) except Exception as e: - return ExportResult.fail(cli_type, f"导出失败: {e}", backup_path) + return ExportResult.fail(cli_type, tr("cli_export_msg.export_failed", error=e), backup_path) def export_to_codex(self, provider: Dict, model: str) -> ExportResult: cli_type = "codex" @@ -595,7 +596,7 @@ def export_to_codex(self, provider: Dict, model: str) -> ExportResult: except CLIExportError as e: return ExportResult.fail(cli_type, str(e), backup_path) except Exception as e: - return ExportResult.fail(cli_type, f"导出失败: {e}", backup_path) + return ExportResult.fail(cli_type, tr("cli_export_msg.export_failed", error=e), backup_path) def export_to_gemini(self, provider: Dict, model: str) -> ExportResult: cli_type = "gemini" @@ -623,7 +624,7 @@ def export_to_gemini(self, provider: Dict, model: str) -> ExportResult: except CLIExportError as e: return ExportResult.fail(cli_type, str(e), backup_path) except Exception as e: - return ExportResult.fail(cli_type, f"导出失败: {e}", backup_path) + return ExportResult.fail(cli_type, tr("cli_export_msg.export_failed", error=e), backup_path) def batch_export( self, provider: Dict, models: Dict[str, str], targets: List[str] @@ -642,9 +643,9 @@ def batch_export( elif cli_type == "gemini": result = self.export_to_gemini(provider, model) else: - result = ExportResult.fail(cli_type, f"未知的 CLI 类型: {cli_type}") + result = ExportResult.fail(cli_type, tr("cli_export_msg.unknown_cli_type", cli_type=cli_type)) except Exception as e: - result = ExportResult.fail(cli_type, f"导出异常: {e}") + result = ExportResult.fail(cli_type, tr("cli_export_msg.export_exception", error=e)) results.append(result) @@ -665,83 +666,83 @@ def validate_exported_config(self, cli_type: str) -> ValidationResult: if cli_type == "claude": settings_path = cli_dir / "settings.json" if not settings_path.exists(): - errors.append("settings.json 文件不存在") + errors.append(tr("cli_export_msg.settings_json_not_found")) else: try: with open(settings_path, "r", encoding="utf-8") as f: config = json.load(f) if "env" not in config: - errors.append("settings.json 缺少 env 字段") + errors.append(tr("cli_export_msg.settings_json_missing_env")) else: env = config["env"] if "ANTHROPIC_BASE_URL" not in env: - errors.append("缺少 ANTHROPIC_BASE_URL") + errors.append(tr("cli_export_msg.missing_anthropic_base_url")) if "ANTHROPIC_AUTH_TOKEN" not in env: - errors.append("缺少 ANTHROPIC_AUTH_TOKEN") + errors.append(tr("cli_export_msg.missing_anthropic_auth_token")) except json.JSONDecodeError as e: - errors.append(f"settings.json 格式错误: {e}") + errors.append(tr("cli_export_msg.settings_json_format_error", error=e)) except Exception as e: - errors.append(f"读取 settings.json 失败: {e}") + errors.append(tr("cli_export_msg.read_settings_json_failed", error=e)) elif cli_type == "codex": auth_path = cli_dir / "auth.json" config_path = cli_dir / "config.toml" if not auth_path.exists(): - errors.append("auth.json 文件不存在") + errors.append(tr("cli_export_msg.auth_json_not_found")) else: try: with open(auth_path, "r", encoding="utf-8") as f: auth = json.load(f) if "OPENAI_API_KEY" not in auth: - errors.append("auth.json 缺少 OPENAI_API_KEY") + errors.append(tr("cli_export_msg.auth_json_missing_key")) except json.JSONDecodeError as e: - errors.append(f"auth.json 格式错误: {e}") + errors.append(tr("cli_export_msg.auth_json_format_error", error=e)) except Exception as e: - errors.append(f"读取 auth.json 失败: {e}") + errors.append(tr("cli_export_msg.read_auth_json_failed", error=e)) if not config_path.exists(): - errors.append("config.toml 文件不存在") + errors.append(tr("cli_export_msg.config_toml_not_found")) else: try: with open(config_path, "r", encoding="utf-8") as f: content = f.read() if "model_provider" not in content: - errors.append("config.toml 缺少 model_provider") + errors.append(tr("cli_export_msg.config_toml_missing_provider")) if "model =" not in content: - errors.append("config.toml 缺少 model") + errors.append(tr("cli_export_msg.config_toml_missing_model")) except Exception as e: - errors.append(f"读取 config.toml 失败: {e}") + errors.append(tr("cli_export_msg.read_config_toml_failed", error=e)) elif cli_type == "gemini": env_path = cli_dir / ".env" settings_path = cli_dir / "settings.json" if not env_path.exists(): - errors.append(".env 文件不存在") + errors.append(tr("cli_export_msg.env_file_not_found")) else: try: with open(env_path, "r", encoding="utf-8") as f: content = f.read() if "GEMINI_API_KEY" not in content: - errors.append(".env 缺少 GEMINI_API_KEY") + errors.append(tr("cli_export_msg.env_missing_api_key")) if "GOOGLE_GEMINI_BASE_URL" not in content: - errors.append(".env 缺少 GOOGLE_GEMINI_BASE_URL") + errors.append(tr("cli_export_msg.env_missing_base_url")) except Exception as e: - errors.append(f"读取 .env 失败: {e}") + errors.append(tr("cli_export_msg.read_env_failed", error=e)) if not settings_path.exists(): - warnings.append("settings.json 文件不存在") + warnings.append(tr("cli_export_msg.settings_json_not_found_warn")) else: try: with open(settings_path, "r", encoding="utf-8") as f: config = json.load(f) if "security" not in config: - warnings.append("settings.json 缺少 security 字段") + warnings.append(tr("cli_export_msg.settings_json_missing_security")) except json.JSONDecodeError as e: - errors.append(f"settings.json 格式错误: {e}") + errors.append(tr("cli_export_msg.settings_json_format_error_gemini", error=e)) except Exception as e: - errors.append(f"读取 settings.json 失败: {e}") + errors.append(tr("cli_export_msg.read_settings_json_failed_gemini", error=e)) if errors: return ValidationResult.failure(errors, warnings) From e62b11f43a969c9b477d4b7740b9c70da6978e75 Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:25:58 +0200 Subject: [PATCH 5/7] i18n(occm_core): localize native providers, plugin and skill manager UI text --- occm_core/native_providers.py | 8 ++-- occm_core/plugin_manager.py | 8 ++-- occm_core/skill_manager.py | 72 +++++++++++++++++------------------ 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/occm_core/native_providers.py b/occm_core/native_providers.py index 3015221..855bccb 100644 --- a/occm_core/native_providers.py +++ b/occm_core/native_providers.py @@ -229,7 +229,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="zhipuai", - name="Zhipu AI (智谱GLM)", + name="Zhipu AI", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, ""), @@ -248,7 +248,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="zhipuai-coding-plan", - name="Zhipu AI Coding Plan (智谱GLM编码套餐)", + name="Zhipu AI Coding Plan", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, ""), @@ -305,7 +305,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="qwen", - name="千问 Qwen", + name="Qwen", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, "sk-..."), @@ -339,7 +339,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="yi", - name="零一万物 Yi", + name="Yi", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, ""), diff --git a/occm_core/plugin_manager.py b/occm_core/plugin_manager.py index 5a791da..30c893d 100644 --- a/occm_core/plugin_manager.py +++ b/occm_core/plugin_manager.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from typing import Any, Dict, List +from .i18n import tr + @dataclass class PluginConfig: @@ -88,7 +90,7 @@ def install_npm_plugin( return True except Exception as e: - print(f"安装插件失败: {e}") + print(tr("plugin_mgr.install_failed", error=e)) return False @staticmethod @@ -111,7 +113,7 @@ def uninstall_plugin(config: Dict[str, Any], plugin: PluginConfig) -> bool: return False except Exception as e: - print(f"卸载插件失败: {e}") + print(tr("plugin_mgr.uninstall_failed", error=e)) return False @staticmethod @@ -126,6 +128,6 @@ def check_npm_version(package_name: str) -> str: data = response.json() return data.get("version", "") except Exception as e: - print(f"检查版本失败: {e}") + print(tr("plugin_mgr.check_version_failed", error=e)) return "" diff --git a/occm_core/skill_manager.py b/occm_core/skill_manager.py index 8dd2d7d..3a51eb7 100644 --- a/occm_core/skill_manager.py +++ b/occm_core/skill_manager.py @@ -44,19 +44,19 @@ def get_project_paths() -> Dict[str, Path]: @staticmethod def validate_skill_name(name: str) -> Tuple[bool, str]: if not name: - return False, "名称不能为空" + return False, tr("skill_manager.name_empty") if len(name) > 64: - return False, "名称不能超过 64 字符" + return False, tr("skill_manager.name_too_long") if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", name): - return False, "名称格式错误:只能使用小写字母、数字、单连字符分隔" + return False, tr("skill_manager.name_format_error") return True, "" @staticmethod def validate_description(desc: str) -> Tuple[bool, str]: if not desc: - return False, "描述不能为空" + return False, tr("skill_manager.desc_empty") if len(desc) > 1024: - return False, "描述不能超过 1024 字符" + return False, tr("skill_manager.desc_too_long") return True, "" @staticmethod @@ -164,10 +164,10 @@ def discover_all(cls) -> List[DiscoveredSkill]: skills.append(skill) seen_names.add(skill.name) except Exception as e: - print(f"解析 skill 失败 {skill_dir.name}: {e}") + print(tr("skill_manager.parse_failed", name=skill_dir.name, error=e)) continue except Exception as e: - print(f"遍历目录失败 {base_path}: {e}") + print(tr("skill_manager.scan_dir_failed", path=base_path, error=e)) continue return skills @@ -420,7 +420,7 @@ def scan_skill(cls, skill_path: Path) -> Dict[str, Any]: "line": 0, "code": "", "level": "critical", - "description": f"扫描失败: {str(e)}", + "description": tr("skill_manager.scan_failed", error=str(e)), } ], "level": "unknown", @@ -494,7 +494,7 @@ def parse_source(source: str) -> Tuple[str, Dict[str, str]]: if os.path.exists(source): return "local", {"path": source} - raise ValueError(f"无法识别的来源格式: {source}") + raise ValueError(tr("skill_manager.unrecognized_source", source=source)) @staticmethod def install_from_github( @@ -513,7 +513,7 @@ def install_from_github( try: if progress_callback: - progress_callback("正在下载...") + progress_callback(tr("skill_manager.downloading")) zip_url = ( f"https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip" @@ -522,11 +522,11 @@ def install_from_github( if response.status_code == 404: if progress_callback: - progress_callback("检测分支...") + progress_callback(tr("skill_manager.detecting_branch")) detected_branch = SkillInstaller.detect_default_branch(owner, repo) if detected_branch != branch: if progress_callback: - progress_callback(f"使用分支: {detected_branch}") + progress_callback(tr("skill_manager.using_branch", branch=detected_branch)) branch = detected_branch zip_url = f"https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip" response = requests.get(zip_url, stream=True, timeout=30) @@ -534,7 +534,7 @@ def install_from_github( response.raise_for_status() if progress_callback: - progress_callback("正在解压...") + progress_callback(tr("skill_manager.extracting")) with tempfile.TemporaryDirectory() as temp_dir: zip_path = Path(temp_dir) / "skill.zip" @@ -550,7 +550,7 @@ def install_from_github( if subdir: skill_dir = extracted_dir / subdir if not skill_dir.exists(): - return False, f"子目录不存在: {subdir}" + return False, tr("skill_manager.subdir_not_found", subdir=subdir) else: skill_dir = extracted_dir @@ -564,15 +564,15 @@ def install_from_github( if not skill_file: return ( False, - f"未找到 SKILL.md 或 SKILL.txt 文件{f' (在 {subdir} 中)' if subdir else ''}", + tr("skill_manager.skill_file_not_found_in_subdir", subdir=subdir) if subdir else tr("skill_manager.skill_file_not_found"), ) skill = SkillDiscovery.parse_skill_file(skill_file) if not skill: - return False, "SKILL 文件格式错误" + return False, tr("skill_manager.skill_file_format_error") if progress_callback: - progress_callback("正在安装...") + progress_callback(tr("skill_manager.installing")) skill_target = target_dir / skill.name if skill_target.exists(): @@ -609,14 +609,14 @@ def install_from_github( json.dump(meta, f, indent=2, ensure_ascii=False) if progress_callback: - progress_callback("安装完成!") + progress_callback(tr("skill_manager.install_complete")) - return True, f"Skill '{skill.name}' 安装成功" + return True, tr("skill_manager.install_success", name=skill.name) except requests.exceptions.RequestException as e: - return False, f"网络错误: {str(e)}" + return False, tr("skill_manager.network_error", error=str(e)) except Exception as e: - return False, f"安装失败: {str(e)}" + return False, tr("skill_manager.install_failed", error=str(e)) @staticmethod def install_from_local( @@ -627,18 +627,18 @@ def install_from_local( try: source = Path(source_path) if not source.exists(): - return False, f"路径不存在: {source_path}" + return False, tr("skill_manager.path_not_found", path=source_path) skill_md = source / "SKILL.md" if not skill_md.exists(): - return False, "未找到 SKILL.md 文件" + return False, tr("skill_manager.skill_md_not_found") skill = SkillDiscovery.parse_skill_file(skill_md) if not skill: - return False, "SKILL.md 格式错误" + return False, tr("skill_manager.skill_md_format_error") if progress_callback: - progress_callback("正在复制...") + progress_callback(tr("skill_manager.copying")) skill_target = target_dir / skill.name if skill_target.exists(): @@ -657,12 +657,12 @@ def install_from_local( json.dump(meta, f, indent=2, ensure_ascii=False) if progress_callback: - progress_callback("安装完成!") + progress_callback(tr("skill_manager.install_complete")) - return True, f"Skill '{skill.name}' 安装成功" + return True, tr("skill_manager.install_success", name=skill.name) except Exception as e: - return False, f"安装失败: {str(e)}" + return False, tr("skill_manager.install_failed", error=str(e)) class SkillUpdater: @@ -685,7 +685,7 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "current_commit": None, "latest_commit": None, "meta": None, - "status": "本地", + "status": tr("skill_manager.status_local"), } ) continue @@ -702,7 +702,7 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "current_commit": None, "latest_commit": None, "meta": meta, - "status": "本地", + "status": tr("skill_manager.status_local"), } ) continue @@ -728,15 +728,15 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "has_update": has_update, "current_commit": current_commit[:7] if current_commit - else "未知", + else tr("skill_manager.status_unknown"), "latest_commit": latest_commit[:7], "meta": meta, - "status": "有更新" if has_update else "最新", + "status": tr("skill_manager.status_has_update") if has_update else tr("skill_manager.status_latest"), } ) except Exception as e: - print(f"检查更新失败 {skill.name}: {e}") + print(tr("skill_manager.check_update_failed", name=skill.name, error=e)) updates.append( { "skill": skill, @@ -744,7 +744,7 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "current_commit": None, "latest_commit": None, "meta": meta if "meta" in locals() else None, - "status": "检查失败", + "status": tr("skill_manager.status_check_failed"), } ) @@ -755,7 +755,7 @@ def update_skill( skill: DiscoveredSkill, meta: dict, progress_callback=None ) -> Tuple[bool, str]: if meta.get("source") != "github": - return False, "仅支持更新从 GitHub 安装的 Skills" + return False, tr("skill_manager.update_github_only") try: target_dir = skill.path.parent.parent @@ -773,4 +773,4 @@ def update_skill( return success, message except Exception as e: - return False, f"更新失败: {str(e)}" + return False, tr("skill_manager.update_failed", error=str(e)) From 68f8158ad509cdf922632b9a2a76b1b0ac6b7e8b Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:26:04 +0200 Subject: [PATCH 6/7] i18n(occm_web): localize web edition UI text --- occm_web/__main__.py | 12 ++++++------ occm_web/app.py | 2 +- occm_web/auth.py | 36 ++++++++++++++++++------------------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/occm_web/__main__.py b/occm_web/__main__.py index cbce204..9dc74f8 100644 --- a/occm_web/__main__.py +++ b/occm_web/__main__.py @@ -26,12 +26,12 @@ "--config-dir", type=str, default="", - help="自定义配置目录路径 (默认 ~/.config/opencode/)", + help="Custom config directory path (default ~/.config/opencode/)", ) _parser.add_argument( "--no-browser", action="store_true", - help="启动时不自动打开浏览器", + help="Don't auto-open browser on startup", ) _args = _parser.parse_args() os.environ["_OCCM_MP_CHILD"] = "1" @@ -67,14 +67,14 @@ if not _is_mp_child and auth_manager is not None: generated_password = auth_manager.ensure_admin_password() if generated_password: - print("\n[OCCM Web] 首次启动已生成管理密码,请立即保存:") - print(f"[OCCM Web] 管理员密码: {generated_password}\n") + print("\n[OCCM Web] Admin password generated on first run, please save immediately:") + print(f"[OCCM Web] Admin password: {generated_password}\n") if not _is_mp_child: from occm_core import ConfigPaths as _CP # type: ignore - print(f"[OCCM Web] 配置目录: {_CP.get_config_base_dir()}") - print(f"[OCCM Web] 访问地址: http://{_host}:{_port}") + print(f"[OCCM Web] Config directory: {_CP.get_config_base_dir()}") + print(f"[OCCM Web] Access URL: http://{_host}:{_port}") # 自动打开浏览器 if not _is_mp_child and not _no_browser: diff --git a/occm_web/app.py b/occm_web/app.py index 3878825..b93f696 100644 --- a/occm_web/app.py +++ b/occm_web/app.py @@ -29,7 +29,7 @@ def _register_middlewares() -> None: def _register_exception_handler(debug: bool) -> None: @app.exception_handler(Exception) async def _global_exception_handler(_: Request, exc: Exception) -> JSONResponse: - logger.exception("未处理异常: %s", exc) + logger.exception("Unhandled exception: %s", exc) return JSONResponse({"ok": False, "error": str(exc)}, status_code=500) diff --git a/occm_web/auth.py b/occm_web/auth.py index cf0d1a3..3801415 100644 --- a/occm_web/auth.py +++ b/occm_web/auth.py @@ -90,7 +90,7 @@ def verify_login(self, password: str, client_key: str) -> tuple[bool, str]: if state.lock_until and now < state.lock_until: remain = int((state.lock_until - now).total_seconds() // 60) + 1 - return False, f"登录失败过多,请 {remain} 分钟后再试" + return False, tr("auth.login_locked", remain=remain) stored_hash = self._config.get("password_hash", "") if not stored_hash or not self._verify_password(password, stored_hash): @@ -98,12 +98,12 @@ def verify_login(self, password: str, client_key: str) -> tuple[bool, str]: if state.attempts >= 5: state.lock_until = now + timedelta(minutes=15) state.attempts = 0 - return False, "登录失败 5 次,已锁定 15 分钟" - return False, f"密码错误,还可尝试 {5 - state.attempts} 次" + return False, tr("auth.login_failed_locked") + return False, tr("auth.password_wrong", remain=5 - state.attempts) state.attempts = 0 state.lock_until = None - return True, "登录成功" + return True, tr("auth.login_success") def create_token(self, subject: str = "admin") -> str: now = datetime.now(UTC) @@ -137,13 +137,13 @@ def clear_auth_cookie(self, response: JSONResponse) -> None: def change_password(self, old_password: str, new_password: str) -> tuple[bool, str]: if len(new_password) < 8: - return False, "新密码至少 8 位" + return False, tr("auth.new_password_min_length") old_hash = self._config.get("password_hash", "") if not self._verify_password(old_password, old_hash): - return False, "原密码错误" + return False, tr("auth.old_password_wrong") self._config["password_hash"] = self._hash_password(new_password) self._save_config(self._config) - return True, "密码修改成功" + return True, tr("auth.password_changed") def _extract_request(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Request | None: @@ -204,7 +204,7 @@ async def auth_login(request: Request) -> JSONResponse: @app.post("/api/auth/logout") async def auth_logout() -> JSONResponse: - response = JSONResponse({"ok": True, "message": "已退出登录"}) + response = JSONResponse({"ok": True, "message": tr("auth.logged_out")}) auth.clear_auth_cookie(response) return response @@ -212,7 +212,7 @@ async def auth_logout() -> JSONResponse: async def auth_change_password(request: Request) -> JSONResponse: token = request.cookies.get(COOKIE_NAME, "") if not token or not auth.decode_token(token): - return JSONResponse({"ok": False, "message": "未登录"}, status_code=401) + return JSONResponse({"ok": False, "message": tr("auth.not_logged_in")}, status_code=401) payload = await request.json() old_password = str(payload.get("old_password", "")) @@ -235,9 +235,9 @@ async def login_page(request: Request) -> None: ): with ui.card().classes("w-[360px] p-6 gap-4"): title = ui.label(tr("app.title")).classes("text-xl font-bold") - subtitle = ui.label("OCCM Web 登录").classes("text-sm text-gray-500") + subtitle = ui.label(tr("auth.web_login_subtitle")).classes("text-sm text-gray-500") password = ui.input( - "管理密码", password=True, password_toggle_button=True + tr("auth.admin_password_label"), password=True, password_toggle_button=True ).classes("w-full") async def do_login() -> None: @@ -256,10 +256,10 @@ async def do_login() -> None: ui.navigate.to("/") else: ui.notify( - (result or {}).get("message", "登录失败"), type="negative" + (result or {}).get("message", tr("auth.login_failed_fallback")), type="negative" ) - ui.button("登录", on_click=do_login).classes("w-full") + ui.button(tr("auth.login_button"), on_click=do_login).classes("w-full") title.update() subtitle.update() @@ -272,10 +272,10 @@ async def change_password_page(request: Request) -> None: with ui.card().classes("w-[420px] p-6 gap-4"): ui.label(tr("common.update")).classes("text-lg font-bold") old_password = ui.input( - "原密码", password=True, password_toggle_button=True + tr("auth.old_password_label"), password=True, password_toggle_button=True ).classes("w-full") new_password = ui.input( - "新密码", password=True, password_toggle_button=True + tr("auth.new_password_label"), password=True, password_toggle_button=True ).classes("w-full") async def do_change() -> None: @@ -296,12 +296,12 @@ async def do_change() -> None: result = await ui.run_javascript(script) if result and result.get("ok"): ui.notify( - (result or {}).get("message", "密码已更新"), type="positive" + (result or {}).get("message", tr("auth.password_updated_fallback")), type="positive" ) ui.navigate.to("/") else: ui.notify( - (result or {}).get("message", "修改失败"), type="negative" + (result or {}).get("message", tr("auth.change_failed_fallback")), type="negative" ) - ui.button("提交", on_click=do_change).classes("w-full") + ui.button(tr("auth.submit_button"), on_click=do_change).classes("w-full") From faef7b5dbe27a2f53f14e8d1c67655c8b9810446 Mon Sep 17 00:00:00 2001 From: Qyperion Date: Wed, 4 Mar 2026 15:26:10 +0200 Subject: [PATCH 7/7] i18n: localize desktop UI text and fix preset model config lookup in English mode --- opencode_config_manager_fluent.py | 1009 +++++++++++++++-------------- 1 file changed, 514 insertions(+), 495 deletions(-) diff --git a/opencode_config_manager_fluent.py b/opencode_config_manager_fluent.py index d4820db..4bdaade 100644 --- a/opencode_config_manager_fluent.py +++ b/opencode_config_manager_fluent.py @@ -56,18 +56,27 @@ def exception_handler(exc_type, exc_value, exc_traceback): from PyQt5.QtWidgets import QMessageBox, QApplication app = QApplication.instance() or QApplication(sys.argv) + try: + title = tr("crash.title") + msg = tr("crash.message", log_file=crash_log_file) + except Exception: + title = "OCCM Crash" + msg = (f"The application encountered an error and needs to close.\n\n" + f"Error log saved to:\n{crash_log_file}\n\n" + f"Please send this log file to the developer.\n\n" + f"GitHub: https://github.com/icysaintdx/OpenCode-Config-Manager/issues") QMessageBox.critical( None, - "OCCM 崩溃", - f"应用程序遇到错误并需要关闭。\n\n" - f"错误日志已保存到:\n{crash_log_file}\n\n" - f"请将此日志文件发送给开发者以帮助修复问题。\n\n" - f"GitHub: https://github.com/icysaintdx/OpenCode-Config-Manager/issues", + title, + msg, ) except: - # 如果 GUI 无法显示,至少打印到控制台 + try: + console_msg = tr("crash.console_msg", log_file=crash_log_file) + except Exception: + console_msg = f"OCCM Crash - log saved to: {crash_log_file}" print(f"\n{'=' * 80}") - print(f"OCCM 崩溃 - 日志已保存到: {crash_log_file}") + print(console_msg) print(f"{'=' * 80}\n") # 不再调用默认处理器,避免 GUI 进程直接退出 @@ -297,7 +306,7 @@ class ProviderValidationError(CLIExportError): def __init__(self, missing_fields: List[str]): self.missing_fields = missing_fields - super().__init__(f"Provider 配置不完整: 缺少 {', '.join(missing_fields)}") + super().__init__(tr("cli_exception.provider_incomplete", fields=', '.join(missing_fields))) class ConfigWriteError(CLIExportError): @@ -306,7 +315,7 @@ class ConfigWriteError(CLIExportError): def __init__(self, path: Path, reason: str): self.path = path self.reason = reason - super().__init__(f"写入配置失败 ({path}): {reason}") + super().__init__(tr("cli_exception.write_failed", path=path, reason=reason)) class ConfigParseError(CLIExportError): @@ -316,7 +325,7 @@ def __init__(self, path: Path, format_type: str, reason: str): self.path = path self.format_type = format_type self.reason = reason - super().__init__(f"解析 {format_type} 配置失败 ({path}): {reason}") + super().__init__(tr("cli_exception.parse_failed", format_type=format_type, path=path, reason=reason)) class BackupError(CLIExportError): @@ -325,7 +334,7 @@ class BackupError(CLIExportError): def __init__(self, cli_type: str, reason: str): self.cli_type = cli_type self.reason = reason - super().__init__(f"备份 {cli_type} 配置失败: {reason}") + super().__init__(tr("cli_exception.backup_failed", cli_type=cli_type, reason=reason)) class RestoreError(CLIExportError): @@ -334,7 +343,7 @@ class RestoreError(CLIExportError): def __init__(self, backup_path: Path, reason: str): self.backup_path = backup_path self.reason = reason - super().__init__(f"恢复备份失败 ({backup_path}): {reason}") + super().__init__(tr("cli_exception.restore_failed", backup_path=backup_path, reason=reason)) # ==================== Agent 分组管理 ==================== @@ -516,7 +525,7 @@ def load_groups(self) -> None: "default_group_id": None, } except Exception as e: - print(f"加载分组配置失败: {e}") + print(f"{tr('agent_group_mgr.load_failed')}: {e}") self.groups_data = { "version": "1.0.0", "groups": [], @@ -542,7 +551,7 @@ def save_groups(self) -> None: with open(self.groups_file, "w", encoding="utf-8") as f: json.dump(self.groups_data, f, indent=2, ensure_ascii=False) except Exception as e: - print(f"保存分组配置失败: {e}") + print(f"{tr('agent_group_mgr.save_failed')}: {e}") raise def backup_groups(self) -> Optional[Path]: @@ -570,7 +579,7 @@ def backup_groups(self) -> Optional[Path]: return backup_file except Exception as e: - print(f"备份分组配置失败: {e}") + print(f"{tr('agent_group_mgr.backup_failed')}: {e}") return None def _cleanup_old_backups(self, keep_count: int = 10) -> None: @@ -591,7 +600,7 @@ def _cleanup_old_backups(self, keep_count: int = 10) -> None: for backup_file in backup_files[keep_count:]: backup_file.unlink() except Exception as e: - print(f"清理旧备份失败: {e}") + print(f"{tr('agent_group_mgr.cleanup_failed')}: {e}") # ========== 分组CRUD操作 ========== @@ -914,7 +923,7 @@ def export_group(self, group_id: str, file_path: Path) -> bool: return True except Exception as e: - print(f"导出分组失败: {e}") + print(f"{tr('agent_group_mgr.export_failed')}: {e}") return False def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str]: @@ -934,7 +943,7 @@ def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str # 验证格式 if "group" not in import_data: - print("导入文件格式错误:缺少group字段") + print(tr('agent_group_mgr.import_format_error')) return None group = import_data["group"] @@ -947,7 +956,7 @@ def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str break if existing_group and not overwrite: - print(f"分组 '{group['name']}' 已存在") + print(tr('agent_group_mgr.group_exists', name=group['name'])) return None if existing_group and overwrite: @@ -969,7 +978,7 @@ def import_group(self, file_path: Path, overwrite: bool = False) -> Optional[str icon=group.get("icon", "📁"), ) except Exception as e: - print(f"导入分组失败: {e}") + print(tr("agent_group_mgr.import_failed", error=str(e))) return None # ========== 统计信息 ========== @@ -1379,7 +1388,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="zhipuai", - name="Zhipu AI (智谱GLM)", + name="Zhipu AI", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, ""), @@ -1398,7 +1407,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="zhipuai-coding-plan", - name="Zhipu AI Coding Plan (智谱GLM编码套餐)", + name="Zhipu AI Coding Plan", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, ""), @@ -1455,7 +1464,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="qwen", - name="千问 Qwen", + name="Qwen", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, "sk-..."), @@ -1489,7 +1498,7 @@ class NativeProviderConfig: ), NativeProviderConfig( id="yi", - name="零一万物 Yi", + name="Yi", sdk="@ai-sdk/openai-compatible", auth_fields=[ AuthField("apiKey", "API Key", "password", True, ""), @@ -2961,10 +2970,20 @@ def get_resource_path(relative_path: str) -> Path: def get_tooltip(key: str) -> str: - """获取tooltip文本,如果不存在返回空字符串""" + """Get tooltip text from locale, fall back to TOOLTIPS dict""" + translated = tr(f"tooltip.{key}") + if translated != f"tooltip.{key}": + return translated return TOOLTIPS.get(key, "") +def _tr_preset(section: str, key: str, fallback: str = "") -> str: + """Translate a preset description, falling back to the dict value.""" + translated = tr(f"preset_desc.{section}.{key}") + if translated != f"preset_desc.{section}.{key}": + return translated + return fallback + # ==================== 预设常用模型(含完整配置) ==================== # 根据 OpenCode 官方文档 (https://opencode.ai/docs/models/) # - options: 模型的默认配置参数,每次调用都会使用 @@ -3536,7 +3555,7 @@ def get_config_file_info(cls, path: Path) -> Dict: return { "exists": True, "size": stat.st_size, - "size_str": f"{stat.st_size:,} 字节", + "size_str": tr("cli_export_msg.file_size_bytes", size=f"{stat.st_size:,}"), "mtime": datetime.fromtimestamp(stat.st_mtime), "mtime_str": datetime.fromtimestamp(stat.st_mtime).strftime( "%Y-%m-%d %H:%M:%S" @@ -3967,12 +3986,12 @@ def load_json(path: Path) -> Optional[Dict]: except json.JSONDecodeError as e2: # 详细记录解析失败原因 print(f"Load failed {path}:") - print(f" - 标准JSON解析失败: {e1}") - print(f" - JSONC解析失败: {e2}") - print(f" - 文件大小: {len(content)} 字节") + print(f" - {tr('config_mgr.json_parse_failed', error=str(e1))}") + print(f" - {tr('config_mgr.jsonc_parse_failed', error=str(e2))}") + print(f" - {tr('cli_export_msg.file_size_bytes', size=str(len(content)))}") # 打印前200个字符用于调试 preview = content[:200].replace("\n", "\\n") - print(f" - 文件预览: {preview}...") + print(f" - {tr('config_mgr.file_preview', preview=preview)}") return None except Exception as e: print(f"Load failed {path}: {e}") @@ -4253,7 +4272,7 @@ def atomic_write_json(self, path: Path, data: Dict) -> None: except json.JSONDecodeError as e: if temp_path.exists(): temp_path.unlink() - raise ConfigWriteError(path, f"JSON 格式验证失败: {e}") + raise ConfigWriteError(path, f"{tr('cli_export_msg.json_validation_failed', error=str(e))}") except Exception as e: if temp_path.exists(): temp_path.unlink() @@ -4298,7 +4317,7 @@ def set_file_permissions(self, path: Path, mode: int = 0o600) -> None: try: path.chmod(mode) except Exception as e: - print(f"设置文件权限失败 ({path}): {e}") + print(f"{tr('cli_export_msg.set_permissions_failed', path=str(path), error=str(e))}") def write_claude_settings(self, config: Dict, merge: bool = True) -> None: """写入 Claude settings.json @@ -4486,7 +4505,7 @@ def restore_backup(self, backup_path: Path, cli_type: str) -> bool: """ try: if not backup_path.exists(): - raise RestoreError(backup_path, "备份目录不存在") + raise RestoreError(backup_path, tr("cli_export_msg.restore_backup_dir_missing")) cli_dir = CLIConfigWriter.get_cli_dir(cli_type) cli_dir.mkdir(parents=True, exist_ok=True) @@ -4545,7 +4564,7 @@ def list_backups(self, cli_type: str) -> List[BackupInfo]: backups.sort(key=lambda x: x.created_at, reverse=True) except Exception as e: - print(f"列出备份失败: {e}") + print(f"{tr('cli_export_msg.list_backups_failed', error=str(e))}") return backups @@ -4562,7 +4581,7 @@ def cleanup_old_backups(self, cli_type: str) -> None: try: shutil.rmtree(backup.path) except Exception as e: - print(f"删除旧备份失败 ({backup.path}): {e}") + print(f"{tr('cli_export_msg.delete_old_backup_failed', path=str(backup.path), error=str(e))}") class CLIConfigGenerator: @@ -4750,19 +4769,19 @@ def validate_provider(self, provider: Dict) -> ValidationResult: "baseURL", "" ) if not base_url or not base_url.strip(): - errors.append("缺少 API 地址 (baseURL)") + errors.append(tr("cli_export_msg.missing_base_url")) # 检查 apiKey api_key = provider.get("apiKey", "") or provider.get("options", {}).get( "apiKey", "" ) if not api_key or not api_key.strip(): - errors.append("缺少 API 密钥 (apiKey)") + errors.append(tr("cli_export_msg.missing_api_key")) # 检查 Model 配置 models = provider.get("models", {}) if not models: - warnings.append("未配置任何模型") + warnings.append(tr("cli_export_msg.no_models_configured")) if errors: return ValidationResult.failure(errors, warnings) @@ -4802,7 +4821,7 @@ def export_to_claude(self, provider: Dict, model: str) -> ExportResult: except CLIExportError as e: return ExportResult.fail(cli_type, str(e), backup_path) except Exception as e: - return ExportResult.fail(cli_type, f"导出失败: {e}", backup_path) + return ExportResult.fail(cli_type, f"{tr('cli_export_msg.export_failed', error=str(e))}", backup_path) def export_to_codex(self, provider: Dict, model: str) -> ExportResult: """导出到 Codex CLI @@ -4844,7 +4863,7 @@ def export_to_codex(self, provider: Dict, model: str) -> ExportResult: except CLIExportError as e: return ExportResult.fail(cli_type, str(e), backup_path) except Exception as e: - return ExportResult.fail(cli_type, f"导出失败: {e}", backup_path) + return ExportResult.fail(cli_type, f"{tr('cli_export_msg.export_failed', error=str(e))}", backup_path) def export_to_gemini(self, provider: Dict, model: str) -> ExportResult: """导出到 Gemini CLI @@ -4886,7 +4905,7 @@ def export_to_gemini(self, provider: Dict, model: str) -> ExportResult: except CLIExportError as e: return ExportResult.fail(cli_type, str(e), backup_path) except Exception as e: - return ExportResult.fail(cli_type, f"导出失败: {e}", backup_path) + return ExportResult.fail(cli_type, f"{tr('cli_export_msg.export_failed', error=str(e))}", backup_path) def batch_export( self, provider: Dict, models: Dict[str, str], targets: List[str] @@ -4914,9 +4933,9 @@ def batch_export( elif cli_type == "gemini": result = self.export_to_gemini(provider, model) else: - result = ExportResult.fail(cli_type, f"未知的 CLI 类型: {cli_type}") + result = ExportResult.fail(cli_type, f"{tr('cli_export_msg.unknown_cli_type', cli_type=cli_type)}") except Exception as e: - result = ExportResult.fail(cli_type, f"导出异常: {e}") + result = ExportResult.fail(cli_type, f"{tr('cli_export_msg.export_exception', error=str(e))}") results.append(result) @@ -4944,83 +4963,83 @@ def validate_exported_config(self, cli_type: str) -> ValidationResult: if cli_type == "claude": settings_path = cli_dir / "settings.json" if not settings_path.exists(): - errors.append("settings.json 文件不存在") + errors.append(tr("cli_export_msg.settings_json_not_found")) else: try: with open(settings_path, "r", encoding="utf-8") as f: config = json.load(f) if "env" not in config: - errors.append("settings.json 缺少 env 字段") + errors.append(tr("cli_export_msg.settings_json_missing_env")) else: env = config["env"] if "ANTHROPIC_BASE_URL" not in env: - errors.append("缺少 ANTHROPIC_BASE_URL") + errors.append(tr("cli_export_msg.missing_anthropic_base_url")) if "ANTHROPIC_AUTH_TOKEN" not in env: - errors.append("缺少 ANTHROPIC_AUTH_TOKEN") + errors.append(tr("cli_export_msg.missing_anthropic_auth_token")) except json.JSONDecodeError as e: - errors.append(f"settings.json 格式错误: {e}") + errors.append(f"{tr('cli_export_msg.settings_json_format_error', error=str(e))}") except Exception as e: - errors.append(f"读取 settings.json 失败: {e}") + errors.append(f"{tr('cli_export_msg.read_settings_json_failed', error=str(e))}") elif cli_type == "codex": auth_path = cli_dir / "auth.json" config_path = cli_dir / "config.toml" if not auth_path.exists(): - errors.append("auth.json 文件不存在") + errors.append(tr("cli_export_msg.auth_json_not_found")) else: try: with open(auth_path, "r", encoding="utf-8") as f: auth = json.load(f) if "OPENAI_API_KEY" not in auth: - errors.append("auth.json 缺少 OPENAI_API_KEY") + errors.append(tr("cli_export_msg.auth_json_missing_key")) except json.JSONDecodeError as e: - errors.append(f"auth.json 格式错误: {e}") + errors.append(f"{tr('cli_export_msg.auth_json_format_error', error=str(e))}") except Exception as e: - errors.append(f"读取 auth.json 失败: {e}") + errors.append(f"{tr('cli_export_msg.read_auth_json_failed', error=str(e))}") if not config_path.exists(): - errors.append("config.toml 文件不存在") + errors.append(tr("cli_export_msg.config_toml_not_found")) else: try: with open(config_path, "r", encoding="utf-8") as f: content = f.read() if "model_provider" not in content: - errors.append("config.toml 缺少 model_provider") + errors.append(tr("cli_export_msg.config_toml_missing_provider")) if "model =" not in content: - errors.append("config.toml 缺少 model") + errors.append(tr("cli_export_msg.config_toml_missing_model")) except Exception as e: - errors.append(f"读取 config.toml 失败: {e}") + errors.append(f"{tr('cli_export_msg.read_config_toml_failed', error=str(e))}") elif cli_type == "gemini": env_path = cli_dir / ".env" settings_path = cli_dir / "settings.json" if not env_path.exists(): - errors.append(".env 文件不存在") + errors.append(tr("cli_export_msg.dot_env_not_found")) else: try: with open(env_path, "r", encoding="utf-8") as f: content = f.read() if "GEMINI_API_KEY" not in content: - errors.append(".env 缺少 GEMINI_API_KEY") + errors.append(tr("cli_export_msg.dot_env_missing_gemini_key")) if "GOOGLE_GEMINI_BASE_URL" not in content: - errors.append(".env 缺少 GOOGLE_GEMINI_BASE_URL") + errors.append(tr("cli_export_msg.dot_env_missing_gemini_url")) except Exception as e: - errors.append(f"读取 .env 失败: {e}") + errors.append(f"{tr('cli_export_msg.read_dot_env_failed', error=str(e))}") if not settings_path.exists(): - warnings.append("settings.json 文件不存在") + warnings.append(tr("cli_export_msg.settings_json_warning_not_found")) else: try: with open(settings_path, "r", encoding="utf-8") as f: config = json.load(f) if "security" not in config: - warnings.append("settings.json 缺少 security 字段") + warnings.append(tr("cli_export_msg.settings_json_missing_security")) except json.JSONDecodeError as e: - errors.append(f"settings.json 格式错误: {e}") + errors.append(f"{tr('cli_export_msg.settings_json_format_error', error=str(e))}") except Exception as e: - errors.append(f"读取 settings.json 失败: {e}") + errors.append(f"{tr('cli_export_msg.read_settings_json_failed', error=str(e))}") if errors: return ValidationResult.failure(errors, warnings) @@ -5077,14 +5096,14 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": "root", - "message": "配置文件无法解析或读取失败", + "message": tr("validator.config_parse_failed"), } ) return issues if not isinstance(config, dict): issues.append( - {"level": "error", "path": "root", "message": "配置根必须是对象类型"} + {"level": "error", "path": "root", "message": tr("validator.root_must_be_object")} ) return issues @@ -5094,7 +5113,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "root", - "message": "配置为空,尚未添加任何Provider", + "message": tr("validator.config_empty"), } ) return issues @@ -5106,7 +5125,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "$schema", - "message": "建议设置 $schema 为 https://opencode.ai/config.json", + "message": tr("validator.schema_recommend"), } ) @@ -5117,7 +5136,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "provider", - "message": "未配置任何 Provider", + "message": tr("validator.no_providers"), } ) if not isinstance(providers, dict): @@ -5125,7 +5144,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": "provider", - "message": "provider 必须是对象类型", + "message": tr("validator.provider_must_be_object"), } ) return issues @@ -5139,7 +5158,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": provider_path, - "message": f"Provider '{provider_name}' 的值必须是对象,当前是 {type(provider_data).__name__}", + "message": tr("validator.provider_value_not_object", name=provider_name, type=type(provider_data).__name__), } ) continue @@ -5151,7 +5170,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.{field}", - "message": f"Provider '{provider_name}' 缺少必需字段 '{field}'", + "message": tr("validator.provider_missing_field", name=provider_name, field=field), } ) elif ConfigValidator._is_blank(provider_data.get(field)): @@ -5159,7 +5178,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.{field}", - "message": f"Provider '{provider_name}' 的 '{field}' 为空", + "message": tr("validator.provider_field_empty", name=provider_name, field=field), } ) @@ -5170,7 +5189,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.npm", - "message": f"Provider '{provider_name}' 的 npm 包 '{npm}' 不在已知列表中", + "message": tr("validator.provider_unknown_npm", name=provider_name, npm=npm), } ) @@ -5181,7 +5200,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.options", - "message": f"Provider '{provider_name}' 的 options 必须是对象", + "message": tr("validator.provider_options_not_object", name=provider_name), } ) else: @@ -5191,7 +5210,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.options.{opt_field}", - "message": f"Provider '{provider_name}' 的 options 缺少 '{opt_field}'", + "message": tr("validator.provider_options_missing", name=provider_name, field=opt_field), } ) elif ConfigValidator._is_blank(options.get(opt_field)): @@ -5199,7 +5218,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.options.{opt_field}", - "message": f"Provider '{provider_name}' 的 options.{opt_field} 为空", + "message": tr("validator.provider_options_empty", name=provider_name, field=opt_field), } ) @@ -5210,7 +5229,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{provider_path}.models", - "message": f"Provider '{provider_name}' 的 models 必须是对象", + "message": tr("validator.provider_models_not_object", name=provider_name), } ) else: @@ -5219,7 +5238,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{provider_path}.models", - "message": f"Provider '{provider_name}' 没有配置任何模型", + "message": tr("validator.provider_no_models", name=provider_name), } ) for model_id, model_data in models.items(): @@ -5229,7 +5248,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": model_path, - "message": f"Provider '{provider_name}' 存在空模型ID", + "message": tr("validator.provider_empty_model_id", name=provider_name), } ) continue @@ -5238,7 +5257,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": model_path, - "message": f"Model '{model_id}' 的值必须是对象", + "message": tr("validator.model_value_not_object", name=model_id), } ) continue @@ -5250,7 +5269,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{model_path}.limit", - "message": f"Model '{model_id}' 的 limit 应该是对象", + "message": tr("validator.model_limit_should_be_object", name=model_id), } ) elif limit: @@ -5261,7 +5280,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{model_path}.limit.context", - "message": f"Model '{model_id}' 的 context 应该是整数", + "message": tr("validator.model_context_should_be_int", name=model_id), } ) if output is not None and not isinstance(output, int): @@ -5269,7 +5288,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{model_path}.limit.output", - "message": f"Model '{model_id}' 的 output 应该是整数", + "message": tr("validator.model_output_should_be_int", name=model_id), } ) @@ -5277,7 +5296,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: mcp = config.get("mcp", {}) if mcp and not isinstance(mcp, dict): issues.append( - {"level": "error", "path": "mcp", "message": "mcp 必须是对象类型"} + {"level": "error", "path": "mcp", "message": tr("validator.mcp_must_be_object")} ) elif isinstance(mcp, dict): for mcp_name, mcp_data in mcp.items(): @@ -5287,7 +5306,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": mcp_path, - "message": f"MCP '{mcp_name}' 的值必须是对象", + "message": tr("validator.mcp_value_not_object", name=mcp_name), } ) continue @@ -5298,7 +5317,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{mcp_path}.command", - "message": f"Local MCP '{mcp_name}' 缺少 command 字段", + "message": tr("validator.mcp_local_missing_command", name=mcp_name), } ) elif mcp_type == "remote" and "url" not in mcp_data: @@ -5306,7 +5325,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{mcp_path}.url", - "message": f"Remote MCP '{mcp_name}' 缺少 url 字段", + "message": tr("validator.mcp_remote_missing_url", name=mcp_name), } ) @@ -5314,7 +5333,7 @@ def validate_opencode_config(config: Dict) -> List[Dict]: agent = config.get("agent", {}) if agent and not isinstance(agent, dict): issues.append( - {"level": "error", "path": "agent", "message": "agent 必须是对象类型"} + {"level": "error", "path": "agent", "message": tr("validator.agent_must_be_object")} ) return issues @@ -5328,23 +5347,23 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: issues = [] if not config: issues.append( - {"level": "error", "path": "root", "message": "配置文件为空或无法解析"} + {"level": "error", "path": "root", "message": tr("validator.config_empty_or_unparseable")} ) return issues if not isinstance(config, dict): issues.append( - {"level": "error", "path": "root", "message": "配置根必须是对象类型"} + {"level": "error", "path": "root", "message": tr("validator.omo_root_must_be_object")} ) return issues agents = config.get("agents", {}) if not agents: issues.append( - {"level": "warning", "path": "agents", "message": "未配置任何 Agent"} + {"level": "warning", "path": "agents", "message": tr("validator.no_agents")} ) if agents and not isinstance(agents, dict): issues.append( - {"level": "error", "path": "agents", "message": "agents 必须是对象类型"} + {"level": "error", "path": "agents", "message": tr("validator.agents_must_be_object")} ) return issues @@ -5356,7 +5375,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": agent_path, - "message": "Agent 名称为空", + "message": tr("validator.agent_name_empty"), } ) continue @@ -5365,7 +5384,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": agent_path, - "message": f"Agent '{agent_name}' 的值必须是对象", + "message": tr("validator.agent_value_not_object", name=agent_name), } ) continue @@ -5375,7 +5394,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{agent_path}.{field}", - "message": f"Agent '{agent_name}' 缺少必需字段 '{field}'", + "message": tr("validator.agent_missing_field", name=agent_name, field=field), } ) elif ConfigValidator._is_blank(agent_data.get(field)): @@ -5383,7 +5402,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{agent_path}.{field}", - "message": f"Agent '{agent_name}' 的 '{field}' 为空", + "message": tr("validator.agent_field_empty", name=agent_name, field=field), } ) if "description" in agent_data and ConfigValidator._is_blank( @@ -5393,7 +5412,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{agent_path}.description", - "message": f"Agent '{agent_name}' 的 description 为空", + "message": tr("validator.agent_desc_empty", name=agent_name), } ) @@ -5403,7 +5422,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": "categories", - "message": "未配置任何 Category", + "message": tr("validator.no_categories"), } ) if categories and not isinstance(categories, dict): @@ -5411,7 +5430,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": "categories", - "message": "categories 必须是对象类型", + "message": tr("validator.categories_must_be_object"), } ) return issues @@ -5424,7 +5443,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": category_path, - "message": "Category 名称为空", + "message": tr("validator.category_name_empty"), } ) continue @@ -5433,7 +5452,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": category_path, - "message": f"Category '{category_name}' 的值必须是对象", + "message": tr("validator.category_value_not_object", name=category_name), } ) continue @@ -5443,7 +5462,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{category_path}.{field}", - "message": f"Category '{category_name}' 缺少必需字段 '{field}'", + "message": tr("validator.category_missing_field", name=category_name, field=field), } ) elif ConfigValidator._is_blank(category_data.get(field)): @@ -5451,7 +5470,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "error", "path": f"{category_path}.{field}", - "message": f"Category '{category_name}' 的 '{field}' 为空", + "message": tr("validator.category_field_empty", name=category_name, field=field), } ) @@ -5463,7 +5482,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{category_path}.temperature", - "message": f"Category '{category_name}' 的 temperature 应该是数字", + "message": tr("validator.category_temp_not_number", name=category_name), } ) if "description" in category_data and ConfigValidator._is_blank( @@ -5473,7 +5492,7 @@ def validate_ohmyopencode_config(config: Dict) -> List[Dict]: { "level": "warning", "path": f"{category_path}.description", - "message": f"Category '{category_name}' 的 description 为空", + "message": tr("validator.category_desc_empty", name=category_name), } ) @@ -5496,7 +5515,7 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: fixed_providers = {} for provider_name, provider_data in providers.items(): if not isinstance(provider_data, dict): - fixes.append(f"跳过无效 Provider '{provider_name}' (值不是对象)") + fixes.append(tr("validator.fix_skip_invalid_provider", name=provider_name)) continue # 确保必需字段存在 @@ -5505,7 +5524,7 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: # 确保 npm 字段存在 if "npm" not in fixed_provider: fixed_provider["npm"] = "@ai-sdk/openai" - fixes.append(f"Provider '{provider_name}': 添加默认 npm 字段") + fixes.append(tr("validator.fix_add_default_npm", name=provider_name)) # 确保 options 字段存在且为对象 if "options" not in fixed_provider or not isinstance( @@ -5514,23 +5533,23 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: fixed_provider["options"] = fixed_provider.get("options", {}) if not isinstance(fixed_provider["options"], dict): fixed_provider["options"] = {} - fixes.append(f"Provider '{provider_name}': 修复 options 字段") + fixes.append(tr("validator.fix_options_field", name=provider_name)) # 确保 options 中有 baseURL 和 apiKey if "baseURL" not in fixed_provider["options"]: fixed_provider["options"]["baseURL"] = "" - fixes.append(f"Provider '{provider_name}': 添加空 baseURL") + fixes.append(tr("validator.fix_add_empty_baseurl", name=provider_name)) if "apiKey" not in fixed_provider["options"]: fixed_provider["options"]["apiKey"] = "" - fixes.append(f"Provider '{provider_name}': 添加空 apiKey") + fixes.append(tr("validator.fix_add_empty_apikey", name=provider_name)) # 确保 models 字段存在且为对象 if "models" not in fixed_provider: fixed_provider["models"] = {} - fixes.append(f"Provider '{provider_name}': 添加空 models 字段") + fixes.append(tr("validator.fix_add_empty_models", name=provider_name)) elif not isinstance(fixed_provider.get("models"), dict): fixed_provider["models"] = {} - fixes.append(f"Provider '{provider_name}': 修复 models 字段为对象") + fixes.append(tr("validator.fix_models_to_object", name=provider_name)) # 规范化 models.limit,避免写入 limit: {} 触发上游校验失败 for model_id, model_cfg in list(fixed_provider.get("models", {}).items()): @@ -5543,7 +5562,7 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: if not isinstance(limit, dict): model_cfg.pop("limit", None) fixes.append( - f"Provider '{provider_name}' Model '{model_id}': 移除无效 limit" + tr("validator.fix_remove_invalid_limit", name=provider_name, model=model_id) ) continue @@ -5558,7 +5577,7 @@ def fix_provider_structure(config: Dict) -> Tuple[Dict, List[str]]: else: model_cfg.pop("limit", None) fixes.append( - f"Provider '{provider_name}' Model '{model_id}': 移除空 limit" + tr("validator.fix_remove_empty_limit", name=provider_name, model=model_id) ) # 规范化字段顺序: npm, name, options, models @@ -5589,20 +5608,20 @@ def get_issues_summary(issues: List[Dict]) -> str: lines = [] if errors: - lines.append(f"❌ {len(errors)} 个错误:") + lines.append(tr("validator.errors_count", count=len(errors))) for e in errors[:5]: # 最多显示5个 lines.append(f" • {e['message']}") if len(errors) > 5: - lines.append(f" ... 还有 {len(errors) - 5} 个错误") + lines.append(tr("validator.more_errors", count=len(errors) - 5)) if warnings: - lines.append(f"⚠️ {len(warnings)} 个警告:") + lines.append(tr("validator.warnings_count", count=len(warnings))) for w in warnings[:5]: lines.append(f" • {w['message']}") if len(warnings) > 5: - lines.append(f" ... 还有 {len(warnings) - 5} 个警告") + lines.append(tr("validator.more_warnings", count=len(warnings) - 5)) - return "\n".join(lines) if lines else "✅ 配置格式正确" + return "\n".join(lines) if lines else tr("validator.config_ok") class ModelRegistry: @@ -6192,7 +6211,7 @@ def _extract_model_ids(self, data: Any) -> List[str]: def _fetch_models(self, provider_name: str, options: Dict[str, Any]) -> None: urls = self._build_urls(options) if not urls: - self.fetch_finished.emit(provider_name, [], "未配置模型列表地址") + self.fetch_finished.emit(provider_name, [], tr("model_fetch.no_model_list_url")) return api_key = (options.get("apiKey") or "").strip() @@ -6210,11 +6229,11 @@ def _fetch_models(self, provider_name: str, options: Dict[str, Any]) -> None: if model_ids: self.fetch_finished.emit(provider_name, model_ids, "") return - last_error = "未返回可用模型列表" + last_error = tr("model_fetch.no_models_returned") except Exception as e: last_error = str(e) - self.fetch_finished.emit(provider_name, [], last_error or "获取失败") + self.fetch_finished.emit(provider_name, [], last_error or tr("model_fetch.fetch_failed")) class VersionChecker(QObject): @@ -6278,7 +6297,7 @@ def _check_update(self): error_msg = "" if e.code == 403: # GitHub API 速率限制 - error_msg = f"GitHub API速率限制(403),将在6小时后重试" + error_msg = tr("version_checker.rate_limited") print(f"Version check failed: {error_msg}") # 增加冷却时间到 6 小时 self.check_interval = 21600 @@ -6287,7 +6306,7 @@ def _check_update(self): print(f"Version check failed: {error_msg}") self.check_failed.emit(error_msg) except urllib.error.URLError as e: - error_msg = f"网络错误 - {e.reason}" + error_msg = tr("version_checker.network_error", reason=str(e.reason)) print(f"Version check failed: {error_msg}") self.check_failed.emit(error_msg) except Exception as e: @@ -6578,7 +6597,7 @@ def create_path_label(text): oc_layout.addWidget(self.oc_path_label, 1) oc_view_btn = ToolButton(FIF.VIEW, paths_card) - oc_view_btn.setToolTip("查看配置文件") + oc_view_btn.setToolTip(tr("config_viewer.view_config")) oc_view_btn.clicked.connect( lambda: self._view_config_file(ConfigPaths.get_opencode_config()) ) @@ -6614,7 +6633,7 @@ def create_path_label(text): ohmy_layout.addWidget(self.ohmy_path_label, 1) ohmy_view_btn = ToolButton(FIF.VIEW, paths_card) - ohmy_view_btn.setToolTip("查看配置文件") + ohmy_view_btn.setToolTip(tr("config_viewer.view_config")) ohmy_view_btn.clicked.connect( lambda: self._view_config_file(ConfigPaths.get_ohmyopencode_config()) ) @@ -6651,7 +6670,7 @@ def create_path_label(text): auth_layout.addWidget(self.auth_path_label, 1) auth_view_btn = ToolButton(FIF.VIEW, paths_card) - auth_view_btn.setToolTip("查看认证文件") + auth_view_btn.setToolTip(tr("config_viewer.view_auth")) auth_view_btn.clicked.connect( lambda: self._view_config_file(auth_manager.auth_path) ) @@ -6854,7 +6873,7 @@ def _view_config_file(self, config_path: Path): # 创建对话框 dialog = QDialog(self) - dialog.setWindowTitle(f"查看配置文件 - {config_path.name}") + dialog.setWindowTitle(f"{tr('config_viewer.view_config')} - {config_path.name}") dialog.resize(900, 700) # 应用对话框主题样式 @@ -6934,14 +6953,14 @@ def _view_config_file(self, config_path: Path): btn_layout.addStretch() # 保存按钮(初始禁用)- 先创建以便在lambda中引用 - save_btn = PrimaryPushButton("保存", dialog) + save_btn = PrimaryPushButton(tr("common.save"), dialog) save_btn.setEnabled(False) save_btn.clicked.connect( lambda: self._save_config_file(text_edit, config_path, dialog) ) # 编辑按钮 - edit_btn = PushButton("编辑", dialog) + edit_btn = PushButton(tr("common.edit"), dialog) # 使用闭包捕获变量 def on_edit_clicked(): @@ -6953,7 +6972,7 @@ def on_edit_clicked(): btn_layout.addWidget(save_btn) # 关闭按钮 - close_btn = PushButton("关闭", dialog) + close_btn = PushButton(tr("common.close"), dialog) close_btn.clicked.connect(dialog.accept) btn_layout.addWidget(close_btn) @@ -6962,7 +6981,7 @@ def on_edit_clicked(): dialog.exec_() except Exception as e: - self.show_error("错误", f"无法读取配置文件: {str(e)}") + self.show_error(tr("common.error"), tr("config_viewer.cannot_read", error=str(e))) def _enable_edit_mode( self, @@ -6975,9 +6994,9 @@ def _enable_edit_mode( # 自动备份 try: self.main_window.backup_manager.backup(config_path, tag="before_edit") - InfoBar.success("已备份", "已自动备份配置文件", parent=self) + InfoBar.success(tr("config_viewer.backed_up"), tr("config_viewer.backed_up"), parent=self) except Exception as e: - self.show_warning("备份失败", f"无法备份配置文件: {str(e)}") + self.show_warning(tr("config_viewer.backup_failed"), tr("config_viewer.backup_failed", error=str(e))) return # 启用编辑 @@ -7022,7 +7041,7 @@ def _save_config_file( json.loads(content) except json.JSONDecodeError as e: - self.show_error("JSON格式错误", f"配置文件格式不正确:\n{str(e)}") + self.show_error(tr("config_viewer.json_format_error"), tr("config_viewer.json_invalid", error=str(e))) return # 保存文件 @@ -7037,12 +7056,12 @@ def _save_config_file( config_path ) - self.show_success("保存成功", "配置文件已保存") + self.show_success(tr("config_viewer.save_success"), tr("config_viewer.config_saved")) self.main_window.notify_config_changed() dialog.accept() except Exception as e: - self.show_error("保存失败", f"无法保存配置文件: {str(e)}") + self.show_error(tr("config_viewer.save_failed"), tr("config_viewer.cannot_save", error=str(e))) def _copy_to_clipboard(self, text: str): """复制文本到剪贴板""" @@ -7325,12 +7344,12 @@ def _custom_add_models( def _custom_resolve_model_category(self, model_id: str) -> str: lower = model_id.lower() if "claude" in lower: - return tr("provider.claude_series") + return "Claude 系列" if "gemini" in lower: - return tr("provider.gemini_series") + return "Gemini 系列" if any(token in lower for token in ("gpt", "openai", "codex", "o1")): - return tr("provider.openai_series") - return tr("provider.other_models") + return "OpenAI/Codex 系列" + return "其他模型" def _custom_get_preset_for_category( self, category: str, preset_name: str @@ -7360,13 +7379,13 @@ def _custom_apply_batch_config( "thinking": False, "variants": False, } - if category == tr("provider.claude_series_short"): + if category == "Claude 系列": support["thinking"] = True support["variants"] = True - elif category == tr("provider.openai_series_short"): + elif category == "OpenAI/Codex 系列": support["options"] = True support["variants"] = True - elif category == tr("provider.gemini_series_short"): + elif category == "Gemini 系列": support["thinking"] = True support["variants"] = True @@ -7375,7 +7394,7 @@ def _custom_apply_batch_config( return result base_preset = self._custom_get_preset_for_category( - category, MODEL_PRESET_DEFAULT.get(category, "基础") + category, MODEL_PRESET_DEFAULT.get(category, tr("home.base_preset")) ) for key in ( @@ -7850,7 +7869,7 @@ def _custom_query_provider_usage( except Exception as openai_error: # 两种方式都失败,抛出错误 raise Exception( - f"余额查询失败。NewAPI: {str(newapi_error)[:50]}... OpenAI API: {str(openai_error)[:50]}..." + tr("balance.query_failed_both", newapi=str(newapi_error)[:50], openai=str(openai_error)[:50]) ) def _custom_query_newapi_usage(self, base_url: str, api_key: str) -> Dict[str, Any]: @@ -7868,13 +7887,13 @@ def _custom_query_newapi_usage(self, base_url: str, api_key: str) -> Dict[str, A response_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"NewAPI 查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.newapi_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"NewAPI 请求失败: {str(e)}") + raise Exception(tr("balance.newapi_request_failed", error=str(e))) # 解析 NewAPI 响应 if not response_data.get("code") or "data" not in response_data: - raise Exception("NewAPI 响应格式不正确") + raise Exception(tr("balance.newapi_invalid_response")) usage_data = response_data["data"] @@ -7931,9 +7950,9 @@ def _custom_query_openai_usage(self, base_url: str, api_key: str) -> Dict[str, A subscription_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"订阅信息查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.subscription_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"请求订阅信息失败: {str(e)}") + raise Exception(tr("balance.subscription_request_failed", error=str(e))) total_balance = subscription_data.get("hard_limit_usd", 0.0) access_until = subscription_data.get("access_until", 0) @@ -7955,9 +7974,9 @@ def _custom_query_openai_usage(self, base_url: str, api_key: str) -> Dict[str, A usage_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"使用情况查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.usage_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"请求使用情况失败: {str(e)}") + raise Exception(tr("balance.usage_request_failed", error=str(e))) # total_usage 是以美分为单位,需要除以100转换为美元 total_usage_cents = usage_data.get("total_usage", 0.0) @@ -7991,9 +8010,9 @@ def _custom_query_openai_usage(self, base_url: str, api_key: str) -> Dict[str, A subscription_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"订阅信息查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.subscription_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"请求订阅信息失败: {str(e)}") + raise Exception(tr("balance.subscription_request_failed", error=str(e))) total_balance = subscription_data.get("hard_limit_usd", 0.0) access_until = subscription_data.get("access_until", 0) @@ -8015,9 +8034,9 @@ def _custom_query_openai_usage(self, base_url: str, api_key: str) -> Dict[str, A usage_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"使用情况查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.usage_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"请求使用情况失败: {str(e)}") + raise Exception(tr("balance.usage_request_failed", error=str(e))) # total_usage 是以美分为单位,需要除以100转换为美元 total_usage_cents = usage_data.get("total_usage", 0.0) @@ -8204,7 +8223,7 @@ def _on_native_test(self): test_url = base_url.rstrip("/") + provider.test_endpoint # 执行测试 - self.show_warning("测试中", "正在测试连接...") + self.show_warning(tr("common.testing"), tr("balance.testing_connection")) start_time = time.time() try: @@ -8243,8 +8262,8 @@ def _on_native_delete(self): # 确认删除 msg_box = FluentMessageBox( - "确认删除", - f"确定要删除 {provider.name} 的配置吗?\n这将删除认证信息和选项配置。", + tr("native_provider.confirm_delete_title"), + tr("native_provider.confirm_delete_msg", name=provider.name), self, ) if msg_box.exec_(): @@ -8276,10 +8295,10 @@ def _on_native_detect_configured(self): break if detected: - msg = "检测到以下已配置的 Provider:\n\n" + "\n".join(detected) - self.show_success("检测完成", msg) + msg = tr("native_provider.detected_providers", list="\n".join(detected)) + self.show_success(tr("native_provider.detect_complete"), msg) else: - self.show_warning("检测完成", "未检测到已配置的 Provider") + self.show_warning(tr("native_provider.detect_complete"), tr("native_provider.no_provider_detected")) def _is_balance_query_supported( self, provider_id: str, base_url: str @@ -8297,11 +8316,11 @@ def _is_balance_query_supported( if provider_id in unsupported_ids: return ( False, - "当前 Provider 暂不支持通用余额接口查询(仅支持兼容 NewAPI/OpenAI 计费接口的服务)。", + tr("balance.not_supported"), ) if not base_url: - return False, "未配置可用的 baseURL,无法查询余额。" + return False, tr("balance.no_base_url") return True, "" @@ -8360,7 +8379,7 @@ def _on_native_query_balance(self): supported, reason = self._is_balance_query_supported(provider.id, base_url) if not supported: - self.show_warning("余额查询不可用", reason) + self.show_warning(tr("balance.not_available"), reason) return # 显示加载提示 @@ -8451,14 +8470,14 @@ def _setup_ui(self): # API 类型(如果是 NewAPI) api_type = self.usage_data.get("api_type", "openai") if api_type == "newapi": - api_type_label = CaptionLabel(f"API 类型: NewAPI / One-API", self) + api_type_label = CaptionLabel(tr("balance.api_type_newapi"), self) api_type_label.setStyleSheet("color: #888888;") layout.addWidget(api_type_label) # Token 名称 token_name = self.usage_data.get("token_name", "") if token_name: - token_label = CaptionLabel(f"Token 名称: {token_name}", self) + token_label = CaptionLabel(tr("balance.token_name", name=token_name), self) token_label.setStyleSheet("color: #888888;") layout.addWidget(token_label) @@ -8484,7 +8503,7 @@ def _setup_ui(self): key_quota_layout = QHBoxLayout() key_quota_layout.addWidget(BodyLabel(tr("provider.key_quota") + ":", self)) if is_unlimited: - key_quota_value = BodyLabel("🔓 无限", self) + key_quota_value = BodyLabel(f"🔓 {tr('common.unlimited')}", self) key_quota_value.setStyleSheet( "font-weight: bold; color: #107c10; font-size: 15px;" ) @@ -8503,7 +8522,7 @@ def _setup_ui(self): key_balance_layout = QHBoxLayout() key_balance_layout.addWidget(BodyLabel(tr("provider.key_balance") + ":", self)) if is_unlimited: - key_balance_value = BodyLabel("🔓 无限", self) + key_balance_value = BodyLabel(f"🔓 {tr('common.unlimited')}", self) key_balance_value.setStyleSheet( "font-weight: bold; color: #107c10; font-size: 15px;" ) @@ -8734,7 +8753,7 @@ def _on_add(self): # 验证 Provider 是否存在且结构完整 if self.provider_name not in config["provider"]: InfoBar.error( - "错误", + tr("common.error"), f'Provider "{self.provider_name}" 不存在,请先在 Provider 管理页面创建', parent=self, ) @@ -8745,7 +8764,7 @@ def _on_add(self): # 检查 Provider 结构是否完整 if "npm" not in provider or "options" not in provider: InfoBar.error( - "错误", + tr("common.error"), f'Provider "{self.provider_name}" 配置不完整,请先在 Provider 管理页面完善配置', parent=self, ) @@ -8905,7 +8924,7 @@ def _load_data(self): # 两者都有:绿色,显示"已配置" status_text = tr("native_provider.configured") status_color = "#4CAF50" # 绿色 - status_tooltip = "auth.json + 环境变量" + status_tooltip = tr("native_provider.status_auth_and_env") elif has_auth: # 只有auth.json:绿色,显示"已配置" status_text = tr("native_provider.configured") @@ -8915,7 +8934,7 @@ def _load_data(self): # 只有环境变量:蓝色,显示"已配置(环境变量)" status_text = tr("native_provider.configured") + "(ENV)" status_color = "#2196F3" # 蓝色 - status_tooltip = "环境变量已配置,但未保存到auth.json" + status_tooltip = tr("native_provider.status_env_only") else: # 都没有:灰色,显示"未配置" status_text = tr("native_provider.not_configured") @@ -8999,7 +9018,7 @@ def _on_test(self): if not api_key: self.show_error( tr("provider.test_failed"), - tr("provider.api_key_not_found") + "。请先配置Provider或设置环境变量。", + tr("provider.api_key_hint"), ) return @@ -9051,7 +9070,7 @@ def _on_test(self): test_url = base_url.rstrip("/") + provider.test_endpoint # 执行测试 - InfoBar.info("测试中", f"正在测试连接: {test_url}", parent=self) + InfoBar.info(tr("common.testing"), tr("native_provider.testing_url", url=test_url), parent=self) start_time = time.time() try: @@ -9090,8 +9109,8 @@ def _on_delete(self): # 确认删除 msg_box = FluentMessageBox( - "确认删除", - f"确定要删除 {provider.name} 的配置吗?\n这将删除认证信息和选项配置。", + tr("native_provider.confirm_delete_title"), + tr("native_provider.confirm_delete_msg", name=provider.name), self, ) if msg_box.exec_() != QMessageBox.Yes: @@ -9101,7 +9120,7 @@ def _on_delete(self): try: self.auth_manager.delete_provider_auth(provider.id) except Exception as e: - self.show_error("删除失败", f"无法删除认证配置: {e}") + self.show_error(tr("native_provider.delete_failed"), tr("native_provider.cannot_delete_auth", error=str(e))) return # 删除选项 @@ -9114,7 +9133,7 @@ def _on_delete(self): self.main_window.opencode_config = config self.main_window.save_opencode_config() - self.show_success("删除成功", f"{provider.name} 配置已删除") + self.show_success(tr("native_provider.delete_success"), tr("native_provider.config_deleted_msg", name=provider.name)) self._load_data() def _on_detect_configured(self): @@ -9146,24 +9165,24 @@ def _on_detect_configured(self): message = "" if auth_providers: - message += f"📁 auth.json中已配置 ({len(auth_providers)}个):\n" + message += tr("native_provider.auth_json_count", count=len(auth_providers)) + "\n" message += "\n".join([f" ✓ {name}" for name in auth_providers]) message += "\n\n" if env_providers: - message += f"🔧 环境变量已配置 ({len(env_providers)}个):\n" + message += tr("native_provider.env_var_count", count=len(env_providers)) + "\n" message += "\n".join([f" ✓ {name}" for name in env_providers]) - message += "\n\n💡 提示: 环境变量配置的Provider可以直接使用," - message += '但建议点击"配置Provider"保存到auth.json以便管理' + message += tr("native_provider.env_var_hint") + message += tr("native_provider.env_var_hint2") # 显示结果对话框 - w = FluentMessageBox("检测结果", message.strip(), self) + w = FluentMessageBox(tr("native_provider.detect_result"), message.strip(), self) w.exec_() # 刷新列表 self._load_data() else: - InfoBar.info("检测结果", "未检测到已配置的原生Provider", parent=self) + InfoBar.info(tr("native_provider.detect_result"), tr("native_provider.no_native_detected"), parent=self) def _is_balance_query_supported( self, provider_id: str, base_url: str @@ -9181,11 +9200,11 @@ def _is_balance_query_supported( if provider_id in unsupported_ids: return ( False, - "当前 Provider 暂不支持通用余额接口查询(仅支持兼容 NewAPI/OpenAI 计费接口的服务)。", + tr("balance.not_supported"), ) if not base_url: - return False, "未配置可用的 baseURL,无法查询余额。" + return False, tr("balance.no_base_url") return True, "" @@ -9224,7 +9243,7 @@ def _on_query_balance(self): supported, reason = self._is_balance_query_supported(provider.id, base_url) if not supported: - self.show_warning("余额查询不可用", reason) + self.show_warning(tr("balance.not_available"), reason) return # 显示加载提示 @@ -9282,7 +9301,7 @@ def _query_provider_usage(self, base_url: str, api_key: str) -> Dict[str, Any]: except Exception as openai_error: # 两种方式都失败,抛出错误 raise Exception( - f"余额查询失败。NewAPI: {str(newapi_error)[:50]}... OpenAI API: {str(openai_error)[:50]}..." + tr("balance.query_failed_both", newapi=str(newapi_error)[:50], openai=str(openai_error)[:50]) ) def _query_newapi_usage(self, base_url: str, api_key: str) -> Dict[str, Any]: @@ -9300,13 +9319,13 @@ def _query_newapi_usage(self, base_url: str, api_key: str) -> Dict[str, Any]: response_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"NewAPI 查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.newapi_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"NewAPI 请求失败: {str(e)}") + raise Exception(tr("balance.newapi_request_failed", error=str(e))) # 解析 NewAPI 响应 if not response_data.get("code") or "data" not in response_data: - raise Exception("NewAPI 响应格式不正确") + raise Exception(tr("balance.newapi_invalid_response")) usage_data = response_data["data"] @@ -9360,9 +9379,9 @@ def _query_openai_usage(self, base_url: str, api_key: str) -> Dict[str, Any]: subscription_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"订阅信息查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.subscription_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"请求订阅信息失败: {str(e)}") + raise Exception(tr("balance.subscription_request_failed", error=str(e))) total_balance = subscription_data.get("hard_limit_usd", 0.0) access_until = subscription_data.get("access_until", 0) @@ -9384,9 +9403,9 @@ def _query_openai_usage(self, base_url: str, api_key: str) -> Dict[str, Any]: usage_data = json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: error_body = e.read().decode("utf-8") if e.fp else "" - raise Exception(f"使用情况查询失败: {e.code} - {error_body}") + raise Exception(tr("balance.usage_query_failed", code=str(e.code), body=error_body)) except Exception as e: - raise Exception(f"请求使用情况失败: {str(e)}") + raise Exception(tr("balance.usage_request_failed", error=str(e))) # total_usage 是以美分为单位,需要除以100转换为美元 total_usage_cents = usage_data.get("total_usage", 0.0) @@ -9667,14 +9686,14 @@ def _group_models(self) -> Dict[str, List[str]]: def _get_group_key(self, model_id: str, mode: str) -> str: lower = model_id.lower() - if mode == "前缀分组": + if mode == tr("provider.group_prefix"): if "-" in model_id: return model_id.split("-", 1)[0] if "/" in model_id: return model_id.split("/", 1)[0] - return model_id[:1].upper() if model_id else "其他" - if mode == "首字母": - return model_id[:1].upper() if model_id else "其他" + return model_id[:1].upper() if model_id else tr("provider.other_models_short") + if mode == tr("provider.group_letter"): + return model_id[:1].upper() if model_id else tr("provider.other_models_short") # 厂商识别 if "claude" in lower: return tr("provider.claude_series_short") @@ -9685,7 +9704,14 @@ def _get_group_key(self, model_id: str, mode: str) -> str: return tr("provider.other_models_short") def _resolve_category_for_preset(self, model_id: str) -> str: - return self._get_group_key(model_id, "厂商识别") + lower = model_id.lower() + if "claude" in lower: + return "Claude 系列" + if "gemini" in lower: + return "Gemini 系列" + if any(token in lower for token in ("gpt", "openai", "codex", "o1")): + return "OpenAI/Codex 系列" + return "其他模型" def _refresh_preset_combo(self): return @@ -9694,16 +9720,16 @@ def _get_preset_names(self, category: str) -> List[str]: names = list(MODEL_PRESET_PACKS.get(category, {}).keys()) names += list(MODEL_PRESET_CUSTOM.get(category, {}).keys()) if not names: - names.append("基础") + names.append(tr("home.base_preset")) return names def _get_default_preset_for_category(self, category: str) -> Dict[str, Any]: - preset_name = MODEL_PRESET_DEFAULT.get(category, "基础") + preset_name = MODEL_PRESET_DEFAULT.get(category, tr("home.base_preset")) return self._get_preset(category, preset_name) def _get_bulk_category(self) -> str: if not self._visible_model_ids: - return tr("provider.other_models_short") + return "其他模型" return self._resolve_category_for_preset(self._visible_model_ids[0]) def _get_category_bulk_support(self, category: str) -> Dict[str, bool]: @@ -9715,13 +9741,13 @@ def _get_category_bulk_support(self, category: str) -> Dict[str, bool]: "thinking": False, "variants": False, } - if category == tr("provider.claude_series_short"): + if category == "Claude 系列": support["thinking"] = True support["variants"] = True - elif category == tr("provider.openai_series_short"): + elif category == "OpenAI/Codex 系列": support["options"] = True support["variants"] = True - elif category == tr("provider.gemini_series_short"): + elif category == "Gemini 系列": support["thinking"] = True support["variants"] = True return support @@ -9829,7 +9855,7 @@ def _refresh_models(self): match_mode = self.match_mode_combo.currentText() pattern = keyword.lower() regex = None - if pattern and match_mode == "正则": + if pattern and match_mode == tr("match_mode.regex"): try: regex = re.compile(pattern, re.IGNORECASE) except re.error: @@ -9843,13 +9869,13 @@ def _refresh_models(self): ): continue if pattern: - if match_mode == "包含": + if match_mode == tr("match_mode.contains"): if pattern not in model_id.lower(): continue - elif match_mode == "前缀": + elif match_mode == tr("match_mode.prefix"): if not model_id.lower().startswith(pattern): continue - elif match_mode == "正则": + elif match_mode == tr("match_mode.regex"): if not regex or not regex.search(model_id): continue self._build_model_row(model_id) @@ -10078,7 +10104,7 @@ def _on_save(self): else: InfoBar.warning( tr("common.info"), - tr("provider.no_base_url") + ",跳过自动拉取", + tr("provider.no_base_url") + tr("provider.no_base_url_skip"), parent=self, ) @@ -10210,7 +10236,7 @@ def _setup_ui(self): input_widget.setCurrentText(current_value) else: input_widget = LineEdit(option_card) - input_widget.setPlaceholderText(field.default or "可选") + input_widget.setPlaceholderText(field.default or tr("common.optional")) input_widget.setText(str(current_options.get(field.key, ""))) field_layout.addWidget(input_widget, 1) @@ -10259,8 +10285,8 @@ def _on_save(self): if self.provider.id in custom_providers: if custom_providers[self.provider.id].get("npm"): msg_box = FluentMessageBox( - "配置冲突", - f"已存在同名的自定义 Provider '{self.provider.id}'。\n继续保存?", + tr("provider_dialog.config_conflict"), + tr("provider_dialog.conflict_msg", id=self.provider.id), self, ) if msg_box.exec_() != QMessageBox.Yes: @@ -10275,14 +10301,14 @@ def _on_save(self): if value: auth_data[field.key] = value elif field.required: - QMessageBox.warning(self, "验证失败", f"{field.label} 是必填项") + QMessageBox.warning(self, tr("provider_dialog.validation_required"), tr("provider_dialog.field_required", label=field.label)) return # 保存认证 try: self.auth_manager.set_provider_auth(self.provider.id, auth_data) except Exception as e: - QMessageBox.critical(self, "保存失败", f"无法保存认证配置: {e}") + QMessageBox.critical(self, tr("provider_dialog.save_failed"), tr("provider_dialog.cannot_save_auth", error=str(e))) return # 保存选项 @@ -10580,7 +10606,7 @@ def _on_fetch_models(self): if provider_name in UNSUPPORTED_FETCH_PROVIDERS: self.show_warning( tr("provider.fetch_failed"), - f"{native_provider.name} 不支持通过API获取模型列表。\n请手动添加模型或参考官方文档。", + tr("model_fetch.unsupported_fetch", name=native_provider.name), ) return @@ -10614,14 +10640,14 @@ def _on_fetch_models(self): if not base_url: self.show_error( tr("provider.fetch_failed"), - "无法确定API地址。请先配置Provider的baseURL。", + tr("model_fetch.cannot_determine_url"), ) return # 构建模型列表API URL models_url = base_url.rstrip("/") + "/models" - InfoBar.info("获取中", f"正在从 {models_url} 获取模型列表...", parent=self) + InfoBar.info(tr("common.loading"), tr("model_fetch.fetching", url=models_url), parent=self) try: req = urllib.request.Request(models_url) @@ -10645,7 +10671,7 @@ def _on_fetch_models(self): models = data if not models: - self.show_warning("获取完成", "API返回的模型列表为空") + self.show_warning(tr("common.success"), tr("model_fetch.empty_list")) return # 显示模型选择对话框 @@ -10654,16 +10680,16 @@ def _on_fetch_models(self): ) if dialog.exec_(): self._load_models(provider_name) - self.show_success("添加成功", f"已添加 {dialog.added_count} 个模型") + self.show_success(tr("common.success"), tr("model_fetch.models_added", count=dialog.added_count)) except urllib.error.HTTPError as e: if e.code in (401, 403): # 认证失败 if not api_key: - error_msg = f"HTTP {e.code}: {e.reason}\n\n该API需要认证。请先配置Provider的API Key。" + error_msg = tr("model_fetch.auth_required", code=e.code, reason=e.reason) else: error_msg = ( - f"HTTP {e.code}: {e.reason}\n\nAPI Key可能无效或已过期。" + tr("model_fetch.key_invalid", code=e.code, reason=e.reason) ) else: error_msg = f"HTTP {e.code}: {e.reason}" @@ -10788,7 +10814,7 @@ def _setup_ui(self): id_layout = QHBoxLayout() id_layout.addWidget(BodyLabel(tr("model.model_id") + ":", self)) self.id_edit = LineEdit(self) - self.id_edit.setPlaceholderText("如: claude-sonnet-4-5-20250929") + self.id_edit.setPlaceholderText(tr("model_dialog.placeholder_model_id")) self.id_edit.setToolTip(get_tooltip("model_id")) if self.is_edit: self.id_edit.setEnabled(False) @@ -11174,7 +11200,7 @@ def _add_full_thinking_config(self): options["thinking"] = {"type": "enabled", "budgetTokens": 16000} self._refresh_options_table() InfoBar.success( - "成功", "已添加 Claude Thinking 配置", parent=self, duration=2000 + tr("common.success"), tr("model_dialog.thinking_added"), parent=self, duration=2000 ) def _add_gemini_thinking_config(self, budget): @@ -11259,7 +11285,7 @@ def _add_variant(self): try: config = json.loads(self.variant_config_edit.toPlainText().strip() or "{}") except json.JSONDecodeError as e: - InfoBar.error("错误", f"JSON 格式错误: {e}", parent=self) + InfoBar.error(tr("common.error"), tr("model_dialog.json_error", error=str(e)), parent=self) return self.current_model_data.setdefault("variants", {})[name] = config self._refresh_variants_table() @@ -11339,7 +11365,7 @@ def _on_save(self): """保存模型""" model_id = self.id_edit.text().strip() if not model_id: - InfoBar.error("错误", "请输入模型 ID", parent=self) + InfoBar.error(tr("common.error"), tr("model_dialog.enter_model_id"), parent=self) return config = self.main_window.opencode_config @@ -11353,7 +11379,7 @@ def _on_save(self): # 验证 Provider 是否存在且结构完整 if self.provider_name not in config["provider"]: InfoBar.error( - "错误", + tr("common.error"), f'Provider "{self.provider_name}" 不存在,请先在 Provider 管理页面创建', parent=self, ) @@ -11364,7 +11390,7 @@ def _on_save(self): # 检查 Provider 结构是否完整 if "npm" not in provider or "options" not in provider: InfoBar.error( - "错误", + tr("common.error"), f'Provider "{self.provider_name}" 配置不完整,请先在 Provider 管理页面完善配置', parent=self, ) @@ -11378,7 +11404,7 @@ def _on_save(self): # 检查名称冲突 if not self.is_edit and model_id in models: - InfoBar.error("错误", f'模型 "{model_id}" 已存在', parent=self) + InfoBar.error(tr("common.error"), tr("model_dialog.model_exists", id=model_id), parent=self) return # 保存数据 @@ -11428,8 +11454,8 @@ def _on_save(self): if errors: msg = "\n".join(f"• {e['message']}" for e in errors[:8]) if len(errors) > 8: - msg += f"\n... 还有 {len(errors) - 8} 个错误" - InfoBar.error("错误", f"配置校验失败:\n{msg}", parent=self) + msg += tr("model_dialog.validation_more_errors", count=len(errors) - 8) + InfoBar.error(tr("common.error"), tr("model_dialog.validation_failed", msg=msg), parent=self) return options = self.current_model_data.get("options", {}) @@ -11465,17 +11491,17 @@ def _setup_ui(self): # 标题 title = SubtitleLabel( - f"从 {self.provider_name} 获取到 {len(self.models)} 个模型", self + tr("model_fetch.fetched_count", count=len(self.models), provider=self.provider_name), self ) layout.addWidget(title) # 全选/取消全选 select_layout = QHBoxLayout() - self.select_all_btn = PushButton("全选", self) + self.select_all_btn = PushButton(tr("common.select_all"), self) self.select_all_btn.clicked.connect(self._select_all) select_layout.addWidget(self.select_all_btn) - self.deselect_all_btn = PushButton("取消全选", self) + self.deselect_all_btn = PushButton(tr("common.deselect_all"), self) self.deselect_all_btn.clicked.connect(self._deselect_all) select_layout.addWidget(self.deselect_all_btn) @@ -11485,7 +11511,7 @@ def _setup_ui(self): # 模型列表表格 self.table = TableWidget(self) self.table.setColumnCount(3) - self.table.setHorizontalHeaderLabels(["选择", "模型ID", "创建时间"]) + self.table.setHorizontalHeaderLabels([tr("model_dialog.select_column_check"), tr("model_dialog.select_column_id"), tr("model_dialog.select_column_time")]) header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Fixed) @@ -11570,7 +11596,7 @@ def _on_add(self): selected_models.append(model_id) if not selected_models: - InfoBar.warning("提示", "请至少选择一个模型", parent=self) + InfoBar.warning(tr("common.info"), tr("model_dialog.select_at_least_one"), parent=self) return # 添加到配置 @@ -11699,8 +11725,8 @@ def _on_add(self): # 验证 Provider 是否存在且结构完整 if self.provider_name not in config["provider"]: InfoBar.error( - "错误", - f'Provider "{self.provider_name}" 不存在,请先在 Provider 管理页面创建', + tr("common.error"), + tr("model_dialog.provider_not_found", name=self.provider_name), parent=self, ) return @@ -11710,8 +11736,8 @@ def _on_add(self): # 检查 Provider 结构是否完整 if "npm" not in provider or "options" not in provider: InfoBar.error( - "错误", - f'Provider "{self.provider_name}" 配置不完整,请先在 Provider 管理页面完善配置', + tr("common.error"), + tr("model_dialog.provider_incomplete", name=self.provider_name), parent=self, ) return @@ -12181,7 +12207,7 @@ class MCPDialog(BaseDialog): "type": "local", "command": ["uvx", "mcp-server-fetch"], "environment": {}, - "description": "抓取网页内容与资源的基础 MCP 服务器", + "description": tr("mcp.preset_fetch"), "tags": ["stdio", "http", "web"], "homepage": "https://github.com/modelcontextprotocol/servers", "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/fetch", @@ -12191,7 +12217,7 @@ class MCPDialog(BaseDialog): "type": "local", "command": ["npx", "-y", "@modelcontextprotocol/server-time"], "environment": {}, - "description": "提供时间相关工具的轻量 MCP 服务器", + "description": tr("mcp.preset_time"), "tags": ["stdio", "time", "utility"], "homepage": "https://github.com/modelcontextprotocol/servers", "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/time", @@ -12201,7 +12227,7 @@ class MCPDialog(BaseDialog): "type": "local", "command": ["npx", "-y", "@modelcontextprotocol/server-memory"], "environment": {}, - "description": "提供记忆图谱能力的 MCP 服务器", + "description": tr("mcp.preset_memory"), "tags": ["stdio", "memory", "graph"], "homepage": "https://github.com/modelcontextprotocol/servers", "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/memory", @@ -12215,7 +12241,7 @@ class MCPDialog(BaseDialog): "@modelcontextprotocol/server-sequential-thinking", ], "environment": {}, - "description": "结构化推理与分步思考的 MCP 服务器", + "description": tr("mcp.preset_thinking"), "tags": ["stdio", "thinking", "reasoning"], "homepage": "https://github.com/modelcontextprotocol/servers", "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking", @@ -12225,7 +12251,7 @@ class MCPDialog(BaseDialog): "type": "local", "command": ["npx", "-y", "@upstash/context7-mcp"], "environment": {}, - "description": "提供最新文档检索的 Context7 MCP", + "description": tr("mcp.preset_context7"), "tags": ["stdio", "docs", "search"], "homepage": "https://context7.com", "docs": "https://github.com/upstash/context7/blob/master/README.md", @@ -12235,7 +12261,7 @@ class MCPDialog(BaseDialog): "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"], "environment": {}, - "description": "连接 Chrome DevTools 的调试 MCP 服务器", + "description": tr("mcp.preset_chrome"), "tags": ["stdio", "browser", "devtools"], "homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp", "docs": "https://github.com/ChromeDevTools/chrome-devtools-mcp", @@ -12245,7 +12271,7 @@ class MCPDialog(BaseDialog): "type": "local", "command": ["npx", "-y", "open-web-mcp"], "environment": {}, - "description": "开放网页搜索与打开页面的 MCP 服务器", + "description": tr("mcp.preset_websearch"), "tags": ["stdio", "web", "search"], "homepage": "https://github.com/modelcontextprotocol/servers", "docs": "https://github.com/modelcontextprotocol/servers", @@ -12263,7 +12289,7 @@ class MCPDialog(BaseDialog): "ide-assistant", ], "environment": {}, - "description": "提供本地项目理解与指令执行的 Serena MCP", + "description": tr("mcp.preset_serena"), "tags": ["stdio", "local", "automation"], "homepage": "https://github.com/oraios/serena", "docs": "https://oraios.github.io/serena/", @@ -13605,7 +13631,7 @@ def _init_agent_tables(self): # 如果配置中没有描述,使用预设描述 if not description: - description = PRESET_AGENTS.get(agent_id, "") + description = _tr_preset('omo_agents', agent_id, PRESET_AGENTS.get(agent_id, '')) desc_item = QTableWidgetItem(description) self.omo_table.setItem(i, 2, desc_item) @@ -13851,7 +13877,7 @@ def _load_data(self): ) desc = data.get("description", "") if not desc: - desc = PRESET_OPENCODE_AGENTS.get(name, {}).get("description", "") + desc = _tr_preset('opencode_agents', name, PRESET_OPENCODE_AGENTS.get(name, {}).get('description', '')) desc_item = QTableWidgetItem(desc[:50] + "..." if len(desc) > 50 else desc) desc_item.setToolTip(desc) self.table.setItem(row, 3, desc_item) @@ -13949,7 +13975,7 @@ def _on_group_applied(self, group_id: str): ), ) except Exception as e: - self.show_error(tr("common.error"), f"应用分组失败: {str(e)}") + self.show_error(tr("common.error"), tr("agent_group.apply_failed", error=str(e))) class OpenCodeAgentDialog(BaseDialog): @@ -14237,7 +14263,7 @@ def _on_save(self): desc = self.desc_edit.text().strip() if not desc: - InfoBar.error(tr("common.error"), "请输入 Agent 描述", parent=self) + InfoBar.error(tr("common.error"), tr("agent.enter_description"), parent=self) return config = self.main_window.opencode_config @@ -14304,7 +14330,7 @@ def _on_save(self): agent_data["permission"] = permission except json.JSONDecodeError as e: InfoBar.error( - tr("common.error"), f"权限配置 JSON 格式错误: {e}", parent=self + tr("common.error"), tr("agent.permission_json_error", error=str(e)), parent=self ) return @@ -14338,7 +14364,8 @@ def _setup_ui(self): self.agent_list = ListWidget(self) self.agent_list.setSelectionMode(QAbstractItemView.MultiSelection) for name, data in PRESET_OPENCODE_AGENTS.items(): - self.agent_list.addItem(f"{name} - {data.get('description', '')[:40]}") + translated_desc = _tr_preset('opencode_agents', name, data.get('description', '')) + self.agent_list.addItem(f"{name} - {translated_desc[:40]}") layout.addWidget(self.agent_list) # 按钮 @@ -14378,7 +14405,7 @@ def _on_add(self): preset = PRESET_OPENCODE_AGENTS[name] config["agent"][name] = { "mode": preset.get("mode", "subagent"), - "description": preset.get("description", ""), + "description": _tr_preset('opencode_agents', name, preset.get('description', '')), } if "tools" in preset: config["agent"][name]["tools"] = preset["tools"] @@ -15256,8 +15283,8 @@ def save_opencode_config(self): if jsonc_warning and not getattr(self, "_opencode_jsonc_warned", False): self._opencode_jsonc_warned = True InfoBar.warning( - title="JSONC 注释已丢失", - content="原配置文件包含注释,保存后注释已丢失。已自动备份原文件。", + title=tr("config_reload.jsonc_comments_lost_title"), + content=tr("config_reload.jsonc_comments_lost_msg"), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15279,8 +15306,8 @@ def save_ohmyopencode_config(self): if jsonc_warning and not getattr(self, "_ohmyopencode_jsonc_warned", False): self._ohmyopencode_jsonc_warned = True InfoBar.warning( - title="JSONC 注释已丢失", - content="原配置文件包含注释,保存后注释已丢失。已自动备份原文件。", + title=tr("config_reload.jsonc_comments_lost_title"), + content=tr("config_reload.jsonc_comments_lost_msg"), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15413,12 +15440,7 @@ def _check_external_file_changes(self): def _handle_external_change(self, config_name: str, path: Path): """处理外部修改提示""" - msg = ( - f"检测到 {config_name} 配置文件已被外部修改。\n\n" - "请选择如何处理:\n" - "• 点击【确定】重新加载文件内容(可能覆盖当前界面数据)\n" - "• 点击【取消】保留当前界面数据(文件保持外部修改)" - ) + msg = tr("config_reload.external_change_msg", name=config_name) dialog = FluentMessageBox(tr("dialog.config_file_changed"), msg, self) if dialog.exec_(): # 重新加载并刷新哈希 @@ -15429,7 +15451,7 @@ def _handle_external_change(self, config_name: str, path: Path): if errors: msg = "\n".join(f"• {e['message']}" for e in errors[:8]) if len(errors) > 8: - msg += f"\n... 还有 {len(errors) - 8} 个错误" + msg += tr("config_reload.more_errors_summary", count=len(errors) - 8) InfoBar.error( tr("common.error"), tr("dialog.reload_failed", msg=msg), @@ -15444,8 +15466,8 @@ def _handle_external_change(self, config_name: str, path: Path): if hasattr(self, "home_page"): self.home_page._load_stats() InfoBar.success( - title="已重新加载", - content=f"已加载 {config_name} 最新配置", + title=tr("config_reload.reloaded_title"), + content=tr("config_reload.reloaded_msg", name=config_name), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15463,8 +15485,8 @@ def _handle_external_change(self, config_name: str, path: Path): path, self.ohmyopencode_config, tag="external-keep" ) InfoBar.warning( - title="保持当前数据", - content=f"未重新加载 {config_name},当前界面数据保持不变", + title=tr("config_reload.keep_current_title"), + content=tr("config_reload.keep_current_msg", name=config_name), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15528,21 +15550,18 @@ def _show_conflict_dialog(self, conflicts: list): json_info = ConfigPaths.get_config_file_info(json_path) jsonc_info = ConfigPaths.get_config_file_info(jsonc_path) - msg = f"""检测到 {config_name} 同时存在两个配置文件: - -📄 {json_path.name} - 大小: {json_info.get("size_str", "未知")} - 修改时间: {json_info.get("mtime_str", "未知")} - -📄 {jsonc_path.name} - 大小: {jsonc_info.get("size_str", "未知")} - 修改时间: {jsonc_info.get("mtime_str", "未知")} - -⚠️ 当前程序会优先加载 .jsonc 文件。 - -请选择要使用的配置文件: -• 点击「确定」使用 .json 文件(删除 .jsonc) -• 点击「取消」使用 .jsonc 文件(保持现状)""" + unknown = tr("common.unknown") if "common.unknown" != tr("common.unknown") else "N/A" + msg = ( + tr("config_reload.dual_config_detected", name=config_name) + + f"\n\n📄 {json_path.name}" + + f"\n {tr('config_reload.size_label', size=json_info.get('size_str', unknown))}" + + f"\n {tr('config_reload.mtime_label', mtime=json_info.get('mtime_str', unknown))}" + + f"\n\n📄 {jsonc_path.name}" + + f"\n {tr('config_reload.size_label', size=jsonc_info.get('size_str', unknown))}" + + f"\n {tr('config_reload.mtime_label', mtime=jsonc_info.get('mtime_str', unknown))}" + + tr("config_reload.dual_config_warning") + + tr("config_reload.dual_config_prompt") + ) dialog = FluentMessageBox( tr("dialog.config_file_conflict", config_name=config_name), msg, self @@ -15556,8 +15575,8 @@ def _show_conflict_dialog(self, conflicts: list): # 删除 .jsonc jsonc_path.unlink() InfoBar.success( - title="已切换配置", - content=f"已删除 {jsonc_path.name},将使用 {json_path.name}", + title=tr("config_reload.switched_title"), + content=tr("config_reload.switched_msg", jsonc=jsonc_path.name, json=json_path.name), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15566,8 +15585,8 @@ def _show_conflict_dialog(self, conflicts: list): ) except Exception as e: InfoBar.error( - title="删除失败", - content=f"无法删除 {jsonc_path.name}: {e}", + title=tr("config_reload.delete_failed_title"), + content=tr("config_reload.delete_failed_msg", file=jsonc_path.name, error=str(e)), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15577,8 +15596,8 @@ def _show_conflict_dialog(self, conflicts: list): else: # 用户选择保持现状(使用 .jsonc) InfoBar.info( - title="保持现状", - content=f"将继续使用 {jsonc_path.name}", + title=tr("config_reload.keep_status_title"), + content=tr("config_reload.keep_status_msg", file=jsonc_path.name), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15660,14 +15679,13 @@ def _fix_config(self): self.save_opencode_config() # 显示修复结果 - fix_msg = f"已完成 {len(fixes)} 项修复:\n" + "\n".join( - f"• {f}" for f in fixes[:10] - ) + fix_list = "\n".join(f"\u2022 {f}" for f in fixes[:10]) + fix_msg = tr("config_reload.fix_msg", count=len(fixes), fixes=fix_list) if len(fixes) > 10: - fix_msg += f"\n... 还有 {len(fixes) - 10} 项" + fix_msg += tr("config_reload.fix_more", count=len(fixes) - 10) InfoBar.success( - title="配置已修复", + title=tr("config_reload.fix_complete"), content=fix_msg, orient=Qt.Orientation.Horizontal, isClosable=True, @@ -15681,8 +15699,8 @@ def _fix_config(self): self.home_page._load_stats() else: InfoBar.info( - title="无需修复", - content="配置结构已经正确", + title=tr("config_reload.no_fix_needed_title"), + content=tr("config_reload.no_fix_needed_msg"), orient=Qt.Orientation.Horizontal, isClosable=True, position=InfoBarPosition.TOP_RIGHT, @@ -15817,7 +15835,7 @@ def _load_data(self): # 描述列添加 tooltip 显示全部 desc = data.get("description", "") if not desc: - desc = PRESET_AGENTS.get(name, "") + desc = _tr_preset('omo_agents', name, PRESET_AGENTS.get(name, '')) desc_item = QTableWidgetItem(desc[:50] + "..." if len(desc) > 50 else desc) desc_item.setToolTip(desc) self.table.setItem(row, 2, desc_item) @@ -15970,7 +15988,7 @@ def _on_group_applied(self, group_id: str): ), ) except Exception as e: - self.show_error(tr("common.error"), f"应用分组失败: {str(e)}") + self.show_error(tr("common.error"), tr("agent_group.apply_failed", error=str(e))) class OhMyAgentDialog(BaseDialog): @@ -16110,7 +16128,8 @@ def _setup_ui(self): # 预设列表 self.list_widget = ListWidget(self) for name, desc in PRESET_AGENTS.items(): - self.list_widget.addItem(f"{name} - {desc}") + translated_desc = _tr_preset('omo_agents', name, desc) + self.list_widget.addItem(f"{name} - {translated_desc}") layout.addWidget(self.list_widget) # 绑定模型 @@ -16154,7 +16173,7 @@ def _on_add(self): # 解析选中的预设 text = current.text() name = text.split(" - ")[0] - desc = PRESET_AGENTS.get(name, "") + desc = _tr_preset('omo_agents', name, PRESET_AGENTS.get(name, '')) config = self.main_window.ohmyopencode_config if config is None: @@ -16290,7 +16309,7 @@ def _load_data(self): # 描述列添加 tooltip 显示全部 desc = data.get("description", "") if not desc: - desc = PRESET_CATEGORIES.get(name, {}).get("description", "") + desc = _tr_preset('categories', name, PRESET_CATEGORIES.get(name, {}).get('description', '')) desc_item = QTableWidgetItem(desc[:30] + "..." if len(desc) > 30 else desc) desc_item.setToolTip(desc) self.table.setItem(row, 3, desc_item) @@ -16403,7 +16422,7 @@ def __init__(self, main_window, category_name: str = None, parent=None): self.is_edit = category_name is not None self.setWindowTitle( - "编辑 Category" if self.is_edit else tr("category.add_category") + tr("category.edit_title") if self.is_edit else tr("category.add_category") ) self.setMinimumWidth(450) self._setup_ui() @@ -16514,7 +16533,7 @@ def _on_save(self): config["categories"] = {} if not self.is_edit and name in config["categories"]: - InfoBar.error("错误", f'Category "{name}" 已存在', parent=self) + InfoBar.error(tr("common.error"), tr("category.already_exists", name=name), parent=self) return config["categories"][name] = { @@ -16548,7 +16567,7 @@ def _setup_ui(self): self.list_widget = ListWidget(self) for name, data in PRESET_CATEGORIES.items(): temp = data.get("temperature", 0.7) - desc = data.get("description", "") + desc = _tr_preset('categories', name, data.get('description', '')) self.list_widget.addItem(f"{name} (temp={temp}) - {desc}") layout.addWidget(self.list_widget) @@ -16667,11 +16686,11 @@ def validate_skill_name(name: str) -> Tuple[bool, str]: (是否有效, 错误信息) """ if not name: - return False, "名称不能为空" + return False, tr("skill_ui.name_empty") if len(name) > 64: - return False, "名称不能超过 64 字符" + return False, tr("skill_ui.name_too_long") if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", name): - return False, "名称格式错误:只能使用小写字母、数字、单连字符分隔" + return False, tr("skill_ui.name_format_error") return True, "" @staticmethod @@ -16681,9 +16700,9 @@ def validate_description(desc: str) -> Tuple[bool, str]: 规则:1-1024 字符 """ if not desc: - return False, "描述不能为空" + return False, tr("skill_ui.desc_empty") if len(desc) > 1024: - return False, "描述不能超过 1024 字符" + return False, tr("skill_ui.desc_too_long") return True, "" @staticmethod @@ -16813,11 +16832,11 @@ def discover_all(cls) -> List[DiscoveredSkill]: seen_names.add(skill.name) except Exception as e: # 解析单个skill失败,记录但继续处理其他skills - print(f"解析 skill 失败 {skill_dir.name}: {e}") + print(tr("skill_ui.parse_failed", name=skill_dir.name, error=str(e))) continue except Exception as e: # 遍历目录失败,记录但继续处理其他路径 - print(f"遍历目录失败 {base_path}: {e}") + print(tr("skill_ui.scan_dir_failed", path=str(base_path), error=str(e))) continue return skills @@ -17045,7 +17064,7 @@ def __init__(self, parent=None): skillsmp_label = HyperlinkLabel(self.widget) skillsmp_label.setUrl("https://skillsmp.com/") skillsmp_label.setText("🌐 SkillsMP.com") - skillsmp_label.setToolTip("访问 SkillsMP.com 浏览更多社区技能") + skillsmp_label.setToolTip(tr("skill_ui.visit_skillsmp")) browse_more_layout.addWidget(skillsmp_label) browse_more_layout.addSpacing(20) @@ -17054,7 +17073,7 @@ def __init__(self, parent=None): composio_label = HyperlinkLabel(self.widget) composio_label.setUrl("https://github.com/ComposioHQ/awesome-claude-skills") composio_label.setText("🌐 ComposioHQ Skills") - composio_label.setToolTip("访问 ComposioHQ 浏览更多社区技能") + composio_label.setToolTip(tr("skill_ui.visit_composio")) browse_more_layout.addWidget(composio_label) browse_more_layout.addStretch() @@ -17230,7 +17249,7 @@ def scan_skill(cls, skill_path: Path) -> Dict[str, Any]: "line": 0, "code": "", "level": "critical", - "description": f"扫描失败: {str(e)}", + "description": tr("skill_ui.scan_failed_desc", error=str(e)), } ], "level": "unknown", @@ -17433,7 +17452,7 @@ def parse_source(source: str) -> Tuple[str, Dict[str, str]]: if os.path.exists(source): return "local", {"path": source} - raise ValueError(f"无法识别的来源格式: {source}") + raise ValueError(tr("skill_ui.unrecognized_source", source=source)) @staticmethod def install_from_github( @@ -17465,7 +17484,7 @@ def install_from_github( try: # 1. 下载 ZIP if progress_callback: - progress_callback("正在下载...") + progress_callback(tr("skill_ui.downloading")) zip_url = ( f"https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip" @@ -17475,11 +17494,11 @@ def install_from_github( # 如果404,尝试检测并使用正确的分支 if response.status_code == 404: if progress_callback: - progress_callback("检测分支...") + progress_callback(tr("skill_ui.detecting_branch")) detected_branch = SkillInstaller.detect_default_branch(owner, repo) if detected_branch != branch: if progress_callback: - progress_callback(f"使用分支: {detected_branch}") + progress_callback(tr("skill_ui.using_branch", branch=detected_branch)) branch = detected_branch zip_url = f"https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip" response = requests.get(zip_url, stream=True, timeout=30) @@ -17488,7 +17507,7 @@ def install_from_github( # 2. 解压到临时目录 if progress_callback: - progress_callback("正在解压...") + progress_callback(tr("skill_ui.extracting")) with tempfile.TemporaryDirectory() as temp_dir: zip_path = Path(temp_dir) / "skill.zip" @@ -17506,7 +17525,7 @@ def install_from_github( if subdir: skill_dir = extracted_dir / subdir if not skill_dir.exists(): - return False, f"子目录不存在: {subdir}" + return False, tr("skill_ui.subdir_not_found", subdir=subdir) else: skill_dir = extracted_dir @@ -17521,17 +17540,17 @@ def install_from_github( if not skill_file: return ( False, - f"未找到 SKILL.md 或 SKILL.txt 文件{f' (在 {subdir} 中)' if subdir else ''}", + tr("skill_ui.skill_file_not_found_in", subdir=subdir) if subdir else tr("skill_ui.skill_file_not_found"), ) # 4. 解析 Skill 名称 skill = SkillDiscovery.parse_skill_file(skill_file) if not skill: - return False, "SKILL 文件格式错误" + return False, tr("skill_ui.skill_format_error") # 5. 复制到目标目录 if progress_callback: - progress_callback("正在安装...") + progress_callback(tr("skill_ui.installing")) skill_target = target_dir / skill.name if skill_target.exists(): @@ -17571,14 +17590,14 @@ def install_from_github( json.dump(meta, f, indent=2, ensure_ascii=False) if progress_callback: - progress_callback("安装完成!") + progress_callback(tr("skill_ui.install_complete")) - return True, f"Skill '{skill.name}' 安装成功" + return True, tr("skill_ui.install_success", name=skill.name) except requests.exceptions.RequestException as e: - return False, f"网络错误: {str(e)}" + return False, tr("skill_ui.network_error", error=str(e)) except Exception as e: - return False, f"安装失败: {str(e)}" + return False, tr("skill_ui.install_failed", error=str(e)) @staticmethod def install_from_local( @@ -17599,21 +17618,21 @@ def install_from_local( try: source = Path(source_path) if not source.exists(): - return False, f"路径不存在: {source_path}" + return False, tr("skill_ui.path_not_found", path=str(source_path)) # 查找 SKILL.md skill_md = source / "SKILL.md" if not skill_md.exists(): - return False, "未找到 SKILL.md 文件" + return False, tr("skill_ui.skill_md_not_found") # 解析 Skill skill = SkillDiscovery.parse_skill_file(skill_md) if not skill: - return False, "SKILL.md 格式错误" + return False, tr("skill_ui.skill_md_format_error") # 复制到目标目录 if progress_callback: - progress_callback("正在复制...") + progress_callback(tr("skill_ui.copying")) skill_target = target_dir / skill.name if skill_target.exists(): @@ -17633,12 +17652,12 @@ def install_from_local( json.dump(meta, f, indent=2, ensure_ascii=False) if progress_callback: - progress_callback("安装完成!") + progress_callback(tr("skill_ui.install_complete")) - return True, f"Skill '{skill.name}' 安装成功" + return True, tr("skill_ui.install_success", name=skill.name) except Exception as e: - return False, f"安装失败: {str(e)}" + return False, tr("skill_ui.install_failed", error=str(e)) # ==================== Skill 更新器 ==================== @@ -17671,7 +17690,7 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "current_commit": None, "latest_commit": None, "meta": None, - "status": "本地", + "status": tr("common.local"), } ) continue @@ -17689,7 +17708,7 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "current_commit": None, "latest_commit": None, "meta": meta, - "status": "本地", + "status": tr("common.local"), } ) continue @@ -17716,15 +17735,15 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "has_update": has_update, "current_commit": current_commit[:7] if current_commit - else "未知", + else tr("common.unknown"), "latest_commit": latest_commit[:7], "meta": meta, - "status": "有更新" if has_update else "最新", + "status": tr("common.has_update") if has_update else tr("common.latest"), } ) except Exception as e: - print(f"检查更新失败 {skill.name}: {e}") + print(tr("skill_ui.check_update_failed", name=skill.name, error=str(e))) updates.append( { "skill": skill, @@ -17732,7 +17751,7 @@ def check_updates(skills: List[DiscoveredSkill]) -> List[Dict[str, Any]]: "current_commit": None, "latest_commit": None, "meta": meta if "meta" in locals() else None, - "status": "检查失败", + "status": tr("common.check_failed"), } ) @@ -17753,7 +17772,7 @@ def update_skill( (是否成功, 消息) """ if meta.get("source") != "github": - return False, "仅支持更新从 GitHub 安装的 Skills" + return False, tr("skill_ui.github_only_update") try: # 重新安装 @@ -17774,7 +17793,7 @@ def update_skill( return success, message except Exception as e: - return False, f"更新失败: {str(e)}" + return False, tr("skill_ui.update_failed", error=str(e)) # ==================== Skill 安装对话框 ==================== @@ -18240,7 +18259,7 @@ def _on_skill_selected(self, item): else "" ) self.detail_compat.setText( - f"兼容: {skill.compatibility}" if skill.compatibility else "" + tr("skill_ui.compatibility", value=skill.compatibility) if skill.compatibility else "" ) self.detail_path.setText(f"{tr('skill.skill_path')}: {skill.path}") self.detail_content.setText(skill.content) @@ -18270,17 +18289,17 @@ def _on_edit_skill(self): path_str = str(self._current_skill.path) if ".claude" in path_str: if str(Path.home()) in path_str: - self.create_loc_combo.setCurrentText("Claude 全局 (~/.claude/skills/)") + self.create_loc_combo.setCurrentText(tr("skill_ui.claude_global_loc")) else: - self.create_loc_combo.setCurrentText("Claude 项目 (.claude/skills/)") + self.create_loc_combo.setCurrentText(tr("skill_ui.claude_project_loc")) else: if str(Path.home()) in path_str: self.create_loc_combo.setCurrentText( - "OpenCode 全局 (~/.config/opencode/skills/)" + tr("skill_ui.opencode_global_loc") ) else: self.create_loc_combo.setCurrentText( - "OpenCode 项目 (.opencode/skills/)" + tr("skill_ui.opencode_project_loc") ) def _on_delete_skill(self): @@ -18307,7 +18326,7 @@ def _on_delete_skill(self): self._refresh_skill_list() self._clear_detail() except Exception as e: - self.show_error("错误", f"删除失败: {e}") + self.show_error(tr("common.error"), tr("skill_ui.delete_failed", error=str(e))) def _on_open_skill_folder(self): """打开 Skill 所在目录""" @@ -18451,21 +18470,21 @@ def _on_save_skill(self): # 验证 valid, msg = SkillDiscovery.validate_skill_name(name) if not valid: - self.show_error("名称错误", msg) + self.show_error(tr("skill_ui.name_error"), msg) return valid, msg = SkillDiscovery.validate_description(desc) if not valid: - self.show_error("描述错误", msg) + self.show_error(tr("skill_ui.desc_error"), msg) return # 确定保存路径 loc_text = self.create_loc_combo.currentText() - if "OpenCode 全局" in loc_text: + if tr("skill_ui.opencode_global_loc") in loc_text: base_path = Path.home() / ".config" / "opencode" / "skills" - elif "OpenCode 项目" in loc_text: + elif tr("skill_ui.opencode_project_loc") in loc_text: base_path = Path.cwd() / ".opencode" / "skills" - elif "Claude 全局" in loc_text: + elif tr("skill_ui.claude_global_loc") in loc_text: base_path = Path.home() / ".claude" / "skills" else: base_path = Path.cwd() / ".claude" / "skills" @@ -18487,7 +18506,7 @@ def _on_save_skill(self): # 默认内容 if not content: - content = "## What I do\n\n- 描述功能\n\n## Instructions\n\n- 具体指令" + content = tr("skill_ui.default_content") skill_content = f"---\n{frontmatter}\n---\n\n{content}\n" @@ -18503,7 +18522,7 @@ def _on_save_skill(self): self._refresh_skill_list() self._on_clear_create_form() except Exception as e: - self.show_error("错误", f"保存失败: {e}") + self.show_error(tr("common.error"), tr("skill_ui.save_failed", error=str(e))) def _on_clear_create_form(self): """清空创建表单""" @@ -18910,7 +18929,7 @@ def _on_open_market(self): branch = SkillInstaller.detect_default_branch(owner, repo_name) # 更新进度提示 - install_dialog.update_progress(f"正在安装 {skill['name']}...") + install_dialog.update_progress(tr("skill_ui.installing_skill", name=skill["name"])) success, message = SkillInstaller.install_from_github( owner, @@ -18922,13 +18941,13 @@ def _on_open_market(self): ) if success: - self.show_success("成功", message) + self.show_success(tr("common.success"), message) self._refresh_skill_list() else: - self.show_error("失败", message) + self.show_error(tr("common.failed"), message) except Exception as e: - self.show_error("错误", f"安装失败: {str(e)}") + self.show_error(tr("common.error"), tr("skill_ui.install_failed", error=str(e))) def _on_scan_skill(self): """扫描选中的 Skill""" @@ -18944,7 +18963,7 @@ def _on_scan_skill(self): dialog.exec_() except Exception as e: - self.show_error("错误", f"扫描失败: {str(e)}") + self.show_error(tr("common.error"), tr("skill_ui.scan_failed", error=str(e))) def _on_install_skill(self): """安装 Skill""" @@ -18983,15 +19002,15 @@ def _on_install_skill(self): ) if success: - self.show_success("成功", message) + self.show_success(tr("common.success"), message) self._refresh_skill_list() else: - self.show_error("失败", message) + self.show_error(tr("common.failed"), message) except ValueError as e: - self.show_error("错误", str(e)) + self.show_error(tr("common.error"), str(e)) except Exception as e: - self.show_error("错误", f"安装失败: {str(e)}") + self.show_error(tr("common.error"), tr("skill_ui.install_failed", error=str(e))) def _on_check_updates(self): """检查更新""" @@ -18999,11 +19018,11 @@ def _on_check_updates(self): skills = SkillDiscovery.discover_all() if not skills: - self.show_warning("提示", "未发现任何 Skills") + self.show_warning(tr("common.info"), tr("skill_ui.no_skills_found")) return # 显示进度对话框 - progress = ProgressDialog("正在检查更新...", self) + progress = ProgressDialog(tr("skill_ui.checking_updates"), self) progress.show() QApplication.processEvents() @@ -19019,7 +19038,7 @@ def _on_check_updates(self): selected = update_dialog.get_selected_updates() if not selected: - self.show_warning("提示", "未选择任何 Skills") + self.show_warning(tr("common.info"), tr("skill_ui.no_skills_selected")) return # 更新选中的 Skills @@ -19027,7 +19046,7 @@ def _on_check_updates(self): except Exception as e: progress.close() - self.show_error("错误", f"检查更新失败: {str(e)}") + self.show_error(tr("common.error"), tr("skill_ui.check_updates_failed", error=str(e))) def _update_selected_skills(self, selected_updates: List[Dict[str, Any]]): """更新选中的 Skills""" @@ -19036,7 +19055,7 @@ def _update_selected_skills(self, selected_updates: List[Dict[str, Any]]): failed_skills = [] # 创建进度对话框 - progress = ProgressDialog(f"正在更新 Skills (0/{total})...", self) + progress = ProgressDialog(tr("skill_ui.updating_skills", total=total), self) progress.show() QApplication.processEvents() @@ -19044,7 +19063,7 @@ def _update_selected_skills(self, selected_updates: List[Dict[str, Any]]): skill = update["skill"] meta = update["meta"] - progress.setLabelText(f"正在更新 {skill.name} ({i + 1}/{total})...") + progress.setLabelText(tr("skill_ui.updating_skill", name=skill.name, current=i+1, total=total)) QApplication.processEvents() success, message = SkillUpdater.update_skill(skill, meta) @@ -19058,16 +19077,16 @@ def _update_selected_skills(self, selected_updates: List[Dict[str, Any]]): # 显示结果 if success_count == total: - self.show_success("成功", f"成功更新 {success_count} 个 Skills") + self.show_success(tr("common.success"), tr("skill_ui.update_success", count=success_count)) elif success_count > 0: failed_msg = "\n".join(failed_skills) self.show_warning( - "部分成功", - f"成功更新 {success_count} 个,失败 {len(failed_skills)} 个\n\n失败详情:\n{failed_msg}", + tr("skill_ui.partial_success"), + tr("skill_ui.partial_success_msg", success=success_count, failed=len(failed_skills)) + f"\n\n{failed_msg}", ) else: failed_msg = "\n".join(failed_skills) - self.show_error("失败", f"所有更新均失败\n\n详情:\n{failed_msg}") + self.show_error(tr("common.failed"), tr("skill_ui.all_failed", details=failed_msg)) # 刷新列表 self._refresh_skill_list() @@ -19079,7 +19098,7 @@ class ProgressDialog(MessageBoxBase): def __init__(self, message: str, parent=None): super().__init__(parent) - self.titleLabel = SubtitleLabel("请稍候", self) + self.titleLabel = SubtitleLabel(tr("common.please_wait"), self) self.label = BodyLabel(message, self.widget) self.viewLayout.addWidget(self.titleLabel) @@ -19765,7 +19784,7 @@ def _check_pending_timeouts(self): latency_ms=None, ping_ms=None, checked_at=datetime.now(), - message="请求超时", + message=tr("monitor.request_timeout"), ) self._on_single_result(result) if not self._pending_targets: @@ -19887,26 +19906,26 @@ def _check_target(self, target: MonitorTarget) -> MonitorResult: if not getattr(self, "_chat_test_enabled", True): # 对话测试已暂停,根据 Ping 结果判定状态 if not target.base_url: - message = "未配置 baseURL" + message = tr("monitor.no_base_url_configured") elif ping_ms is not None: status = "operational" - message = "对话测试已暂停 (Ping 正常)" + message = tr("monitor.chat_test_paused") elif origin: status = "error" - message = "Ping 失败" + message = tr("monitor.ping_failed") else: status = "no_config" - message = "未配置有效的主机" + message = tr("monitor.no_valid_host_configured") elif not target.base_url: - message = "未配置 baseURL" + message = tr("monitor.no_base_url_configured") elif not target.api_key: - message = "未配置 apiKey" + message = tr("monitor.no_apikey_configured") else: # 发送最小请求 try: url = _build_chat_url(target.base_url) if not url: - raise ValueError("baseURL 无效") + raise ValueError(tr("monitor.base_url_invalid")) payload = json.dumps( { "model": target.model_id, @@ -19929,16 +19948,16 @@ def _check_target(self, target: MonitorTarget) -> MonitorResult: latency_ms = int((time.time() - start) * 1000) if latency_ms <= DEGRADED_THRESHOLD_MS: status = "operational" - message = "正常" + message = tr("monitor.status_normal") else: status = "degraded" - message = f"延迟较高 ({latency_ms}ms)" + message = tr("monitor.latency_high", ms=latency_ms) except urllib.error.HTTPError as e: status = "failed" - message = "鉴权失败" if e.code in (401, 403) else f"HTTP {e.code}" + message = tr("monitor.auth_failed") if e.code in (401, 403) else f"HTTP {e.code}" except urllib.error.URLError as e: status = "error" - message = f"连接失败: {e.reason}" + message = tr("monitor.connection_failed", reason=str(e.reason)) except Exception as e: status = "error" message = str(e)[:50] @@ -21501,7 +21520,7 @@ def _on_single_export(self, cli_type: str): tr("cli_export.restored"), tr("cli_export.auto_restored") ) except Exception as e: - self.show_error("导出失败", str(e)) + self.show_error(tr("export_ui.export_failed"), str(e)) def _on_batch_export(self): """批量导出""" @@ -21631,7 +21650,7 @@ class CLIBackupRestoreDialog(QDialog): def __init__(self, backup_manager: CLIBackupManager, parent=None): super().__init__(parent) self.backup_manager = backup_manager - self.setWindowTitle("恢复备份") + self.setWindowTitle(tr("export_ui.restore_backup_title")) self.setMinimumSize(500, 400) self._setup_ui() self._load_backups() @@ -21710,9 +21729,9 @@ def _on_restore(self): if success: self.accept() else: - InfoBar.error(title="恢复失败", content="无法恢复备份", parent=self) + InfoBar.error(title=tr("export_ui.restore_failed"), content=tr("export_ui.cannot_restore"), parent=self) except Exception as e: - InfoBar.error(title="恢复失败", content=str(e), parent=self) + InfoBar.error(title=tr("export_ui.restore_failed"), content=str(e), parent=self) class CommonConfigEditDialog(QDialog): @@ -21722,7 +21741,7 @@ def __init__(self, cli_type: str, initial_config: str = "", parent=None): super().__init__(parent) self.cli_type = cli_type self.initial_config = initial_config - self.setWindowTitle(f"编辑 {cli_type.upper()} 通用配置") + self.setWindowTitle(tr("export_ui.edit_config_title", type=cli_type.upper())) self.setMinimumSize(600, 450) self._setup_ui() @@ -21733,9 +21752,9 @@ def _setup_ui(self): # 说明 if self.cli_type == "codex": hint_text = ( - "编辑 Codex 通用配置 (TOML 格式),这些配置会合并到 config.toml 中" + tr("export_ui.edit_codex_hint") ) - placeholder = """# 示例通用配置 + placeholder = f"""# {tr('export_ui.example_general_config')} model_reasoning_effort = "high" disable_response_storage = true @@ -21744,8 +21763,8 @@ def _setup_ui(self): max_entries = 1000""" language = "toml" else: # gemini - hint_text = "编辑 Gemini 通用配置 (ENV 格式),这些配置会合并到 .env 文件中" - placeholder = """# 示例通用配置 + hint_text = tr("export_ui.edit_gemini_hint") + placeholder = f"""# {tr('export_ui.example_general_config')} GEMINI_TIMEOUT=30000 GEMINI_MAX_RETRIES=3""" language = "env" @@ -21872,7 +21891,7 @@ def _setup_ui(self): ) preview_layout = preview_card.layout() preview_layout.addWidget( - BodyLabel("点击“预览转换”在弹窗中查看左右对照。", preview_card) + BodyLabel(tr("export_ui.click_preview"), preview_card) ) # 按钮 @@ -21910,7 +21929,7 @@ def _refresh_scan(self): path_item = QTableWidgetItem(info["path"]) path_item.setToolTip(info["path"]) self.config_table.setItem(row, 1, path_item) - status = tr("import.detected") if info["exists"] else "未找到" + status = tr("import.detected") if info["exists"] else tr("export_ui.not_found") self.config_table.setItem(row, 2, QTableWidgetItem(status)) def _select_manual_file(self): @@ -21974,7 +21993,7 @@ def _preview_convert(self): self._last_converted = converted dialog = BaseDialog(self) - dialog.setWindowTitle("配置转换预览") + dialog.setWindowTitle(tr("export_ui.preview_title")) dialog.setMinimumSize(900, 520) layout = QVBoxLayout(dialog) @@ -21984,7 +22003,7 @@ def _preview_convert(self): columns_layout.setSpacing(12) left_layout = QVBoxLayout() - left_layout.addWidget(SubtitleLabel("原始配置", dialog)) + left_layout.addWidget(SubtitleLabel(tr("export_ui.original_config"), dialog)) source_edit = TextEdit(dialog) source_edit.setReadOnly(True) source_edit.setPlainText( @@ -22024,7 +22043,7 @@ def update_convert_match(): columns_layout.addLayout(right_layout, 1) layout.addLayout(columns_layout, 1) - close_btn = PrimaryPushButton("关闭", dialog) + close_btn = PrimaryPushButton(tr("common.close"), dialog) close_btn.clicked.connect(dialog.accept) layout.addWidget(close_btn, alignment=Qt.AlignRight) @@ -22038,7 +22057,7 @@ def _import_selected(self): """导入选中的配置""" row = self.config_table.currentRow() if row < 0: - self.show_warning(tr("common.info"), "请先选择要导入的配置") + self.show_warning(tr("common.info"), tr("export_ui.select_config_first")) return source = self.config_table.item(row, 0).text() @@ -22081,8 +22100,8 @@ def _apply_import(self, source: str, converted: Dict[str, Any]): perm_count = len(converted.get("permission", {})) w = FluentMessageBox( - "确认导入", - f"将导入以下配置:\n• Provider: {provider_count} 个\n• 权限: {perm_count} 个\n\n是否继续?", + tr("export_ui.confirm_import"), + tr("export_ui.import_summary", providers=provider_count, permissions=perm_count), self, ) if not w.exec_(): @@ -22097,7 +22116,7 @@ def _apply_import(self, source: str, converted: Dict[str, Any]): for provider_name, provider_data in converted.get("provider", {}).items(): if provider_name in config.get("provider", {}): w2 = FluentMessageBox( - "冲突", f'Provider "{provider_name}" 已存在,是否覆盖?', self + tr("export_ui.conflict_title"), f'Provider "{provider_name}" 已存在,是否覆盖?', self ) if not w2.exec_(): continue @@ -22116,7 +22135,7 @@ def _apply_import(self, source: str, converted: Dict[str, Any]): def _confirm_mapping(self): """手动确认映射""" if not self._last_converted: - self.show_warning(tr("common.info"), "请先预览转换结果") + self.show_warning(tr("common.info"), tr("export_ui.preview_first")) return dialog = ImportMappingDialog( self.main_window, self._last_converted, parent=self @@ -22129,7 +22148,7 @@ def _confirm_mapping(self): self.tr("common.info"), self.tr("common.no_valid_import_config") ) return - self._apply_import("手动确认", confirmed) + self._apply_import(tr("import.manual_confirm"), confirmed) # ==================== Backup 对话框 ==================== @@ -22142,7 +22161,7 @@ def __init__(self, main_window, converted: Dict[str, Any], parent=None): self.converted = converted or {} self._confirmed: Dict[str, Any] = {} - self.setWindowTitle("确认导入映射") + self.setWindowTitle(tr("export_ui.confirm_mapping_title")) self.setMinimumWidth(560) self.setFixedHeight(520) self._setup_ui() @@ -22151,7 +22170,7 @@ def _setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(12) - layout.addWidget(SubtitleLabel("请确认必要字段", self)) + layout.addWidget(SubtitleLabel(tr("export_ui.confirm_fields"), self)) scroll = QScrollArea(self) scroll.setWidgetResizable(True) @@ -22217,7 +22236,7 @@ def _setup_ui(self): cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(cancel_btn) - ok_btn = PrimaryPushButton("确认导入", self) + ok_btn = PrimaryPushButton(tr("export_ui.confirm_import_btn"), self) ok_btn.clicked.connect(self._on_confirm) btn_layout.addWidget(ok_btn) @@ -22413,11 +22432,11 @@ def _preview_backup(self): with open(backup_path, "r", encoding="utf-8") as f: content = f.read() except Exception as e: - InfoBar.error("错误", f"无法读取备份内容: {e}", parent=self) + InfoBar.error(tr("common.error"), tr("export_ui.cannot_read_backup", error=str(e)), parent=self) return dialog = BaseDialog(self) - dialog.setWindowTitle("备份内容预览") + dialog.setWindowTitle(tr("export_ui.backup_preview_title")) dialog.setMinimumSize(700, 500) layout = QVBoxLayout(dialog) layout.setSpacing(12) @@ -22425,7 +22444,7 @@ def _preview_backup(self): text_edit.setReadOnly(True) text_edit.setPlainText(content) layout.addWidget(text_edit) - close_btn = PrimaryPushButton("关闭", dialog) + close_btn = PrimaryPushButton(tr("common.close"), dialog) close_btn.clicked.connect(dialog.accept) layout.addWidget(close_btn) dialog.exec_() @@ -22449,8 +22468,8 @@ def _restore_backup(self): target_path = ConfigPaths.get_ohmyopencode_config() w = FluentMessageBox( - "确认恢复", - f"确定要恢复此备份吗?\n当前配置将被覆盖(会先自动备份)。", + tr("export_ui.confirm_restore"), + tr("export_ui.confirm_restore_msg"), self, ) if w.exec_(): @@ -22596,7 +22615,7 @@ def install_npm_plugin( return True except Exception as e: - print(f"安装插件失败: {e}") + print(tr("plugin_ui.install_plugin_failed", error=str(e))) return False @staticmethod @@ -22622,7 +22641,7 @@ def uninstall_plugin(config: Dict[str, Any], plugin: PluginConfig) -> bool: return False except Exception as e: - print(f"卸载插件失败: {e}") + print(tr("plugin_ui.uninstall_plugin_failed", error=str(e))) return False @staticmethod @@ -22637,7 +22656,7 @@ def check_npm_version(package_name: str) -> str: data = response.json() return data.get("version", "") except Exception as e: - print(f"检查版本失败: {e}") + print(tr("plugin_ui.check_version_failed", error=str(e))) return "" @@ -22647,34 +22666,34 @@ def check_npm_version(package_name: str) -> str: { "name": "opencode-skills", "display_name": "OpenCode Skills", - "description": "自动发现和注册Skills为动态工具,支持Anthropic Agent Skills规范", + "description": tr("plugin_ui.plugin_skill_discovery"), "npm_package": "opencode-skills", "homepage": "https://github.com/malhashemi/opencode-skills", - "category": "工具增强", + "category": tr("plugin_ui.plugin_category_tools"), }, { "name": "opencode-sessions", "display_name": "OpenCode Sessions", - "description": "多Agent协作和工作流编排,支持回合制讨论和并行探索", + "description": tr("plugin_ui.plugin_multi_agent"), "npm_package": "opencode-sessions", "homepage": "https://github.com/malhashemi/opencode-sessions", - "category": "协作增强", + "category": tr("plugin_ui.plugin_category_collab"), }, { "name": "opencode-helicone-session", "display_name": "Helicone Session", - "description": "自动注入Helicone会话ID和名称,用于LLM请求分组和追踪", + "description": tr("plugin_ui.plugin_helicone"), "npm_package": "opencode-helicone-session", "homepage": "", - "category": "监控追踪", + "category": tr("plugin_ui.plugin_category_monitor"), }, { "name": "opencode-wakatime", "display_name": "WakaTime", - "description": "代码时间追踪,自动记录编码时间和项目统计", + "description": tr("plugin_ui.plugin_wakatime"), "npm_package": "opencode-wakatime", "homepage": "", - "category": "监控追踪", + "category": tr("plugin_ui.plugin_category_monitor"), }, ] @@ -22683,7 +22702,7 @@ class PluginPage(BasePage): """Plugin 插件管理页面 - 包含插件管理和Oh My OpenCode管理""" def __init__(self, main_window, parent=None): - super().__init__("Plugin 插件管理", parent) + super().__init__(tr("plugin_ui.page_title"), parent) self.main_window = main_window self._setup_ui() self._load_plugins() @@ -22699,7 +22718,7 @@ def _setup_ui(self): """初始化UI""" # 标签页切换 self.pivot = Pivot(self) - self.pivot.addItem(routeKey="plugins", text="插件管理") + self.pivot.addItem(routeKey="plugins", text=tr("plugin_ui.tab_plugins")) self.pivot.addItem(routeKey="ohmyopencode", text="Oh My OpenCode") self.pivot.setCurrentItem("plugins") self.pivot.currentItemChanged.connect(self._on_tab_changed) @@ -22738,7 +22757,7 @@ def _create_plugins_widget(self) -> QWidget: # 搜索框 self.search_edit = SearchLineEdit(widget) - self.search_edit.setPlaceholderText("搜索插件...") + self.search_edit.setPlaceholderText(tr("plugin_ui.search_placeholder")) self.search_edit.setFixedWidth(300) self.search_edit.textChanged.connect(self._on_search) btn_layout.addWidget(self.search_edit) @@ -22746,17 +22765,17 @@ def _create_plugins_widget(self) -> QWidget: btn_layout.addStretch() # 安装插件按钮 - self.install_btn = PrimaryPushButton("➕ 安装插件", widget) + self.install_btn = PrimaryPushButton(f"➕ {tr('plugin_ui.install_plugin_btn')}", widget) self.install_btn.clicked.connect(self._on_install) btn_layout.addWidget(self.install_btn) # 检查更新按钮 - self.check_update_btn = PushButton("🔄 检查更新", widget) + self.check_update_btn = PushButton(f"🔄 {tr('plugin_ui.check_update_btn')}", widget) self.check_update_btn.clicked.connect(self._on_check_updates) btn_layout.addWidget(self.check_update_btn) # 插件市场按钮 - self.market_btn = PushButton("🛒 插件市场", widget) + self.market_btn = PushButton(f"🛒 {tr('plugin_ui.market_btn')}", widget) self.market_btn.clicked.connect(self._on_open_market) btn_layout.addWidget(self.market_btn) @@ -22766,7 +22785,7 @@ def _create_plugins_widget(self) -> QWidget: self.table = TableWidget(widget) self.table.setColumnCount(6) self.table.setHorizontalHeaderLabels( - ["插件名称", "版本", "类型", "状态", "描述", "操作"] + tr("plugin_ui.table_headers").split(",") ) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeMode.Stretch @@ -22801,22 +22820,22 @@ def _create_ohmy_widget(self) -> QWidget: # 顶部状态栏 status_row = QHBoxLayout() - self.ohmy_status_label = BodyLabel("检测中...", widget) + self.ohmy_status_label = BodyLabel(tr("plugin_ui.detecting"), widget) status_row.addWidget(self.ohmy_status_label) status_row.addStretch() # 启用/禁用按钮 - self.ohmy_enable_btn = PushButton("启用插件", widget) + self.ohmy_enable_btn = PushButton(tr("plugin_ui.enable_plugin"), widget) self.ohmy_enable_btn.clicked.connect(self._on_toggle_ohmy_enable) status_row.addWidget(self.ohmy_enable_btn) # 刷新按钮 - self.ohmy_refresh_btn = PushButton(FIF.SYNC, "刷新状态", widget) + self.ohmy_refresh_btn = PushButton(FIF.SYNC, tr("plugin_ui.refresh_status"), widget) self.ohmy_refresh_btn.clicked.connect(self._on_refresh_ohmy_status) status_row.addWidget(self.ohmy_refresh_btn) # 配置按钮 - self.ohmy_config_btn = PushButton("打开配置文件", widget) + self.ohmy_config_btn = PushButton(tr("plugin_ui.open_config"), widget) self.ohmy_config_btn.clicked.connect(self._on_config_ohmy) status_row.addWidget(self.ohmy_config_btn) @@ -22884,18 +22903,18 @@ def _on_toggle_ohmy_enable(self): new_plugins.append(plugin) config[field_name] = new_plugins self.main_window.save_opencode_config() - self.show_success("成功", "Oh My OpenCode 已禁用") + self.show_success(tr("common.success"), tr("plugin_ui.disabled_success")) elif self._ohmy_installed: # 已安装但未启用:添加到plugins plugins.append("oh-my-opencode") config[field_name] = plugins self.main_window.save_opencode_config() - self.show_success("成功", "Oh My OpenCode 已启用") + self.show_success(tr("common.success"), tr("plugin_ui.enabled_success")) else: # 未安装:提示安装 self.show_warning( - "提示", - "请先通过npm安装oh-my-opencode插件:\nnpm install -g oh-my-opencode", + tr("common.info"), + tr("plugin_ui.npm_install_hint"), ) # 刷新状态 @@ -22917,7 +22936,7 @@ def _on_refresh_ohmy_status(self): # 刷新显示 self._load_ohmy_data() - self.show_success("成功", "状态已刷新") + self.show_success(tr("common.success"), tr("plugin_ui.status_refreshed")) def _on_ohmy_tab_changed(self, route_key: str): """切换Oh My OpenCode标签页""" @@ -23085,24 +23104,24 @@ def _load_ohmy_data(self): # 根据安装和启用状态显示不同提示 if ohmy_installed and ohmy_enabled: - self.ohmy_status_label.setText("✅ 已安装且已启用") + self.ohmy_status_label.setText(f"✅ {tr('plugin_ui.status_installed_enabled')}") self.ohmy_status_label.setStyleSheet("color: #4CAF50;") - self.ohmy_enable_btn.setText("禁用插件") + self.ohmy_enable_btn.setText(tr("plugin_ui.disable_plugin")) self.ohmy_enable_btn.setEnabled(True) elif ohmy_installed and not ohmy_enabled: - self.ohmy_status_label.setText("⚠️ 已安装但未启用") + self.ohmy_status_label.setText(f"⚠️ {tr('plugin_ui.status_installed_disabled')}") self.ohmy_status_label.setStyleSheet("color: #ff9800;") - self.ohmy_enable_btn.setText("启用插件") + self.ohmy_enable_btn.setText(tr("plugin_ui.enable_plugin")) self.ohmy_enable_btn.setEnabled(True) elif not ohmy_installed and ohmy_enabled: - self.ohmy_status_label.setText("❌ 配置异常:已启用但配置文件不存在") + self.ohmy_status_label.setText(f"❌ {tr('plugin_ui.status_config_error')}") self.ohmy_status_label.setStyleSheet("color: #f44336;") - self.ohmy_enable_btn.setText("禁用插件") + self.ohmy_enable_btn.setText(tr("plugin_ui.disable_plugin")) self.ohmy_enable_btn.setEnabled(True) else: - self.ohmy_status_label.setText("❌ 未安装") + self.ohmy_status_label.setText(f"❌ {tr('plugin_ui.status_not_installed')}") self.ohmy_status_label.setStyleSheet("color: #f44336;") - self.ohmy_enable_btn.setText("安装插件") + self.ohmy_enable_btn.setText(tr("plugin_ui.install_plugin_label")) self.ohmy_enable_btn.setEnabled(True) # 保存状态供按钮使用 @@ -23195,7 +23214,7 @@ def _load_ohmy_agents(self): desc = data.get("description", "") if not desc: - desc = PRESET_AGENTS.get(name, "") + desc = _tr_preset('omo_agents', name, PRESET_AGENTS.get(name, '')) desc_item = QTableWidgetItem(desc[:50] + "..." if len(desc) > 50 else desc) desc_item.setToolTip(desc) self.ohmy_agent_table.setItem(row, 2, desc_item) @@ -23257,7 +23276,7 @@ def _load_ohmy_categories(self): desc = data.get("description", "") if not desc: - desc = PRESET_CATEGORIES.get(name, {}).get("description", "") + desc = _tr_preset('categories', name, PRESET_CATEGORIES.get(name, {}).get('description', '')) desc_item = QTableWidgetItem(desc[:30] + "..." if len(desc) > 30 else desc) desc_item.setToolTip(desc) self.ohmy_category_table.setItem(row, 3, desc_item) @@ -23289,7 +23308,7 @@ def _on_config_ohmy(self): else: subprocess.Popen(["xdg-open", str(config_path)]) else: - self.show_warning("提示", "配置文件不存在,请先安装Oh My OpenCode插件") + self.show_warning(tr("common.info"), tr("plugin_ui.config_not_exist")) def _on_add_ohmy_agent(self): """添加Oh My Agent""" @@ -23418,11 +23437,11 @@ def _load_plugins(self): self.table.setItem(row, 1, QTableWidgetItem(plugin.version)) # 类型 - type_text = "npm" if plugin.type == "npm" else "本地" + type_text = "npm" if plugin.type == "npm" else tr("plugin_ui.type_local") self.table.setItem(row, 2, QTableWidgetItem(type_text)) # 状态 - status_text = "✅ 已启用" if plugin.enabled else "❌ 已禁用" + status_text = f"✅ {tr('plugin_ui.status_enabled')}" if plugin.enabled else f"❌ {tr('plugin_ui.status_disabled')}" self.table.setItem(row, 3, QTableWidgetItem(status_text)) # 描述 @@ -23437,7 +23456,7 @@ def _load_plugins(self): # 卸载按钮 uninstall_btn = PushButton("🗑️", btn_widget) uninstall_btn.setFixedSize(32, 28) - uninstall_btn.setToolTip("卸载插件") + uninstall_btn.setToolTip(tr("plugin_ui.uninstall_tooltip")) uninstall_btn.clicked.connect( lambda checked, p=plugin: self._on_uninstall(p) ) @@ -23463,22 +23482,22 @@ def _on_install(self): def _on_uninstall(self, plugin: PluginConfig): """卸载插件""" w = FluentMessageBox( - "确认卸载", - f"确定要卸载插件 {plugin.name} 吗?\n\n注意:OpenCode需要重启后才会生效。", + tr("plugin_ui.confirm_uninstall"), + tr("plugin_ui.confirm_uninstall_msg", name=plugin.name), self, ) if w.exec_(): config = self.main_window.opencode_config or {} if PluginManager.uninstall_plugin(config, plugin): self.main_window.save_opencode_config() - InfoBar.success("成功", f"插件 {plugin.name} 已卸载", parent=self) + InfoBar.success(tr("common.success"), tr("plugin_ui.uninstall_success", name=plugin.name), parent=self) self._load_plugins() else: - InfoBar.error("失败", "卸载插件失败", parent=self) + InfoBar.error(tr("common.failed"), tr("plugin_ui.uninstall_failed"), parent=self) def _on_check_updates(self): """检查更新""" - InfoBar.info("提示", "正在检查更新...", parent=self) + InfoBar.info(tr("common.info"), tr("plugin_ui.checking_updates"), parent=self) # TODO: 实现更新检测逻辑 pass @@ -23495,7 +23514,7 @@ class PluginInstallDialog(BaseDialog): def __init__(self, main_window, parent=None): super().__init__(parent) self.main_window = main_window - self.setWindowTitle("安装插件") + self.setWindowTitle(tr("plugin_ui.install_dialog_title")) self.setFixedWidth(500) self._setup_ui() @@ -23505,15 +23524,15 @@ def _setup_ui(self): layout.setSpacing(16) # 安装方式选择 - method_label = BodyLabel("安装方式:", self) + method_label = BodyLabel(tr("plugin_ui.install_method"), self) layout.addWidget(method_label) - self.npm_radio = RadioButton("从npm安装", self) + self.npm_radio = RadioButton(tr("plugin_ui.from_npm"), self) self.npm_radio.setChecked(True) self.npm_radio.toggled.connect(self._on_method_changed) layout.addWidget(self.npm_radio) - self.local_radio = RadioButton("从本地文件安装", self) + self.local_radio = RadioButton(tr("plugin_ui.from_local"), self) self.local_radio.toggled.connect(self._on_method_changed) layout.addWidget(self.local_radio) @@ -23522,16 +23541,16 @@ def _setup_ui(self): npm_layout = QVBoxLayout(self.npm_widget) npm_layout.setContentsMargins(0, 0, 0, 0) - npm_label = BodyLabel("npm包名:", self) + npm_label = BodyLabel(tr("plugin_ui.npm_package_name"), self) npm_layout.addWidget(npm_label) self.npm_edit = LineEdit(self) self.npm_edit.setPlaceholderText( - "例如: opencode-skills 或 opencode-skills@0.1.0" + tr("plugin_ui.npm_placeholder") ) npm_layout.addWidget(self.npm_edit) - hint_label = CaptionLabel("支持普通包和scoped包(如@my-org/plugin)", self) + hint_label = CaptionLabel(tr("plugin_ui.npm_hint"), self) hint_label.setTextColor(QColor(150, 150, 150), QColor(150, 150, 150)) npm_layout.addWidget(hint_label) @@ -23542,16 +23561,16 @@ def _setup_ui(self): local_layout = QVBoxLayout(self.local_widget) local_layout.setContentsMargins(0, 0, 0, 0) - local_label = BodyLabel("本地文件:", self) + local_label = BodyLabel(tr("plugin_ui.local_file"), self) local_layout.addWidget(local_label) file_layout = QHBoxLayout() self.file_edit = LineEdit(self) - self.file_edit.setPlaceholderText("选择.js或.ts文件") + self.file_edit.setPlaceholderText(tr("plugin_ui.local_placeholder")) self.file_edit.setReadOnly(True) file_layout.addWidget(self.file_edit) - self.browse_btn = PushButton("浏览...", self) + self.browse_btn = PushButton(tr("plugin_ui.browse_btn"), self) self.browse_btn.clicked.connect(self._on_browse_file) file_layout.addWidget(self.browse_btn) @@ -23564,11 +23583,11 @@ def _setup_ui(self): btn_layout = QHBoxLayout() btn_layout.addStretch() - self.cancel_btn = PushButton("取消", self) + self.cancel_btn = PushButton(tr("common.cancel"), self) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.cancel_btn) - self.install_btn = PrimaryPushButton("安装", self) + self.install_btn = PrimaryPushButton(tr("common.install"), self) self.install_btn.clicked.connect(self._on_install) btn_layout.addWidget(self.install_btn) @@ -23588,7 +23607,7 @@ def _on_browse_file(self): from PyQt5.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( - self, "选择插件文件", "", "JavaScript/TypeScript Files (*.js *.ts)" + self, tr("plugin_ui.select_plugin_file"), "", "JavaScript/TypeScript Files (*.js *.ts)" ) if file_path: self.file_edit.setText(file_path) @@ -23599,7 +23618,7 @@ def _on_install(self): # npm安装 package_name = self.npm_edit.text().strip() if not package_name: - InfoBar.error("错误", "请输入npm包名", parent=self) + InfoBar.error(tr("common.error"), tr("plugin_ui.enter_npm_name"), parent=self) return # 解析包名和版本 @@ -23621,22 +23640,22 @@ def _on_install(self): if PluginManager.install_npm_plugin(config, name, version): self.main_window.save_opencode_config() InfoBar.success( - "成功", - f"插件 {package_name} 已添加到配置\n\nOpenCode将在下次启动时自动安装", + tr("common.success"), + tr("plugin_ui.plugin_added", name=package_name), parent=self, ) self.accept() else: - InfoBar.error("失败", "安装插件失败", parent=self) + InfoBar.error(tr("common.failed"), tr("plugin_ui.install_failed"), parent=self) else: # 本地文件安装 file_path = self.file_edit.text().strip() if not file_path: - InfoBar.error("错误", "请选择插件文件", parent=self) + InfoBar.error(tr("common.error"), tr("plugin_ui.select_plugin_file_first"), parent=self) return - InfoBar.info("提示", "本地插件安装功能暂未实现", parent=self) + InfoBar.info(tr("common.info"), tr("plugin_ui.local_not_implemented"), parent=self) class PluginMarketDialog(BaseDialog): @@ -23645,7 +23664,7 @@ class PluginMarketDialog(BaseDialog): def __init__(self, main_window, parent=None): super().__init__(parent) self.main_window = main_window - self.setWindowTitle("插件市场") + self.setWindowTitle(tr("plugin_ui.market_title")) self.setFixedSize(800, 600) self._setup_ui() self._load_market() @@ -23656,13 +23675,13 @@ def _setup_ui(self): layout.setSpacing(16) # 标题 - title_label = SubtitleLabel("预设插件", self) + title_label = SubtitleLabel(tr("plugin_ui.preset_plugins"), self) layout.addWidget(title_label) # 插件列表 self.table = TableWidget(self) self.table.setColumnCount(4) - self.table.setHorizontalHeaderLabels(["插件名称", "分类", "描述", "操作"]) + self.table.setHorizontalHeaderLabels(tr("plugin_ui.market_headers").split(",")) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeMode.ResizeToContents ) @@ -23683,7 +23702,7 @@ def _setup_ui(self): btn_layout = QHBoxLayout() btn_layout.addStretch() - self.close_btn = PrimaryPushButton("关闭", self) + self.close_btn = PrimaryPushButton(tr("common.close"), self) self.close_btn.clicked.connect(self.accept) btn_layout.addWidget(self.close_btn) @@ -23716,7 +23735,7 @@ def _load_market(self): btn_layout.setSpacing(4) # 安装按钮 - install_btn = PrimaryPushButton("安装", btn_widget) + install_btn = PrimaryPushButton(tr("common.install"), btn_widget) install_btn.setFixedSize(60, 28) install_btn.clicked.connect( lambda checked, info=plugin_info: self._on_install_from_market(info) @@ -23733,12 +23752,12 @@ def _on_install_from_market(self, plugin_info: Dict[str, Any]): if PluginManager.install_npm_plugin(config, package_name, ""): self.main_window.save_opencode_config() InfoBar.success( - "成功", - f"插件 {plugin_info['display_name']} 已添加到配置\n\nOpenCode将在下次启动时自动安装", + tr("common.success"), + tr("plugin_ui.plugin_added", name=plugin_info['display_name']), parent=self, ) else: - InfoBar.error("失败", "安装插件失败", parent=self) + InfoBar.error(tr("common.failed"), tr("plugin_ui.install_failed"), parent=self) # ==================== 程序入口 ====================