From 0517ee609ee6d4568bbdcddb2c541addc7ec7670 Mon Sep 17 00:00:00 2001 From: YuanNiancai Date: Wed, 21 Jan 2026 13:56:49 +0800 Subject: [PATCH] feat: complete internationalization (i18n) for components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive i18n translation support for the following components: 1. GitSettings.jsx - Git configuration interface 2. ApiKeysSettings.jsx - API keys settings 3. CredentialsSettings.jsx - Credentials settings (GitHub tokens) 4. TasksSettings.jsx - TaskMaster task management settings 5. ChatInterface.jsx - Chat interface (major translation work) New translation files: - src/i18n/locales/en/chat.json - English chat interface translations - src/i18n/locales/zh-CN/chat.json - Chinese chat interface translations ChatInterface.jsx translations: - Code block copy buttons (Copy, Copied, Copy code) - Message type labels (User, Error, Tool, Claude, Cursor, Codex) - Tool settings tooltip - Search result display (pattern, in, results) - Codex permission modes (Default, Accept Edits, Bypass Permissions, Plan) - Input placeholder and hint text - Keyboard shortcut hints (Ctrl+Enter/Enter modes) - Command menu button i18n configuration updates: - Registered chat namespace in config.js - Extended settings.json translations (git, apiKeys, tasks, agents, mcpServers sections) 完成以下组件的 i18n 翻译工作: 1. GitSettings.jsx - Git 配置界面 2. ApiKeysSettings.jsx - API 密钥设置 3. CredentialsSettings.jsx - 凭据设置(GitHub Token) 4. TasksSettings.jsx - TaskMaster 任务管理设置 5. ChatInterface.jsx - 聊天界面(主要翻译工作) 新增翻译文件: - src/i18n/locales/en/chat.json - 英文聊天界面翻译 - src/i18n/locales/zh-CN/chat.json - 中文聊天界面翻译 ChatInterface.jsx 翻译内容: - 代码块复制按钮 - 消息类型标签 - 工具设置提示 - 搜索结果显示 - Codex 权限模式(默认、编辑、无限制、计划模式) - 输入框占位符和提示文本 - 键盘快捷键提示 - 命令菜单按钮 更新 i18n 配置: - 在 config.js 中注册 chat 命名空间 - 扩展 settings.json 翻译(git、apiKeys、tasks、agents、mcpServers 等部分) --- src/components/ApiKeysSettings.jsx | 60 ++-- src/components/ChatInterface.jsx | 87 ++--- src/components/CredentialsSettings.jsx | 62 ++-- src/components/GitSettings.jsx | 18 +- src/components/Settings.jsx | 156 ++++---- src/components/TasksSettings.jsx | 24 +- src/components/settings/AccountContent.jsx | 30 +- src/components/settings/AgentListItem.jsx | 8 +- src/components/settings/McpServersContent.jsx | 63 ++-- .../settings/PermissionsContent.jsx | 109 +++--- src/i18n/config.js | 6 +- src/i18n/locales/en/chat.json | 111 ++++++ src/i18n/locales/en/settings.json | 340 +++++++++++++++++- src/i18n/locales/zh-CN/chat.json | 111 ++++++ src/i18n/locales/zh-CN/settings.json | 340 +++++++++++++++++- 15 files changed, 1214 insertions(+), 311 deletions(-) create mode 100644 src/i18n/locales/en/chat.json create mode 100644 src/i18n/locales/zh-CN/chat.json diff --git a/src/components/ApiKeysSettings.jsx b/src/components/ApiKeysSettings.jsx index 2687d83..1ab27df 100644 --- a/src/components/ApiKeysSettings.jsx +++ b/src/components/ApiKeysSettings.jsx @@ -3,8 +3,10 @@ import { Button } from './ui/button'; import { Input } from './ui/input'; import { Key, Plus, Trash2, Eye, EyeOff, Copy, Check, Github } from 'lucide-react'; import { authenticatedFetch } from '../utils/api'; +import { useTranslation } from 'react-i18next'; function ApiKeysSettings() { + const { t } = useTranslation('settings'); const [apiKeys, setApiKeys] = useState([]); const [githubTokens, setGithubTokens] = useState([]); const [loading, setLoading] = useState(true); @@ -63,7 +65,7 @@ function ApiKeysSettings() { }; const deleteApiKey = async (keyId) => { - if (!confirm('Are you sure you want to delete this API key?')) return; + if (!confirm(t('apiKeys.confirmDelete'))) return; try { await authenticatedFetch(`/api/settings/api-keys/${keyId}`, { @@ -113,7 +115,7 @@ function ApiKeysSettings() { }; const deleteGithubToken = async (tokenId) => { - if (!confirm('Are you sure you want to delete this GitHub token?')) return; + if (!confirm(t('apiKeys.github.confirmDelete'))) return; try { await authenticatedFetch(`/api/settings/credentials/${tokenId}`, { @@ -144,7 +146,7 @@ function ApiKeysSettings() { }; if (loading) { - return
Loading...
; + return
{t('apiKeys.loading')}
; } return ( @@ -152,9 +154,9 @@ function ApiKeysSettings() { {/* New API Key Alert */} {newlyCreatedKey && (
-

⚠️ Save Your API Key

+

{t('apiKeys.newKey.alertTitle')}

- This is the only time you'll see this key. Store it securely. + {t('apiKeys.newKey.alertMessage')}

@@ -174,7 +176,7 @@ function ApiKeysSettings() { className="mt-3" onClick={() => setNewlyCreatedKey(null)} > - I've saved it + {t('apiKeys.newKey.iveSavedIt')}
)} @@ -184,33 +186,33 @@ function ApiKeysSettings() {
-

API Keys

+

{t('apiKeys.title')}

- Generate API keys to access the external API from other applications. + {t('apiKeys.description')}

{showNewKeyForm && (
setNewKeyName(e.target.value)} className="mb-2" />
- +
@@ -218,7 +220,7 @@ function ApiKeysSettings() {
{apiKeys.length === 0 ? ( -

No API keys created yet.

+

{t('apiKeys.empty')}

) : ( apiKeys.map((key) => (
{key.key_name}
{key.api_key}
- Created: {new Date(key.created_at).toLocaleDateString()} - {key.last_used && ` • Last used: ${new Date(key.last_used).toLocaleDateString()}`} + {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()} + {key.last_used && ` • ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`}
@@ -239,7 +241,7 @@ function ApiKeysSettings() { variant={key.is_active ? 'outline' : 'secondary'} onClick={() => toggleApiKey(key.id, key.is_active)} > - {key.is_active ? 'Active' : 'Inactive'} + {key.is_active ? t('apiKeys.status.active') : t('apiKeys.status.inactive')}

- Add GitHub Personal Access Tokens to clone private repositories via the external API. + {t('apiKeys.github.description')}

{showNewTokenForm && (
setNewTokenName(e.target.value)} className="mb-2" @@ -286,7 +288,7 @@ function ApiKeysSettings() {
setNewGithubToken(e.target.value)} className="mb-2 pr-10" @@ -300,13 +302,13 @@ function ApiKeysSettings() {
- +
@@ -314,7 +316,7 @@ function ApiKeysSettings() {
{githubTokens.length === 0 ? ( -

No GitHub tokens added yet.

+

{t('apiKeys.github.empty')}

) : ( githubTokens.map((token) => (
{token.credential_name}
- Added: {new Date(token.created_at).toLocaleDateString()} + {t('apiKeys.github.added')} {new Date(token.created_at).toLocaleDateString()}
@@ -333,7 +335,7 @@ function ApiKeysSettings() { variant={token.is_active ? 'outline' : 'secondary'} onClick={() => toggleGithubToken(token.id, token.is_active)} > - {token.is_active ? 'Active' : 'Inactive'} + {token.is_active ? t('apiKeys.status.active') : t('apiKeys.status.inactive')}
diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 42e0ee0..9c5e4e7 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -30,6 +30,7 @@ import CursorLogo from './CursorLogo.jsx'; import CodexLogo from './CodexLogo.jsx'; import NextTaskBanner from './NextTaskBanner.jsx'; import { useTasksSettings } from '../contexts/TasksSettingsContext'; +import { useTranslation } from 'react-i18next'; import ClaudeStatus from './ClaudeStatus'; import TokenUsagePie from './TokenUsagePie'; @@ -336,27 +337,27 @@ function grantClaudeToolPermission(entry) { } // Common markdown components to ensure consistent rendering (tables, inline code, links, etc.) -const markdownComponents = { - code: ({ node, inline, className, children, ...props }) => { - const [copied, setCopied] = React.useState(false); - const raw = Array.isArray(children) ? children.join('') : String(children ?? ''); - const looksMultiline = /[\r\n]/.test(raw); - const inlineDetected = inline || (node && node.type === 'inlineCode'); - const shouldInline = inlineDetected || !looksMultiline; // fallback to inline if single-line +const CodeBlock = ({ node, inline, className, children, ...props }) => { + const { t } = useTranslation('chat'); + const [copied, setCopied] = React.useState(false); + const raw = Array.isArray(children) ? children.join('') : String(children ?? ''); + const looksMultiline = /[\r\n]/.test(raw); + const inlineDetected = inline || (node && node.type === 'inlineCode'); + const shouldInline = inlineDetected || !looksMultiline; // fallback to inline if single-line - // Inline code rendering - if (shouldInline) { - return ( - - {children} - - ); - } + // Inline code rendering + if (shouldInline) { + return ( + + {children} + + ); + } // Extract language from className (format: language-xxx) const match = /language-(\w+)/.exec(className || ''); @@ -411,15 +412,15 @@ const markdownComponents = { type="button" onClick={handleCopy} className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 focus:opacity-100 active:opacity-100 transition-opacity text-xs px-2 py-1 rounded-md bg-gray-700/80 hover:bg-gray-700 text-white border border-gray-600" - title={copied ? 'Copied' : 'Copy code'} - aria-label={copied ? 'Copied' : 'Copy code'} + title={copied ? t('codeBlock.copied') : t('codeBlock.copyCode')} + aria-label={copied ? t('codeBlock.copied') : t('codeBlock.copyCode')} > {copied ? ( - Copied + {t('codeBlock.copied')} ) : ( @@ -427,7 +428,7 @@ const markdownComponents = { - Copy + {t('codeBlock.copy')} )} @@ -452,7 +453,11 @@ const markdownComponents = {
); - }, + }; + +// Common markdown components to ensure consistent rendering (tables, inline code, links, etc.) +const markdownComponents = { + code: CodeBlock, blockquote: ({ children }) => (
{children} @@ -485,6 +490,7 @@ const markdownComponents = { // Memoized message component to prevent unnecessary re-renders const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }) => { + const { t } = useTranslation('chat'); const isGrouped = prevMessage && prevMessage.type === message.type && ((prevMessage.type === 'assistant') || (prevMessage.type === 'user') || @@ -587,7 +593,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile )}
- {message.type === 'error' ? 'Error' : message.type === 'tool' ? 'Tool' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : (localStorage.getItem('selected-provider') || 'claude') === 'codex' ? 'Codex' : 'Claude')} + {message.type === 'error' ? t('messageTypes.error') : message.type === 'tool' ? t('messageTypes.tool') : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? t('messageTypes.cursor') : (localStorage.getItem('selected-provider') || 'claude') === 'codex' ? t('messageTypes.codex') : t('messageTypes.claude'))}
)} @@ -615,8 +621,8 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile const input = JSON.parse(message.toolInput); return ( - {input.pattern && pattern: {input.pattern}} - {input.path && in: {input.path}} + {input.pattern && {t('search.pattern')} {input.pattern}} + {input.path && {t('search.in')} {input.path}} ); } catch (e) { @@ -629,7 +635,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile href={`#tool-result-${message.toolId}`} className="flex-shrink-0 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center gap-1" > - results + {t('tools.searchResults')} @@ -673,7 +679,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile onShowSettings(); }} className="p-2 rounded-lg hover:bg-white/60 dark:hover:bg-gray-800/60 transition-all duration-200 group/btn backdrop-blur-sm" - title="Tool Settings" + title={t('tools.settings')} > @@ -1851,6 +1857,7 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => { // This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations. function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onSessionProcessing, onSessionNotProcessing, processingSessions, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, externalMessageUpdate, onTaskClick, onShowAllTasks }) { const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings(); + const { t } = useTranslation('chat'); const [input, setInput] = useState(() => { if (typeof window !== 'undefined' && selectedProject) { return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || ''; @@ -5191,7 +5198,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300 border-orange-300 dark:border-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/30' : 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/30' }`} - title="Click to change permission mode (or press Tab in input)" + title={t('input.clickToChangeMode')} >
- {permissionMode === 'default' && 'Default Mode'} - {permissionMode === 'acceptEdits' && 'Accept Edits'} - {permissionMode === 'bypassPermissions' && 'Bypass Permissions'} - {permissionMode === 'plan' && 'Plan Mode'} + {permissionMode === 'default' && t('codex.modes.default')} + {permissionMode === 'acceptEdits' && t('codex.modes.acceptEdits')} + {permissionMode === 'bypassPermissions' && t('codex.modes.bypassPermissions')} + {permissionMode === 'plan' && t('codex.modes.plan')}
@@ -5236,7 +5243,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }} className="relative w-8 h-8 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800" - title="Show all commands" + title={t('input.showAllCommands')} > lineHeight * 2; setIsTextareaExpanded(isExpanded); }} - placeholder={`Type / for commands, @ for files, or ask ${provider === 'cursor' ? 'Cursor' : 'Claude'} anything...`} + placeholder={t('input.placeholder', { provider: provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : t('messageTypes.claude') })} disabled={isLoading} className="chat-input-placeholder block w-full pl-12 pr-20 sm:pr-40 py-1.5 sm:py-4 bg-transparent rounded-2xl focus:outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 disabled:opacity-50 resize-none min-h-[50px] sm:min-h-[80px] max-h-[40vh] sm:max-h-[300px] overflow-y-auto text-sm sm:text-base leading-[21px] sm:leading-6 transition-all duration-200" style={{ height: '50px' }} @@ -5431,7 +5438,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess type="button" onClick={open} className="absolute left-2 top-1/2 transform -translate-y-1/2 p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" - title="Attach images" + title={t('input.attachImages')} > @@ -5480,8 +5487,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess input.trim() ? 'opacity-0' : 'opacity-100' }`}> {sendByCtrlEnter - ? "Ctrl+Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands" - : "Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands"} + ? t('input.hintText.ctrlEnter') + : t('input.hintText.enter')}
diff --git a/src/components/CredentialsSettings.jsx b/src/components/CredentialsSettings.jsx index 1150b9f..cc7d424 100644 --- a/src/components/CredentialsSettings.jsx +++ b/src/components/CredentialsSettings.jsx @@ -5,8 +5,10 @@ import { Key, Plus, Trash2, Eye, EyeOff, Copy, Check, Github, ExternalLink } fro import { useVersionCheck } from '../hooks/useVersionCheck'; import { version } from '../../package.json'; import { authenticatedFetch } from '../utils/api'; +import { useTranslation } from 'react-i18next'; function CredentialsSettings() { + const { t } = useTranslation('settings'); const [apiKeys, setApiKeys] = useState([]); const [githubCredentials, setGithubCredentials] = useState([]); const [loading, setLoading] = useState(true); @@ -69,7 +71,7 @@ function CredentialsSettings() { }; const deleteApiKey = async (keyId) => { - if (!confirm('Are you sure you want to delete this API key?')) return; + if (!confirm(t('apiKeys.confirmDelete'))) return; try { await authenticatedFetch(`/api/settings/api-keys/${keyId}`, { @@ -121,7 +123,7 @@ function CredentialsSettings() { }; const deleteGithubCredential = async (credentialId) => { - if (!confirm('Are you sure you want to delete this GitHub token?')) return; + if (!confirm(t('apiKeys.github.confirmDelete'))) return; try { await authenticatedFetch(`/api/settings/credentials/${credentialId}`, { @@ -152,7 +154,7 @@ function CredentialsSettings() { }; if (loading) { - return
Loading...
; + return
{t('apiKeys.loading')}
; } return ( @@ -160,9 +162,9 @@ function CredentialsSettings() { {/* New API Key Alert */} {newlyCreatedKey && (
-

⚠️ Save Your API Key

+

{t('apiKeys.newKey.alertTitle')}

- This is the only time you'll see this key. Store it securely. + {t('apiKeys.newKey.alertMessage')}

@@ -182,7 +184,7 @@ function CredentialsSettings() { className="mt-3" onClick={() => setNewlyCreatedKey(null)} > - I've saved it + {t('apiKeys.newKey.iveSavedIt')}
)} @@ -192,20 +194,20 @@ function CredentialsSettings() {
-

API Keys

+

{t('apiKeys.title')}

- Generate API keys to access the external API from other applications. + {t('apiKeys.description')}

- API Documentation + {t('apiKeys.apiDocsLink')}
@@ -221,15 +223,15 @@ function CredentialsSettings() { {showNewKeyForm && (
setNewKeyName(e.target.value)} className="mb-2" />
- +
@@ -237,7 +239,7 @@ function CredentialsSettings() {
{apiKeys.length === 0 ? ( -

No API keys created yet.

+

{t('apiKeys.empty')}

) : ( apiKeys.map((key) => (
{key.key_name}
{key.api_key}
- Created: {new Date(key.created_at).toLocaleDateString()} - {key.last_used && ` • Last used: ${new Date(key.last_used).toLocaleDateString()}`} + {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()} + {key.last_used && ` • ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`}
@@ -258,7 +260,7 @@ function CredentialsSettings() { variant={key.is_active ? 'outline' : 'secondary'} onClick={() => toggleApiKey(key.id, key.is_active)} > - {key.is_active ? 'Active' : 'Inactive'} + {key.is_active ? t('apiKeys.status.active') : t('apiKeys.status.inactive')}

- Add GitHub Personal Access Tokens to clone private repositories. You can also pass tokens directly in API requests without storing them. + {t('apiKeys.github.descriptionAlt')}

{showNewGithubForm && (
setNewGithubName(e.target.value)} /> @@ -305,7 +307,7 @@ function CredentialsSettings() {
setNewGithubToken(e.target.value)} className="pr-10" @@ -320,20 +322,20 @@ function CredentialsSettings() {
setNewGithubDescription(e.target.value)} />
- +
@@ -343,14 +345,14 @@ function CredentialsSettings() { rel="noopener noreferrer" className="text-xs text-primary hover:underline block" > - How to create a GitHub Personal Access Token → + {t('apiKeys.github.form.howToCreate')}
)}
{githubCredentials.length === 0 ? ( -

No GitHub tokens added yet.

+

{t('apiKeys.github.empty')}

) : ( githubCredentials.map((credential) => (
{credential.description}
)}
- Added: {new Date(credential.created_at).toLocaleDateString()} + {t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()}
@@ -372,7 +374,7 @@ function CredentialsSettings() { variant={credential.is_active ? 'outline' : 'secondary'} onClick={() => toggleGithubCredential(credential.id, credential.is_active)} > - {credential.is_active ? 'Active' : 'Inactive'} + {credential.is_active ? t('apiKeys.status.active') : t('apiKeys.status.inactive')} {saveStatus === 'success' && (
- Saved successfully + {t('git.status.success')}
)}
diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx index cc8809f..9f541e9 100644 --- a/src/components/Settings.jsx +++ b/src/components/Settings.jsx @@ -950,7 +950,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {

- Settings + {t('title')}

@@ -1035,10 +1035,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
- Dark Mode + {t('appearanceSettings.darkMode.label')}
- Toggle between light and dark themes + {t('appearanceSettings.darkMode.description')}
@@ -1096,17 +1096,17 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) { {/* Code Editor Settings */}
-

Code Editor

+

{t('appearanceSettings.codeEditor.title')}

{/* Editor Theme */}
- Editor Theme + {t('appearanceSettings.codeEditor.theme.label')}
- Default theme for the code editor + {t('appearanceSettings.codeEditor.theme.description')}
@@ -1589,22 +1589,22 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{ setMcpFormData(prev => ({...prev, name: e.target.value})); }} - placeholder="my-server" + placeholder={t('mcpForm.placeholders.serverName')} required />
- + {mcpFormData.importMode === 'form' && (