add i18n feat && Add partial translation

This commit is contained in:
YuanNiancai
2026-01-16 19:11:19 +08:00
parent 42b2d5e1d9
commit 4216676395
32 changed files with 3934 additions and 220 deletions

View File

@@ -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>
) : (

View 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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />
</>
)}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>