feat: complete internationalization (i18n) for components

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 等部分)
This commit is contained in:
YuanNiancai
2026-01-21 13:56:49 +08:00
parent 50f8c4ba72
commit 0517ee609e
15 changed files with 1214 additions and 311 deletions

View File

@@ -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 <div className="text-muted-foreground">Loading...</div>;
return <div className="text-muted-foreground">{t('apiKeys.loading')}</div>;
}
return (
@@ -152,9 +154,9 @@ function ApiKeysSettings() {
{/* New API Key Alert */}
{newlyCreatedKey && (
<div className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<h4 className="font-semibold text-yellow-500 mb-2"> Save Your API Key</h4>
<h4 className="font-semibold text-yellow-500 mb-2">{t('apiKeys.newKey.alertTitle')}</h4>
<p className="text-sm text-muted-foreground mb-3">
This is the only time you'll see this key. Store it securely.
{t('apiKeys.newKey.alertMessage')}
</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-background/50 rounded font-mono text-sm break-all">
@@ -174,7 +176,7 @@ function ApiKeysSettings() {
className="mt-3"
onClick={() => setNewlyCreatedKey(null)}
>
I've saved it
{t('apiKeys.newKey.iveSavedIt')}
</Button>
</div>
)}
@@ -184,33 +186,33 @@ function ApiKeysSettings() {
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Key className="h-5 w-5" />
<h3 className="text-lg font-semibold">API Keys</h3>
<h3 className="text-lg font-semibold">{t('apiKeys.title')}</h3>
</div>
<Button
size="sm"
onClick={() => setShowNewKeyForm(!showNewKeyForm)}
>
<Plus className="h-4 w-4 mr-1" />
New API Key
{t('apiKeys.newButton')}
</Button>
</div>
<p className="text-sm text-muted-foreground mb-4">
Generate API keys to access the external API from other applications.
{t('apiKeys.description')}
</p>
{showNewKeyForm && (
<div className="mb-4 p-4 border rounded-lg bg-card">
<Input
placeholder="API Key Name (e.g., Production Server)"
placeholder={t('apiKeys.form.placeholder')}
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
className="mb-2"
/>
<div className="flex gap-2">
<Button onClick={createApiKey}>Create</Button>
<Button onClick={createApiKey}>{t('apiKeys.form.createButton')}</Button>
<Button variant="outline" onClick={() => setShowNewKeyForm(false)}>
Cancel
{t('apiKeys.form.cancelButton')}
</Button>
</div>
</div>
@@ -218,7 +220,7 @@ function ApiKeysSettings() {
<div className="space-y-2">
{apiKeys.length === 0 ? (
<p className="text-sm text-muted-foreground italic">No API keys created yet.</p>
<p className="text-sm text-muted-foreground italic">{t('apiKeys.empty')}</p>
) : (
apiKeys.map((key) => (
<div
@@ -229,8 +231,8 @@ function ApiKeysSettings() {
<div className="font-medium">{key.key_name}</div>
<code className="text-xs text-muted-foreground">{key.api_key}</code>
<div className="text-xs text-muted-foreground mt-1">
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()}`}
</div>
</div>
<div className="flex items-center gap-2">
@@ -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')}
</Button>
<Button
size="sm"
@@ -260,25 +262,25 @@ function ApiKeysSettings() {
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Github className="h-5 w-5" />
<h3 className="text-lg font-semibold">GitHub Tokens</h3>
<h3 className="text-lg font-semibold">{t('apiKeys.github.title')}</h3>
</div>
<Button
size="sm"
onClick={() => setShowNewTokenForm(!showNewTokenForm)}
>
<Plus className="h-4 w-4 mr-1" />
Add Token
{t('apiKeys.github.addButton')}
</Button>
</div>
<p className="text-sm text-muted-foreground mb-4">
Add GitHub Personal Access Tokens to clone private repositories via the external API.
{t('apiKeys.github.description')}
</p>
{showNewTokenForm && (
<div className="mb-4 p-4 border rounded-lg bg-card">
<Input
placeholder="Token Name (e.g., Personal Repos)"
placeholder={t('apiKeys.github.form.namePlaceholder')}
value={newTokenName}
onChange={(e) => setNewTokenName(e.target.value)}
className="mb-2"
@@ -286,7 +288,7 @@ function ApiKeysSettings() {
<div className="relative">
<Input
type={showToken['new'] ? 'text' : 'password'}
placeholder="GitHub Personal Access Token (ghp_...)"
placeholder={t('apiKeys.github.form.tokenPlaceholder')}
value={newGithubToken}
onChange={(e) => setNewGithubToken(e.target.value)}
className="mb-2 pr-10"
@@ -300,13 +302,13 @@ function ApiKeysSettings() {
</button>
</div>
<div className="flex gap-2">
<Button onClick={createGithubToken}>Add Token</Button>
<Button onClick={createGithubToken}>{t('apiKeys.github.form.addButton')}</Button>
<Button variant="outline" onClick={() => {
setShowNewTokenForm(false);
setNewTokenName('');
setNewGithubToken('');
}}>
Cancel
{t('apiKeys.github.form.cancelButton')}
</Button>
</div>
</div>
@@ -314,7 +316,7 @@ function ApiKeysSettings() {
<div className="space-y-2">
{githubTokens.length === 0 ? (
<p className="text-sm text-muted-foreground italic">No GitHub tokens added yet.</p>
<p className="text-sm text-muted-foreground italic">{t('apiKeys.github.empty')}</p>
) : (
githubTokens.map((token) => (
<div
@@ -324,7 +326,7 @@ function ApiKeysSettings() {
<div className="flex-1">
<div className="font-medium">{token.credential_name}</div>
<div className="text-xs text-muted-foreground mt-1">
Added: {new Date(token.created_at).toLocaleDateString()}
{t('apiKeys.github.added')} {new Date(token.created_at).toLocaleDateString()}
</div>
</div>
<div className="flex items-center gap-2">
@@ -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')}
</Button>
<Button
size="sm"
@@ -351,9 +353,9 @@ function ApiKeysSettings() {
{/* Documentation Link */}
<div className="p-4 bg-muted/50 rounded-lg">
<h4 className="font-semibold mb-2">External API Documentation</h4>
<h4 className="font-semibold mb-2">{t('apiKeys.documentation.title')}</h4>
<p className="text-sm text-muted-foreground mb-3">
Learn how to use the external API to trigger Claude/Cursor sessions from your applications.
{t('apiKeys.documentation.description')}
</p>
<a
href="/EXTERNAL_API.md"
@@ -361,7 +363,7 @@ function ApiKeysSettings() {
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
>
View API Documentation
{t('apiKeys.documentation.viewLink')}
</a>
</div>
</div>

View File

@@ -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 (
<code
className={`font-mono text-[0.9em] px-1.5 py-0.5 rounded-md bg-gray-100 text-gray-900 border border-gray-200 dark:bg-gray-800/60 dark:text-gray-100 dark:border-gray-700 whitespace-pre-wrap break-words ${
className || ''
}`}
{...props}
>
{children}
</code>
);
}
// Inline code rendering
if (shouldInline) {
return (
<code
className={`font-mono text-[0.9em] px-1.5 py-0.5 rounded-md bg-gray-100 text-gray-900 border border-gray-200 dark:bg-gray-800/60 dark:text-gray-100 dark:border-gray-700 whitespace-pre-wrap break-words ${
className || ''
}`}
{...props}
>
{children}
</code>
);
}
// 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 ? (
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
Copied
{t('codeBlock.copied')}
</span>
) : (
<span className="flex items-center gap-1">
@@ -427,7 +428,7 @@ const markdownComponents = {
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
Copy
{t('codeBlock.copy')}
</span>
)}
</button>
@@ -452,7 +453,11 @@ const markdownComponents = {
</SyntaxHighlighter>
</div>
);
},
};
// Common markdown components to ensure consistent rendering (tables, inline code, links, etc.)
const markdownComponents = {
code: CodeBlock,
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-600 dark:text-gray-400 my-2">
{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
</div>
)}
<div className="text-sm font-medium text-gray-900 dark:text-white">
{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'))}
</div>
</div>
)}
@@ -615,8 +621,8 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
const input = JSON.parse(message.toolInput);
return (
<span className="font-mono truncate flex-1 min-w-0">
{input.pattern && <span>pattern: <span className="text-blue-600 dark:text-blue-400">{input.pattern}</span></span>}
{input.path && <span className="ml-2">in: {input.path}</span>}
{input.pattern && <span>{t('search.pattern')} <span className="text-blue-600 dark:text-blue-400">{input.pattern}</span></span>}
{input.path && <span className="ml-2">{t('search.in')} {input.path}</span>}
</span>
);
} 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"
>
<span>results</span>
<span>{t('tools.searchResults')}</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
@@ -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')}
>
<svg className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover/btn:text-blue-600 dark:group-hover/btn:text-blue-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
@@ -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')}
>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
@@ -5204,10 +5211,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
: 'bg-blue-500'
}`} />
<span>
{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')}
</span>
</div>
</button>
@@ -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')}
>
<svg
className="w-5 h-5"
@@ -5421,7 +5428,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const isExpanded = e.target.scrollHeight > 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')}
>
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
@@ -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')}
</div>
</div>
</form>

View File

@@ -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 <div className="text-muted-foreground">Loading...</div>;
return <div className="text-muted-foreground">{t('apiKeys.loading')}</div>;
}
return (
@@ -160,9 +162,9 @@ function CredentialsSettings() {
{/* New API Key Alert */}
{newlyCreatedKey && (
<div className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<h4 className="font-semibold text-yellow-500 mb-2"> Save Your API Key</h4>
<h4 className="font-semibold text-yellow-500 mb-2">{t('apiKeys.newKey.alertTitle')}</h4>
<p className="text-sm text-muted-foreground mb-3">
This is the only time you'll see this key. Store it securely.
{t('apiKeys.newKey.alertMessage')}
</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-background/50 rounded font-mono text-sm break-all">
@@ -182,7 +184,7 @@ function CredentialsSettings() {
className="mt-3"
onClick={() => setNewlyCreatedKey(null)}
>
I've saved it
{t('apiKeys.newKey.iveSavedIt')}
</Button>
</div>
)}
@@ -192,20 +194,20 @@ function CredentialsSettings() {
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Key className="h-5 w-5" />
<h3 className="text-lg font-semibold">API Keys</h3>
<h3 className="text-lg font-semibold">{t('apiKeys.title')}</h3>
</div>
<Button
size="sm"
onClick={() => setShowNewKeyForm(!showNewKeyForm)}
>
<Plus className="h-4 w-4 mr-1" />
New API Key
{t('apiKeys.newButton')}
</Button>
</div>
<div className="mb-4">
<p className="text-sm text-muted-foreground mb-2">
Generate API keys to access the external API from other applications.
{t('apiKeys.description')}
</p>
<a
href="/api-docs.html"
@@ -213,7 +215,7 @@ function CredentialsSettings() {
rel="noopener noreferrer"
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
>
API Documentation
{t('apiKeys.apiDocsLink')}
<ExternalLink className="h-3 w-3" />
</a>
</div>
@@ -221,15 +223,15 @@ function CredentialsSettings() {
{showNewKeyForm && (
<div className="mb-4 p-4 border rounded-lg bg-card">
<Input
placeholder="API Key Name (e.g., Production Server)"
placeholder={t('apiKeys.form.placeholder')}
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
className="mb-2"
/>
<div className="flex gap-2">
<Button onClick={createApiKey}>Create</Button>
<Button onClick={createApiKey}>{t('apiKeys.form.createButton')}</Button>
<Button variant="outline" onClick={() => setShowNewKeyForm(false)}>
Cancel
{t('apiKeys.form.cancelButton')}
</Button>
</div>
</div>
@@ -237,7 +239,7 @@ function CredentialsSettings() {
<div className="space-y-2">
{apiKeys.length === 0 ? (
<p className="text-sm text-muted-foreground italic">No API keys created yet.</p>
<p className="text-sm text-muted-foreground italic">{t('apiKeys.empty')}</p>
) : (
apiKeys.map((key) => (
<div
@@ -248,8 +250,8 @@ function CredentialsSettings() {
<div className="font-medium">{key.key_name}</div>
<code className="text-xs text-muted-foreground">{key.api_key}</code>
<div className="text-xs text-muted-foreground mt-1">
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()}`}
</div>
</div>
<div className="flex items-center gap-2">
@@ -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')}
</Button>
<Button
size="sm"
@@ -279,25 +281,25 @@ function CredentialsSettings() {
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Github className="h-5 w-5" />
<h3 className="text-lg font-semibold">GitHub Credentials</h3>
<h3 className="text-lg font-semibold">{t('apiKeys.github.title')}</h3>
</div>
<Button
size="sm"
onClick={() => setShowNewGithubForm(!showNewGithubForm)}
>
<Plus className="h-4 w-4 mr-1" />
Add Token
{t('apiKeys.github.addButton')}
</Button>
</div>
<p className="text-sm text-muted-foreground mb-4">
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')}
</p>
{showNewGithubForm && (
<div className="mb-4 p-4 border rounded-lg bg-card space-y-3">
<Input
placeholder="Token Name (e.g., Personal Repos)"
placeholder={t('apiKeys.github.form.namePlaceholder')}
value={newGithubName}
onChange={(e) => setNewGithubName(e.target.value)}
/>
@@ -305,7 +307,7 @@ function CredentialsSettings() {
<div className="relative">
<Input
type={showToken['new'] ? 'text' : 'password'}
placeholder="GitHub Personal Access Token (ghp_...)"
placeholder={t('apiKeys.github.form.tokenPlaceholder')}
value={newGithubToken}
onChange={(e) => setNewGithubToken(e.target.value)}
className="pr-10"
@@ -320,20 +322,20 @@ function CredentialsSettings() {
</div>
<Input
placeholder="Description (optional)"
placeholder={t('apiKeys.github.form.descriptionPlaceholder')}
value={newGithubDescription}
onChange={(e) => setNewGithubDescription(e.target.value)}
/>
<div className="flex gap-2">
<Button onClick={createGithubCredential}>Add Token</Button>
<Button onClick={createGithubCredential}>{t('apiKeys.github.form.addButton')}</Button>
<Button variant="outline" onClick={() => {
setShowNewGithubForm(false);
setNewGithubName('');
setNewGithubToken('');
setNewGithubDescription('');
}}>
Cancel
{t('apiKeys.github.form.cancelButton')}
</Button>
</div>
@@ -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')}
</a>
</div>
)}
<div className="space-y-2">
{githubCredentials.length === 0 ? (
<p className="text-sm text-muted-foreground italic">No GitHub tokens added yet.</p>
<p className="text-sm text-muted-foreground italic">{t('apiKeys.github.empty')}</p>
) : (
githubCredentials.map((credential) => (
<div
@@ -363,7 +365,7 @@ function CredentialsSettings() {
<div className="text-xs text-muted-foreground">{credential.description}</div>
)}
<div className="text-xs text-muted-foreground mt-1">
Added: {new Date(credential.created_at).toLocaleDateString()}
{t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()}
</div>
</div>
<div className="flex items-center gap-2">
@@ -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')}
</Button>
<Button
size="sm"
@@ -406,7 +408,7 @@ function CredentialsSettings() {
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 rounded-full hover:bg-green-500/20 transition-colors not-italic font-medium"
>
<span className="text-[10px]">Update available: v{latestVersion}</span>
<span className="text-[10px]">{t('apiKeys.version.updateAvailable', { version: latestVersion })}</span>
<ExternalLink className="h-2.5 w-2.5" />
</a>
)}

View File

@@ -3,8 +3,10 @@ import { Button } from './ui/button';
import { Input } from './ui/input';
import { GitBranch, Check } from 'lucide-react';
import { authenticatedFetch } from '../utils/api';
import { useTranslation } from 'react-i18next';
function GitSettings() {
const { t } = useTranslation('settings');
const [gitName, setGitName] = useState('');
const [gitEmail, setGitEmail] = useState('');
const [gitConfigLoading, setGitConfigLoading] = useState(false);
@@ -61,17 +63,17 @@ function GitSettings() {
<div>
<div className="flex items-center gap-2 mb-4">
<GitBranch className="h-5 w-5" />
<h3 className="text-lg font-semibold">Git Configuration</h3>
<h3 className="text-lg font-semibold">{t('git.title')}</h3>
</div>
<p className="text-sm text-muted-foreground mb-4">
Configure your git identity for commits. These settings will be applied globally via <code className="bg-muted px-2 py-0.5 rounded text-xs">git config --global</code>
{t('git.description')}
</p>
<div className="p-4 border rounded-lg bg-card space-y-3">
<div>
<label htmlFor="settings-git-name" className="block text-sm font-medium text-foreground mb-2">
Git Name
{t('git.name.label')}
</label>
<Input
id="settings-git-name"
@@ -83,13 +85,13 @@ function GitSettings() {
className="w-full"
/>
<p className="mt-1 text-xs text-muted-foreground">
Your name for git commits
{t('git.name.help')}
</p>
</div>
<div>
<label htmlFor="settings-git-email" className="block text-sm font-medium text-foreground mb-2">
Git Email
{t('git.email.label')}
</label>
<Input
id="settings-git-email"
@@ -101,7 +103,7 @@ function GitSettings() {
className="w-full"
/>
<p className="mt-1 text-xs text-muted-foreground">
Your email for git commits
{t('git.email.help')}
</p>
</div>
@@ -110,13 +112,13 @@ function GitSettings() {
onClick={saveGitConfig}
disabled={gitConfigSaving || !gitName || !gitEmail}
>
{gitConfigSaving ? 'Saving...' : 'Save Configuration'}
{gitConfigSaving ? t('git.actions.saving') : t('git.actions.save')}
</Button>
{saveStatus === 'success' && (
<div className="text-sm text-green-600 dark:text-green-400 flex items-center gap-2">
<Check className="w-4 h-4" />
Saved successfully
{t('git.status.success')}
</div>
)}
</div>

View File

@@ -950,7 +950,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center gap-3">
<SettingsIcon className="w-5 h-5 md:w-6 md:h-6 text-blue-600" />
<h2 className="text-lg md:text-xl font-semibold text-foreground">
Settings
{t('title')}
</h2>
</div>
<Button
@@ -975,7 +975,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
Agents
{t('mainTabs.agents')}
</button>
<button
onClick={() => setActiveTab('appearance')}
@@ -985,7 +985,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
Appearance
{t('mainTabs.appearance')}
</button>
<button
onClick={() => setActiveTab('git')}
@@ -996,7 +996,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
}`}
>
<GitBranch className="w-4 h-4 inline mr-2" />
Git
{t('mainTabs.git')}
</button>
<button
onClick={() => setActiveTab('api')}
@@ -1007,7 +1007,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
}`}
>
<Key className="w-4 h-4 inline mr-2" />
API & Tokens
{t('mainTabs.apiTokens')}
</button>
<button
onClick={() => setActiveTab('tasks')}
@@ -1017,7 +1017,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
Tasks
{t('mainTabs.tasks')}
</button>
</div>
</div>
@@ -1035,10 +1035,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Dark Mode
{t('appearanceSettings.darkMode.label')}
</div>
<div className="text-sm text-muted-foreground">
Toggle between light and dark themes
{t('appearanceSettings.darkMode.description')}
</div>
</div>
<button
@@ -1076,10 +1076,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Project Sorting
{t('appearanceSettings.projectSorting.label')}
</div>
<div className="text-sm text-muted-foreground">
How projects are ordered in the sidebar
{t('appearanceSettings.projectSorting.description')}
</div>
</div>
<select
@@ -1087,8 +1087,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
onChange={(e) => setProjectSortOrder(e.target.value)}
className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-32"
>
<option value="name">Alphabetical</option>
<option value="date">Recent Activity</option>
<option value="name">{t('appearanceSettings.projectSorting.alphabetical')}</option>
<option value="date">{t('appearanceSettings.projectSorting.recentActivity')}</option>
</select>
</div>
</div>
@@ -1096,17 +1096,17 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{/* Code Editor Settings */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground">Code Editor</h3>
<h3 className="text-lg font-semibold text-foreground">{t('appearanceSettings.codeEditor.title')}</h3>
{/* Editor Theme */}
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Editor Theme
{t('appearanceSettings.codeEditor.theme.label')}
</div>
<div className="text-sm text-muted-foreground">
Default theme for the code editor
{t('appearanceSettings.codeEditor.theme.description')}
</div>
</div>
<button
@@ -1137,10 +1137,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Word Wrap
{t('appearanceSettings.codeEditor.wordWrap.label')}
</div>
<div className="text-sm text-muted-foreground">
Enable word wrapping by default in the editor
{t('appearanceSettings.codeEditor.wordWrap.description')}
</div>
</div>
<button
@@ -1165,10 +1165,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Show Minimap
{t('appearanceSettings.codeEditor.showMinimap.label')}
</div>
<div className="text-sm text-muted-foreground">
Display a minimap for easier navigation in diff view
{t('appearanceSettings.codeEditor.showMinimap.description')}
</div>
</div>
<button
@@ -1193,10 +1193,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Show Line Numbers
{t('appearanceSettings.codeEditor.lineNumbers.label')}
</div>
<div className="text-sm text-muted-foreground">
Display line numbers in the editor
{t('appearanceSettings.codeEditor.lineNumbers.description')}
</div>
</div>
<button
@@ -1221,10 +1221,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Font Size
{t('appearanceSettings.codeEditor.fontSize.label')}
</div>
<div className="text-sm text-muted-foreground">
Editor font size in pixels
{t('appearanceSettings.codeEditor.fontSize.description')}
</div>
</div>
<select
@@ -1452,7 +1452,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="bg-background border border-border rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-4 border-b border-border">
<h3 className="text-lg font-medium text-foreground">
{editingMcpServer ? 'Edit MCP Server' : 'Add MCP Server'}
{editingMcpServer ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
</h3>
<Button variant="ghost" size="sm" onClick={resetMcpForm}>
<X className="w-4 h-4" />
@@ -1472,7 +1472,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
Form Input
{t('mcpForm.importMode.form')}
</button>
<button
type="button"
@@ -1483,7 +1483,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
JSON Import
{t('mcpForm.importMode.json')}
</button>
</div>
)}
@@ -1492,12 +1492,12 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{mcpFormData.importMode === 'form' && editingMcpServer && (
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-3">
<label className="block text-sm font-medium text-foreground mb-2">
Scope
{t('mcpForm.scope.label')}
</label>
<div className="flex items-center gap-2">
{mcpFormData.scope === 'user' ? <Globe className="w-4 h-4" /> : <FolderOpen className="w-4 h-4" />}
<span className="text-sm">
{mcpFormData.scope === 'user' ? 'User (Global)' : 'Project (Local)'}
{mcpFormData.scope === 'user' ? t('mcpForm.scope.userGlobal') : t('mcpForm.scope.projectLocal')}
</span>
{mcpFormData.scope === 'local' && mcpFormData.projectPath && (
<span className="text-xs text-muted-foreground">
@@ -1506,7 +1506,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Scope cannot be changed when editing an existing server
{t('mcpForm.scope.cannotChange')}
</p>
</div>
)}
@@ -1516,7 +1516,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Scope *
{t('mcpForm.scope.label')} *
</label>
<div className="flex gap-2">
<button
@@ -1530,7 +1530,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
>
<div className="flex items-center justify-center gap-2">
<Globe className="w-4 h-4" />
<span>User (Global)</span>
<span>{t('mcpForm.scope.userGlobal')}</span>
</div>
</button>
<button
@@ -1544,14 +1544,14 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
>
<div className="flex items-center justify-center gap-2">
<FolderOpen className="w-4 h-4" />
<span>Project (Local)</span>
<span>{t('mcpForm.scope.projectLocal')}</span>
</div>
</button>
</div>
<p className="text-xs text-muted-foreground mt-2">
{mcpFormData.scope === 'user'
? 'User scope: Available across all projects on your machine'
: 'Local scope: Only available in the selected project'
{mcpFormData.scope === 'user'
? t('mcpForm.scope.userDescription')
: t('mcpForm.scope.projectDescription')
}
</p>
</div>
@@ -1560,7 +1560,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{mcpFormData.scope === 'local' && !editingMcpServer && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Project *
{t('mcpForm.fields.selectProject')} *
</label>
<select
value={mcpFormData.projectPath}
@@ -1568,7 +1568,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
required={mcpFormData.scope === 'local'}
>
<option value="">Select a project...</option>
<option value="">{t('mcpForm.fields.selectProject')}...</option>
{projects.map(project => (
<option key={project.name} value={project.path || project.fullPath}>
{project.displayName || project.name}
@@ -1577,7 +1577,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
</select>
{mcpFormData.projectPath && (
<p className="text-xs text-muted-foreground mt-1">
Path: {mcpFormData.projectPath}
{t('mcpForm.projectPath', { path: mcpFormData.projectPath })}
</p>
)}
</div>
@@ -1589,22 +1589,22 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={mcpFormData.importMode === 'json' ? 'md:col-span-2' : ''}>
<label className="block text-sm font-medium text-foreground mb-2">
Server Name *
{t('mcpForm.fields.serverName')} *
</label>
<Input
value={mcpFormData.name}
onChange={(e) => {
setMcpFormData(prev => ({...prev, name: e.target.value}));
}}
placeholder="my-server"
placeholder={t('mcpForm.placeholders.serverName')}
required
/>
</div>
{mcpFormData.importMode === 'form' && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Transport Type *
{t('mcpForm.fields.transportType')} *
</label>
<select
value={mcpFormData.type}
@@ -1626,7 +1626,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{editingMcpServer && mcpFormData.raw && mcpFormData.importMode === 'form' && (
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-foreground mb-2">
Configuration Details (from {editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config'})
{t('mcpForm.configDetails', { configFile: editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config' })}
</h4>
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded overflow-x-auto">
{JSON.stringify(mcpFormData.raw, null, 2)}
@@ -1639,7 +1639,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-2">
JSON Configuration *
{t('mcpForm.fields.jsonConfig')} *
</label>
<textarea
value={mcpFormData.jsonInput}
@@ -1651,18 +1651,18 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
const parsed = JSON.parse(e.target.value);
// Basic validation
if (!parsed.type) {
setJsonValidationError('Missing required field: type');
setJsonValidationError(t('mcpForm.validation.missingType'));
} else if (parsed.type === 'stdio' && !parsed.command) {
setJsonValidationError('stdio type requires a command field');
setJsonValidationError(t('mcpForm.validation.stdioRequiresCommand'));
} else if ((parsed.type === 'http' || parsed.type === 'sse') && !parsed.url) {
setJsonValidationError(`${parsed.type} type requires a url field`);
setJsonValidationError(t('mcpForm.validation.httpRequiresUrl', { type: parsed.type }));
} else {
setJsonValidationError('');
}
}
} catch (err) {
if (e.target.value.trim()) {
setJsonValidationError('Invalid JSON format');
setJsonValidationError(t('mcpForm.validation.invalidJson'));
} else {
setJsonValidationError('');
}
@@ -1677,7 +1677,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<p className="text-xs text-red-500 mt-1">{jsonValidationError}</p>
)}
<p className="text-xs text-muted-foreground mt-2">
Paste your MCP server configuration in JSON format. Example formats:
{t('mcpForm.validation.jsonHelp')}
<br /> stdio: {`{"type":"stdio","command":"npx","args":["@upstash/context7-mcp"]}`}
<br /> http/sse: {`{"type":"http","url":"https://api.example.com/mcp"}`}
</p>
@@ -1690,7 +1690,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Command *
{t('mcpForm.fields.command')} *
</label>
<Input
value={mcpFormData.config.command}
@@ -1699,10 +1699,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
required
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Arguments (one per line)
{t('mcpForm.fields.arguments')}
</label>
<textarea
value={Array.isArray(mcpFormData.config.args) ? mcpFormData.config.args.join('\n') : ''}
@@ -1718,7 +1718,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{mcpFormData.importMode === 'form' && (mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
URL *
{t('mcpForm.fields.url')} *
</label>
<Input
value={mcpFormData.config.url}
@@ -1734,7 +1734,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{mcpFormData.importMode === 'form' && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Environment Variables (KEY=value, one per line)
{t('mcpForm.fields.envVars')}
</label>
<textarea
value={Object.entries(mcpFormData.config.env || {}).map(([k, v]) => `${k}=${v}`).join('\n')}
@@ -1758,7 +1758,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
{mcpFormData.importMode === 'form' && (mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Headers (KEY=value, one per line)
{t('mcpForm.fields.headers')}
</label>
<textarea
value={Object.entries(mcpFormData.config.headers || {}).map(([k, v]) => `${k}=${v}`).join('\n')}
@@ -1782,14 +1782,14 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={resetMcpForm}>
Cancel
{t('mcpForm.actions.cancel')}
</Button>
<Button
type="submit"
disabled={mcpLoading}
<Button
type="submit"
disabled={mcpLoading}
className="bg-purple-600 hover:bg-purple-700 disabled:opacity-50"
>
{mcpLoading ? 'Saving...' : (editingMcpServer ? 'Update Server' : 'Add Server')}
{mcpLoading ? t('mcpForm.actions.saving') : (editingMcpServer ? t('mcpForm.actions.updateServer') : t('mcpForm.actions.addServer'))}
</Button>
</div>
</form>
@@ -1803,7 +1803,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="bg-background border border-border rounded-lg w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-4 border-b border-border">
<h3 className="text-lg font-medium text-foreground">
{editingCodexMcpServer ? 'Edit MCP Server' : 'Add MCP Server'}
{editingCodexMcpServer ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
</h3>
<Button variant="ghost" size="sm" onClick={resetCodexMcpForm}>
<X className="w-4 h-4" />
@@ -1813,19 +1813,19 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<form onSubmit={handleCodexMcpSubmit} className="p-4 space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Server Name *
{t('mcpForm.fields.serverName')} *
</label>
<Input
value={codexMcpFormData.name}
onChange={(e) => setCodexMcpFormData(prev => ({...prev, name: e.target.value}))}
placeholder="my-mcp-server"
placeholder={t('mcpForm.placeholders.serverName')}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Command *
{t('mcpForm.fields.command')} *
</label>
<Input
value={codexMcpFormData.config?.command || ''}
@@ -1840,7 +1840,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Arguments (one per line)
{t('mcpForm.fields.arguments')}
</label>
<textarea
value={(codexMcpFormData.config?.args || []).join('\n')}
@@ -1856,7 +1856,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Environment Variables (KEY=VALUE, one per line)
{t('mcpForm.fields.envVars')}
</label>
<textarea
value={Object.entries(codexMcpFormData.config?.env || {}).map(([k, v]) => `${k}=${v}`).join('\n')}
@@ -1881,14 +1881,14 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<div className="flex justify-end gap-2 pt-4 border-t border-border">
<Button type="button" variant="outline" onClick={resetCodexMcpForm}>
Cancel
{t('mcpForm.actions.cancel')}
</Button>
<Button
type="submit"
disabled={codexMcpLoading || !codexMcpFormData.name || !codexMcpFormData.config?.command}
className="bg-green-600 hover:bg-green-700 text-white"
>
{codexMcpLoading ? 'Saving...' : (editingCodexMcpServer ? 'Update Server' : 'Add Server')}
{codexMcpLoading ? t('mcpForm.actions.saving') : (editingCodexMcpServer ? t('mcpForm.actions.updateServer') : t('mcpForm.actions.addServer'))}
</Button>
</div>
</form>
@@ -1919,7 +1919,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
Settings saved successfully!
{t('saveStatus.success')}
</div>
)}
{saveStatus === 'error' && (
@@ -1927,31 +1927,31 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
Failed to save settings
{t('saveStatus.error')}
</div>
)}
</div>
<div className="flex items-center gap-3 order-1 sm:order-2">
<Button
variant="outline"
onClick={onClose}
<Button
variant="outline"
onClick={onClose}
disabled={isSaving}
className="flex-1 sm:flex-none h-10 touch-manipulation"
>
Cancel
{t('footerActions.cancel')}
</Button>
<Button
onClick={saveSettings}
<Button
onClick={saveSettings}
disabled={isSaving}
className="flex-1 sm:flex-none h-10 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 touch-manipulation"
>
{isSaving ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
Saving...
{t('saveStatus.saving')}
</div>
) : (
'Save Settings'
t('footerActions.save')
)}
</Button>
</div>

View File

@@ -1,7 +1,9 @@
import { Zap } from 'lucide-react';
import { useTasksSettings } from '../contexts/TasksSettingsContext';
import { useTranslation } from 'react-i18next';
function TasksSettings() {
const { t } = useTranslation('settings');
const {
tasksEnabled,
setTasksEnabled,
@@ -16,7 +18,7 @@ function TasksSettings() {
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="animate-spin w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full"></div>
<span className="text-sm text-muted-foreground">Checking TaskMaster installation...</span>
<span className="text-sm text-muted-foreground">{t('tasks.checking')}</span>
</div>
</div>
) : (
@@ -32,13 +34,13 @@ function TasksSettings() {
</div>
<div className="flex-1">
<div className="font-medium text-orange-900 dark:text-orange-100 mb-2">
TaskMaster AI CLI Not Installed
{t('tasks.notInstalled.title')}
</div>
<div className="text-sm text-orange-800 dark:text-orange-200 space-y-3">
<p>TaskMaster CLI is required to use task management features. Install it to get started:</p>
<p>{t('tasks.notInstalled.description')}</p>
<div className="bg-orange-100 dark:bg-orange-900/50 rounded-lg p-3 font-mono text-sm">
<code>npm install -g task-master-ai</code>
<code>{t('tasks.notInstalled.installCommand')}</code>
</div>
<div>
@@ -51,7 +53,7 @@ function TasksSettings() {
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
</svg>
View on GitHub
{t('tasks.notInstalled.viewOnGitHub')}
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
@@ -59,11 +61,11 @@ function TasksSettings() {
</div>
<div className="space-y-2">
<p className="font-medium">After installation:</p>
<p className="font-medium">{t('tasks.notInstalled.afterInstallation')}</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Restart this application</li>
<li>TaskMaster features will automatically become available</li>
<li>Use <code className="bg-orange-100 dark:bg-orange-800 px-1 rounded">task-master init</code> in your project directory</li>
<li>{t('tasks.notInstalled.steps.restart')}</li>
<li>{t('tasks.notInstalled.steps.autoAvailable')}</li>
<li>Use <code className="bg-orange-100 dark:bg-orange-800 px-1 rounded">task-master init</code> {t('tasks.notInstalled.steps.initCommand')}</li>
</ol>
</div>
</div>
@@ -79,10 +81,10 @@ function TasksSettings() {
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Enable TaskMaster Integration
{t('tasks.settings.enableLabel')}
</div>
<div className="text-sm text-muted-foreground mt-1">
Show TaskMaster tasks, banners, and sidebar indicators across the interface
{t('tasks.settings.enableDescription')}
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">

View File

@@ -4,6 +4,7 @@ import { LogIn } from 'lucide-react';
import ClaudeLogo from '../ClaudeLogo';
import CursorLogo from '../CursorLogo';
import CodexLogo from '../CodexLogo';
import { useTranslation } from 'react-i18next';
const agentConfig = {
claude: {
@@ -39,6 +40,7 @@ const agentConfig = {
};
export default function AccountContent({ agent, authStatus, onLogin }) {
const { t } = useTranslation('settings');
const config = agentConfig[agent];
const { Logo } = config;
@@ -47,8 +49,8 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
<div className="flex items-center gap-3 mb-4">
<Logo className="w-6 h-6" />
<div>
<h3 className="text-lg font-medium text-foreground">{config.name} Account</h3>
<p className="text-sm text-muted-foreground">{config.description}</p>
<h3 className="text-lg font-medium text-foreground">{config.name}</h3>
<p className="text-sm text-muted-foreground">{t(`agents.account.${agent}.description`)}</p>
</div>
</div>
@@ -58,30 +60,30 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
<div className="flex items-center gap-3">
<div className="flex-1">
<div className={`font-medium ${config.textClass}`}>
Connection Status
{t('agents.connectionStatus')}
</div>
<div className={`text-sm ${config.subtextClass}`}>
{authStatus?.loading ? (
'Checking authentication status...'
t('agents.authStatus.checkingAuth')
) : authStatus?.authenticated ? (
`Logged in as ${authStatus.email || 'authenticated user'}`
t('agents.authStatus.loggedInAs', { email: authStatus.email || t('agents.authStatus.authenticatedUser') })
) : (
'Not connected'
t('agents.authStatus.notConnected')
)}
</div>
</div>
<div>
{authStatus?.loading ? (
<Badge variant="secondary" className="bg-gray-100 dark:bg-gray-800">
Checking...
{t('agents.authStatus.checking')}
</Badge>
) : authStatus?.authenticated ? (
<Badge variant="success" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
Connected
{t('agents.authStatus.connected')}
</Badge>
) : (
<Badge variant="secondary" className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300">
Disconnected
{t('agents.authStatus.disconnected')}
</Badge>
)}
</div>
@@ -91,12 +93,12 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
<div className="flex items-center justify-between">
<div>
<div className={`font-medium ${config.textClass}`}>
{authStatus?.authenticated ? 'Re-authenticate' : 'Login'}
{authStatus?.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
</div>
<div className={`text-sm ${config.subtextClass}`}>
{authStatus?.authenticated
? 'Sign in with a different account or refresh credentials'
: `Sign in to your ${config.name} account to enable AI features`}
? t('agents.login.reAuthDescription')
: t('agents.login.description', { agent: config.name })}
</div>
</div>
<Button
@@ -105,7 +107,7 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
size="sm"
>
<LogIn className="w-4 h-4 mr-2" />
{authStatus?.authenticated ? 'Re-login' : 'Login'}
{authStatus?.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
</Button>
</div>
</div>
@@ -113,7 +115,7 @@ export default function AccountContent({ agent, authStatus, onLogin }) {
{authStatus?.error && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="text-sm text-red-600 dark:text-red-400">
Error: {authStatus.error}
{t('agents.error', { error: authStatus.error })}
</div>
</div>
)}

View File

@@ -1,6 +1,7 @@
import ClaudeLogo from '../ClaudeLogo';
import CursorLogo from '../CursorLogo';
import CodexLogo from '../CodexLogo';
import { useTranslation } from 'react-i18next';
const agentConfig = {
claude: {
@@ -42,6 +43,7 @@ const colorClasses = {
};
export default function AgentListItem({ agentId, authStatus, isSelected, onClick, isMobile = false }) {
const { t } = useTranslation('settings');
const config = agentConfig[agentId];
const colors = colorClasses[config.color];
const { Logo } = config;
@@ -84,18 +86,18 @@ export default function AgentListItem({ agentId, authStatus, isSelected, onClick
</div>
<div className="text-xs text-muted-foreground pl-6">
{authStatus?.loading ? (
<span className="text-gray-400">Checking...</span>
<span className="text-gray-400">{t('agents.authStatus.checking')}</span>
) : authStatus?.authenticated ? (
<div className="flex items-center gap-1">
<span className={`w-1.5 h-1.5 rounded-full ${colors.dot}`} />
<span className="truncate max-w-[120px]" title={authStatus.email}>
{authStatus.email || 'Connected'}
{authStatus.email || t('agents.authStatus.connected')}
</span>
</div>
) : (
<div className="flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
<span>Not connected</span>
<span>{t('agents.authStatus.notConnected')}</span>
</div>
)}
</div>

View File

@@ -3,6 +3,7 @@ import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Badge } from '../ui/badge';
import { Server, Plus, Edit3, Trash2, Terminal, Globe, Zap, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const getTransportIcon = (type) => {
switch (type) {
@@ -25,16 +26,17 @@ function ClaudeMcpServers({
serverTools,
toolsLoading,
}) {
const { t } = useTranslation('settings');
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5 text-purple-500" />
<h3 className="text-lg font-medium text-foreground">
MCP Servers
{t('mcpServers.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Model Context Protocol servers provide additional tools and data sources to Claude
{t('mcpServers.description.claude')}
</p>
<div className="flex justify-between items-center">
@@ -44,7 +46,7 @@ function ClaudeMcpServers({
size="sm"
>
<Plus className="w-4 h-4 mr-2" />
Add MCP Server
{t('mcpServers.addButton')}
</Button>
</div>
@@ -60,19 +62,19 @@ function ClaudeMcpServers({
{server.type}
</Badge>
<Badge variant="outline" className="text-xs">
{server.scope === 'local' ? 'local' : server.scope === 'user' ? 'user' : server.scope}
{server.scope === 'local' ? t('mcpServers.scope.local') : server.scope === 'user' ? t('mcpServers.scope.user') : server.scope}
</Badge>
</div>
<div className="text-sm text-muted-foreground space-y-1">
{server.type === 'stdio' && server.config?.command && (
<div>Command: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
<div>{t('mcpServers.config.command')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
)}
{(server.type === 'sse' || server.type === 'http') && server.config?.url && (
<div>URL: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.url}</code></div>
<div>{t('mcpServers.config.url')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.url}</code></div>
)}
{server.config?.args && server.config.args.length > 0 && (
<div>Args: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code></div>
<div>{t('mcpServers.config.args')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code></div>
)}
</div>
@@ -90,13 +92,13 @@ function ClaudeMcpServers({
{/* Tools Discovery Results */}
{serverTools?.[server.id] && serverTools[server.id].tools?.length > 0 && (
<div className="mt-2 p-2 rounded text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200">
<div className="font-medium">Tools ({serverTools[server.id].tools.length}):</div>
<div className="font-medium">{t('mcpServers.tools.title')} {t('mcpServers.tools.count', { count: serverTools[server.id].tools.length })}</div>
<div className="flex flex-wrap gap-1 mt-1">
{serverTools[server.id].tools.slice(0, 5).map((tool, i) => (
<code key={i} className="bg-blue-100 dark:bg-blue-800 px-1 rounded">{tool.name}</code>
))}
{serverTools[server.id].tools.length > 5 && (
<span className="text-xs opacity-75">+{serverTools[server.id].tools.length - 5} more</span>
<span className="text-xs opacity-75">{t('mcpServers.tools.more', { count: serverTools[server.id].tools.length - 5 })}</span>
)}
</div>
</div>
@@ -109,7 +111,7 @@ function ClaudeMcpServers({
variant="ghost"
size="sm"
className="text-gray-600 hover:text-gray-700"
title="Edit server"
title={t('mcpServers.actions.edit')}
>
<Edit3 className="w-4 h-4" />
</Button>
@@ -118,7 +120,7 @@ function ClaudeMcpServers({
variant="ghost"
size="sm"
className="text-red-600 hover:text-red-700"
title="Delete server"
title={t('mcpServers.actions.delete')}
>
<Trash2 className="w-4 h-4" />
</Button>
@@ -128,7 +130,7 @@ function ClaudeMcpServers({
))}
{servers.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No MCP servers configured
{t('mcpServers.empty')}
</div>
)}
</div>
@@ -138,16 +140,17 @@ function ClaudeMcpServers({
// Cursor MCP Servers
function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
const { t } = useTranslation('settings');
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5 text-purple-500" />
<h3 className="text-lg font-medium text-foreground">
MCP Servers
{t('mcpServers.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Model Context Protocol servers provide additional tools and data sources to Cursor
{t('mcpServers.description.cursor')}
</p>
<div className="flex justify-between items-center">
@@ -157,7 +160,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
size="sm"
>
<Plus className="w-4 h-4 mr-2" />
Add MCP Server
{t('mcpServers.addButton')}
</Button>
</div>
@@ -173,7 +176,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
</div>
<div className="text-sm text-muted-foreground">
{server.config?.command && (
<div>Command: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
<div>{t('mcpServers.config.command')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
)}
</div>
</div>
@@ -183,6 +186,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
variant="ghost"
size="sm"
className="text-gray-600 hover:text-gray-700"
title={t('mcpServers.actions.edit')}
>
<Edit3 className="w-4 h-4" />
</Button>
@@ -191,6 +195,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
variant="ghost"
size="sm"
className="text-red-600 hover:text-red-700"
title={t('mcpServers.actions.delete')}
>
<Trash2 className="w-4 h-4" />
</Button>
@@ -200,7 +205,7 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
))}
{servers.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No MCP servers configured
{t('mcpServers.empty')}
</div>
)}
</div>
@@ -210,16 +215,17 @@ function CursorMcpServers({ servers, onAdd, onEdit, onDelete }) {
// Codex MCP Servers
function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
const { t } = useTranslation('settings');
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Server className="w-5 h-5 text-gray-700 dark:text-gray-300" />
<h3 className="text-lg font-medium text-foreground">
MCP Servers
{t('mcpServers.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Model Context Protocol servers provide additional tools and data sources to Codex
{t('mcpServers.description.codex')}
</p>
<div className="flex justify-between items-center">
@@ -229,7 +235,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
size="sm"
>
<Plus className="w-4 h-4 mr-2" />
Add MCP Server
{t('mcpServers.addButton')}
</Button>
</div>
@@ -246,13 +252,13 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
<div className="text-sm text-muted-foreground space-y-1">
{server.config?.command && (
<div>Command: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
<div>{t('mcpServers.config.command')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
)}
{server.config?.args && server.config.args.length > 0 && (
<div>Args: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code></div>
<div>{t('mcpServers.config.args')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code></div>
)}
{server.config?.env && Object.keys(server.config.env).length > 0 && (
<div>Environment: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{Object.entries(server.config.env).map(([k, v]) => `${k}=${v}`).join(', ')}</code></div>
<div>{t('mcpServers.config.environment')}: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{Object.entries(server.config.env).map(([k, v]) => `${k}=${v}`).join(', ')}</code></div>
)}
</div>
</div>
@@ -263,7 +269,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
variant="ghost"
size="sm"
className="text-gray-600 hover:text-gray-700"
title="Edit server"
title={t('mcpServers.actions.edit')}
>
<Edit3 className="w-4 h-4" />
</Button>
@@ -272,7 +278,7 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
variant="ghost"
size="sm"
className="text-red-600 hover:text-red-700"
title="Delete server"
title={t('mcpServers.actions.delete')}
>
<Trash2 className="w-4 h-4" />
</Button>
@@ -282,17 +288,16 @@ function CodexMcpServers({ servers, onAdd, onEdit, onDelete }) {
))}
{servers.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No MCP servers configured
{t('mcpServers.empty')}
</div>
)}
</div>
{/* Help Section */}
<div className="bg-gray-100 dark:bg-gray-800/50 border border-gray-300 dark:border-gray-600 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">About Codex MCP</h4>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">{t('mcpServers.help.title')}</h4>
<p className="text-sm text-gray-700 dark:text-gray-300">
Codex supports stdio-based MCP servers. You can add servers that extend Codex's capabilities
with additional tools and resources.
{t('mcpServers.help.description')}
</p>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Shield, AlertTriangle, Plus, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
// Common tool patterns for Claude
const commonClaudeTools = [
@@ -49,6 +50,7 @@ function ClaudePermissions({
newDisallowedTool,
setNewDisallowedTool,
}) {
const { t } = useTranslation('settings');
const addAllowedTool = (tool) => {
if (tool && !allowedTools.includes(tool)) {
setAllowedTools([...allowedTools, tool]);
@@ -78,7 +80,7 @@ function ClaudePermissions({
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-orange-500" />
<h3 className="text-lg font-medium text-foreground">
Permission Settings
{t('permissions.title')}
</h3>
</div>
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
@@ -91,10 +93,10 @@ function ClaudePermissions({
/>
<div>
<div className="font-medium text-orange-900 dark:text-orange-100">
Skip permission prompts (use with caution)
{t('permissions.skipPermissions.label')}
</div>
<div className="text-sm text-orange-700 dark:text-orange-300">
Equivalent to --dangerously-skip-permissions flag
{t('permissions.skipPermissions.claudeDescription')}
</div>
</div>
</label>
@@ -106,18 +108,18 @@ function ClaudePermissions({
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-medium text-foreground">
Allowed Tools
{t('permissions.allowedTools.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Tools that are automatically allowed without prompting for permission
{t('permissions.allowedTools.description')}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newAllowedTool}
onChange={(e) => setNewAllowedTool(e.target.value)}
placeholder='e.g., "Bash(git log:*)" or "Write"'
placeholder={t('permissions.allowedTools.placeholder')}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -133,14 +135,14 @@ function ClaudePermissions({
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">Add</span>
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
{/* Quick add buttons */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Quick add common tools:
{t('permissions.allowedTools.quickAdd')}
</p>
<div className="flex flex-wrap gap-2">
{commonClaudeTools.map(tool => (
@@ -176,7 +178,7 @@ function ClaudePermissions({
))}
{allowedTools.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
No allowed tools configured
{t('permissions.allowedTools.empty')}
</div>
)}
</div>
@@ -187,18 +189,18 @@ function ClaudePermissions({
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<h3 className="text-lg font-medium text-foreground">
Blocked Tools
{t('permissions.blockedTools.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Tools that are automatically blocked without prompting for permission
{t('permissions.blockedTools.description')}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newDisallowedTool}
onChange={(e) => setNewDisallowedTool(e.target.value)}
placeholder='e.g., "Bash(rm:*)"'
placeholder={t('permissions.blockedTools.placeholder')}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -214,7 +216,7 @@ function ClaudePermissions({
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">Add</span>
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
@@ -236,7 +238,7 @@ function ClaudePermissions({
))}
{disallowedTools.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
No blocked tools configured
{t('permissions.blockedTools.empty')}
</div>
)}
</div>
@@ -245,13 +247,13 @@ function ClaudePermissions({
{/* Help Section */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
Tool Pattern Examples:
{t('permissions.toolExamples.title')}
</h4>
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git log:*)"</code> - Allow all git log commands</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git diff:*)"</code> - Allow all git diff commands</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Write"</code> - Allow all Write tool usage</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(rm:*)"</code> - Block all rm commands (dangerous)</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git log:*)"</code> {t('permissions.toolExamples.bashGitLog')}</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git diff:*)"</code> {t('permissions.toolExamples.bashGitDiff')}</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Write"</code> {t('permissions.toolExamples.write')}</li>
<li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(rm:*)"</code> {t('permissions.toolExamples.bashRm')}</li>
</ul>
</div>
</div>
@@ -271,6 +273,7 @@ function CursorPermissions({
newDisallowedCommand,
setNewDisallowedCommand,
}) {
const { t } = useTranslation('settings');
const addAllowedCommand = (cmd) => {
if (cmd && !allowedCommands.includes(cmd)) {
setAllowedCommands([...allowedCommands, cmd]);
@@ -300,7 +303,7 @@ function CursorPermissions({
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-orange-500" />
<h3 className="text-lg font-medium text-foreground">
Permission Settings
{t('permissions.title')}
</h3>
</div>
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
@@ -313,10 +316,10 @@ function CursorPermissions({
/>
<div>
<div className="font-medium text-orange-900 dark:text-orange-100">
Skip permission prompts (use with caution)
{t('permissions.skipPermissions.label')}
</div>
<div className="text-sm text-orange-700 dark:text-orange-300">
Equivalent to -f flag in Cursor CLI
{t('permissions.skipPermissions.cursorDescription')}
</div>
</div>
</label>
@@ -328,18 +331,18 @@ function CursorPermissions({
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-medium text-foreground">
Allowed Shell Commands
{t('permissions.allowedCommands.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Shell commands that are automatically allowed without prompting
{t('permissions.allowedCommands.description')}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newAllowedCommand}
onChange={(e) => setNewAllowedCommand(e.target.value)}
placeholder='e.g., "Shell(ls)" or "Shell(git status)"'
placeholder={t('permissions.allowedCommands.placeholder')}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -355,14 +358,14 @@ function CursorPermissions({
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">Add</span>
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
{/* Quick add buttons */}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Quick add common commands:
{t('permissions.allowedCommands.quickAdd')}
</p>
<div className="flex flex-wrap gap-2">
{commonCursorCommands.map(cmd => (
@@ -398,7 +401,7 @@ function CursorPermissions({
))}
{allowedCommands.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
No allowed commands configured
{t('permissions.allowedCommands.empty')}
</div>
)}
</div>
@@ -409,18 +412,18 @@ function CursorPermissions({
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<h3 className="text-lg font-medium text-foreground">
Blocked Shell Commands
{t('permissions.blockedCommands.title')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Shell commands that are automatically blocked
{t('permissions.blockedCommands.description')}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newDisallowedCommand}
onChange={(e) => setNewDisallowedCommand(e.target.value)}
placeholder='e.g., "Shell(rm -rf)" or "Shell(sudo)"'
placeholder={t('permissions.blockedCommands.placeholder')}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -436,7 +439,7 @@ function CursorPermissions({
className="h-10 px-4"
>
<Plus className="w-4 h-4 mr-2 sm:mr-0" />
<span className="sm:hidden">Add</span>
<span className="sm:hidden">{t('permissions.actions.add')}</span>
</Button>
</div>
@@ -458,7 +461,7 @@ function CursorPermissions({
))}
{disallowedCommands.length === 0 && (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
No blocked commands configured
{t('permissions.blockedCommands.empty')}
</div>
)}
</div>
@@ -467,13 +470,13 @@ function CursorPermissions({
{/* Help Section */}
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
<h4 className="font-medium text-purple-900 dark:text-purple-100 mb-2">
Shell Command Examples:
{t('permissions.shellExamples.title')}
</h4>
<ul className="text-sm text-purple-800 dark:text-purple-200 space-y-1">
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(ls)"</code> - Allow ls command</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(git status)"</code> - Allow git status</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(npm install)"</code> - Allow npm install</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(rm -rf)"</code> - Block recursive delete</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(ls)"</code> {t('permissions.shellExamples.ls')}</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(git status)"</code> {t('permissions.shellExamples.gitStatus')}</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(npm install)"</code> {t('permissions.shellExamples.npmInstall')}</li>
<li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(rm -rf)"</code> {t('permissions.shellExamples.rmRf')}</li>
</ul>
</div>
</div>
@@ -482,17 +485,18 @@ function CursorPermissions({
// Codex Permissions
function CodexPermissions({ permissionMode, setPermissionMode }) {
const { t } = useTranslation('settings');
return (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-medium text-foreground">
Permission Mode
{t('permissions.codex.permissionMode')}
</h3>
</div>
<p className="text-sm text-muted-foreground">
Controls how Codex handles file modifications and command execution
{t('permissions.codex.description')}
</p>
{/* Default Mode */}
@@ -513,10 +517,9 @@ function CodexPermissions({ permissionMode, setPermissionMode }) {
className="mt-1 w-4 h-4 text-green-600"
/>
<div>
<div className="font-medium text-foreground">Default</div>
<div className="font-medium text-foreground">{t('permissions.codex.modes.default.title')}</div>
<div className="text-sm text-muted-foreground">
Only trusted commands (ls, cat, grep, git status, etc.) run automatically.
Other commands are skipped. Can write to workspace.
{t('permissions.codex.modes.default.description')}
</div>
</div>
</label>
@@ -540,10 +543,9 @@ function CodexPermissions({ permissionMode, setPermissionMode }) {
className="mt-1 w-4 h-4 text-green-600"
/>
<div>
<div className="font-medium text-green-900 dark:text-green-100">Accept Edits</div>
<div className="font-medium text-green-900 dark:text-green-100">{t('permissions.codex.modes.acceptEdits.title')}</div>
<div className="text-sm text-green-700 dark:text-green-300">
All commands run automatically within the workspace.
Full auto mode with sandboxed execution.
{t('permissions.codex.modes.acceptEdits.description')}
</div>
</div>
</label>
@@ -568,12 +570,11 @@ function CodexPermissions({ permissionMode, setPermissionMode }) {
/>
<div>
<div className="font-medium text-orange-900 dark:text-orange-100 flex items-center gap-2">
Bypass Permissions
{t('permissions.codex.modes.bypassPermissions.title')}
<AlertTriangle className="w-4 h-4" />
</div>
<div className="text-sm text-orange-700 dark:text-orange-300">
Full system access with no restrictions. All commands run automatically
with full disk and network access. Use with caution.
{t('permissions.codex.modes.bypassPermissions.description')}
</div>
</div>
</label>
@@ -582,13 +583,13 @@ function CodexPermissions({ permissionMode, setPermissionMode }) {
{/* Technical Details */}
<details className="text-sm">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Technical details
{t('permissions.codex.technicalDetails')}
</summary>
<div className="mt-2 p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-xs text-muted-foreground space-y-2">
<p><strong>Default:</strong> sandboxMode=workspace-write, approvalPolicy=untrusted. Trusted commands: cat, cd, grep, head, ls, pwd, tail, git status/log/diff/show, find (without -exec), etc.</p>
<p><strong>Accept Edits:</strong> sandboxMode=workspace-write, approvalPolicy=never. All commands auto-execute within project directory.</p>
<p><strong>Bypass Permissions:</strong> sandboxMode=danger-full-access, approvalPolicy=never. Full system access, use only in trusted environments.</p>
<p className="text-xs opacity-75">You can override this per-session using the mode button in the chat interface.</p>
<p><strong>{t('permissions.codex.modes.default.title')}:</strong> {t('permissions.codex.technicalInfo.default')}</p>
<p><strong>{t('permissions.codex.modes.acceptEdits.title')}:</strong> {t('permissions.codex.technicalInfo.acceptEdits')}</p>
<p><strong>{t('permissions.codex.modes.bypassPermissions.title')}:</strong> {t('permissions.codex.technicalInfo.bypassPermissions')}</p>
<p className="text-xs opacity-75">{t('permissions.codex.technicalInfo.overrideNote')}</p>
</div>
</details>
</div>

View File

@@ -18,11 +18,13 @@ import enCommon from './locales/en/common.json';
import enSettings from './locales/en/settings.json';
import enAuth from './locales/en/auth.json';
import enSidebar from './locales/en/sidebar.json';
import enChat from './locales/en/chat.json';
import zhCommon from './locales/zh-CN/common.json';
import zhSettings from './locales/zh-CN/settings.json';
import zhAuth from './locales/zh-CN/auth.json';
import zhSidebar from './locales/zh-CN/sidebar.json';
import zhChat from './locales/zh-CN/chat.json';
// Import supported languages configuration
import { languages } from './languages.js';
@@ -53,12 +55,14 @@ i18n
settings: enSettings,
auth: enAuth,
sidebar: enSidebar,
chat: enChat,
},
'zh-CN': {
common: zhCommon,
settings: zhSettings,
auth: zhAuth,
sidebar: zhSidebar,
chat: zhChat,
},
},
@@ -72,7 +76,7 @@ i18n
debug: import.meta.env.DEV,
// Namespaces - load only what's needed
ns: ['common', 'settings', 'auth', 'sidebar'],
ns: ['common', 'settings', 'auth', 'sidebar', 'chat'],
defaultNS: 'common',
// Key separator for nested keys (default: '.')

View File

@@ -0,0 +1,111 @@
{
"codeBlock": {
"copy": "Copy",
"copied": "Copied",
"copyCode": "Copy code"
},
"messageTypes": {
"user": "U",
"error": "Error",
"tool": "Tool",
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex"
},
"tools": {
"settings": "Tool Settings",
"error": "Tool Error",
"result": "Tool Result",
"viewParams": "View input parameters",
"viewRawParams": "View raw parameters",
"viewDiff": "View edit diff for",
"creatingFile": "Creating new file:",
"updatingTodo": "Updating Todo List",
"read": "Read",
"readFile": "Read file",
"updateTodo": "Update todo list",
"readTodo": "Read todo list",
"searchResults": "results"
},
"search": {
"found": "Found {{count}} {{type}}",
"file": "file",
"files": "files",
"pattern": "pattern:",
"in": "in:"
},
"fileOperations": {
"updated": "File updated successfully",
"created": "File created successfully",
"written": "File written successfully",
"diff": "Diff",
"newFile": "New File",
"viewContent": "View file content",
"viewFullOutput": "View full output ({{count}} chars)",
"contentDisplayed": "The file content is displayed in the diff view above"
},
"interactive": {
"title": "Interactive Prompt",
"waiting": "Waiting for your response in the CLI",
"instruction": "Please select an option in your terminal where Claude is running.",
"selectedOption": "✓ Claude selected option {{number}}",
"instructionDetail": "In the CLI, you would select this option interactively using arrow keys or by typing the number."
},
"thinking": {
"title": "Thinking...",
"emoji": "💭 Thinking..."
},
"json": {
"response": "JSON Response"
},
"permissions": {
"grant": "Grant permission for {{tool}}",
"added": "Permission added",
"addTo": "Adds <span class=\"font-mono\">{{entry}}</span> to Allowed Tools.",
"retry": "Permission saved. Retry the request to use the tool.",
"error": "Unable to update permissions. Please try again.",
"openSettings": "Open settings"
},
"todo": {
"updated": "Todo list has been updated successfully",
"current": "Current Todo List"
},
"plan": {
"viewPlan": "📋 View implementation plan",
"title": "Implementation Plan"
},
"usageLimit": {
"resetAt": "Claude usage limit reached. Your limit will reset at **{{time}} {{timezone}}** - {{date}}"
},
"codex": {
"permissionMode": "Permission Mode",
"modes": {
"default": "Default Mode",
"acceptEdits": "Accept Edits",
"bypassPermissions": "Bypass Permissions",
"plan": "Plan Mode"
},
"descriptions": {
"default": "Only trusted commands (ls, cat, grep, git status, etc.) run automatically. Other commands are skipped. Can write to workspace.",
"acceptEdits": "All commands run automatically within the workspace. Full auto mode with sandboxed execution.",
"bypassPermissions": "Full system access with no restrictions. All commands run automatically with full disk and network access. Use with caution.",
"plan": "Planning mode - no commands are executed"
},
"technicalDetails": "Technical details"
},
"input": {
"placeholder": "Type / for commands, @ for files, or ask {{provider}} anything...",
"placeholderDefault": "Type your message...",
"disabled": "Input disabled",
"attachFiles": "Attach files",
"attachImages": "Attach images",
"send": "Send",
"stop": "Stop",
"hintText": {
"ctrlEnter": "Ctrl+Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands",
"enter": "Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands"
},
"clickToChangeMode": "Click to change permission mode (or press Tab in input)",
"showAllCommands": "Show all commands"
}
}

View File

@@ -7,6 +7,7 @@
"appearance": "Appearance"
},
"account": {
"title": "Account",
"language": "Language",
"languageLabel": "Display Language",
"languageDescription": "Choose your preferred language for the interface",
@@ -15,13 +16,6 @@
"profile": "Profile",
"changePassword": "Change Password"
},
"permissions": {
"allowedTools": "Allowed Tools",
"disallowedTools": "Disallowed Tools",
"addTool": "Add Tool",
"removeTool": "Remove Tool",
"description": "Configure which tools Claude can use. Tools must be enabled here before Claude can access them."
},
"mcp": {
"title": "MCP Servers",
"addServer": "Add Server",
@@ -71,5 +65,337 @@
"autoScrollToBottom": "Auto-scroll to bottom",
"sendByCtrlEnter": "Send by Ctrl+Enter",
"sendByCtrlEnterDescription": "When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends."
},
"mainTabs": {
"agents": "Agents",
"appearance": "Appearance",
"git": "Git",
"apiTokens": "API & Tokens",
"tasks": "Tasks"
},
"appearanceSettings": {
"darkMode": {
"label": "Dark Mode",
"description": "Toggle between light and dark themes"
},
"projectSorting": {
"label": "Project Sorting",
"description": "How projects are ordered in the sidebar",
"alphabetical": "Alphabetical",
"recentActivity": "Recent Activity"
},
"codeEditor": {
"title": "Code Editor",
"theme": {
"label": "Editor Theme",
"description": "Default theme for the code editor"
},
"wordWrap": {
"label": "Word Wrap",
"description": "Enable word wrapping by default in the editor"
},
"showMinimap": {
"label": "Show Minimap",
"description": "Display a minimap for easier navigation in diff view"
},
"lineNumbers": {
"label": "Show Line Numbers",
"description": "Display line numbers in the editor"
},
"fontSize": {
"label": "Font Size",
"description": "Editor font size in pixels"
}
}
},
"mcpForm": {
"title": {
"add": "Add MCP Server",
"edit": "Edit MCP Server"
},
"importMode": {
"form": "Form Input",
"json": "JSON Import"
},
"scope": {
"label": "Scope",
"userGlobal": "User (Global)",
"projectLocal": "Project (Local)",
"userDescription": "User scope: Available across all projects on your machine",
"projectDescription": "Local scope: Only available in the selected project",
"cannotChange": "Scope cannot be changed when editing an existing server"
},
"fields": {
"serverName": "Server Name",
"transportType": "Transport Type",
"command": "Command",
"arguments": "Arguments (one per line)",
"jsonConfig": "JSON Configuration",
"url": "URL",
"envVars": "Environment Variables (KEY=value, one per line)",
"headers": "Headers (KEY=value, one per line)",
"selectProject": "Select a project..."
},
"placeholders": {
"serverName": "my-server"
},
"validation": {
"missingType": "Missing required field: type",
"stdioRequiresCommand": "stdio type requires a command field",
"httpRequiresUrl": "{{type}} type requires a url field",
"invalidJson": "Invalid JSON format",
"jsonHelp": "Paste your MCP server configuration in JSON format. Example formats:",
"jsonExampleStdio": "• stdio: {\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"@upstash/context7-mcp\"]}",
"jsonExampleHttp": "• http/sse: {\"type\":\"http\",\"url\":\"https://api.example.com/mcp\"}"
},
"configDetails": "Configuration Details (from {{configFile}})",
"projectPath": "Path: {{path}}",
"actions": {
"cancel": "Cancel",
"saving": "Saving...",
"addServer": "Add Server",
"updateServer": "Update Server"
}
},
"saveStatus": {
"success": "Settings saved successfully!",
"error": "Failed to save settings",
"saving": "Saving..."
},
"footerActions": {
"save": "Save Settings",
"cancel": "Cancel"
},
"git": {
"title": "Git Configuration",
"description": "Configure your git identity for commits. These settings will be applied globally via <code />git config --global<code />",
"name": {
"label": "Git Name",
"help": "Your name for git commits"
},
"email": {
"label": "Git Email",
"help": "Your email for git commits"
},
"actions": {
"save": "Save Configuration",
"saving": "Saving..."
},
"status": {
"success": "Saved successfully"
}
},
"apiKeys": {
"title": "API Keys",
"description": "Generate API keys to access the external API from other applications.",
"newKey": {
"alertTitle": "⚠️ Save Your API Key",
"alertMessage": "This is the only time you'll see this key. Store it securely.",
"iveSavedIt": "I've saved it"
},
"form": {
"placeholder": "API Key Name (e.g., Production Server)",
"createButton": "Create",
"cancelButton": "Cancel"
},
"newButton": "New API Key",
"empty": "No API keys created yet.",
"list": {
"created": "Created:",
"lastUsed": "Last used:"
},
"confirmDelete": "Are you sure you want to delete this API key?",
"status": {
"active": "Active",
"inactive": "Inactive"
},
"github": {
"title": "GitHub Tokens",
"description": "Add GitHub Personal Access Tokens to clone private repositories via the external API.",
"descriptionAlt": "Add GitHub Personal Access Tokens to clone private repositories. You can also pass tokens directly in API requests without storing them.",
"addButton": "Add Token",
"form": {
"namePlaceholder": "Token Name (e.g., Personal Repos)",
"tokenPlaceholder": "GitHub Personal Access Token (ghp_...)",
"descriptionPlaceholder": "Description (optional)",
"addButton": "Add Token",
"cancelButton": "Cancel",
"howToCreate": "How to create a GitHub Personal Access Token →"
},
"empty": "No GitHub tokens added yet.",
"added": "Added:",
"confirmDelete": "Are you sure you want to delete this GitHub token?"
},
"apiDocsLink": "API Documentation",
"documentation": {
"title": "External API Documentation",
"description": "Learn how to use the external API to trigger Claude/Cursor sessions from your applications.",
"viewLink": "View API Documentation →"
},
"loading": "Loading...",
"version": {
"updateAvailable": "Update available: v{{version}}"
}
},
"tasks": {
"checking": "Checking TaskMaster installation...",
"notInstalled": {
"title": "TaskMaster AI CLI Not Installed",
"description": "TaskMaster CLI is required to use task management features. Install it to get started:",
"installCommand": "npm install -g task-master-ai",
"viewOnGitHub": "View on GitHub",
"afterInstallation": "After installation:",
"steps": {
"restart": "Restart this application",
"autoAvailable": "TaskMaster features will automatically become available",
"initCommand": "Use task-master init in your project directory"
}
},
"settings": {
"enableLabel": "Enable TaskMaster Integration",
"enableDescription": "Show TaskMaster tasks, banners, and sidebar indicators across the interface"
}
},
"agents": {
"authStatus": {
"checking": "Checking...",
"connected": "Connected",
"notConnected": "Not connected",
"disconnected": "Disconnected",
"checkingAuth": "Checking authentication status...",
"loggedInAs": "Logged in as {{email}}",
"authenticatedUser": "authenticated user"
},
"account": {
"claude": {
"description": "Anthropic Claude AI assistant"
},
"cursor": {
"description": "Cursor AI-powered code editor"
},
"codex": {
"description": "OpenAI Codex AI assistant"
}
},
"connectionStatus": "Connection Status",
"login": {
"title": "Login",
"reAuthenticate": "Re-authenticate",
"description": "Sign in to your {{agent}} account to enable AI features",
"reAuthDescription": "Sign in with a different account or refresh credentials",
"button": "Login",
"reLoginButton": "Re-login"
},
"error": "Error: {{error}}"
},
"permissions": {
"title": "Permission Settings",
"skipPermissions": {
"label": "Skip permission prompts (use with caution)",
"claudeDescription": "Equivalent to --dangerously-skip-permissions flag",
"cursorDescription": "Equivalent to -f flag in Cursor CLI"
},
"allowedTools": {
"title": "Allowed Tools",
"description": "Tools that are automatically allowed without prompting for permission",
"placeholder": "e.g., \"Bash(git log:*)\" or \"Write\"",
"quickAdd": "Quick add common tools:",
"empty": "No allowed tools configured"
},
"blockedTools": {
"title": "Blocked Tools",
"description": "Tools that are automatically blocked without prompting for permission",
"placeholder": "e.g., \"Bash(rm:*)\"",
"empty": "No blocked tools configured"
},
"allowedCommands": {
"title": "Allowed Shell Commands",
"description": "Shell commands that are automatically allowed without prompting",
"placeholder": "e.g., \"Shell(ls)\" or \"Shell(git status)\"",
"quickAdd": "Quick add common commands:",
"empty": "No allowed commands configured"
},
"blockedCommands": {
"title": "Blocked Shell Commands",
"description": "Shell commands that are automatically blocked",
"placeholder": "e.g., \"Shell(rm -rf)\" or \"Shell(sudo)\"",
"empty": "No blocked commands configured"
},
"toolExamples": {
"title": "Tool Pattern Examples:",
"bashGitLog": "- Allow all git log commands",
"bashGitDiff": "- Allow all git diff commands",
"write": "- Allow all Write tool usage",
"bashRm": "- Block all rm commands (dangerous)"
},
"shellExamples": {
"title": "Shell Command Examples:",
"ls": "- Allow ls command",
"gitStatus": "- Allow git status",
"npmInstall": "- Allow npm install",
"rmRf": "- Block recursive delete"
},
"codex": {
"permissionMode": "Permission Mode",
"description": "Controls how Codex handles file modifications and command execution",
"modes": {
"default": {
"title": "Default",
"description": "Only trusted commands (ls, cat, grep, git status, etc.) run automatically. Other commands are skipped. Can write to workspace."
},
"acceptEdits": {
"title": "Accept Edits",
"description": "All commands run automatically within the workspace. Full auto mode with sandboxed execution."
},
"bypassPermissions": {
"title": "Bypass Permissions",
"description": "Full system access with no restrictions. All commands run automatically with full disk and network access. Use with caution."
}
},
"technicalDetails": "Technical details",
"technicalInfo": {
"default": "sandboxMode=workspace-write, approvalPolicy=untrusted. Trusted commands: cat, cd, grep, head, ls, pwd, tail, git status/log/diff/show, find (without -exec), etc.",
"acceptEdits": "sandboxMode=workspace-write, approvalPolicy=never. All commands auto-execute within project directory.",
"bypassPermissions": "sandboxMode=danger-full-access, approvalPolicy=never. Full system access, use only in trusted environments.",
"overrideNote": "You can override this per-session using the mode button in the chat interface."
}
},
"actions": {
"add": "Add"
}
},
"mcpServers": {
"title": "MCP Servers",
"description": {
"claude": "Model Context Protocol servers provide additional tools and data sources to Claude",
"cursor": "Model Context Protocol servers provide additional tools and data sources to Cursor",
"codex": "Model Context Protocol servers provide additional tools and data sources to Codex"
},
"addButton": "Add MCP Server",
"empty": "No MCP servers configured",
"serverType": "Type",
"scope": {
"local": "local",
"user": "user"
},
"config": {
"command": "Command",
"url": "URL",
"args": "Args",
"environment": "Environment"
},
"tools": {
"title": "Tools",
"count": "({{count}}):",
"more": "+{{count}} more"
},
"actions": {
"edit": "Edit server",
"delete": "Delete server"
},
"help": {
"title": "About Codex MCP",
"description": "Codex supports stdio-based MCP servers. You can add servers that extend Codex's capabilities with additional tools and resources."
}
}
}

View File

@@ -0,0 +1,111 @@
{
"codeBlock": {
"copy": "复制",
"copied": "已复制",
"copyCode": "复制代码"
},
"messageTypes": {
"user": "U",
"error": "错误",
"tool": "工具",
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex"
},
"tools": {
"settings": "工具设置",
"error": "工具错误",
"result": "工具结果",
"viewParams": "查看输入参数",
"viewRawParams": "查看原始参数",
"viewDiff": "查看编辑差异",
"creatingFile": "创建新文件:",
"updatingTodo": "更新待办事项",
"read": "读取",
"readFile": "读取文件",
"updateTodo": "更新待办列表",
"readTodo": "读取待办列表",
"searchResults": "结果"
},
"search": {
"found": "找到 {{count}} 个{{type}}",
"file": "文件",
"files": "文件",
"pattern": "模式:",
"in": "在:"
},
"fileOperations": {
"updated": "文件更新成功",
"created": "文件创建成功",
"written": "文件写入成功",
"diff": "差异",
"newFile": "新文件",
"viewContent": "查看文件内容",
"viewFullOutput": "查看完整输出({{count}} 个字符)",
"contentDisplayed": "文件内容显示在上面的差异视图中"
},
"interactive": {
"title": "交互式提示",
"waiting": "等待您在 CLI 中响应",
"instruction": "请在 Claude 运行的终端中选择一个选项。",
"selectedOption": "✓ Claude 选择了选项 {{number}}",
"instructionDetail": "在 CLI 中,您可以使用方向键或输入数字来交互式地选择此选项。"
},
"thinking": {
"title": "思考中...",
"emoji": "💭 思考中..."
},
"json": {
"response": "JSON 响应"
},
"permissions": {
"grant": "授予 {{tool}} 权限",
"added": "权限已添加",
"addTo": "将 <span class=\"font-mono\">{{entry}}</span> 添加到允许的工具。",
"retry": "权限已保存。重试请求以使用该工具。",
"error": "无法更新权限。请重试。",
"openSettings": "打开设置"
},
"todo": {
"updated": "待办列表已成功更新",
"current": "当前待办列表"
},
"plan": {
"viewPlan": "📋 查看实施计划",
"title": "实施计划"
},
"usageLimit": {
"resetAt": "Claude 使用限制已达到。您的限制将在 **{{time}} {{timezone}}** - {{date}} 重置"
},
"codex": {
"permissionMode": "权限模式",
"modes": {
"default": "默认模式",
"acceptEdits": "编辑模式",
"bypassPermissions": "无限制模式",
"plan": "计划模式"
},
"descriptions": {
"default": "只有受信任的命令ls、cat、grep、git status 等)自动运行。其他命令将被跳过。可以写入工作区。",
"acceptEdits": "工作区内的所有命令自动运行。完全自动模式,具有沙盒执行功能。",
"bypassPermissions": "完全的系统访问,无限制。所有命令自动运行,具有完整的磁盘和网络访问权限。请谨慎使用。",
"plan": "计划模式 - 不执行任何命令"
},
"technicalDetails": "技术细节"
},
"input": {
"placeholder": "输入 / 调用命令,@ 选择文件,或向 {{provider}} 提问...",
"placeholderDefault": "输入您的消息...",
"disabled": "输入已禁用",
"attachFiles": "附加文件",
"attachImages": "附加图片",
"send": "发送",
"stop": "停止",
"hintText": {
"ctrlEnter": "Ctrl+Enter 发送 • Shift+Enter 换行 • Tab 切换模式 • / 斜杠命令",
"enter": "Enter 发送 • Shift+Enter 换行 • Tab 切换模式 • / 斜杠命令"
},
"clickToChangeMode": "点击更改权限模式(或在输入框中按 Tab",
"showAllCommands": "显示所有命令"
}
}

View File

@@ -7,6 +7,7 @@
"appearance": "外观"
},
"account": {
"title": "账户",
"language": "语言",
"languageLabel": "显示语言",
"languageDescription": "选择您偏好的界面语言",
@@ -15,13 +16,6 @@
"profile": "个人资料",
"changePassword": "修改密码"
},
"permissions": {
"allowedTools": "允许的工具",
"disallowedTools": "禁止的工具",
"addTool": "添加工具",
"removeTool": "移除工具",
"description": "配置 Claude 可以使用的工具。工具必须在此处启用后Claude 才能访问它们。"
},
"mcp": {
"title": "MCP 服务器",
"addServer": "添加服务器",
@@ -71,5 +65,337 @@
"autoScrollToBottom": "自动滚动到底部",
"sendByCtrlEnter": "使用 Ctrl+Enter 发送",
"sendByCtrlEnterDescription": "启用后,按 Ctrl+Enter 发送消息,而不是仅按 Enter。这对于使用输入法的用户可以避免意外发送。"
},
"mainTabs": {
"agents": "智能体",
"appearance": "外观",
"git": "Git",
"apiTokens": "API 和令牌",
"tasks": "任务"
},
"appearanceSettings": {
"darkMode": {
"label": "深色模式",
"description": "切换浅色和深色主题"
},
"projectSorting": {
"label": "项目排序",
"description": "项目在侧边栏中的排列方式",
"alphabetical": "按字母顺序",
"recentActivity": "最近活动"
},
"codeEditor": {
"title": "代码编辑器",
"theme": {
"label": "编辑器主题",
"description": "代码编辑器的默认主题"
},
"wordWrap": {
"label": "自动换行",
"description": "在编辑器中默认启用自动换行"
},
"showMinimap": {
"label": "显示缩略图",
"description": "在差异视图中显示缩略图以便于导航"
},
"lineNumbers": {
"label": "显示行号",
"description": "在编辑器中显示行号"
},
"fontSize": {
"label": "字体大小",
"description": "编辑器字体大小px"
}
}
},
"mcpForm": {
"title": {
"add": "添加 MCP 服务器",
"edit": "编辑 MCP 服务器"
},
"importMode": {
"form": "表单输入",
"json": "JSON 导入"
},
"scope": {
"label": "范围",
"userGlobal": "用户(全局)",
"projectLocal": "项目(本地)",
"userDescription": "用户范围:在您机器上的所有项目中可用",
"projectDescription": "本地范围:仅在选定项目中可用",
"cannotChange": "编辑现有服务器时无法更改范围"
},
"fields": {
"serverName": "服务器名称",
"transportType": "传输类型",
"command": "命令",
"arguments": "参数(每行一个)",
"jsonConfig": "JSON 配置",
"url": "URL",
"envVars": "环境变量KEY=值,每行一个)",
"headers": "请求头KEY=值,每行一个)",
"selectProject": "选择项目..."
},
"placeholders": {
"serverName": "我的服务"
},
"validation": {
"missingType": "缺少必填字段type",
"stdioRequiresCommand": "stdio 类型需要 command 字段",
"httpRequiresUrl": "{{type}} 类型需要 url 字段",
"invalidJson": "无效的 JSON 格式",
"jsonHelp": "粘贴您的 MCP 服务器配置JSON 格式)。示例格式:",
"jsonExampleStdio": "• stdio: {\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"@upstash/context7-mcp\"]}",
"jsonExampleHttp": "• http/sse: {\"type\":\"http\",\"url\":\"https://api.example.com/mcp\"}"
},
"configDetails": "配置详细信息(来自 {{configFile}}",
"projectPath": "路径:{{path}}",
"actions": {
"cancel": "取消",
"saving": "保存中...",
"addServer": "添加服务器",
"updateServer": "更新服务器"
}
},
"saveStatus": {
"success": "设置保存成功!",
"error": "保存设置失败",
"saving": "保存中..."
},
"footerActions": {
"save": "保存设置",
"cancel": "取消"
},
"git": {
"title": "Git 配置",
"description": "配置您的 git 提交身份。这些设置将通过 <code />git config --global<code /> 全局应用",
"name": {
"label": "Git 名称",
"help": "您的 git 提交名称"
},
"email": {
"label": "Git 邮箱",
"help": "您的 git 提交邮箱"
},
"actions": {
"save": "保存配置",
"saving": "保存中..."
},
"status": {
"success": "保存成功"
}
},
"apiKeys": {
"title": "API 密钥",
"description": "生成 API 密钥以从其他应用访问外部 API。",
"newKey": {
"alertTitle": "⚠️ 保存您的 API 密钥",
"alertMessage": "这是您唯一一次看到此密钥。请妥善保存。",
"iveSavedIt": "我已保存"
},
"form": {
"placeholder": "API 密钥名称(例如:生产服务器)",
"createButton": "创建",
"cancelButton": "取消"
},
"newButton": "新建 API 密钥",
"empty": "尚未创建 API 密钥。",
"list": {
"created": "创建时间:",
"lastUsed": "最后使用:"
},
"confirmDelete": "确定要删除此 API 密钥吗?",
"status": {
"active": "激活",
"inactive": "未激活"
},
"github": {
"title": "GitHub 令牌",
"description": "添加 GitHub 个人访问令牌以通过外部 API 克隆私有仓库。",
"descriptionAlt": "添加 GitHub 个人访问令牌以克隆私有仓库。您也可以直接在 API 请求中传递令牌而无需存储。",
"addButton": "添加令牌",
"form": {
"namePlaceholder": "令牌名称(例如:个人仓库)",
"tokenPlaceholder": "GitHub 个人访问令牌ghp_...",
"descriptionPlaceholder": "描述(可选)",
"addButton": "添加令牌",
"cancelButton": "取消",
"howToCreate": "如何创建 GitHub 个人访问令牌 →"
},
"empty": "尚未添加 GitHub 令牌。",
"added": "添加时间:",
"confirmDelete": "确定要删除此 GitHub 令牌吗?"
},
"apiDocsLink": "API 文档",
"documentation": {
"title": "外部 API 文档",
"description": "了解如何使用外部 API 从您的应用程序触发 Claude/Cursor 会话。",
"viewLink": "查看 API 文档 →"
},
"loading": "加载中...",
"version": {
"updateAvailable": "有可用更新v{{version}}"
}
},
"tasks": {
"checking": "正在检查 TaskMaster 安装...",
"notInstalled": {
"title": "未安装 TaskMaster AI CLI",
"description": "需要 TaskMaster CLI 才能使用任务管理功能。安装它以开始使用:",
"installCommand": "npm install -g task-master-ai",
"viewOnGitHub": "在 GitHub 上查看",
"afterInstallation": "安装后:",
"steps": {
"restart": "重启此应用程序",
"autoAvailable": "TaskMaster 功能将自动可用",
"initCommand": "在项目目录中使用 task-master init"
}
},
"settings": {
"enableLabel": "启用 TaskMaster 集成",
"enableDescription": "在整个界面中显示 TaskMaster 任务、横幅和侧边栏指示器"
}
},
"agents": {
"authStatus": {
"checking": "检查中...",
"connected": "已连接",
"notConnected": "未连接",
"disconnected": "已断开",
"checkingAuth": "正在检查认证状态...",
"loggedInAs": "登录为 {{email}}",
"authenticatedUser": "已认证用户"
},
"account": {
"claude": {
"description": "Anthropic Claude AI 助手"
},
"cursor": {
"description": "Cursor AI 驱动的代码编辑器"
},
"codex": {
"description": "OpenAI Codex AI 助手"
}
},
"connectionStatus": "连接状态",
"login": {
"title": "登录",
"reAuthenticate": "重新认证",
"description": "登录您的 {{agent}} 账户以启用 AI 功能",
"reAuthDescription": "使用其他账户登录或刷新凭据",
"button": "登录",
"reLoginButton": "重新登录"
},
"error": "错误:{{error}}"
},
"permissions": {
"title": "权限设置",
"skipPermissions": {
"label": "跳过权限提示(请谨慎使用)",
"claudeDescription": "等同于 --dangerously-skip-permissions 标志",
"cursorDescription": "等同于 Cursor CLI 中的 -f 标志"
},
"allowedTools": {
"title": "允许的工具",
"description": "无需权限提示即可自动使用的工具",
"placeholder": "例如:\"Bash(git log:*)\" 或 \"Write\"",
"quickAdd": "快速添加常用工具:",
"empty": "未配置允许的工具"
},
"blockedTools": {
"title": "禁用的工具",
"description": "无需权限提示即可自动禁用的工具",
"placeholder": "例如:\"Bash(rm:*)\"",
"empty": "未配置禁用的工具"
},
"allowedCommands": {
"title": "允许的 Shell 命令",
"description": "无需权限提示即可自动执行的 Shell 命令",
"placeholder": "例如:\"Shell(ls)\" 或 \"Shell(git status)\"",
"quickAdd": "快速添加常用命令:",
"empty": "未配置允许的命令"
},
"blockedCommands": {
"title": "阻止的 Shell 命令",
"description": "自动阻止的 Shell 命令",
"placeholder": "例如:\"Shell(rm -rf)\" 或 \"Shell(sudo)\"",
"empty": "未配置阻止的命令"
},
"toolExamples": {
"title": "工具模式示例:",
"bashGitLog": "- 允许所有 git log 命令",
"bashGitDiff": "- 允许所有 git diff 命令",
"write": "- 允许所有 Write 工具使用",
"bashRm": "- 阻止所有 rm 命令(危险)"
},
"shellExamples": {
"title": "Shell 命令示例:",
"ls": "- 允许 ls 命令",
"gitStatus": "- 允许 git status",
"npmInstall": "- 允许 npm install",
"rmRf": "- 阻止递归删除"
},
"codex": {
"permissionMode": "权限模式",
"description": "控制 Codex 如何处理文件修改和命令执行",
"modes": {
"default": {
"title": "默认",
"description": "只有受信任的命令ls、cat、grep、git status 等)会自动运行。其他命令将被跳过。可以写入工作区。"
},
"acceptEdits": {
"title": "接受编辑",
"description": "所有命令在工作区内自动运行。具有沙箱执行的全自动模式。"
},
"bypassPermissions": {
"title": "绕过权限",
"description": "完全系统访问,无任何限制。所有命令自动运行,具有完整的磁盘和网络访问权限。请谨慎使用。"
}
},
"technicalDetails": "技术详情",
"technicalInfo": {
"default": "sandboxMode=workspace-write, approvalPolicy=untrusted。受信任的命令cat、cd、grep、head、ls、pwd、tail、git status/log/diff/show、find不带 -exec等。",
"acceptEdits": "sandboxMode=workspace-write, approvalPolicy=never。所有命令在项目目录内自动执行。",
"bypassPermissions": "sandboxMode=danger-full-access, approvalPolicy=never。完全系统访问权限仅在可信环境中使用。",
"overrideNote": "您可以使用聊天界面中的模式按钮按会话覆盖此设置。"
}
},
"actions": {
"add": "添加"
}
},
"mcpServers": {
"title": "MCP 服务器",
"description": {
"claude": "Model Context Protocol 服务器为 Claude 提供额外的工具和数据源",
"cursor": "Model Context Protocol 服务器为 Cursor 提供额外的工具和数据源",
"codex": "Model Context Protocol 服务器为 Codex 提供额外的工具和数据源"
},
"addButton": "添加 MCP 服务器",
"empty": "未配置 MCP 服务器",
"serverType": "类型",
"scope": {
"local": "本地",
"user": "用户"
},
"config": {
"command": "命令",
"url": "URL",
"args": "参数",
"environment": "环境变量"
},
"tools": {
"title": "工具",
"count": "{{count}}",
"more": "还有 {{count}} 个"
},
"actions": {
"edit": "编辑服务器",
"delete": "删除服务器"
},
"help": {
"title": "关于 Codex MCP",
"description": "Codex 支持基于 stdio 的 MCP 服务器。您可以添加服务器,通过额外的工具和资源来扩展 Codex 的功能。"
}
}
}