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

@@ -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>
);
}

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>

121
src/i18n/config.js Normal file
View 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
View 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);
};

View 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"
}
}

View 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"
}
}
}

View 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."
}
}

View 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"
}
}

View 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": "退出登录"
}
}

View 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": "创建工作区失败"
}
}
}

View 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。这对于使用输入法的用户可以避免意外发送。"
}
}

View 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": "有可用更新"
}
}

View File

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