mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-04 15:57:34 +00:00
add i18n feat && Add partial translation
This commit is contained in:
40
src/App.jsx
40
src/App.jsx
@@ -36,6 +36,8 @@ import ProtectedRoute from './components/ProtectedRoute';
|
||||
import { useVersionCheck } from './hooks/useVersionCheck';
|
||||
import useLocalStorage from './hooks/useLocalStorage';
|
||||
import { api, authenticatedFetch } from './utils/api';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from './i18n/config.js';
|
||||
|
||||
|
||||
// Main App component with routing
|
||||
@@ -950,24 +952,26 @@ function AppContent() {
|
||||
// Root App component with router
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<WebSocketProvider>
|
||||
<TasksSettingsProvider>
|
||||
<TaskMasterProvider>
|
||||
<ProtectedRoute>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContent />} />
|
||||
<Route path="/session/:sessionId" element={<AppContent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ProtectedRoute>
|
||||
</TaskMasterProvider>
|
||||
</TasksSettingsProvider>
|
||||
</WebSocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<WebSocketProvider>
|
||||
<TasksSettingsProvider>
|
||||
<TaskMasterProvider>
|
||||
<ProtectedRoute>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContent />} />
|
||||
<Route path="/session/:sessionId" element={<AppContent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ProtectedRoute>
|
||||
</TaskMasterProvider>
|
||||
</TasksSettingsProvider>
|
||||
</WebSocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
@@ -9,6 +10,7 @@ import ImageViewer from './ImageViewer';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function FileTree({ selectedProject }) {
|
||||
const { t } = useTranslation();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
||||
@@ -130,11 +132,11 @@ function FileTree({ selectedProject }) {
|
||||
const now = new Date();
|
||||
const past = new Date(date);
|
||||
const diffInSeconds = Math.floor((now - past) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return 'just now';
|
||||
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} min ago`;
|
||||
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
||||
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
||||
|
||||
if (diffInSeconds < 60) return t('fileTree.justNow');
|
||||
if (diffInSeconds < 3600) return t('fileTree.minAgo', { count: Math.floor(diffInSeconds / 60) });
|
||||
if (diffInSeconds < 86400) return t('fileTree.hoursAgo', { count: Math.floor(diffInSeconds / 3600) });
|
||||
if (diffInSeconds < 2592000) return t('fileTree.daysAgo', { count: Math.floor(diffInSeconds / 86400) });
|
||||
return past.toLocaleDateString();
|
||||
};
|
||||
|
||||
@@ -348,7 +350,7 @@ function FileTree({ selectedProject }) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-gray-500 dark:text-gray-400">
|
||||
Loading files...
|
||||
{t('fileTree.loading')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -359,14 +361,14 @@ function FileTree({ selectedProject }) {
|
||||
{/* Header with Search and View Mode Toggle */}
|
||||
<div className="p-4 border-b border-border space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-foreground">Files</h3>
|
||||
<h3 className="text-sm font-medium text-foreground">{t('fileTree.files')}</h3>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={viewMode === 'simple' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => changeViewMode('simple')}
|
||||
title="Simple view"
|
||||
title={t('fileTree.simpleView')}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -375,7 +377,7 @@ function FileTree({ selectedProject }) {
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => changeViewMode('compact')}
|
||||
title="Compact view"
|
||||
title={t('fileTree.compactView')}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -384,7 +386,7 @@ function FileTree({ selectedProject }) {
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => changeViewMode('detailed')}
|
||||
title="Detailed view"
|
||||
title={t('fileTree.detailedView')}
|
||||
>
|
||||
<TableProperties className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -396,7 +398,7 @@ function FileTree({ selectedProject }) {
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search files and folders..."
|
||||
placeholder={t('fileTree.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8 pr-8 h-8 text-sm"
|
||||
@@ -407,7 +409,7 @@ function FileTree({ selectedProject }) {
|
||||
size="sm"
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0 hover:bg-accent"
|
||||
onClick={() => setSearchQuery('')}
|
||||
title="Clear search"
|
||||
title={t('fileTree.clearSearch')}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
@@ -419,23 +421,23 @@ function FileTree({ selectedProject }) {
|
||||
{viewMode === 'detailed' && filteredFiles.length > 0 && (
|
||||
<div className="px-4 pt-2 pb-1 border-b border-border">
|
||||
<div className="grid grid-cols-12 gap-2 px-2 text-xs font-medium text-muted-foreground">
|
||||
<div className="col-span-5">Name</div>
|
||||
<div className="col-span-2">Size</div>
|
||||
<div className="col-span-3">Modified</div>
|
||||
<div className="col-span-2">Permissions</div>
|
||||
<div className="col-span-5">{t('fileTree.name')}</div>
|
||||
<div className="col-span-2">{t('fileTree.size')}</div>
|
||||
<div className="col-span-3">{t('fileTree.modified')}</div>
|
||||
<div className="col-span-2">{t('fileTree.permissions')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
{files.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||
<Folder className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h4 className="font-medium text-foreground mb-1">No files found</h4>
|
||||
<h4 className="font-medium text-foreground mb-1">{t('fileTree.noFilesFound')}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Check if the project path is accessible
|
||||
{t('fileTree.checkProjectPath')}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredFiles.length === 0 && searchQuery ? (
|
||||
@@ -443,9 +445,9 @@ function FileTree({ selectedProject }) {
|
||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||
<Search className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h4 className="font-medium text-foreground mb-1">No matches found</h4>
|
||||
<h4 className="font-medium text-foreground mb-1">{t('fileTree.noMatchesFound')}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Try a different search term or clear the search
|
||||
{t('fileTree.tryDifferentSearch')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
74
src/components/LanguageSelector.jsx
Normal file
74
src/components/LanguageSelector.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Language Selector Component
|
||||
*
|
||||
* A dropdown component for selecting the application language.
|
||||
* Automatically updates the i18n language and persists to localStorage.
|
||||
*
|
||||
* Props:
|
||||
* @param {boolean} compact - If true, uses compact style (default: false)
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { languages } from '../i18n/languages';
|
||||
|
||||
function LanguageSelector({ compact = false }) {
|
||||
const { i18n, t } = useTranslation('settings');
|
||||
|
||||
const handleLanguageChange = (event) => {
|
||||
const newLanguage = event.target.value;
|
||||
i18n.changeLanguage(newLanguage);
|
||||
};
|
||||
|
||||
// Compact style for QuickSettingsPanel
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
{t('account.language')}
|
||||
</span>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={handleLanguageChange}
|
||||
className="w-[100px] 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 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Full style for Settings page
|
||||
return (
|
||||
<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-gray-900 dark:text-gray-100 mb-1">
|
||||
{t('account.languageLabel')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('account.languageDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={handleLanguageChange}
|
||||
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-36"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -1,32 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LoginForm = () => {
|
||||
const { t } = useTranslation('auth');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
|
||||
if (!username || !password) {
|
||||
setError('Please enter both username and password');
|
||||
setError(t('errors.requiredFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
const result = await login(username, password);
|
||||
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
}
|
||||
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
@@ -41,9 +43,9 @@ const LoginForm = () => {
|
||||
<MessageSquare className="w-8 h-8 text-primary-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Welcome Back</h1>
|
||||
<h1 className="text-2xl font-bold text-foreground">{t('login.title')}</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Sign in to your Claude Code UI account
|
||||
{t('login.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +53,7 @@ const LoginForm = () => {
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
|
||||
Username
|
||||
{t('login.username')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -59,7 +61,7 @@ const LoginForm = () => {
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your username"
|
||||
placeholder={t('login.placeholders.username')}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -67,7 +69,7 @@ const LoginForm = () => {
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-1">
|
||||
Password
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -75,7 +77,7 @@ const LoginForm = () => {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your password"
|
||||
placeholder={t('login.placeholders.password')}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -92,7 +94,7 @@ const LoginForm = () => {
|
||||
disabled={isLoading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
{isLoading ? t('login.loading') : t('login.submit')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ChatInterface from './ChatInterface';
|
||||
import FileTree from './FileTree';
|
||||
import CodeEditor from './CodeEditor';
|
||||
@@ -58,6 +59,7 @@ function MainContent({
|
||||
sendByCtrlEnter, // Send by Ctrl+Enter mode for East Asian language input
|
||||
externalMessageUpdate // Trigger for external CLI updates to current session
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [editingFile, setEditingFile] = useState(null);
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||
@@ -238,8 +240,8 @@ function MainContent({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Loading Claude Code UI</h2>
|
||||
<p>Setting up your workspace...</p>
|
||||
<h2 className="text-xl font-semibold mb-2">{t('mainContent.loading')}</h2>
|
||||
<p>{t('mainContent.settingUpWorkspace')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,13 +273,13 @@ function MainContent({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mb-3 text-gray-900 dark:text-white">Choose Your Project</h2>
|
||||
<h2 className="text-2xl font-semibold mb-3 text-gray-900 dark:text-white">{t('mainContent.chooseProject')}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
|
||||
Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.
|
||||
{t('mainContent.selectProjectDescription')}
|
||||
</p>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
💡 <strong>Tip:</strong> {isMobile ? 'Tap the menu button above to access projects' : 'Create a new project by clicking the folder icon in the sidebar'}
|
||||
💡 <strong>{t('mainContent.tip')}:</strong> {isMobile ? t('mainContent.createProjectMobile') : t('mainContent.createProjectDesktop')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,7 +333,7 @@ function MainContent({
|
||||
) : activeTab === 'chat' && !selectedSession ? (
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">
|
||||
New Session
|
||||
{t('mainContent.newSession')}
|
||||
</h2>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{selectedProject.displayName}
|
||||
@@ -340,8 +342,8 @@ function MainContent({
|
||||
) : (
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">
|
||||
{activeTab === 'files' ? 'Project Files' :
|
||||
activeTab === 'git' ? 'Source Control' :
|
||||
{activeTab === 'files' ? t('mainContent.projectFiles') :
|
||||
activeTab === 'git' ? t('tabs.git') :
|
||||
(activeTab === 'tasks' && shouldShowTasksTab) ? 'TaskMaster' :
|
||||
'Project'}
|
||||
</h2>
|
||||
@@ -357,7 +359,7 @@ function MainContent({
|
||||
{/* Modern Tab Navigation - Right Side */}
|
||||
<div className="flex-shrink-0 hidden sm:block">
|
||||
<div className="relative flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<Tooltip content="Chat" position="bottom">
|
||||
<Tooltip content={t('tabs.chat')} position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
|
||||
@@ -370,11 +372,11 @@ function MainContent({
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Chat</span>
|
||||
<span className="hidden md:hidden lg:inline">{t('tabs.chat')}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Shell" position="bottom">
|
||||
<Tooltip content={t('tabs.shell')} position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('shell')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
@@ -387,11 +389,11 @@ function MainContent({
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Shell</span>
|
||||
<span className="hidden md:hidden lg:inline">{t('tabs.shell')}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Files" position="bottom">
|
||||
<Tooltip content={t('tabs.files')} position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('files')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
@@ -404,11 +406,11 @@ function MainContent({
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Files</span>
|
||||
<span className="hidden md:hidden lg:inline">{t('tabs.files')}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Source Control" position="bottom">
|
||||
<Tooltip content={t('tabs.git')} position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('git')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
@@ -421,12 +423,12 @@ function MainContent({
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Source Control</span>
|
||||
<span className="hidden md:hidden lg:inline">{t('tabs.git')}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{shouldShowTasksTab && (
|
||||
<Tooltip content="Tasks" position="bottom">
|
||||
<Tooltip content={t('tabs.tasks')} position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('tasks')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
@@ -439,7 +441,7 @@ function MainContent({
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Tasks</span>
|
||||
<span className="hidden md:hidden lg:inline">{t('tabs.tasks')}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -3,8 +3,10 @@ import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { api } from '../utils/api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
const { t } = useTranslation();
|
||||
// Wizard state
|
||||
const [step, setStep] = useState(1); // 1: Choose type, 2: Configure, 3: Confirm
|
||||
const [workspaceType, setWorkspaceType] = useState(null); // 'existing' or 'new'
|
||||
@@ -88,13 +90,13 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
|
||||
if (step === 1) {
|
||||
if (!workspaceType) {
|
||||
setError('Please select whether you have an existing workspace or want to create a new one');
|
||||
setError(t('projectWizard.errors.selectType'));
|
||||
return;
|
||||
}
|
||||
setStep(2);
|
||||
} else if (step === 2) {
|
||||
if (!workspacePath.trim()) {
|
||||
setError('Please provide a workspace path');
|
||||
setError(t('projectWizard.errors.providePath'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,7 +135,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to create workspace');
|
||||
throw new Error(data.error || t('projectWizard.errors.failedToCreate'));
|
||||
}
|
||||
|
||||
// Success!
|
||||
@@ -144,7 +146,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error creating workspace:', error);
|
||||
setError(error.message || 'Failed to create workspace');
|
||||
setError(error.message || t('projectWizard.errors.failedToCreate'));
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
@@ -165,7 +167,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<FolderPlus className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Create New Project
|
||||
{t('projectWizard.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@@ -195,7 +197,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
{s < step ? <Check className="w-4 h-4" /> : s}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 hidden sm:inline">
|
||||
{s === 1 ? 'Type' : s === 2 ? 'Configure' : 'Confirm'}
|
||||
{s === 1 ? t('projectWizard.steps.type') : s === 2 ? t('projectWizard.steps.configure') : t('projectWizard.steps.confirm')}
|
||||
</span>
|
||||
</div>
|
||||
{s < 3 && (
|
||||
@@ -227,7 +229,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Do you already have a workspace, or would you like to create a new one?
|
||||
{t('projectWizard.step1.question')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Existing Workspace */}
|
||||
@@ -245,10 +247,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h5 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||
Existing Workspace
|
||||
{t('projectWizard.step1.existing.title')}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
I already have a workspace on my server and just need to add it to the project list
|
||||
{t('projectWizard.step1.existing.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,10 +271,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h5 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||
New Workspace
|
||||
{t('projectWizard.step1.new.title')}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create a new workspace, optionally clone from a GitHub repository
|
||||
{t('projectWizard.step1.new.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,14 +290,14 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
{/* Workspace Path */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{workspaceType === 'existing' ? 'Workspace Path' : 'Where should the workspace be created?'}
|
||||
{workspaceType === 'existing' ? t('projectWizard.step2.existingPath') : t('projectWizard.step2.newPath')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
value={workspacePath}
|
||||
onChange={(e) => setWorkspacePath(e.target.value)}
|
||||
placeholder={workspaceType === 'existing' ? '/path/to/existing/workspace' : '/path/to/new/workspace'}
|
||||
placeholder={workspaceType === 'existing' ? t('projectWizard.step2.existingPlaceholder') : t('projectWizard.step2.newPlaceholder')}
|
||||
className="w-full"
|
||||
/>
|
||||
{showPathDropdown && pathSuggestions.length > 0 && (
|
||||
@@ -315,8 +317,8 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{workspaceType === 'existing'
|
||||
? 'Full path to your existing workspace directory'
|
||||
: 'Full path where the new workspace will be created'}
|
||||
? t('projectWizard.step2.existingHelp')
|
||||
: t('projectWizard.step2.newHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -325,17 +327,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
GitHub URL (Optional)
|
||||
{t('projectWizard.step2.githubUrl')}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={githubUrl}
|
||||
onChange={(e) => setGithubUrl(e.target.value)}
|
||||
placeholder="https://github.com/username/repository"
|
||||
placeholder={t('projectWizard.step2.githubPlaceholder')}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Leave empty to create an empty workspace, or provide a GitHub URL to clone
|
||||
{t('projectWizard.step2.githubHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -346,10 +348,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<Key className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h5 className="font-medium text-gray-900 dark:text-white mb-1">
|
||||
GitHub Authentication (Optional)
|
||||
{t('projectWizard.step2.githubAuth')}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Only required for private repositories. Public repos can be cloned without authentication.
|
||||
{t('projectWizard.step2.githubAuthHelp')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -357,7 +359,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
{loadingTokens ? (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Loading stored tokens...
|
||||
{t('projectWizard.step2.loadingTokens')}
|
||||
</div>
|
||||
) : availableTokens.length > 0 ? (
|
||||
<>
|
||||
@@ -371,7 +373,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Stored Token
|
||||
{t('projectWizard.step2.storedToken')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTokenMode('new')}
|
||||
@@ -381,7 +383,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
New Token
|
||||
{t('projectWizard.step2.newToken')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -395,21 +397,21 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
None (Public)
|
||||
{t('projectWizard.step2.nonePublic')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tokenMode === 'stored' ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Token
|
||||
{t('projectWizard.step2.selectToken')}
|
||||
</label>
|
||||
<select
|
||||
value={selectedGithubToken}
|
||||
onChange={(e) => setSelectedGithubToken(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">-- Select a token --</option>
|
||||
<option value="">{t('projectWizard.step2.selectTokenPlaceholder')}</option>
|
||||
{availableTokens.map((token) => (
|
||||
<option key={token.id} value={token.id}>
|
||||
{token.credential_name}
|
||||
@@ -420,17 +422,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
) : tokenMode === 'new' ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
GitHub Token
|
||||
{t('projectWizard.step2.newToken')}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newGithubToken}
|
||||
onChange={(e) => setNewGithubToken(e.target.value)}
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
placeholder={t('projectWizard.step2.tokenPlaceholder')}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
This token will be used only for this operation
|
||||
{t('projectWizard.step2.tokenHelp')}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -439,23 +441,23 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
💡 <strong>Public repositories</strong> don't require authentication. You can skip providing a token if cloning a public repo.
|
||||
{t('projectWizard.step2.publicRepoInfo')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
GitHub Token (Optional for Public Repos)
|
||||
{t('projectWizard.step2.optionalTokenPublic')}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newGithubToken}
|
||||
onChange={(e) => setNewGithubToken(e.target.value)}
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (leave empty for public repos)"
|
||||
placeholder={t('projectWizard.step2.tokenPublicPlaceholder')}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
No stored tokens available. You can add tokens in Settings → API Keys for easier reuse.
|
||||
{t('projectWizard.step2.noTokensHelp')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -472,17 +474,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Review Your Configuration
|
||||
{t('projectWizard.step3.reviewConfig')}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Workspace Type:</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.workspaceType')}</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{workspaceType === 'existing' ? 'Existing Workspace' : 'New Workspace'}
|
||||
{workspaceType === 'existing' ? t('projectWizard.step3.existingWorkspace') : t('projectWizard.step3.newWorkspace')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Path:</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.path')}</span>
|
||||
<span className="font-mono text-xs text-gray-900 dark:text-white break-all">
|
||||
{workspacePath}
|
||||
</span>
|
||||
@@ -490,19 +492,19 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
{workspaceType === 'new' && githubUrl && (
|
||||
<>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Clone From:</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.cloneFrom')}</span>
|
||||
<span className="font-mono text-xs text-gray-900 dark:text-white break-all">
|
||||
{githubUrl}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Authentication:</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.authentication')}</span>
|
||||
<span className="text-xs text-gray-900 dark:text-white">
|
||||
{tokenMode === 'stored' && selectedGithubToken
|
||||
? `Using stored token: ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}`
|
||||
? `${t('projectWizard.step3.usingStoredToken')} ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}`
|
||||
: tokenMode === 'new' && newGithubToken
|
||||
? 'Using provided token'
|
||||
: 'No authentication'}
|
||||
? t('projectWizard.step3.usingProvidedToken')
|
||||
: t('projectWizard.step3.noAuthentication')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -513,10 +515,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
{workspaceType === 'existing'
|
||||
? 'The workspace will be added to your project list and will be available for Claude/Cursor sessions.'
|
||||
? t('projectWizard.step3.existingInfo')
|
||||
: githubUrl
|
||||
? 'A new workspace will be created and the repository will be cloned from GitHub.'
|
||||
: 'An empty workspace directory will be created at the specified path.'}
|
||||
? t('projectWizard.step3.newWithClone')
|
||||
: t('projectWizard.step3.newEmpty')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -531,11 +533,11 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
disabled={isCreating}
|
||||
>
|
||||
{step === 1 ? (
|
||||
'Cancel'
|
||||
t('projectWizard.buttons.cancel')
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
Back
|
||||
{t('projectWizard.buttons.back')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -547,16 +549,16 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
{t('projectWizard.buttons.creating')}
|
||||
</>
|
||||
) : step === 3 ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
Create Project
|
||||
{t('projectWizard.buttons.createProject')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Next
|
||||
{t('projectWizard.buttons.next')}
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
Languages,
|
||||
GripVertical
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DarkModeToggle from './DarkModeToggle';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
|
||||
const QuickSettingsPanel = ({
|
||||
isOpen,
|
||||
@@ -33,6 +35,7 @@ const QuickSettingsPanel = ({
|
||||
onSendByCtrlEnterChange,
|
||||
isMobile
|
||||
}) => {
|
||||
const { t } = useTranslation('settings');
|
||||
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
|
||||
const [whisperMode, setWhisperMode] = useState(() => {
|
||||
return localStorage.getItem('whisperMode') || 'default';
|
||||
@@ -253,7 +256,7 @@ const QuickSettingsPanel = ({
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
Quick Settings
|
||||
{t('quickSettings.title')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -261,25 +264,30 @@ const QuickSettingsPanel = ({
|
||||
<div className={`flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 bg-background ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
||||
{/* Appearance Settings */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Appearance</h4>
|
||||
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">{t('quickSettings.sections.appearance')}</h4>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
{isDarkMode ? <Moon className="h-4 w-4 text-gray-600 dark:text-gray-400" /> : <Sun className="h-4 w-4 text-gray-600 dark:text-gray-400" />}
|
||||
Dark Mode
|
||||
{t('quickSettings.darkMode')}
|
||||
</span>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
{/* Language Selector */}
|
||||
<div>
|
||||
<LanguageSelector compact={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool Display Settings */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Tool Display</h4>
|
||||
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">{t('quickSettings.sections.toolDisplay')}</h4>
|
||||
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Maximize2 className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Auto-expand tools
|
||||
{t('quickSettings.autoExpandTools')}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -292,7 +300,7 @@ const QuickSettingsPanel = ({
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Eye className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Show raw parameters
|
||||
{t('quickSettings.showRawParameters')}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -305,7 +313,7 @@ const QuickSettingsPanel = ({
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Brain className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Show thinking
|
||||
{t('quickSettings.showThinking')}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -317,12 +325,12 @@ const QuickSettingsPanel = ({
|
||||
</div>
|
||||
{/* View Options */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">View Options</h4>
|
||||
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">{t('quickSettings.sections.viewOptions')}</h4>
|
||||
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<ArrowDown className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Auto-scroll to bottom
|
||||
{t('quickSettings.autoScrollToBottom')}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -335,12 +343,12 @@ const QuickSettingsPanel = ({
|
||||
|
||||
{/* Input Settings */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Input Settings</h4>
|
||||
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">{t('quickSettings.sections.inputSettings')}</h4>
|
||||
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Send by Ctrl+Enter
|
||||
{t('quickSettings.sendByCtrlEnter')}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -350,13 +358,13 @@ const QuickSettingsPanel = ({
|
||||
/>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
|
||||
When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends.
|
||||
{t('quickSettings.sendByCtrlEnterDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Whisper Dictation Settings - HIDDEN */}
|
||||
<div className="space-y-2" style={{ display: 'none' }}>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">{t('quickSettings.sections.whisperDictation')}</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-start p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Input } from './ui/input';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn, Key, GitBranch, Check } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import CodexLogo from './CodexLogo';
|
||||
@@ -18,9 +19,11 @@ import AgentListItem from './settings/AgentListItem';
|
||||
import AccountContent from './settings/AccountContent';
|
||||
import PermissionsContent from './settings/PermissionsContent';
|
||||
import McpServersContent from './settings/McpServersContent';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
|
||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
const { t } = useTranslation('settings');
|
||||
const [allowedTools, setAllowedTools] = useState([]);
|
||||
const [disallowedTools, setDisallowedTools] = useState([]);
|
||||
const [newAllowedTool, setNewAllowedTool] = useState('');
|
||||
@@ -1062,6 +1065,11 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="space-y-4">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
|
||||
{/* Project Sorting */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
@@ -1313,7 +1321,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
Account
|
||||
{t('tabs.account')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedCategory('permissions')}
|
||||
@@ -1323,7 +1331,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
Permissions
|
||||
{t('tabs.permissions')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedCategory('mcp')}
|
||||
@@ -1333,7 +1341,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
MCP Servers
|
||||
{t('tabs.mcpServers')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ScrollArea } from './ui/scroll-area';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
@@ -17,28 +18,28 @@ import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
// Move formatTimeAgo outside component to avoid recreation on every render
|
||||
const formatTimeAgo = (dateString, currentTime) => {
|
||||
const formatTimeAgo = (dateString, currentTime, t) => {
|
||||
const date = new Date(dateString);
|
||||
const now = currentTime;
|
||||
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Unknown';
|
||||
return t ? t('status.unknown') : 'Unknown';
|
||||
}
|
||||
|
||||
|
||||
const diffInMs = now - date;
|
||||
const diffInSeconds = Math.floor(diffInMs / 1000);
|
||||
const diffInMinutes = Math.floor(diffInMs / (1000 * 60));
|
||||
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
|
||||
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffInSeconds < 60) return 'Just now';
|
||||
if (diffInMinutes === 1) return '1 min ago';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} mins ago`;
|
||||
if (diffInHours === 1) return '1 hour ago';
|
||||
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
||||
if (diffInDays === 1) return '1 day ago';
|
||||
if (diffInDays < 7) return `${diffInDays} days ago`;
|
||||
|
||||
if (diffInSeconds < 60) return t ? t('time.justNow') : 'Just now';
|
||||
if (diffInMinutes === 1) return t ? t('time.oneMinuteAgo') : '1 min ago';
|
||||
if (diffInMinutes < 60) return t ? t('time.minutesAgo', { count: diffInMinutes }) : `${diffInMinutes} mins ago`;
|
||||
if (diffInHours === 1) return t ? t('time.oneHourAgo') : '1 hour ago';
|
||||
if (diffInHours < 24) return t ? t('time.hoursAgo', { count: diffInHours }) : `${diffInHours} hours ago`;
|
||||
if (diffInDays === 1) return t ? t('time.oneDayAgo') : '1 day ago';
|
||||
if (diffInDays < 7) return t ? t('time.daysAgo', { count: diffInDays }) : `${diffInDays} days ago`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
@@ -63,6 +64,7 @@ function Sidebar({
|
||||
isMobile,
|
||||
onToggleSidebar
|
||||
}) {
|
||||
const { t } = useTranslation('sidebar');
|
||||
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
||||
const [editingProject, setEditingProject] = useState(null);
|
||||
const [showNewProject, setShowNewProject] = useState(false);
|
||||
@@ -304,7 +306,7 @@ function Sidebar({
|
||||
};
|
||||
|
||||
const deleteSession = async (projectName, sessionId, provider = 'claude') => {
|
||||
if (!confirm('Are you sure you want to delete this session? This action cannot be undone.')) {
|
||||
if (!confirm(t('messages.deleteSessionConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,16 +334,16 @@ function Sidebar({
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error('[Sidebar] Failed to delete session:', { status: response.status, error: errorText });
|
||||
alert('Failed to delete session. Please try again.');
|
||||
alert(t('messages.deleteSessionFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Sidebar] Error deleting session:', error);
|
||||
alert('Error deleting session. Please try again.');
|
||||
alert(t('messages.deleteSessionError'));
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProject = async (projectName) => {
|
||||
if (!confirm('Are you sure you want to delete this empty project? This action cannot be undone.')) {
|
||||
if (!confirm(t('messages.deleteProjectConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,34 +358,34 @@ function Sidebar({
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to delete project');
|
||||
alert(error.error || 'Failed to delete project. Please try again.');
|
||||
alert(error.error || t('messages.deleteProjectFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
alert('Error deleting project. Please try again.');
|
||||
alert(t('messages.deleteProjectError'));
|
||||
}
|
||||
};
|
||||
|
||||
const createNewProject = async () => {
|
||||
if (!newProjectPath.trim()) {
|
||||
alert('Please enter a project path');
|
||||
alert(t('messages.enterProjectPath'));
|
||||
return;
|
||||
}
|
||||
|
||||
setCreatingProject(true);
|
||||
|
||||
|
||||
try {
|
||||
const response = await api.createProject(newProjectPath.trim());
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
// Save the path to recent paths before clearing
|
||||
saveToRecentPaths(newProjectPath.trim());
|
||||
|
||||
|
||||
setShowNewProject(false);
|
||||
setNewProjectPath('');
|
||||
|
||||
|
||||
// Refresh projects to show the new one
|
||||
if (window.refreshProjects) {
|
||||
window.refreshProjects();
|
||||
@@ -392,11 +394,11 @@ function Sidebar({
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to create project. Please try again.');
|
||||
alert(error.error || t('messages.createProjectFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
alert('Error creating project. Please try again.');
|
||||
alert(t('messages.createProjectError'));
|
||||
} finally {
|
||||
setCreatingProject(false);
|
||||
}
|
||||
@@ -497,14 +499,14 @@ function Sidebar({
|
||||
<a
|
||||
href="https://cloudcli.ai/dashboard"
|
||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity group"
|
||||
title="View Environments"
|
||||
title={t('tooltips.viewEnvironments')}
|
||||
>
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow">
|
||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-foreground">Claude Code UI</h1>
|
||||
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
||||
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
@@ -513,8 +515,8 @@ function Sidebar({
|
||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-foreground">Claude Code UI</h1>
|
||||
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
||||
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -524,7 +526,7 @@ function Sidebar({
|
||||
size="sm"
|
||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
|
||||
onClick={onToggleSidebar}
|
||||
title="Hide sidebar"
|
||||
title={t('tooltips.hideSidebar')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -548,14 +550,14 @@ function Sidebar({
|
||||
<a
|
||||
href="https://cloudcli.ai/dashboard"
|
||||
className="flex items-center gap-3 active:opacity-70 transition-opacity"
|
||||
title="View Environments"
|
||||
title={t('tooltips.viewEnvironments')}
|
||||
>
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">Claude Code UI</h1>
|
||||
<p className="text-sm text-muted-foreground">Projects</p>
|
||||
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
@@ -564,8 +566,8 @@ function Sidebar({
|
||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">Claude Code UI</h1>
|
||||
<p className="text-sm text-muted-foreground">Projects</p>
|
||||
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -604,10 +606,10 @@ function Sidebar({
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
|
||||
onClick={() => setShowNewProject(true)}
|
||||
title="Create new project"
|
||||
title={t('tooltips.createProject')}
|
||||
>
|
||||
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
|
||||
New Project
|
||||
{t('projects.newProject')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -622,7 +624,7 @@ function Sidebar({
|
||||
}
|
||||
}}
|
||||
disabled={isRefreshing}
|
||||
title="Refresh projects and sessions (Ctrl+R)"
|
||||
title={t('tooltips.refresh')}
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
||||
</Button>
|
||||
@@ -637,7 +639,7 @@ function Sidebar({
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search projects..."
|
||||
placeholder={t('projects.searchPlaceholder')}
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
className="pl-9 h-9 text-sm bg-muted/50 border-0 focus:bg-background focus:ring-1 focus:ring-primary/20"
|
||||
@@ -662,9 +664,9 @@ function Sidebar({
|
||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
||||
<div className="w-6 h-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
</div>
|
||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">Loading projects...</h3>
|
||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.loadingProjects')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fetching your Claude projects and sessions
|
||||
{t('projects.fetchingProjects')}
|
||||
</p>
|
||||
</div>
|
||||
) : projects.length === 0 ? (
|
||||
@@ -672,9 +674,9 @@ function Sidebar({
|
||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
||||
<Folder className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">No projects found</h3>
|
||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.noProjects')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Run Claude CLI in a project directory to get started
|
||||
{t('projects.runClaudeCli')}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredProjects.length === 0 ? (
|
||||
@@ -682,9 +684,9 @@ function Sidebar({
|
||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
||||
<Search className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">No matching projects</h3>
|
||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.noMatchingProjects')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Try adjusting your search term
|
||||
{t('projects.tryDifferentSearch')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -730,7 +732,7 @@ function Sidebar({
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border-2 border-primary/40 focus:border-primary rounded-lg bg-background text-foreground shadow-sm focus:shadow-md transition-all duration-200 focus:outline-none"
|
||||
placeholder="Project name"
|
||||
placeholder={t('projects.projectNamePlaceholder')}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -814,7 +816,7 @@ function Sidebar({
|
||||
toggleStarProject(project.name);
|
||||
}}
|
||||
onTouchEnd={handleTouchClick(() => toggleStarProject(project.name))}
|
||||
title={isStarred ? "Remove from favorites" : "Add to favorites"}
|
||||
title={isStarred ? t('tooltips.removeFromFavorites') : t('tooltips.addToFavorites')}
|
||||
>
|
||||
<Star className={cn(
|
||||
"w-4 h-4 transition-colors",
|
||||
@@ -895,7 +897,7 @@ function Sidebar({
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="Project name"
|
||||
placeholder={t('projects.projectNamePlaceholder')}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveProjectName(project.name);
|
||||
@@ -964,7 +966,7 @@ function Sidebar({
|
||||
e.stopPropagation();
|
||||
toggleStarProject(project.name);
|
||||
}}
|
||||
title={isStarred ? "Remove from favorites" : "Add to favorites"}
|
||||
title={isStarred ? t('tooltips.removeFromFavorites') : t('tooltips.addToFavorites')}
|
||||
>
|
||||
<Star className={cn(
|
||||
"w-3 h-3 transition-colors",
|
||||
@@ -979,7 +981,7 @@ function Sidebar({
|
||||
e.stopPropagation();
|
||||
startEditing(project);
|
||||
}}
|
||||
title="Rename project (F2)"
|
||||
title={t('tooltips.renameProject')}
|
||||
>
|
||||
<Edit3 className="w-3 h-3" />
|
||||
</div>
|
||||
@@ -990,7 +992,7 @@ function Sidebar({
|
||||
e.stopPropagation();
|
||||
deleteProject(project.name);
|
||||
}}
|
||||
title="Delete empty project (Delete)"
|
||||
title={t('tooltips.deleteProject')}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
@@ -1024,7 +1026,7 @@ function Sidebar({
|
||||
))
|
||||
) : getAllSessions(project).length === 0 && !loadingSessions[project.name] ? (
|
||||
<div className="py-2 px-3 text-left">
|
||||
<p className="text-xs text-muted-foreground">No sessions yet</p>
|
||||
<p className="text-xs text-muted-foreground">{t('sessions.noSessions')}</p>
|
||||
</div>
|
||||
) : (
|
||||
getAllSessions(project).map((session) => {
|
||||
@@ -1044,9 +1046,9 @@ function Sidebar({
|
||||
|
||||
// Get session display values
|
||||
const getSessionName = () => {
|
||||
if (isCursorSession) return session.name || 'Untitled Session';
|
||||
if (isCodexSession) return session.summary || session.name || 'Codex Session';
|
||||
return session.summary || 'New Session';
|
||||
if (isCursorSession) return session.name || t('projects.untitledSession');
|
||||
if (isCodexSession) return session.summary || session.name || t('projects.codexSession');
|
||||
return session.summary || t('projects.newSession');
|
||||
};
|
||||
const sessionName = getSessionName();
|
||||
const getSessionTime = () => {
|
||||
@@ -1102,7 +1104,7 @@ function Sidebar({
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<Clock className="w-2.5 h-2.5 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatTimeAgo(sessionTime, currentTime)}
|
||||
{formatTimeAgo(sessionTime, currentTime, t)}
|
||||
</span>
|
||||
{messageCount > 0 && (
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0 ml-auto">
|
||||
@@ -1163,7 +1165,7 @@ function Sidebar({
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<Clock className="w-2.5 h-2.5 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatTimeAgo(sessionTime, currentTime)}
|
||||
{formatTimeAgo(sessionTime, currentTime, t)}
|
||||
</span>
|
||||
{messageCount > 0 && (
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0 ml-auto">
|
||||
@@ -1210,7 +1212,7 @@ function Sidebar({
|
||||
e.stopPropagation();
|
||||
updateSessionSummary(project.name, session.id, editingSessionName);
|
||||
}}
|
||||
title="Save"
|
||||
title={t('tooltips.save')}
|
||||
>
|
||||
<Check className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||
</button>
|
||||
@@ -1221,7 +1223,7 @@ function Sidebar({
|
||||
setEditingSession(null);
|
||||
setEditingSessionName('');
|
||||
}}
|
||||
title="Cancel"
|
||||
title={t('tooltips.cancel')}
|
||||
>
|
||||
<X className="w-3 h-3 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
@@ -1234,9 +1236,9 @@ function Sidebar({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingSession(session.id);
|
||||
setEditingSessionName(session.summary || 'New Session');
|
||||
setEditingSessionName(session.summary || t('projects.newSession'));
|
||||
}}
|
||||
title="Manually edit session name"
|
||||
title={t('tooltips.editSessionName')}
|
||||
>
|
||||
<Edit2 className="w-3 h-3 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
@@ -1247,7 +1249,7 @@ function Sidebar({
|
||||
e.stopPropagation();
|
||||
deleteSession(project.name, session.id, session.__provider);
|
||||
}}
|
||||
title="Delete this session permanently"
|
||||
title={t('tooltips.deleteSession')}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
||||
</button>
|
||||
@@ -1273,18 +1275,18 @@ function Sidebar({
|
||||
{loadingSessions[project.name] ? (
|
||||
<>
|
||||
<div className="w-3 h-3 animate-spin rounded-full border border-muted-foreground border-t-transparent" />
|
||||
Loading...
|
||||
{t('sessions.loading')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
Show more sessions
|
||||
{t('sessions.showMore')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* New Session Button */}
|
||||
{/* Sessions - New Session Button */}
|
||||
<div className="md:hidden px-3 pb-2">
|
||||
<button
|
||||
className="w-full h-8 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md flex items-center justify-center gap-2 font-medium text-xs active:scale-[0.98] transition-all duration-150"
|
||||
@@ -1294,7 +1296,7 @@ function Sidebar({
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
New Session
|
||||
{t('sessions.newSession')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1305,7 +1307,7 @@ function Sidebar({
|
||||
onClick={() => onNewSession(project)}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
New Session
|
||||
{t('sessions.newSession')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -1336,7 +1338,7 @@ function Sidebar({
|
||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||
{releaseInfo?.title || `Version ${latestVersion}`}
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">Update available</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1357,7 +1359,7 @@ function Sidebar({
|
||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||
{releaseInfo?.title || `Version ${latestVersion}`}
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">Update available</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1375,7 +1377,7 @@ function Sidebar({
|
||||
<div className="w-10 h-10 rounded-2xl bg-background/80 flex items-center justify-center">
|
||||
<Settings className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
<span className="text-lg font-medium text-foreground">Settings</span>
|
||||
<span className="text-lg font-medium text-foreground">{t('actions.settings')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1386,7 +1388,7 @@ function Sidebar({
|
||||
onClick={onShowSettings}
|
||||
>
|
||||
<Settings className="w-3 h-3" />
|
||||
<span className="text-xs">Settings</span>
|
||||
<span className="text-xs">{t('actions.settings')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
121
src/i18n/config.js
Normal file
121
src/i18n/config.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* i18n Configuration
|
||||
*
|
||||
* Configures i18next for internationalization support.
|
||||
* Features:
|
||||
* - Lazy-loading of translation namespaces
|
||||
* - Language detection from localStorage
|
||||
* - Fallback to English for missing translations
|
||||
* - Development mode warnings for missing keys
|
||||
*/
|
||||
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// Import translation resources
|
||||
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 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 supported languages configuration
|
||||
import { languages } from './languages.js';
|
||||
|
||||
// Get saved language preference from localStorage
|
||||
const getSavedLanguage = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('userLanguage');
|
||||
// Validate that the saved language is supported
|
||||
if (saved && languages.some(lang => lang.value === saved)) {
|
||||
return saved;
|
||||
}
|
||||
return 'en';
|
||||
} catch {
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize i18next
|
||||
i18n
|
||||
.use(LanguageDetector) // Detect user language
|
||||
.use(initReactI18next) // Pass i18n instance to react-i18next
|
||||
.init({
|
||||
// Resources containing all translations
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon,
|
||||
settings: enSettings,
|
||||
auth: enAuth,
|
||||
sidebar: enSidebar,
|
||||
},
|
||||
'zh-CN': {
|
||||
common: zhCommon,
|
||||
settings: zhSettings,
|
||||
auth: zhAuth,
|
||||
sidebar: zhSidebar,
|
||||
},
|
||||
},
|
||||
|
||||
// Default language
|
||||
lng: getSavedLanguage(),
|
||||
|
||||
// Fallback language when a translation is missing
|
||||
fallbackLng: 'en',
|
||||
|
||||
// Enable debug mode in development (logs missing keys to console)
|
||||
debug: import.meta.env.DEV,
|
||||
|
||||
// Namespaces - load only what's needed
|
||||
ns: ['common', 'settings', 'auth', 'sidebar'],
|
||||
defaultNS: 'common',
|
||||
|
||||
// Key separator for nested keys (default: '.')
|
||||
keySeparator: '.',
|
||||
|
||||
// Namespace separator (default: ':')
|
||||
nsSeparator: ':',
|
||||
|
||||
// Save missing translations (disabled - requires manual review)
|
||||
saveMissing: false,
|
||||
|
||||
// Interpolation settings
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
},
|
||||
|
||||
// React-specific settings
|
||||
react: {
|
||||
useSuspense: true, // Use Suspense for lazy-loading
|
||||
bindI18n: 'languageChanged', // Re-render on language change
|
||||
bindI18nStore: false, // Don't re-render on resource changes
|
||||
},
|
||||
|
||||
// Detection options
|
||||
detection: {
|
||||
// Order of language detection (local storage first)
|
||||
order: ['localStorage'],
|
||||
|
||||
// Keys to look for in localStorage
|
||||
lookupLocalStorage: 'userLanguage',
|
||||
|
||||
// Cache user language
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
});
|
||||
|
||||
// Save language preference when it changes
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
try {
|
||||
localStorage.setItem('userLanguage', lng);
|
||||
} catch (error) {
|
||||
console.error('Failed to save language preference:', error);
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
48
src/i18n/languages.js
Normal file
48
src/i18n/languages.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Supported Languages Configuration
|
||||
*
|
||||
* This file contains the list of supported languages for the application.
|
||||
* Each language includes:
|
||||
* - value: Language code (e.g., 'en', 'zh-CN')
|
||||
* - label: Display name in English
|
||||
* - nativeName: Native language name for display
|
||||
*/
|
||||
|
||||
export const languages = [
|
||||
{
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
nativeName: 'English',
|
||||
},
|
||||
{
|
||||
value: 'zh-CN',
|
||||
label: 'Simplified Chinese',
|
||||
nativeName: '简体中文',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get language object by value
|
||||
* @param {string} value - Language code
|
||||
* @returns {Object|undefined} Language object or undefined if not found
|
||||
*/
|
||||
export const getLanguage = (value) => {
|
||||
return languages.find(lang => lang.value === value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all language values
|
||||
* @returns {string[]} Array of language codes
|
||||
*/
|
||||
export const getLanguageValues = () => {
|
||||
return languages.map(lang => lang.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a language is supported
|
||||
* @param {string} value - Language code to check
|
||||
* @returns {boolean} True if language is supported
|
||||
*/
|
||||
export const isLanguageSupported = (value) => {
|
||||
return languages.some(lang => lang.value === value);
|
||||
};
|
||||
37
src/i18n/locales/en/auth.json
Normal file
37
src/i18n/locales/en/auth.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Welcome Back",
|
||||
"description": "Sign in to your Claude Code UI account",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"submit": "Sign In",
|
||||
"loading": "Signing in...",
|
||||
"errors": {
|
||||
"invalidCredentials": "Invalid username or password",
|
||||
"requiredFields": "Please fill in all fields",
|
||||
"networkError": "Network error. Please try again."
|
||||
},
|
||||
"placeholders": {
|
||||
"username": "Enter your username",
|
||||
"password": "Enter your password"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "Create Account",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"submit": "Create Account",
|
||||
"loading": "Creating account...",
|
||||
"errors": {
|
||||
"passwordMismatch": "Passwords do not match",
|
||||
"usernameTaken": "Username is already taken",
|
||||
"weakPassword": "Password is too weak"
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "Sign Out",
|
||||
"confirm": "Are you sure you want to sign out?",
|
||||
"button": "Sign Out"
|
||||
}
|
||||
}
|
||||
190
src/i18n/locales/en/common.json
Normal file
190
src/i18n/locales/en/common.json
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"buttons": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"create": "Create",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"submit": "Submit",
|
||||
"retry": "Retry",
|
||||
"refresh": "Refresh",
|
||||
"search": "Search",
|
||||
"clear": "Clear",
|
||||
"copy": "Copy",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"browse": "Browse"
|
||||
},
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"shell": "Shell",
|
||||
"files": "Files",
|
||||
"git": "Source Control",
|
||||
"tasks": "Tasks"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Loading...",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"failed": "Failed",
|
||||
"pending": "Pending",
|
||||
"completed": "Completed",
|
||||
"inProgress": "In Progress"
|
||||
},
|
||||
"messages": {
|
||||
"savedSuccessfully": "Saved successfully",
|
||||
"deletedSuccessfully": "Deleted successfully",
|
||||
"updatedSuccessfully": "Updated successfully",
|
||||
"operationFailed": "Operation failed",
|
||||
"networkError": "Network error. Please check your connection.",
|
||||
"unauthorized": "Unauthorized. Please log in.",
|
||||
"notFound": "Not found",
|
||||
"invalidInput": "Invalid input",
|
||||
"requiredField": "This field is required",
|
||||
"unknownError": "An unknown error occurred"
|
||||
},
|
||||
"navigation": {
|
||||
"settings": "Settings",
|
||||
"home": "Home",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"common": {
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"darkMode": "Dark Mode",
|
||||
"lightMode": "Light Mode",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"optional": "Optional",
|
||||
"version": "Version",
|
||||
"select": "Select",
|
||||
"selectAll": "Select All",
|
||||
"deselectAll": "Deselect All"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "Just now",
|
||||
"minutesAgo": "{{count}} mins ago",
|
||||
"hoursAgo": "{{count}} hours ago",
|
||||
"daysAgo": "{{count}} days ago",
|
||||
"yesterday": "Yesterday"
|
||||
},
|
||||
"fileOperations": {
|
||||
"newFile": "New File",
|
||||
"newFolder": "New Folder",
|
||||
"rename": "Rename",
|
||||
"move": "Move",
|
||||
"copyPath": "Copy Path",
|
||||
"openInEditor": "Open in Editor"
|
||||
},
|
||||
"mainContent": {
|
||||
"loading": "Loading Claude Code UI",
|
||||
"settingUpWorkspace": "Setting up your workspace...",
|
||||
"chooseProject": "Choose Your Project",
|
||||
"selectProjectDescription": "Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.",
|
||||
"tip": "Tip",
|
||||
"createProjectMobile": "Tap the menu button above to access projects",
|
||||
"createProjectDesktop": "Create a new project by clicking the folder icon in the sidebar",
|
||||
"newSession": "New Session",
|
||||
"untitledSession": "Untitled Session",
|
||||
"projectFiles": "Project Files"
|
||||
},
|
||||
"fileTree": {
|
||||
"loading": "Loading files...",
|
||||
"files": "Files",
|
||||
"simpleView": "Simple view",
|
||||
"compactView": "Compact view",
|
||||
"detailedView": "Detailed view",
|
||||
"searchPlaceholder": "Search files and folders...",
|
||||
"clearSearch": "Clear search",
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"modified": "Modified",
|
||||
"permissions": "Permissions",
|
||||
"noFilesFound": "No files found",
|
||||
"checkProjectPath": "Check if the project path is accessible",
|
||||
"noMatchesFound": "No matches found",
|
||||
"tryDifferentSearch": "Try a different search term or clear the search",
|
||||
"justNow": "just now",
|
||||
"minAgo": "{{count}} min ago",
|
||||
"hoursAgo": "{{count}} hours ago",
|
||||
"daysAgo": "{{count}} days ago"
|
||||
},
|
||||
"projectWizard": {
|
||||
"title": "Create New Project",
|
||||
"steps": {
|
||||
"type": "Type",
|
||||
"configure": "Configure",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"step1": {
|
||||
"question": "Do you already have a workspace, or would you like to create a new one?",
|
||||
"existing": {
|
||||
"title": "Existing Workspace",
|
||||
"description": "I already have a workspace on my server and just need to add it to the project list"
|
||||
},
|
||||
"new": {
|
||||
"title": "New Workspace",
|
||||
"description": "Create a new workspace, optionally clone from a GitHub repository"
|
||||
}
|
||||
},
|
||||
"step2": {
|
||||
"existingPath": "Workspace Path",
|
||||
"newPath": "Where should the workspace be created?",
|
||||
"existingPlaceholder": "/path/to/existing/workspace",
|
||||
"newPlaceholder": "/path/to/new/workspace",
|
||||
"existingHelp": "Full path to your existing workspace directory",
|
||||
"newHelp": "Full path where the new workspace will be created",
|
||||
"githubUrl": "GitHub URL (Optional)",
|
||||
"githubPlaceholder": "https://github.com/username/repository",
|
||||
"githubHelp": "Leave empty to create an empty workspace, or provide a GitHub URL to clone",
|
||||
"githubAuth": "GitHub Authentication (Optional)",
|
||||
"githubAuthHelp": "Only required for private repositories. Public repos can be cloned without authentication.",
|
||||
"loadingTokens": "Loading stored tokens...",
|
||||
"storedToken": "Stored Token",
|
||||
"newToken": "New Token",
|
||||
"nonePublic": "None (Public)",
|
||||
"selectToken": "Select Token",
|
||||
"selectTokenPlaceholder": "-- Select a token --",
|
||||
"tokenPlaceholder": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"tokenHelp": "This token will be used only for this operation",
|
||||
"publicRepoInfo": "Public repositories don't require authentication. You can skip providing a token if cloning a public repo.",
|
||||
"noTokensHelp": "No stored tokens available. You can add tokens in Settings → API Keys for easier reuse.",
|
||||
"optionalTokenPublic": "GitHub Token (Optional for Public Repos)",
|
||||
"tokenPublicPlaceholder": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (leave empty for public repos)"
|
||||
},
|
||||
"step3": {
|
||||
"reviewConfig": "Review Your Configuration",
|
||||
"workspaceType": "Workspace Type:",
|
||||
"existingWorkspace": "Existing Workspace",
|
||||
"newWorkspace": "New Workspace",
|
||||
"path": "Path:",
|
||||
"cloneFrom": "Clone From:",
|
||||
"authentication": "Authentication:",
|
||||
"usingStoredToken": "Using stored token:",
|
||||
"usingProvidedToken": "Using provided token",
|
||||
"noAuthentication": "No authentication",
|
||||
"existingInfo": "The workspace will be added to your project list and will be available for Claude/Cursor sessions.",
|
||||
"newWithClone": "A new workspace will be created and the repository will be cloned from GitHub.",
|
||||
"newEmpty": "An empty workspace directory will be created at the specified path."
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Cancel",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"createProject": "Create Project",
|
||||
"creating": "Creating..."
|
||||
},
|
||||
"errors": {
|
||||
"selectType": "Please select whether you have an existing workspace or want to create a new one",
|
||||
"providePath": "Please provide a workspace path",
|
||||
"failedToCreate": "Failed to create workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/i18n/locales/en/settings.json
Normal file
75
src/i18n/locales/en/settings.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Settings",
|
||||
"tabs": {
|
||||
"account": "Account",
|
||||
"permissions": "Permissions",
|
||||
"mcpServers": "MCP Servers",
|
||||
"appearance": "Appearance"
|
||||
},
|
||||
"account": {
|
||||
"language": "Language",
|
||||
"languageLabel": "Display Language",
|
||||
"languageDescription": "Choose your preferred language for the interface",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"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",
|
||||
"editServer": "Edit Server",
|
||||
"deleteServer": "Delete Server",
|
||||
"serverName": "Server Name",
|
||||
"serverType": "Server Type",
|
||||
"config": "Configuration",
|
||||
"testConnection": "Test Connection",
|
||||
"status": "Status",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"scope": {
|
||||
"label": "Scope",
|
||||
"user": "User",
|
||||
"project": "Project"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"theme": "Theme",
|
||||
"codeEditor": "Code Editor",
|
||||
"editorTheme": "Editor Theme",
|
||||
"wordWrap": "Word Wrap",
|
||||
"showMinimap": "Show Minimap",
|
||||
"lineNumbers": "Line Numbers",
|
||||
"fontSize": "Font Size"
|
||||
},
|
||||
"actions": {
|
||||
"saveChanges": "Save Changes",
|
||||
"resetToDefaults": "Reset to Defaults",
|
||||
"cancelChanges": "Cancel Changes"
|
||||
},
|
||||
"quickSettings": {
|
||||
"title": "Quick Settings",
|
||||
"sections": {
|
||||
"appearance": "Appearance",
|
||||
"toolDisplay": "Tool Display",
|
||||
"viewOptions": "View Options",
|
||||
"inputSettings": "Input Settings",
|
||||
"whisperDictation": "Whisper Dictation"
|
||||
},
|
||||
"darkMode": "Dark Mode",
|
||||
"autoExpandTools": "Auto-expand tools",
|
||||
"showRawParameters": "Show raw parameters",
|
||||
"showThinking": "Show thinking",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
102
src/i18n/locales/en/sidebar.json
Normal file
102
src/i18n/locales/en/sidebar.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"projects": {
|
||||
"title": "Projects",
|
||||
"newProject": "New Project",
|
||||
"deleteProject": "Delete Project",
|
||||
"renameProject": "Rename Project",
|
||||
"noProjects": "No projects found",
|
||||
"loadingProjects": "Loading projects...",
|
||||
"searchPlaceholder": "Search projects...",
|
||||
"projectNamePlaceholder": "Project name",
|
||||
"starred": "Starred",
|
||||
"all": "All",
|
||||
"untitledSession": "Untitled Session",
|
||||
"newSession": "New Session",
|
||||
"codexSession": "Codex Session",
|
||||
"fetchingProjects": "Fetching your Claude projects and sessions",
|
||||
"noMatchingProjects": "No matching projects",
|
||||
"tryDifferentSearch": "Try adjusting your search term",
|
||||
"runClaudeCli": "Run Claude CLI in a project directory to get started"
|
||||
},
|
||||
"app": {
|
||||
"title": "Claude Code UI",
|
||||
"subtitle": "AI coding assistant interface"
|
||||
},
|
||||
"sessions": {
|
||||
"title": "Sessions",
|
||||
"newSession": "New Session",
|
||||
"deleteSession": "Delete Session",
|
||||
"renameSession": "Rename Session",
|
||||
"noSessions": "No sessions yet",
|
||||
"loadingSessions": "Loading sessions...",
|
||||
"unnamed": "Unnamed",
|
||||
"loading": "Loading...",
|
||||
"showMore": "Show more sessions"
|
||||
},
|
||||
"tooltips": {
|
||||
"viewEnvironments": "View Environments",
|
||||
"hideSidebar": "Hide sidebar",
|
||||
"createProject": "Create new project",
|
||||
"refresh": "Refresh projects and sessions (Ctrl+R)",
|
||||
"renameProject": "Rename project (F2)",
|
||||
"deleteProject": "Delete empty project (Delete)",
|
||||
"addToFavorites": "Add to favorites",
|
||||
"removeFromFavorites": "Remove from favorites",
|
||||
"editSessionName": "Manually edit session name",
|
||||
"deleteSession": "Delete this session permanently",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"navigation": {
|
||||
"chat": "Chat",
|
||||
"files": "Files",
|
||||
"git": "Git",
|
||||
"terminal": "Terminal",
|
||||
"tasks": "Tasks"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
"settings": "Settings",
|
||||
"collapseAll": "Collapse All",
|
||||
"expandAll": "Expand All",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"rename": "Rename"
|
||||
},
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"thinking": "Thinking...",
|
||||
"error": "Error",
|
||||
"aborted": "Aborted",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "Just now",
|
||||
"oneMinuteAgo": "1 min ago",
|
||||
"minutesAgo": "{{count}} mins ago",
|
||||
"oneHourAgo": "1 hour ago",
|
||||
"hoursAgo": "{{count}} hours ago",
|
||||
"oneDayAgo": "1 day ago",
|
||||
"daysAgo": "{{count}} days ago"
|
||||
},
|
||||
"messages": {
|
||||
"deleteConfirm": "Are you sure you want to delete this?",
|
||||
"renameSuccess": "Renamed successfully",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"errorOccurred": "An error occurred",
|
||||
"deleteSessionConfirm": "Are you sure you want to delete this session? This action cannot be undone.",
|
||||
"deleteProjectConfirm": "Are you sure you want to delete this empty project? This action cannot be undone.",
|
||||
"enterProjectPath": "Please enter a project path",
|
||||
"deleteSessionFailed": "Failed to delete session. Please try again.",
|
||||
"deleteSessionError": "Error deleting session. Please try again.",
|
||||
"deleteProjectFailed": "Failed to delete project. Please try again.",
|
||||
"deleteProjectError": "Error deleting project. Please try again.",
|
||||
"createProjectFailed": "Failed to create project. Please try again.",
|
||||
"createProjectError": "Error creating project. Please try again."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Update available"
|
||||
}
|
||||
}
|
||||
37
src/i18n/locales/zh-CN/auth.json
Normal file
37
src/i18n/locales/zh-CN/auth.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "欢迎回来",
|
||||
"description": "登录您的 Claude Code UI 账户",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"submit": "登录",
|
||||
"loading": "登录中...",
|
||||
"errors": {
|
||||
"invalidCredentials": "用户名或密码无效",
|
||||
"requiredFields": "请填写所有字段",
|
||||
"networkError": "网络错误,请重试。"
|
||||
},
|
||||
"placeholders": {
|
||||
"username": "输入您的用户名",
|
||||
"password": "输入您的密码"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "创建账户",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"submit": "创建账户",
|
||||
"loading": "创建账户中...",
|
||||
"errors": {
|
||||
"passwordMismatch": "密码不匹配",
|
||||
"usernameTaken": "用户名已被占用",
|
||||
"weakPassword": "密码强度太弱"
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "退出登录",
|
||||
"confirm": "确定要退出登录吗?",
|
||||
"button": "退出登录"
|
||||
}
|
||||
}
|
||||
190
src/i18n/locales/zh-CN/common.json
Normal file
190
src/i18n/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"buttons": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"create": "创建",
|
||||
"edit": "编辑",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"submit": "提交",
|
||||
"retry": "重试",
|
||||
"refresh": "刷新",
|
||||
"search": "搜索",
|
||||
"clear": "清除",
|
||||
"copy": "复制",
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"browse": "浏览"
|
||||
},
|
||||
"tabs": {
|
||||
"chat": "聊天",
|
||||
"shell": "终端",
|
||||
"files": "文件",
|
||||
"git": "源代码管理",
|
||||
"tasks": "任务"
|
||||
},
|
||||
"status": {
|
||||
"loading": "加载中...",
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"failed": "失败",
|
||||
"pending": "待处理",
|
||||
"completed": "已完成",
|
||||
"inProgress": "进行中"
|
||||
},
|
||||
"messages": {
|
||||
"savedSuccessfully": "保存成功",
|
||||
"deletedSuccessfully": "删除成功",
|
||||
"updatedSuccessfully": "更新成功",
|
||||
"operationFailed": "操作失败",
|
||||
"networkError": "网络错误,请检查您的连接。",
|
||||
"unauthorized": "未授权,请登录。",
|
||||
"notFound": "未找到",
|
||||
"invalidInput": "输入无效",
|
||||
"requiredField": "此字段为必填项",
|
||||
"unknownError": "发生未知错误"
|
||||
},
|
||||
"navigation": {
|
||||
"settings": "设置",
|
||||
"home": "首页",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"logout": "退出登录"
|
||||
},
|
||||
"common": {
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"darkMode": "深色模式",
|
||||
"lightMode": "浅色模式",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"optional": "可选",
|
||||
"version": "版本",
|
||||
"select": "选择",
|
||||
"selectAll": "全选",
|
||||
"deselectAll": "取消全选"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "{{count}} 分钟前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"daysAgo": "{{count}} 天前",
|
||||
"yesterday": "昨天"
|
||||
},
|
||||
"fileOperations": {
|
||||
"newFile": "新建文件",
|
||||
"newFolder": "新建文件夹",
|
||||
"rename": "重命名",
|
||||
"move": "移动",
|
||||
"copyPath": "复制路径",
|
||||
"openInEditor": "在编辑器中打开"
|
||||
},
|
||||
"mainContent": {
|
||||
"loading": "正在加载 Claude Code UI",
|
||||
"settingUpWorkspace": "正在设置您的工作空间...",
|
||||
"chooseProject": "选择您的项目",
|
||||
"selectProjectDescription": "从侧边栏选择一个项目以开始使用 Claude 进行编程。每个项目包含您的聊天会话和文件历史。",
|
||||
"tip": "提示",
|
||||
"createProjectMobile": "点击上方的菜单按钮以访问项目",
|
||||
"createProjectDesktop": "点击侧边栏中的文件夹图标以创建新项目",
|
||||
"newSession": "新会话",
|
||||
"untitledSession": "未命名会话",
|
||||
"projectFiles": "项目文件"
|
||||
},
|
||||
"fileTree": {
|
||||
"loading": "正在加载文件...",
|
||||
"files": "文件",
|
||||
"simpleView": "简单视图",
|
||||
"compactView": "紧凑视图",
|
||||
"detailedView": "详细视图",
|
||||
"searchPlaceholder": "搜索文件和文件夹...",
|
||||
"clearSearch": "清除搜索",
|
||||
"name": "名称",
|
||||
"size": "大小",
|
||||
"modified": "修改时间",
|
||||
"permissions": "权限",
|
||||
"noFilesFound": "未找到文件",
|
||||
"checkProjectPath": "检查项目路径是否可访问",
|
||||
"noMatchesFound": "未找到匹配项",
|
||||
"tryDifferentSearch": "尝试不同的搜索词或清除搜索",
|
||||
"justNow": "刚刚",
|
||||
"minAgo": "{{count}} 分钟前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"daysAgo": "{{count}} 天前"
|
||||
},
|
||||
"projectWizard": {
|
||||
"title": "创建新项目",
|
||||
"steps": {
|
||||
"type": "类型",
|
||||
"configure": "配置",
|
||||
"confirm": "确认"
|
||||
},
|
||||
"step1": {
|
||||
"question": "您已经有工作区,还是想创建一个新的工作区?",
|
||||
"existing": {
|
||||
"title": "现有工作区",
|
||||
"description": "我的服务器上已经有工作区,只需要将其添加到项目列表中"
|
||||
},
|
||||
"new": {
|
||||
"title": "新建工作区",
|
||||
"description": "创建一个新工作区,可选择从 GitHub 仓库克隆"
|
||||
}
|
||||
},
|
||||
"step2": {
|
||||
"existingPath": "工作区路径",
|
||||
"newPath": "应该在哪里创建工作区?",
|
||||
"existingPlaceholder": "/path/to/existing/workspace",
|
||||
"newPlaceholder": "/path/to/new/workspace",
|
||||
"existingHelp": "您现有工作区目录的完整路径",
|
||||
"newHelp": "将创建新工作区的完整路径",
|
||||
"githubUrl": "GitHub URL(可选)",
|
||||
"githubPlaceholder": "https://github.com/username/repository",
|
||||
"githubHelp": "留空以创建空工作区,或提供 GitHub URL 以克隆",
|
||||
"githubAuth": "GitHub 身份验证(可选)",
|
||||
"githubAuthHelp": "仅私有仓库需要。公共仓库无需身份验证即可克隆。",
|
||||
"loadingTokens": "正在加载已保存的令牌...",
|
||||
"storedToken": "已保存的令牌",
|
||||
"newToken": "新令牌",
|
||||
"nonePublic": "无(公共)",
|
||||
"selectToken": "选择令牌",
|
||||
"selectTokenPlaceholder": "-- 选择令牌 --",
|
||||
"tokenPlaceholder": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"tokenHelp": "此令牌仅用于此操作",
|
||||
"publicRepoInfo": "公共仓库不需要身份验证。如果克隆公共仓库,可以跳过提供令牌。",
|
||||
"noTokensHelp": "没有可用的已保存令牌。您可以在 设置 → API 密钥 中添加令牌以便重复使用。",
|
||||
"optionalTokenPublic": "GitHub 令牌(公共仓库可选)",
|
||||
"tokenPublicPlaceholder": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(公共仓库可留空)"
|
||||
},
|
||||
"step3": {
|
||||
"reviewConfig": "查看您的配置",
|
||||
"workspaceType": "工作区类型:",
|
||||
"existingWorkspace": "现有工作区",
|
||||
"newWorkspace": "新建工作区",
|
||||
"path": "路径:",
|
||||
"cloneFrom": "克隆自:",
|
||||
"authentication": "身份验证:",
|
||||
"usingStoredToken": "使用已保存的令牌:",
|
||||
"usingProvidedToken": "使用提供的令牌",
|
||||
"noAuthentication": "无身份验证",
|
||||
"existingInfo": "工作区将被添加到您的项目列表中,并可用于 Claude/Cursor 会话。",
|
||||
"newWithClone": "将创建新工作区,并从 GitHub 克隆仓库。",
|
||||
"newEmpty": "将在指定路径创建一个空的工作区目录。"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "取消",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"createProject": "创建项目",
|
||||
"creating": "创建中..."
|
||||
},
|
||||
"errors": {
|
||||
"selectType": "请选择您已有现有工作区还是想创建新工作区",
|
||||
"providePath": "请提供工作区路径",
|
||||
"failedToCreate": "创建工作区失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/i18n/locales/zh-CN/settings.json
Normal file
75
src/i18n/locales/zh-CN/settings.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "设置",
|
||||
"tabs": {
|
||||
"account": "账户",
|
||||
"permissions": "权限",
|
||||
"mcpServers": "MCP 服务器",
|
||||
"appearance": "外观"
|
||||
},
|
||||
"account": {
|
||||
"language": "语言",
|
||||
"languageLabel": "显示语言",
|
||||
"languageDescription": "选择您偏好的界面语言",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"profile": "个人资料",
|
||||
"changePassword": "修改密码"
|
||||
},
|
||||
"permissions": {
|
||||
"allowedTools": "允许的工具",
|
||||
"disallowedTools": "禁止的工具",
|
||||
"addTool": "添加工具",
|
||||
"removeTool": "移除工具",
|
||||
"description": "配置 Claude 可以使用的工具。工具必须在此处启用后,Claude 才能访问它们。"
|
||||
},
|
||||
"mcp": {
|
||||
"title": "MCP 服务器",
|
||||
"addServer": "添加服务器",
|
||||
"editServer": "编辑服务器",
|
||||
"deleteServer": "删除服务器",
|
||||
"serverName": "服务器名称",
|
||||
"serverType": "服务器类型",
|
||||
"config": "配置",
|
||||
"testConnection": "测试连接",
|
||||
"status": "状态",
|
||||
"connected": "已连接",
|
||||
"disconnected": "未连接",
|
||||
"scope": {
|
||||
"label": "范围",
|
||||
"user": "用户",
|
||||
"project": "项目"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "外观",
|
||||
"theme": "主题",
|
||||
"codeEditor": "代码编辑器",
|
||||
"editorTheme": "编辑器主题",
|
||||
"wordWrap": "自动换行",
|
||||
"showMinimap": "显示缩略图",
|
||||
"lineNumbers": "行号",
|
||||
"fontSize": "字体大小"
|
||||
},
|
||||
"actions": {
|
||||
"saveChanges": "保存更改",
|
||||
"resetToDefaults": "重置为默认值",
|
||||
"cancelChanges": "取消更改"
|
||||
},
|
||||
"quickSettings": {
|
||||
"title": "快速设置",
|
||||
"sections": {
|
||||
"appearance": "外观",
|
||||
"toolDisplay": "工具显示",
|
||||
"viewOptions": "视图选项",
|
||||
"inputSettings": "输入设置",
|
||||
"whisperDictation": "Whisper 听写"
|
||||
},
|
||||
"darkMode": "深色模式",
|
||||
"autoExpandTools": "自动展开工具",
|
||||
"showRawParameters": "显示原始参数",
|
||||
"showThinking": "显示思考过程",
|
||||
"autoScrollToBottom": "自动滚动到底部",
|
||||
"sendByCtrlEnter": "使用 Ctrl+Enter 发送",
|
||||
"sendByCtrlEnterDescription": "启用后,按 Ctrl+Enter 发送消息,而不是仅按 Enter。这对于使用输入法的用户可以避免意外发送。"
|
||||
}
|
||||
}
|
||||
102
src/i18n/locales/zh-CN/sidebar.json
Normal file
102
src/i18n/locales/zh-CN/sidebar.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"projects": {
|
||||
"title": "项目",
|
||||
"newProject": "新建项目",
|
||||
"deleteProject": "删除项目",
|
||||
"renameProject": "重命名项目",
|
||||
"noProjects": "未找到项目",
|
||||
"loadingProjects": "加载项目中...",
|
||||
"searchPlaceholder": "搜索项目...",
|
||||
"projectNamePlaceholder": "项目名称",
|
||||
"starred": "星标",
|
||||
"all": "全部",
|
||||
"untitledSession": "未命名会话",
|
||||
"newSession": "新会话",
|
||||
"codexSession": "Codex 会话",
|
||||
"fetchingProjects": "正在获取您的 Claude 项目和会话",
|
||||
"noMatchingProjects": "未找到匹配的项目",
|
||||
"tryDifferentSearch": "尝试调整您的搜索词",
|
||||
"runClaudeCli": "在项目目录中运行 Claude CLI 以开始使用"
|
||||
},
|
||||
"app": {
|
||||
"title": "Claude Code UI",
|
||||
"subtitle": "AI 编程助手"
|
||||
},
|
||||
"sessions": {
|
||||
"title": "会话",
|
||||
"newSession": "新建会话",
|
||||
"deleteSession": "删除会话",
|
||||
"renameSession": "重命名会话",
|
||||
"noSessions": "暂无会话",
|
||||
"loadingSessions": "加载会话中...",
|
||||
"unnamed": "未命名",
|
||||
"loading": "加载中...",
|
||||
"showMore": "显示更多会话"
|
||||
},
|
||||
"tooltips": {
|
||||
"viewEnvironments": "查看环境",
|
||||
"hideSidebar": "隐藏侧边栏",
|
||||
"createProject": "创建新项目",
|
||||
"refresh": "刷新项目和会话 (Ctrl+R)",
|
||||
"renameProject": "重命名项目 (F2)",
|
||||
"deleteProject": "删除空项目 (Delete)",
|
||||
"addToFavorites": "添加到收藏",
|
||||
"removeFromFavorites": "从收藏移除",
|
||||
"editSessionName": "手动编辑会话名称",
|
||||
"deleteSession": "永久删除此会话",
|
||||
"save": "保存",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"navigation": {
|
||||
"chat": "聊天",
|
||||
"files": "文件",
|
||||
"git": "Git",
|
||||
"terminal": "终端",
|
||||
"tasks": "任务"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "刷新",
|
||||
"settings": "设置",
|
||||
"collapseAll": "全部折叠",
|
||||
"expandAll": "全部展开",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"rename": "重命名"
|
||||
},
|
||||
"status": {
|
||||
"active": "活动",
|
||||
"inactive": "非活动",
|
||||
"thinking": "思考中...",
|
||||
"error": "错误",
|
||||
"aborted": "已中止",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "刚刚",
|
||||
"oneMinuteAgo": "1 分钟前",
|
||||
"minutesAgo": "{{count}} 分钟前",
|
||||
"oneHourAgo": "1 小时前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"oneDayAgo": "1 天前",
|
||||
"daysAgo": "{{count}} 天前"
|
||||
},
|
||||
"messages": {
|
||||
"deleteConfirm": "确定要删除吗?",
|
||||
"renameSuccess": "重命名成功",
|
||||
"deleteSuccess": "删除成功",
|
||||
"errorOccurred": "发生错误",
|
||||
"deleteSessionConfirm": "确定要删除此会话吗?此操作无法撤销。",
|
||||
"deleteProjectConfirm": "确定要删除此空项目吗?此操作无法撤销。",
|
||||
"enterProjectPath": "请输入项目路径",
|
||||
"deleteSessionFailed": "删除会话失败,请重试。",
|
||||
"deleteSessionError": "删除会话时出错,请重试。",
|
||||
"deleteProjectFailed": "删除项目失败,请重试。",
|
||||
"deleteProjectError": "删除项目时出错,请重试。",
|
||||
"createProjectFailed": "创建项目失败,请重试。",
|
||||
"createProjectError": "创建项目时出错,请重试。"
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "有可用更新"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import App from './App.jsx'
|
||||
import './index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
// Initialize i18n
|
||||
import './i18n/config.js'
|
||||
|
||||
// Clean up stale service workers on app load to prevent caching issues after builds
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
|
||||
Reference in New Issue
Block a user