feat: v0.13.0 — add i18n support with Chinese/English translations
Implements a zero-dependency i18n framework with TypeScript type safety. Thanks to @gy212 for the original i18n infrastructure design (PR #89) and comprehensive translation files (PR #90-96) that inspired this implementation. ## New files (5) - src/i18n/en.ts: English translations (source of truth, ~270 keys), exports TranslationKey type for compile-time key completeness - src/i18n/zh.ts: Chinese translations, typed as Record<TranslationKey, string> - src/i18n/index.ts: translate() function with {param} interpolation, Locale type, SUPPORTED_LOCALES array - src/components/layout/I18nProvider.tsx: React Context provider, loads/persists locale preference via /api/settings/app - src/hooks/useTranslation.ts: useTranslation() hook returning { locale, setLocale, t } ## Infrastructure wiring - src/app/layout.tsx: I18nProvider wrapped inside ThemeProvider - src/app/api/settings/app/route.ts: added "locale" to ALLOWED_KEYS for persistence ## Language picker - src/components/settings/GeneralSection.tsx: added language selection dropdown (Select component) in Settings > General, using SUPPORTED_LOCALES and setLocale() ## String replacements (33 component files) All hardcoded user-facing strings replaced with t() calls: High priority (chat): - ChatListPanel.tsx: session list, search, empty state, relative time - NavRail.tsx: nav labels, theme toggle, auto-approve indicator - MessageInput.tsx: command descriptions, mode labels, attach tooltip - MessageList.tsx: empty state, load earlier, loading indicator - StreamingMessage.tsx: thinking, permission actions - chat/[id]/page.tsx: session title fallback Settings: - GeneralSection.tsx: update card, auto-approve section, warning dialog - SettingsLayout.tsx: title, description, sidebar labels - CliSettingsSection.tsx: form/json tabs, buttons, dynamic field labels (skipDangerousModePermissionPrompt etc.), Enabled/Disabled states - ProviderForm.tsx: dialog title, form labels, buttons - ProviderManager.tsx: status badges, delete dialog, section headers Extensions: - extensions/page.tsx: title, tab labels - SkillsManager.tsx: empty state, loading, search - SkillEditor.tsx: toolbar buttons, placeholder - SkillListItem.tsx: delete confirmation - CreateSkillDialog.tsx: form labels, scope, templates, validation Plugins: - McpManager.tsx: header, tabs, loading state - McpServerList.tsx: empty state, badges, field labels - McpServerEditor.tsx: dialog title, form labels, buttons - ConfigEditor.tsx: save/format buttons Layout: - RightPanel.tsx: panel header, open/close tooltips - FileTree.tsx: search placeholder, empty states - FilePreview.tsx: back button, line count, copy path - DocPreview.tsx: iframe title - ConnectionStatus.tsx: install prompts, version display - InstallWizard.tsx: phase descriptions, button labels - ImportSessionDialog.tsx: search, relative time, import buttons - FolderPicker.tsx: dialog title, loading, buttons - TaskList.tsx: filter tabs, input placeholder, empty states
This commit is contained in:
1 parent
85940dfde3
commit
c7c3ba5302
38 files changed
+980
-217
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codepilot",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.33",
|
||||
"@hugeicons/core-free-icons": "^3.1.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "op7418",
|
||||
|
||||
@@ -10,6 +10,7 @@ const ALLOWED_KEYS = [
|
||||
'anthropic_auth_token',
|
||||
'anthropic_base_url',
|
||||
'dangerously_skip_permissions',
|
||||
'locale',
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Loading02Icon, PencilEdit01Icon } from "@hugeicons/core-free-icons";
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { usePanel } from '@/hooks/usePanel';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface ChatSessionPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -29,9 +30,10 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
|
||||
const [editTitle, setEditTitle] = useState('');
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
const { setWorkingDirectory, setSessionId, setSessionTitle: setPanelSessionTitle, setPanelOpen } = usePanel();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleStartEditTitle = useCallback(() => {
|
||||
setEditTitle(sessionTitle || 'New Conversation');
|
||||
setEditTitle(sessionTitle || t('chat.newConversation'));
|
||||
setIsEditingTitle(true);
|
||||
}, [sessionTitle]);
|
||||
|
||||
@@ -88,7 +90,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
|
||||
}
|
||||
setSessionId(id);
|
||||
setPanelOpen(true);
|
||||
const title = data.session.title || 'New Conversation';
|
||||
const title = data.session.title || t('chat.newConversation');
|
||||
setSessionTitle(title);
|
||||
setPanelSessionTitle(title);
|
||||
setSessionModel(data.session.model || '');
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Loading02Icon } from "@hugeicons/core-free-icons";
|
||||
import { SkillsManager } from "@/components/skills/SkillsManager";
|
||||
import { McpManager } from "@/components/plugins/McpManager";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
type ExtTab = "skills" | "mcp";
|
||||
|
||||
@@ -28,15 +29,16 @@ function ExtensionsPageInner() {
|
||||
const searchParams = useSearchParams();
|
||||
const initialTab = (searchParams.get("tab") as ExtTab) || "skills";
|
||||
const [tab, setTab] = useState<ExtTab>(initialTab);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="px-6 pt-4 pb-0">
|
||||
<h1 className="text-xl font-semibold mb-3">Extensions</h1>
|
||||
<h1 className="text-xl font-semibold mb-3">{t('extensions.title')}</h1>
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as ExtTab)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="skills">Skills</TabsTrigger>
|
||||
<TabsTrigger value="mcp">MCP Servers</TabsTrigger>
|
||||
<TabsTrigger value="skills">{t('extensions.skills')}</TabsTrigger>
|
||||
<TabsTrigger value="mcp">{t('extensions.mcpServers')}</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/layout/ThemeProvider";
|
||||
import { I18nProvider } from "@/components/layout/I18nProvider";
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -30,7 +31,9 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
<I18nProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface FolderEntry {
|
||||
name: string;
|
||||
@@ -40,6 +41,7 @@ interface FolderPickerProps {
|
||||
}
|
||||
|
||||
export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: FolderPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [currentDir, setCurrentDir] = useState('');
|
||||
const [parentDir, setParentDir] = useState<string | null>(null);
|
||||
const [directories, setDirectories] = useState<FolderEntry[]>([]);
|
||||
@@ -99,7 +101,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select Project Folder</DialogTitle>
|
||||
<DialogTitle>{t('folderPicker.title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Path input */}
|
||||
@@ -162,11 +164,11 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
|
||||
<ScrollArea className="h-64">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||
Loading...
|
||||
{t('folderPicker.loading')}
|
||||
</div>
|
||||
) : directories.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||
No subdirectories
|
||||
{t('folderPicker.noSubdirs')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-1">
|
||||
@@ -188,11 +190,11 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{t('folderPicker.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSelect} className="gap-2">
|
||||
<HugeiconsIcon icon={FolderOpenIcon} className="h-4 w-4" />
|
||||
Select This Folder
|
||||
{t('folderPicker.select')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
StopIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { TranslationKey } from '@/i18n';
|
||||
import {
|
||||
PromptInput,
|
||||
PromptInputTextarea,
|
||||
@@ -65,6 +67,7 @@ interface PopoverItem {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
descriptionKey?: TranslationKey;
|
||||
builtIn?: boolean;
|
||||
immediate?: boolean;
|
||||
installedSource?: "agents" | "claude";
|
||||
@@ -91,15 +94,15 @@ const COMMAND_PROMPTS: Record<string, string> = {
|
||||
};
|
||||
|
||||
const BUILT_IN_COMMANDS: PopoverItem[] = [
|
||||
{ label: 'help', value: '/help', description: 'Show available commands and tips', builtIn: true, immediate: true, icon: HelpCircleIcon },
|
||||
{ label: 'clear', value: '/clear', description: 'Clear conversation history', builtIn: true, immediate: true, icon: Delete02Icon },
|
||||
{ label: 'cost', value: '/cost', description: 'Show token usage statistics', builtIn: true, immediate: true, icon: Coins01Icon },
|
||||
{ label: 'compact', value: '/compact', description: 'Compress conversation context', builtIn: true, icon: FileZipIcon },
|
||||
{ label: 'doctor', value: '/doctor', description: 'Diagnose project health', builtIn: true, icon: Stethoscope02Icon },
|
||||
{ label: 'init', value: '/init', description: 'Initialize CLAUDE.md for project', builtIn: true, icon: FileEditIcon },
|
||||
{ label: 'review', value: '/review', description: 'Review code quality', builtIn: true, icon: SearchList01Icon },
|
||||
{ label: 'terminal-setup', value: '/terminal-setup', description: 'Configure terminal settings', builtIn: true, icon: CommandLineIcon },
|
||||
{ label: 'memory', value: '/memory', description: 'Edit project memory file', builtIn: true, icon: BrainIcon },
|
||||
{ label: 'help', value: '/help', description: 'Show available commands and tips', descriptionKey: 'messageInput.helpDesc', builtIn: true, immediate: true, icon: HelpCircleIcon },
|
||||
{ label: 'clear', value: '/clear', description: 'Clear conversation history', descriptionKey: 'messageInput.clearDesc', builtIn: true, immediate: true, icon: Delete02Icon },
|
||||
{ label: 'cost', value: '/cost', description: 'Show token usage statistics', descriptionKey: 'messageInput.costDesc', builtIn: true, immediate: true, icon: Coins01Icon },
|
||||
{ label: 'compact', value: '/compact', description: 'Compress conversation context', descriptionKey: 'messageInput.compactDesc', builtIn: true, icon: FileZipIcon },
|
||||
{ label: 'doctor', value: '/doctor', description: 'Diagnose project health', descriptionKey: 'messageInput.doctorDesc', builtIn: true, icon: Stethoscope02Icon },
|
||||
{ label: 'init', value: '/init', description: 'Initialize CLAUDE.md for project', descriptionKey: 'messageInput.initDesc', builtIn: true, icon: FileEditIcon },
|
||||
{ label: 'review', value: '/review', description: 'Review code quality', descriptionKey: 'messageInput.reviewDesc', builtIn: true, icon: SearchList01Icon },
|
||||
{ label: 'terminal-setup', value: '/terminal-setup', description: 'Configure terminal settings', descriptionKey: 'messageInput.terminalSetupDesc', builtIn: true, icon: CommandLineIcon },
|
||||
{ label: 'memory', value: '/memory', description: 'Edit project memory file', descriptionKey: 'messageInput.memoryDesc', builtIn: true, icon: BrainIcon },
|
||||
];
|
||||
|
||||
interface ModeOption {
|
||||
@@ -183,11 +186,12 @@ function FileAwareSubmitButton({
|
||||
*/
|
||||
function AttachFileButton() {
|
||||
const attachments = usePromptInputAttachments();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PromptInputButton
|
||||
onClick={() => attachments.openFileDialog()}
|
||||
tooltip="Attach files"
|
||||
tooltip={t('messageInput.attachFiles')}
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
</PromptInputButton>
|
||||
@@ -331,6 +335,7 @@ export function MessageInput({
|
||||
mode = 'code',
|
||||
onModeChange,
|
||||
}: MessageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -752,9 +757,9 @@ export function MessageInput({
|
||||
<HugeiconsIcon icon={CommandLineIcon} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="font-mono text-xs truncate">{item.label}</span>
|
||||
{item.description && (
|
||||
{(item.descriptionKey || item.description) && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||
{item.description}
|
||||
{item.descriptionKey ? t(item.descriptionKey) : item.description}
|
||||
</span>
|
||||
)}
|
||||
{!item.builtIn && item.installedSource && (
|
||||
@@ -919,7 +924,7 @@ export function MessageInput({
|
||||
)}
|
||||
onClick={() => onModeChange?.(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
{opt.value === 'code' ? t('messageInput.modeCode') : opt.value === 'plan' ? t('messageInput.modePlan') : opt.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { Message, PermissionRequestEvent } from '@/types';
|
||||
import {
|
||||
Conversation,
|
||||
@@ -57,6 +58,7 @@ export function MessageList({
|
||||
loadingMore,
|
||||
onLoadMore,
|
||||
}: MessageListProps) {
|
||||
const { t } = useTranslation();
|
||||
// Scroll anchor: preserve position when older messages are prepended
|
||||
const anchorIdRef = useRef<string | null>(null);
|
||||
const prevMessageCountRef = useRef(messages.length);
|
||||
@@ -86,7 +88,7 @@ export function MessageList({
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<ConversationEmptyState
|
||||
title="Claude Chat"
|
||||
description="Start a conversation with Claude. Ask questions, get help with code, or explore ideas."
|
||||
description={t('messageList.emptyDescription')}
|
||||
icon={<CodePilotLogo className="h-16 w-16" />}
|
||||
/>
|
||||
</div>
|
||||
@@ -103,7 +105,7 @@ export function MessageList({
|
||||
disabled={loadingMore}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loadingMore ? 'Loading...' : 'Load earlier messages'}
|
||||
{loadingMore ? t('messageList.loading') : t('messageList.loadEarlier')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import {
|
||||
Message as AIMessage,
|
||||
MessageContent,
|
||||
@@ -295,6 +296,7 @@ export function StreamingMessage({
|
||||
permissionResolved,
|
||||
onForceStop,
|
||||
}: StreamingMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const runningTools = toolUses.filter(
|
||||
(tool) => !toolResults.some((r) => r.tool_use_id === tool.id)
|
||||
);
|
||||
@@ -430,18 +432,18 @@ export function StreamingMessage({
|
||||
variant="default"
|
||||
onClick={() => onPermissionResponse?.('allow_session')}
|
||||
>
|
||||
Allow for Session
|
||||
{t('streaming.allowForSession')}
|
||||
</ConfirmationAction>
|
||||
)}
|
||||
</ConfirmationActions>
|
||||
</ConfirmationRequest>
|
||||
|
||||
<ConfirmationAccepted>
|
||||
<p className="text-xs text-green-600 dark:text-green-400">Allowed</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-400">{t('streaming.allowed')}</p>
|
||||
</ConfirmationAccepted>
|
||||
|
||||
<ConfirmationRejected>
|
||||
<p className="text-xs text-red-600 dark:text-red-400">Denied</p>
|
||||
<p className="text-xs text-red-600 dark:text-red-400">{t('streaming.denied')}</p>
|
||||
</ConfirmationRejected>
|
||||
</Confirmation>
|
||||
)}
|
||||
@@ -454,7 +456,7 @@ export function StreamingMessage({
|
||||
{/* Loading indicator when no content yet */}
|
||||
{isStreaming && !content && toolUses.length === 0 && !pendingPermission && (
|
||||
<div className="py-2">
|
||||
<Shimmer>Thinking...</Shimmer>
|
||||
<Shimmer>{t('streaming.thinking')}</Shimmer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePanel } from "@/hooks/usePanel";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { ConnectionStatus } from "./ConnectionStatus";
|
||||
import { ImportSessionDialog } from "./ImportSessionDialog";
|
||||
import { FolderPicker } from "@/components/chat/FolderPicker";
|
||||
@@ -35,7 +36,7 @@ interface ChatListPanelProps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateStr: string): string {
|
||||
function formatRelativeTime(dateStr: string, t: (key: import('@/i18n').TranslationKey, params?: Record<string, string | number>) => string): string {
|
||||
const date = new Date(dateStr.includes("T") ? dateStr : dateStr + "Z");
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
@@ -43,10 +44,10 @@ function formatRelativeTime(dateStr: string): string {
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m`;
|
||||
if (diffHr < 24) return `${diffHr}h`;
|
||||
if (diffDay < 7) return `${diffDay}d`;
|
||||
if (diffMin < 1) return t('chatList.justNow');
|
||||
if (diffMin < 60) return t('chatList.minutesAgo', { n: diffMin });
|
||||
if (diffHr < 24) return t('chatList.hoursAgo', { n: diffHr });
|
||||
if (diffDay < 7) return t('chatList.daysAgo', { n: diffDay });
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
@@ -117,6 +118,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { streamingSessionId, pendingApprovalSessionId, workingDirectory } = usePanel();
|
||||
const { t } = useTranslation();
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [hoveredSession, setHoveredSession] = useState<string | null>(null);
|
||||
const [deletingSession, setDeletingSession] = useState<string | null>(null);
|
||||
@@ -331,7 +333,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
New Chat
|
||||
{t('chatList.newConversation')}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -342,10 +344,10 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
onClick={() => setFolderPickerOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={FolderOpenIcon} className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Open project folder</span>
|
||||
<span className="sr-only">{t('chatList.addProjectFolder')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Open project folder</TooltipContent>
|
||||
<TooltipContent side="bottom">{t('chatList.addProjectFolder')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +359,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
className="absolute left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search threads..."
|
||||
placeholder={t('chatList.searchSessions')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-8 pl-7 text-xs"
|
||||
@@ -374,7 +376,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={FileImportIcon} className="h-3 w-3" />
|
||||
Import CLI Session
|
||||
{t('chatList.importFromCli')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -383,7 +385,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
<div className="flex flex-col pb-3">
|
||||
{filteredSessions.length === 0 ? (
|
||||
<p className="px-2.5 py-3 text-[11px] text-muted-foreground/60">
|
||||
{searchQuery ? "No matching threads" : "No conversations yet"}
|
||||
{searchQuery ? "No matching threads" : t('chatList.noSessions')}
|
||||
</p>
|
||||
) : (
|
||||
projectGroups.map((group) => {
|
||||
@@ -539,7 +541,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
{badgeCfg.label}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground/40">
|
||||
{formatRelativeTime(session.updated_at)}
|
||||
{formatRelativeTime(session.updated_at, t)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -560,12 +562,12 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<span className="sr-only">
|
||||
Delete session
|
||||
{t('chatList.delete')}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
Delete
|
||||
{t('chatList.delete')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { InstallWizard } from "@/components/layout/InstallWizard";
|
||||
|
||||
interface ClaudeStatus {
|
||||
@@ -23,6 +24,7 @@ const BACKED_OFF_INTERVAL = 60_000; // 60s after 3 consecutive stable results
|
||||
const STABLE_THRESHOLD = 3;
|
||||
|
||||
export function ConnectionStatus() {
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState<ClaudeStatus | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
@@ -150,7 +152,7 @@ export function ConnectionStatus() {
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{connected ? "Claude Code Connected" : "Claude Code Not Connected"}
|
||||
{connected ? t('connection.installed') : t('connection.notInstalled')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{connected
|
||||
@@ -165,7 +167,7 @@ export function ConnectionStatus() {
|
||||
<span className="block h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-500" />
|
||||
<div>
|
||||
<p className="font-medium text-emerald-700 dark:text-emerald-400">Active</p>
|
||||
<p className="text-xs text-muted-foreground">Version {status?.version}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('connection.version', { version: status?.version ?? '' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,7 +179,7 @@ export function ConnectionStatus() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-1.5">1. Install Claude Code</h4>
|
||||
<h4 className="font-medium mb-1.5">1. {t('connection.installClaude')}</h4>
|
||||
<code className="block rounded-md bg-muted px-3 py-2 text-xs">
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
</code>
|
||||
@@ -206,7 +208,7 @@ export function ConnectionStatus() {
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Install Claude Code Automatically
|
||||
{t('connection.installAuto')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -218,7 +220,7 @@ export function ConnectionStatus() {
|
||||
variant="outline"
|
||||
onClick={handleManualRefresh}
|
||||
>
|
||||
Refresh
|
||||
{t('connection.refresh')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { code } from "@streamdown/code";
|
||||
import { math } from "@streamdown/math";
|
||||
import { mermaid } from "@streamdown/mermaid";
|
||||
import { usePanel } from "@/hooks/usePanel";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { FilePreview as FilePreviewType } from "@/types";
|
||||
|
||||
const streamdownPlugins = { cjk, code, math, mermaid };
|
||||
@@ -252,13 +253,14 @@ function RenderedView({
|
||||
content: string;
|
||||
filePath: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (isHtml(filePath)) {
|
||||
return (
|
||||
<iframe
|
||||
srcDoc={content}
|
||||
sandbox=""
|
||||
className="h-full w-full border-0"
|
||||
title="HTML Preview"
|
||||
title={t('docPreview.htmlPreview')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||
import { type Locale, type TranslationKey, translate } from '@/i18n';
|
||||
|
||||
interface I18nContextValue {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
||||
}
|
||||
|
||||
export const I18nContext = createContext<I18nContextValue>({
|
||||
locale: 'en',
|
||||
setLocale: () => {},
|
||||
t: (key) => key,
|
||||
});
|
||||
|
||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocaleState] = useState<Locale>('en');
|
||||
|
||||
// Load persisted locale on mount
|
||||
useEffect(() => {
|
||||
async function loadLocale() {
|
||||
try {
|
||||
const res = await fetch('/api/settings/app');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const saved = data.settings?.locale;
|
||||
if (saved === 'en' || saved === 'zh') {
|
||||
setLocaleState(saved);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore — default to 'en'
|
||||
}
|
||||
}
|
||||
loadLocale();
|
||||
}, []);
|
||||
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
setLocaleState(newLocale);
|
||||
// Persist to app settings
|
||||
fetch('/api/settings/app', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: { locale: newLocale } }),
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const t = useCallback(
|
||||
(key: TranslationKey, params?: Record<string, string | number>) =>
|
||||
translate(locale, key, params),
|
||||
[locale],
|
||||
);
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={{ locale, setLocale, t }}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
FileImportIcon,
|
||||
MessageAddIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ClaudeSessionInfo {
|
||||
@@ -45,7 +46,7 @@ interface ImportSessionDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateStr: string): string {
|
||||
function formatRelativeTime(dateStr: string): { key: 'import.justNow' | 'import.minutesAgo' | 'import.hoursAgo' | 'import.daysAgo'; params?: Record<string, number> } | string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
@@ -53,10 +54,10 @@ function formatRelativeTime(dateStr: string): string {
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
if (diffDay < 7) return `${diffDay}d ago`;
|
||||
if (diffMin < 1) return { key: 'import.justNow' };
|
||||
if (diffMin < 60) return { key: 'import.minutesAgo', params: { n: diffMin } };
|
||||
if (diffHr < 24) return { key: 'import.hoursAgo', params: { n: diffHr } };
|
||||
if (diffDay < 7) return { key: 'import.daysAgo', params: { n: diffDay } };
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
@@ -71,6 +72,7 @@ export function ImportSessionDialog({
|
||||
onOpenChange,
|
||||
}: ImportSessionDialogProps) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [sessions, setSessions] = useState<ClaudeSessionInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [importing, setImporting] = useState<string | null>(null);
|
||||
@@ -154,7 +156,7 @@ export function ImportSessionDialog({
|
||||
icon={FileImportIcon}
|
||||
className="h-5 w-5 text-primary"
|
||||
/>
|
||||
Import CLI Session
|
||||
{t('import.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Browse and import conversations from Claude Code CLI. Imported
|
||||
@@ -169,7 +171,7 @@ export function ImportSessionDialog({
|
||||
className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search by project, message, or branch..."
|
||||
placeholder={t('import.searchSessions')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8 text-sm"
|
||||
@@ -204,8 +206,8 @@ export function ImportSessionDialog({
|
||||
/>
|
||||
<p className="text-sm">
|
||||
{searchQuery
|
||||
? "No matching sessions"
|
||||
: "No Claude Code CLI sessions found"}
|
||||
? t('import.noSessions')
|
||||
: t('import.noSessions')}
|
||||
</p>
|
||||
<p className="text-xs mt-1 opacity-60">
|
||||
{searchQuery
|
||||
@@ -264,7 +266,7 @@ export function ImportSessionDialog({
|
||||
icon={Loading02Icon}
|
||||
className="h-3 w-3 mr-1 animate-spin"
|
||||
/>
|
||||
Importing...
|
||||
{t('import.importing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -272,7 +274,7 @@ export function ImportSessionDialog({
|
||||
icon={FileImportIcon}
|
||||
className="h-3 w-3 mr-1"
|
||||
/>
|
||||
Import
|
||||
{t('import.import')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -295,14 +297,17 @@ export function ImportSessionDialog({
|
||||
icon={MessageAddIcon}
|
||||
className="h-2.5 w-2.5"
|
||||
/>
|
||||
{totalMessages} msg{totalMessages !== 1 ? "s" : ""}
|
||||
{t(totalMessages !== 1 ? 'import.messagesPlural' : 'import.messages', { n: totalMessages })}
|
||||
</span>
|
||||
<span className="flex items-center gap-0.5 shrink-0">
|
||||
<HugeiconsIcon
|
||||
icon={ClockIcon}
|
||||
className="h-2.5 w-2.5"
|
||||
/>
|
||||
{formatRelativeTime(session.updatedAt)}
|
||||
{(() => {
|
||||
const rel = formatRelativeTime(session.updatedAt);
|
||||
return typeof rel === 'string' ? rel : t(rel.key, rel.params);
|
||||
})()}
|
||||
</span>
|
||||
<span className="shrink-0">
|
||||
{formatFileSize(session.fileSize)}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Copy01Icon,
|
||||
Download04Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface InstallProgress {
|
||||
status: "idle" | "running" | "success" | "failed" | "cancelled";
|
||||
@@ -93,6 +94,7 @@ export function InstallWizard({
|
||||
onOpenChange,
|
||||
onInstallComplete,
|
||||
}: InstallWizardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [phase, setPhase] = useState<WizardPhase>("checking");
|
||||
const [progress, setProgress] = useState<InstallProgress | null>(null);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
@@ -249,7 +251,7 @@ export function InstallWizard({
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Install Claude Code</DialogTitle>
|
||||
<DialogTitle>{t('install.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{phase === "confirm"
|
||||
? "Claude Code CLI was not detected. Install it now?"
|
||||
@@ -292,7 +294,7 @@ export function InstallWizard({
|
||||
{phase === "checking" && steps.length === 0 && (
|
||||
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||
<HugeiconsIcon icon={Loading02Icon} className="size-4 animate-spin" />
|
||||
<span>Checking environment...</span>
|
||||
<span>{t('install.checkingPrereqs')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -330,7 +332,7 @@ export function InstallWizard({
|
||||
Already installed
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Claude Code is already available.
|
||||
{t('install.alreadyInstalled')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,7 +344,7 @@ export function InstallWizard({
|
||||
<HugeiconsIcon icon={Tick01Icon} className="size-5 text-emerald-500 shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-emerald-700 dark:text-emerald-400">
|
||||
Installation complete
|
||||
{t('install.complete')}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Claude Code CLI has been installed successfully.
|
||||
@@ -374,7 +376,7 @@ export function InstallWizard({
|
||||
onClick={handleCopyLogs}
|
||||
>
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
{copied ? "Copied" : "Copy Logs"}
|
||||
{copied ? t('install.copied') : t('install.copyLogs')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -382,28 +384,28 @@ export function InstallWizard({
|
||||
{phase === "confirm" && (
|
||||
<Button size="sm" onClick={handleConfirmInstall}>
|
||||
<HugeiconsIcon icon={Download04Icon} />
|
||||
Install
|
||||
{t('install.install')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Installing: cancel button */}
|
||||
{phase === "installing" && (
|
||||
<Button variant="destructive" size="sm" onClick={cancelInstall}>
|
||||
Cancel
|
||||
{t('install.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Failed: retry */}
|
||||
{phase === "failed" && (
|
||||
<Button size="sm" onClick={checkPrereqs}>
|
||||
Retry
|
||||
{t('install.retry')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Success / already-installed: done */}
|
||||
{(phase === "success" || phase === "already-installed") && (
|
||||
<Button size="sm" onClick={handleDone}>
|
||||
Done
|
||||
{t('install.done')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
|
||||
|
||||
interface NavRailProps {
|
||||
@@ -38,6 +40,12 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navLabelKeys: Record<string, TranslationKey> = {
|
||||
'Chats': 'nav.chats',
|
||||
'Extensions': 'nav.extensions',
|
||||
'Settings': 'nav.settings',
|
||||
};
|
||||
const emptySubscribe = useCallback(() => () => {}, []);
|
||||
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
|
||||
const isChatRoute = pathname === "/chat" || pathname.startsWith("/chat/");
|
||||
@@ -76,7 +84,7 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
}}
|
||||
>
|
||||
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
|
||||
<span className="sr-only">{item.label}</span>
|
||||
<span className="sr-only">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="relative">
|
||||
@@ -91,7 +99,7 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
>
|
||||
<Link href={item.href}>
|
||||
<HugeiconsIcon icon={item.icon} className="h-4 w-4" />
|
||||
<span className="sr-only">{item.label}</span>
|
||||
<span className="sr-only">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
{item.href === "/settings" && hasUpdate && (
|
||||
@@ -100,7 +108,7 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
</div>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{item.label}</TooltipContent>
|
||||
<TooltipContent side="right">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
@@ -118,7 +126,7 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Auto-approve is ON</TooltipContent>
|
||||
<TooltipContent side="right">{t('nav.autoApproveOn')}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{mounted && (
|
||||
@@ -135,11 +143,11 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
) : (
|
||||
<HugeiconsIcon icon={Moon02Icon} className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
<span className="sr-only">{t('nav.toggleTheme')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{theme === "dark" ? "Light mode" : "Dark mode"}
|
||||
{theme === "dark" ? t('nav.lightMode') : t('nav.darkMode')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { usePanel } from "@/hooks/usePanel";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { FileTree } from "@/components/project/FileTree";
|
||||
|
||||
interface RightPanelProps {
|
||||
@@ -18,6 +19,7 @@ interface RightPanelProps {
|
||||
|
||||
export function RightPanel({ width }: RightPanelProps) {
|
||||
const { panelOpen, setPanelOpen, workingDirectory, previewFile, setPreviewFile } = usePanel();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFileAdd = useCallback((path: string) => {
|
||||
window.dispatchEvent(new CustomEvent('attach-file-to-chat', { detail: { path } }));
|
||||
@@ -56,10 +58,10 @@ export function RightPanel({ width }: RightPanelProps) {
|
||||
onClick={() => setPanelOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={StructureFolderIcon} className="h-4 w-4" />
|
||||
<span className="sr-only">Open panel</span>
|
||||
<span className="sr-only">{t('panel.openPanel')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">Open panel</TooltipContent>
|
||||
<TooltipContent side="left">{t('panel.openPanel')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
@@ -69,7 +71,7 @@ export function RightPanel({ width }: RightPanelProps) {
|
||||
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-background lg:flex" style={{ width: width ?? 288 }}>
|
||||
{/* Header */}
|
||||
<div className="flex h-12 mt-5 shrink-0 items-center justify-between px-4">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Files</span>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{t('panel.files')}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -78,10 +80,10 @@ export function RightPanel({ width }: RightPanelProps) {
|
||||
onClick={() => setPanelOpen(false)}
|
||||
>
|
||||
<HugeiconsIcon icon={PanelRightCloseIcon} className="h-4 w-4" />
|
||||
<span className="sr-only">Close panel</span>
|
||||
<span className="sr-only">{t('panel.closePanel')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">Close panel</TooltipContent>
|
||||
<TooltipContent side="left">{t('panel.closePanel')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface ConfigEditorProps {
|
||||
value: string;
|
||||
@@ -12,6 +13,7 @@ interface ConfigEditorProps {
|
||||
}
|
||||
|
||||
export function ConfigEditor({ value, onSave, label }: ConfigEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [text, setText] = useState(value);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -52,7 +54,7 @@ export function ConfigEditor({ value, onSave, label }: ConfigEditorProps) {
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleFormat}>
|
||||
Format
|
||||
|
||||
@@ -8,9 +8,11 @@ import { PlusSignIcon, ListViewIcon, CodeIcon, Loading02Icon } from "@hugeicons/
|
||||
import { McpServerList } from "@/components/plugins/McpServerList";
|
||||
import { McpServerEditor } from "@/components/plugins/McpServerEditor";
|
||||
import { ConfigEditor } from "@/components/plugins/ConfigEditor";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { MCPServer } from "@/types";
|
||||
|
||||
export function McpManager() {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<Record<string, MCPServer>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
@@ -140,7 +142,7 @@ export function McpManager() {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold">MCP Servers</h3>
|
||||
<h3 className="text-lg font-semibold">{t('extensions.mcpServers')}</h3>
|
||||
{serverCount > 0 && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({serverCount})
|
||||
@@ -153,7 +155,7 @@ export function McpManager() {
|
||||
</div>
|
||||
<Button size="sm" className="gap-1" onClick={handleAdd}>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
Add Server
|
||||
{t('mcp.addServer')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -167,11 +169,11 @@ export function McpManager() {
|
||||
<TabsList>
|
||||
<TabsTrigger value="list" className="gap-1.5">
|
||||
<HugeiconsIcon icon={ListViewIcon} className="h-3.5 w-3.5" />
|
||||
Servers
|
||||
{t('mcp.listTab')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="json" className="gap-1.5">
|
||||
<HugeiconsIcon icon={CodeIcon} className="h-3.5 w-3.5" />
|
||||
JSON Config
|
||||
{t('mcp.jsonTab')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -179,7 +181,7 @@ export function McpManager() {
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-muted-foreground">
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
|
||||
<p className="text-sm">Loading MCP servers...</p>
|
||||
<p className="text-sm">{t('mcp.loadingServers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<McpServerList
|
||||
@@ -194,7 +196,7 @@ export function McpManager() {
|
||||
<ConfigEditor
|
||||
value={JSON.stringify(servers, null, 2)}
|
||||
onSave={handleJsonSave}
|
||||
label="MCP Server Configuration"
|
||||
label={t('mcp.serverConfig')}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ServerStack01Icon, Wifi01Icon, GlobeIcon, CodeIcon } from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { MCPServer } from '@/types';
|
||||
|
||||
type ServerType = 'stdio' | 'sse' | 'http';
|
||||
@@ -35,6 +36,7 @@ export function McpServerEditor({
|
||||
onSave,
|
||||
}: McpServerEditorProps) {
|
||||
const isEditing = !!initialName;
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(initialName || '');
|
||||
const [serverType, setServerType] = useState<ServerType>(
|
||||
initialServer?.type || 'stdio'
|
||||
@@ -177,13 +179,13 @@ export function McpServerEditor({
|
||||
<DialogContent className="sm:max-w-[550px] max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing ? `Edit MCP Server: ${initialName}` : 'Add MCP Server'}
|
||||
{isEditing ? `${t('mcp.editServer')}: ${initialName}` : t('mcp.addServer')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-name">Server Name</Label>
|
||||
<Label htmlFor="server-name">{t('mcp.serverName')}</Label>
|
||||
<Input
|
||||
id="server-name"
|
||||
value={name}
|
||||
@@ -206,7 +208,7 @@ export function McpServerEditor({
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
Form
|
||||
{t('mcp.formTab')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={jsonMode ? 'default' : 'outline'}
|
||||
@@ -237,7 +239,7 @@ export function McpServerEditor({
|
||||
}}
|
||||
>
|
||||
<HugeiconsIcon icon={CodeIcon} className="h-3.5 w-3.5" />
|
||||
JSON
|
||||
{t('mcp.jsonEditTab')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -257,7 +259,7 @@ export function McpServerEditor({
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Server Type</Label>
|
||||
<Label>{t('mcp.serverType')}</Label>
|
||||
<Tabs
|
||||
value={serverType}
|
||||
onValueChange={(v) => {
|
||||
@@ -285,7 +287,7 @@ export function McpServerEditor({
|
||||
{serverType === 'stdio' ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-command">Command</Label>
|
||||
<Label htmlFor="server-command">{t('mcp.command')}</Label>
|
||||
<Input
|
||||
id="server-command"
|
||||
value={command}
|
||||
@@ -299,7 +301,7 @@ export function McpServerEditor({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-args">Arguments (one per line)</Label>
|
||||
<Label htmlFor="server-args">{t('mcp.argsLabel')}</Label>
|
||||
<Textarea
|
||||
id="server-args"
|
||||
value={args}
|
||||
@@ -312,7 +314,7 @@ export function McpServerEditor({
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-url">URL</Label>
|
||||
<Label htmlFor="server-url">{t('mcp.url')}</Label>
|
||||
<Input
|
||||
id="server-url"
|
||||
value={url}
|
||||
@@ -330,7 +332,7 @@ export function McpServerEditor({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-headers">Headers (JSON)</Label>
|
||||
<Label htmlFor="server-headers">{t('mcp.headers')}</Label>
|
||||
<Textarea
|
||||
id="server-headers"
|
||||
value={headersText}
|
||||
@@ -346,7 +348,7 @@ export function McpServerEditor({
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-env">Environment Variables (JSON)</Label>
|
||||
<Label htmlFor="server-env">{t('mcp.envVars')}</Label>
|
||||
<Textarea
|
||||
id="server-env"
|
||||
value={envText}
|
||||
@@ -366,10 +368,10 @@ export function McpServerEditor({
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{isEditing ? 'Save Changes' : 'Add Server'}
|
||||
{isEditing ? t('mcp.saveChanges') : t('mcp.addServer')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { IconSvgElement } from "@hugeicons/react";
|
||||
import { Delete02Icon, PencilIcon, ServerStack01Icon, Wifi01Icon, GlobeIcon } from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { MCPServer } from '@/types';
|
||||
|
||||
interface McpServerListProps {
|
||||
@@ -27,15 +28,16 @@ function getServerTypeInfo(server: MCPServer) {
|
||||
}
|
||||
|
||||
export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps) {
|
||||
const { t } = useTranslation();
|
||||
const entries = Object.entries(servers);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<HugeiconsIcon icon={ServerStack01Icon} className="h-10 w-10 mb-3 opacity-50" />
|
||||
<p className="text-sm">No MCP servers configured</p>
|
||||
<p className="text-sm">{t('mcp.noServers')}</p>
|
||||
<p className="text-xs mt-1">
|
||||
Add an MCP server to extend Claude's capabilities
|
||||
{t('mcp.noServersDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -56,7 +58,7 @@ export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps)
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
Configured
|
||||
{t('provider.configured')}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs mt-1 font-mono">
|
||||
@@ -89,7 +91,7 @@ export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps)
|
||||
<CardContent className="pt-0">
|
||||
{server.args && server.args.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<p className="text-xs text-muted-foreground mb-1">Arguments:</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">{t('mcp.arguments')}</p>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{server.args.map((arg, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs font-mono">
|
||||
@@ -101,7 +103,7 @@ export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps)
|
||||
)}
|
||||
{server.env && Object.keys(server.env).length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Environment:</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">{t('mcp.environment')}</p>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{Object.keys(server.env).map((key) => (
|
||||
<Badge key={key} variant="outline" className="text-xs font-mono">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { usePanel } from "@/hooks/usePanel";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { FilePreview as FilePreviewType } from "@/types";
|
||||
|
||||
interface FilePreviewProps {
|
||||
@@ -18,6 +19,7 @@ interface FilePreviewProps {
|
||||
|
||||
export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
const { workingDirectory } = usePanel();
|
||||
const { t } = useTranslation();
|
||||
const [preview, setPreview] = useState<FilePreviewType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -33,12 +35,12 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Failed to load file");
|
||||
throw new Error(data.error || t('filePreview.failedToLoad'));
|
||||
}
|
||||
const data = await res.json();
|
||||
setPreview(data.preview);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load file");
|
||||
setError(err instanceof Error ? err.message : t('filePreview.failedToLoad'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -66,7 +68,7 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<Button variant="ghost" size="icon-sm" onClick={onBack}>
|
||||
<HugeiconsIcon icon={ArrowLeft01Icon} className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Back to file tree</span>
|
||||
<span className="sr-only">{t('filePreview.backToTree')}</span>
|
||||
</Button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
@@ -79,7 +81,7 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className="sr-only">Copy path</span>
|
||||
<span className="sr-only">{t('filePreview.copyPath')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +92,7 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
{preview.language}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{preview.line_count} lines
|
||||
{t('filePreview.lines', { count: preview.line_count })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -110,7 +112,7 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
onClick={onBack}
|
||||
className="mt-2 text-xs"
|
||||
>
|
||||
Back to file tree
|
||||
{t('filePreview.backToTree')}
|
||||
</Button>
|
||||
</div>
|
||||
) : preview ? (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FileTreeFolder,
|
||||
FileTreeFile,
|
||||
} from "@/components/ai-elements/file-tree";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface FileTreeProps {
|
||||
@@ -109,6 +110,7 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree
|
||||
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fetchTree = useCallback(async () => {
|
||||
if (!workingDirectory) {
|
||||
@@ -154,7 +156,7 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<HugeiconsIcon icon={Search01Icon} className="absolute left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
placeholder="Filter files..."
|
||||
placeholder={t('fileTree.filterFiles')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-7 pl-7 text-xs"
|
||||
@@ -168,7 +170,7 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree
|
||||
className="h-7 w-7 shrink-0"
|
||||
>
|
||||
<HugeiconsIcon icon={RefreshIcon} className={cn("h-3 w-3", loading && "animate-spin")} />
|
||||
<span className="sr-only">Refresh</span>
|
||||
<span className="sr-only">{t('fileTree.refresh')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -180,7 +182,7 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree
|
||||
</div>
|
||||
) : tree.length === 0 ? (
|
||||
<p className="py-4 text-center text-xs text-muted-foreground">
|
||||
{workingDirectory ? 'No files found' : 'Select a project folder to view files'}
|
||||
{workingDirectory ? t('fileTree.noFiles') : t('fileTree.selectFolder')}
|
||||
</p>
|
||||
) : (
|
||||
<AIFileTree
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { TaskItem, TaskStatus } from "@/types";
|
||||
|
||||
interface TaskListProps {
|
||||
@@ -17,6 +18,7 @@ interface TaskListProps {
|
||||
type FilterTab = "all" | "in_progress" | "completed";
|
||||
|
||||
export function TaskList({ sessionId }: TaskListProps) {
|
||||
const { t } = useTranslation();
|
||||
const [tasks, setTasks] = useState<TaskItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
@@ -101,9 +103,9 @@ export function TaskList({ sessionId }: TaskListProps) {
|
||||
});
|
||||
|
||||
const filterTabs: { key: FilterTab; label: string }[] = [
|
||||
{ key: "all", label: "All" },
|
||||
{ key: "in_progress", label: "Active" },
|
||||
{ key: "completed", label: "Done" },
|
||||
{ key: "all", label: t('tasks.all') },
|
||||
{ key: "in_progress", label: t('tasks.active') },
|
||||
{ key: "completed", label: t('tasks.done') },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -129,7 +131,7 @@ export function TaskList({ sessionId }: TaskListProps) {
|
||||
{/* Add task input */}
|
||||
<div className="flex items-center gap-1 pb-2">
|
||||
<Input
|
||||
placeholder="Add a task..."
|
||||
placeholder={t('tasks.addPlaceholder')}
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -144,7 +146,7 @@ export function TaskList({ sessionId }: TaskListProps) {
|
||||
disabled={!newTitle.trim()}
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Add task</span>
|
||||
<span className="sr-only">{t('tasks.addTask')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -152,11 +154,11 @@ export function TaskList({ sessionId }: TaskListProps) {
|
||||
<ScrollArea className="flex-1">
|
||||
{loading && tasks.length === 0 ? (
|
||||
<p className="py-4 text-center text-xs text-muted-foreground">
|
||||
Loading tasks...
|
||||
{t('tasks.loading')}
|
||||
</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="py-4 text-center text-xs text-muted-foreground">
|
||||
{tasks.length === 0 ? "No tasks yet" : "No matching tasks"}
|
||||
{tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatching')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1.5 pb-4">
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
SlidersHorizontalIcon,
|
||||
Loading02Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
|
||||
interface SettingsData {
|
||||
[key: string]: unknown;
|
||||
@@ -55,6 +57,19 @@ export function CliSettingsSection() {
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingSaveAction, setPendingSaveAction] = useState<"form" | "json" | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const knownFieldKeys: Record<string, { label: TranslationKey; description: TranslationKey }> = {
|
||||
permissions: { label: 'cli.permissions', description: 'cli.permissionsDesc' },
|
||||
env: { label: 'cli.envVars', description: 'cli.envVarsDesc' },
|
||||
};
|
||||
|
||||
// Map dynamic CLI settings keys to translation keys (for fields not in KNOWN_FIELDS)
|
||||
const dynamicFieldLabels: Record<string, TranslationKey> = {
|
||||
skipDangerousModePermissionPrompt: 'cli.field.skipDangerousModePermissionPrompt',
|
||||
verbose: 'cli.field.verbose',
|
||||
theme: 'cli.field.theme',
|
||||
};
|
||||
|
||||
const fetchSettings = useCallback(async () => {
|
||||
try {
|
||||
@@ -132,7 +147,7 @@ export function CliSettingsSection() {
|
||||
setJsonText(JSON.stringify(parsed, null, 2));
|
||||
setJsonError("");
|
||||
} catch {
|
||||
setJsonError("Cannot format: invalid JSON");
|
||||
setJsonError(t('cli.formatError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,7 +164,7 @@ export function CliSettingsSection() {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">Loading settings...</span>
|
||||
<span className="ml-2 text-sm text-muted-foreground">{t('cli.loadingSettings')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -160,11 +175,11 @@ export function CliSettingsSection() {
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="form" className="gap-2">
|
||||
<HugeiconsIcon icon={SlidersHorizontalIcon} className="h-4 w-4" />
|
||||
Visual Editor
|
||||
{t('cli.form')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="json" className="gap-2">
|
||||
<HugeiconsIcon icon={CodeIcon} className="h-4 w-4" />
|
||||
JSON Editor
|
||||
{t('cli.json')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -175,8 +190,8 @@ export function CliSettingsSection() {
|
||||
key={field.key}
|
||||
className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm"
|
||||
>
|
||||
<Label className="text-sm font-medium">{field.label}</Label>
|
||||
<p className="mb-2 text-xs text-muted-foreground">{field.description}</p>
|
||||
<Label className="text-sm font-medium">{t(knownFieldKeys[field.key]?.label ?? field.label as TranslationKey)}</Label>
|
||||
<p className="mb-2 text-xs text-muted-foreground">{t(knownFieldKeys[field.key]?.description ?? field.description as TranslationKey)}</p>
|
||||
<Textarea
|
||||
value={
|
||||
typeof settings[field.key] === "object"
|
||||
@@ -204,7 +219,7 @@ export function CliSettingsSection() {
|
||||
key={key}
|
||||
className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm"
|
||||
>
|
||||
<Label className="text-sm font-medium">{key}</Label>
|
||||
<Label className="text-sm font-medium">{dynamicFieldLabels[key] ? t(dynamicFieldLabels[key]) : key}</Label>
|
||||
{typeof value === "boolean" ? (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Switch
|
||||
@@ -212,7 +227,7 @@ export function CliSettingsSection() {
|
||||
onCheckedChange={(checked) => updateField(key, checked)}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{value ? "Enabled" : "Disabled"}
|
||||
{value ? t('common.enabled') : t('common.disabled')}
|
||||
</span>
|
||||
</div>
|
||||
) : typeof value === "string" ? (
|
||||
@@ -245,15 +260,15 @@ export function CliSettingsSection() {
|
||||
) : (
|
||||
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
|
||||
)}
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
{saving ? t('provider.saving') : t('cli.save')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset} disabled={!hasChanges} className="gap-2">
|
||||
<HugeiconsIcon icon={ReloadIcon} className="h-4 w-4" />
|
||||
Reset
|
||||
{t('cli.reset')}
|
||||
</Button>
|
||||
{saveSuccess && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Settings saved successfully
|
||||
{t('cli.settingsSaved')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -280,19 +295,19 @@ export function CliSettingsSection() {
|
||||
) : (
|
||||
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
|
||||
)}
|
||||
{saving ? "Saving..." : "Save JSON"}
|
||||
{saving ? t('provider.saving') : t('cli.save')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleFormatJson} className="gap-2">
|
||||
<HugeiconsIcon icon={CodeIcon} className="h-4 w-4" />
|
||||
Format
|
||||
{t('cli.format')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset} className="gap-2">
|
||||
<HugeiconsIcon icon={ReloadIcon} className="h-4 w-4" />
|
||||
Reset
|
||||
{t('cli.reset')}
|
||||
</Button>
|
||||
{saveSuccess && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
Settings saved successfully
|
||||
{t('cli.settingsSaved')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -304,15 +319,15 @@ export function CliSettingsSection() {
|
||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Save</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('cli.confirmSaveTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will overwrite your current ~/.claude/settings.json file. Are you sure you want to continue?
|
||||
{t('cli.confirmSaveDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => pendingSaveAction && handleSave(pendingSaveAction)}>
|
||||
Save
|
||||
{t('common.save')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -16,17 +16,21 @@ import {
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ReloadIcon, Loading02Icon } from "@hugeicons/core-free-icons";
|
||||
import { useUpdate } from "@/hooks/useUpdate";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
function UpdateCard() {
|
||||
const { updateInfo, checking, checkForUpdates } = useUpdate();
|
||||
const { t } = useTranslation();
|
||||
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">CodePilot</h2>
|
||||
<p className="text-xs text-muted-foreground">Version {currentVersion}</p>
|
||||
<h2 className="text-sm font-medium">{t('settings.codepilot')}</h2>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.version', { version: currentVersion })}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -40,7 +44,7 @@ function UpdateCard() {
|
||||
) : (
|
||||
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{checking ? "Checking..." : "Check for Updates"}
|
||||
{checking ? t('settings.checking') : t('settings.checkForUpdates')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +54,7 @@ function UpdateCard() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
<span className="text-sm">
|
||||
Update available: <span className="font-medium">v{updateInfo.latestVersion}</span>
|
||||
{t('settings.updateAvailable', { version: updateInfo.latestVersion })}
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
@@ -58,11 +62,11 @@ function UpdateCard() {
|
||||
className="h-auto p-0 text-sm"
|
||||
onClick={() => window.open(updateInfo.releaseUrl, "_blank")}
|
||||
>
|
||||
View Release
|
||||
{t('settings.viewRelease')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">You're on the latest version.</p>
|
||||
<p className="text-sm text-muted-foreground">{t('settings.latestVersion')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -74,6 +78,7 @@ export function GeneralSection() {
|
||||
const [skipPermissions, setSkipPermissions] = useState(false);
|
||||
const [showSkipPermWarning, setShowSkipPermWarning] = useState(false);
|
||||
const [skipPermSaving, setSkipPermSaving] = useState(false);
|
||||
const { t, locale, setLocale } = useTranslation();
|
||||
|
||||
const fetchAppSettings = useCallback(async () => {
|
||||
try {
|
||||
@@ -129,10 +134,9 @@ export function GeneralSection() {
|
||||
<div className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">Auto-approve All Actions</h2>
|
||||
<h2 className="text-sm font-medium">{t('settings.autoApproveTitle')}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Skip all permission checks and auto-approve every tool action.
|
||||
This is dangerous and should only be used for trusted tasks.
|
||||
{t('settings.autoApproveDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -144,42 +148,59 @@ export function GeneralSection() {
|
||||
{skipPermissions && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500" />
|
||||
All tool actions will be auto-approved without confirmation. Use with caution.
|
||||
{t('settings.autoApproveWarning')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Language picker */}
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t('settings.language')}</h2>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.languageDesc')}</p>
|
||||
</div>
|
||||
<Select value={locale} onValueChange={(v) => setLocale(v as Locale)}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SUPPORTED_LOCALES.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skip-permissions warning dialog */}
|
||||
<AlertDialog open={showSkipPermWarning} onOpenChange={setShowSkipPermWarning}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Enable Auto-approve All Actions?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.autoApproveDialogTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
This will bypass all permission checks. Claude will be able to
|
||||
execute any tool action without asking for your confirmation,
|
||||
including:
|
||||
{t('settings.autoApproveDialogDesc')}
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>Running arbitrary shell commands</li>
|
||||
<li>Reading, writing, and deleting files</li>
|
||||
<li>Making network requests</li>
|
||||
<li>{t('settings.autoApproveShellCommands')}</li>
|
||||
<li>{t('settings.autoApproveFileOps')}</li>
|
||||
<li>{t('settings.autoApproveNetwork')}</li>
|
||||
</ul>
|
||||
<p className="font-medium text-orange-600 dark:text-orange-400">
|
||||
Only enable this if you fully trust the task at hand. This
|
||||
setting applies to all new chat sessions.
|
||||
{t('settings.autoApproveTrustWarning')}
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('settings.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => saveSkipPermissions(true)}
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white"
|
||||
>
|
||||
Enable Auto-approve
|
||||
{t('settings.enableAutoApprove')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Loading02Icon, ArrowDown01Icon, ArrowUp01Icon } from "@hugeicons/core-free-icons";
|
||||
import type { ApiProvider } from "@/types";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
const PROVIDER_PRESETS: Record<string, { base_url: string; extra_env: string }> = {
|
||||
anthropic: { base_url: "https://api.anthropic.com", extra_env: "{}" },
|
||||
@@ -75,6 +76,7 @@ export function ProviderForm({
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Reset form when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -178,7 +180,7 @@ export function ProviderForm({
|
||||
<DialogContent className="max-w-[28rem] overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{mode === "edit" ? "Edit Provider" : "Add Provider"}
|
||||
{mode === "edit" ? t('provider.editProvider') : t('provider.addProvider')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{mode === "edit"
|
||||
@@ -190,7 +192,7 @@ export function ProviderForm({
|
||||
<form onSubmit={handleSubmit} className="space-y-4 min-w-0">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-name" className="text-xs text-muted-foreground">
|
||||
Name
|
||||
{t('provider.name')}
|
||||
</Label>
|
||||
<Input
|
||||
id="provider-name"
|
||||
@@ -203,7 +205,7 @@ export function ProviderForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-type" className="text-xs text-muted-foreground">
|
||||
Provider Type
|
||||
{t('provider.providerType')}
|
||||
</Label>
|
||||
<Select value={providerType} onValueChange={handleTypeChange}>
|
||||
<SelectTrigger className="w-full text-sm">
|
||||
@@ -221,7 +223,7 @@ export function ProviderForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-base-url" className="text-xs text-muted-foreground">
|
||||
API Base URL
|
||||
{t('provider.baseUrl')}
|
||||
</Label>
|
||||
<Input
|
||||
id="provider-base-url"
|
||||
@@ -234,7 +236,7 @@ export function ProviderForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-api-key" className="text-xs text-muted-foreground">
|
||||
API Key
|
||||
{t('provider.apiKey')}
|
||||
</Label>
|
||||
<Input
|
||||
id="provider-api-key"
|
||||
@@ -256,14 +258,14 @@ export function ProviderForm({
|
||||
icon={showAdvanced ? ArrowUp01Icon : ArrowDown01Icon}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
Advanced Options
|
||||
{t('provider.advancedOptions')}
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="space-y-4 border-t border-border/50 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-extra-env" className="text-xs text-muted-foreground">
|
||||
Extra Environment Variables (JSON)
|
||||
{t('provider.extraEnvVars')} (JSON)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="provider-extra-env"
|
||||
@@ -277,11 +279,11 @@ export function ProviderForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-notes" className="text-xs text-muted-foreground">
|
||||
Notes
|
||||
{t('provider.notes')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="provider-notes"
|
||||
placeholder="Optional notes about this provider..."
|
||||
placeholder={t('provider.notesPlaceholder')}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="text-sm"
|
||||
@@ -302,13 +304,13 @@ export function ProviderForm({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving} className="gap-2">
|
||||
{saving && (
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{saving ? "Saving..." : mode === "edit" ? "Update" : "Add Provider"}
|
||||
{saving ? t('provider.saving') : mode === "edit" ? t('provider.update') : t('provider.addProvider')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { ProviderForm } from "./ProviderForm";
|
||||
import type { ProviderFormData } from "./ProviderForm";
|
||||
import type { ApiProvider } from "@/types";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
const QUICK_PRESETS = [
|
||||
{ name: "Anthropic", provider_type: "anthropic", base_url: "https://api.anthropic.com" },
|
||||
@@ -46,6 +47,7 @@ export function ProviderManager() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [envDetected, setEnvDetected] = useState<Record<string, string>>({});
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Form dialog state
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
@@ -215,7 +217,7 @@ export function ProviderManager() {
|
||||
</div>
|
||||
<Button size="sm" className="gap-1" onClick={handleAdd}>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
Add Provider
|
||||
{t('provider.addProvider')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -241,7 +243,7 @@ export function ProviderManager() {
|
||||
? "text-muted-foreground"
|
||||
: "text-green-700 dark:text-green-400"
|
||||
}`}>
|
||||
Environment variables detected
|
||||
{t('provider.envDetected')}
|
||||
</p>
|
||||
{hasActiveProvider ? (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 text-muted-foreground">
|
||||
@@ -284,7 +286,7 @@ export function ProviderManager() {
|
||||
<div className="flex flex-col items-center gap-3 py-8 text-muted-foreground">
|
||||
<HugeiconsIcon icon={ServerStack01Icon} className="h-10 w-10 opacity-30" />
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium">No providers configured</p>
|
||||
<p className="text-sm font-medium">{t('provider.noProviders')}</p>
|
||||
<p className="text-xs mt-0.5">
|
||||
{Object.keys(envDetected).length > 0
|
||||
? "Using environment variables. Add a provider below to override."
|
||||
@@ -336,7 +338,7 @@ export function ProviderManager() {
|
||||
</Badge>
|
||||
{isActive && (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 text-green-600 dark:text-green-400 border-green-500/30">
|
||||
Active
|
||||
{t('provider.active')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -407,7 +409,7 @@ export function ProviderManager() {
|
||||
{/* Quick presets row (when providers exist) */}
|
||||
{!loading && providers.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground mr-1">Quick add:</span>
|
||||
<span className="text-xs text-muted-foreground mr-1">{t('provider.quickPresets')}:</span>
|
||||
{QUICK_PRESETS.map((preset) => (
|
||||
<Button
|
||||
key={preset.name}
|
||||
@@ -437,19 +439,19 @@ export function ProviderManager() {
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Provider</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('provider.deleteProvider')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{deleteTarget?.name}"? This action cannot be undone.
|
||||
{t('provider.deleteConfirm', { name: deleteTarget?.name ?? '' })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={deleting}>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="bg-destructive text-white hover:bg-destructive/90"
|
||||
>
|
||||
{deleting ? "Deleting..." : "Delete"}
|
||||
{deleting ? t('provider.deleting') : t('common.delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -12,6 +12,8 @@ import { cn } from "@/lib/utils";
|
||||
import { GeneralSection } from "./GeneralSection";
|
||||
import { ProviderManager } from "./ProviderManager";
|
||||
import { CliSettingsSection } from "./CliSettingsSection";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
|
||||
type Section = "general" | "providers" | "cli";
|
||||
|
||||
@@ -39,6 +41,13 @@ function getInitialSection(): Section {
|
||||
|
||||
export function SettingsLayout() {
|
||||
const [activeSection, setActiveSection] = useState<Section>(getInitialSection);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const settingsLabelKeys: Record<string, TranslationKey> = {
|
||||
'General': 'settings.general',
|
||||
'Providers': 'settings.providers',
|
||||
'Claude CLI': 'settings.claudeCli',
|
||||
};
|
||||
|
||||
// Sync hash on mount and on popstate
|
||||
useEffect(() => {
|
||||
@@ -60,9 +69,9 @@ export function SettingsLayout() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b border-border/50 px-6 pt-4 pb-4">
|
||||
<h1 className="text-xl font-semibold">Settings</h1>
|
||||
<h1 className="text-xl font-semibold">{t('settings.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage CodePilot and Claude CLI settings
|
||||
{t('settings.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +90,7 @@ export function SettingsLayout() {
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={item.icon} className="h-4 w-4 shrink-0" />
|
||||
{item.label}
|
||||
{t(settingsLabelKeys[item.label])}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Loading02Icon, GlobeIcon, FolderOpenIcon } from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CreateSkillDialogProps {
|
||||
@@ -58,6 +59,7 @@ export function CreateSkillDialog({
|
||||
onOpenChange,
|
||||
onCreate,
|
||||
}: CreateSkillDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
const [scope, setScope] = useState<"global" | "project">("project");
|
||||
const [templateIdx, setTemplateIdx] = useState(0);
|
||||
@@ -67,11 +69,11 @@ export function CreateSkillDialog({
|
||||
const handleCreate = async () => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
setError("Name is required");
|
||||
setError(t('skills.nameRequired'));
|
||||
return;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
||||
setError("Name can only contain letters, numbers, hyphens, and underscores");
|
||||
setError(t('skills.nameInvalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ export function CreateSkillDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Skill</DialogTitle>
|
||||
<DialogTitle>{t('skills.createSkill')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new slash command skill. It will be saved as a .md file.
|
||||
</DialogDescription>
|
||||
@@ -104,7 +106,7 @@ export function CreateSkillDialog({
|
||||
<div className="space-y-4 py-2">
|
||||
{/* Name input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="skill-name">Name</Label>
|
||||
<Label htmlFor="skill-name">{t('skills.skillName')}</Label>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<Input
|
||||
@@ -124,7 +126,7 @@ export function CreateSkillDialog({
|
||||
|
||||
{/* Scope selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Scope</Label>
|
||||
<Label>{t('skills.scope')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -137,7 +139,7 @@ export function CreateSkillDialog({
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={FolderOpenIcon} className="h-4 w-4" />
|
||||
Project
|
||||
{t('skills.project')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -150,7 +152,7 @@ export function CreateSkillDialog({
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={GlobeIcon} className="h-4 w-4" />
|
||||
Global
|
||||
{t('skills.global')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -162,11 +164,11 @@ export function CreateSkillDialog({
|
||||
|
||||
{/* Template selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Template</Label>
|
||||
<Label>{t('skills.template')}</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{TEMPLATES.map((t, i) => (
|
||||
{TEMPLATES.map((tpl, i) => (
|
||||
<button
|
||||
key={t.label}
|
||||
key={tpl.label}
|
||||
type="button"
|
||||
onClick={() => setTemplateIdx(i)}
|
||||
className={cn(
|
||||
@@ -176,7 +178,7 @@ export function CreateSkillDialog({
|
||||
: "border-border hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
{tpl.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -191,11 +193,11 @@ export function CreateSkillDialog({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={creating} className="gap-2">
|
||||
{creating && <HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />}
|
||||
Create Skill
|
||||
{t('skills.createSkill')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { SkillItem } from "./SkillListItem";
|
||||
|
||||
type ViewMode = "edit" | "preview" | "split";
|
||||
@@ -34,6 +35,7 @@ interface SkillEditorProps {
|
||||
}
|
||||
|
||||
export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [content, setContent] = useState(skill.content);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("edit");
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -153,7 +155,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
<HugeiconsIcon icon={Edit02Icon} className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Edit</TooltipContent>
|
||||
<TooltipContent>{t('skills.edit')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -165,7 +167,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
<HugeiconsIcon icon={EyeIcon} className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Preview</TooltipContent>
|
||||
<TooltipContent>{t('skills.preview')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -177,7 +179,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
<HugeiconsIcon icon={LayoutTwoColumnIcon} className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Split</TooltipContent>
|
||||
<TooltipContent>{t('skills.splitView')}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="w-px h-4 bg-border mx-1" />
|
||||
@@ -194,7 +196,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
) : (
|
||||
<HugeiconsIcon icon={FloppyDiskIcon} className="h-3 w-3" />
|
||||
)}
|
||||
{saving ? "Saving" : saved ? "Saved" : "Save"}
|
||||
{saving ? "Saving" : saved ? t('skills.saved') : t('skills.save')}
|
||||
</Button>
|
||||
|
||||
{/* Delete */}
|
||||
@@ -217,7 +219,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="h-full w-full resize-none rounded-none border-0 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0 min-h-[400px]"
|
||||
placeholder="Write your skill prompt in Markdown..."
|
||||
placeholder={t('skills.placeholder')}
|
||||
/>
|
||||
)}
|
||||
{viewMode === "preview" && (
|
||||
@@ -231,7 +233,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="h-full w-full resize-none rounded-none border-0 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
placeholder="Write your skill prompt in Markdown..."
|
||||
placeholder={t('skills.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 overflow-auto">
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
export interface SkillItem {
|
||||
name: string;
|
||||
@@ -34,6 +35,7 @@ export function SkillListItem({
|
||||
onSelect,
|
||||
onDelete,
|
||||
}: SkillListItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
@@ -108,7 +110,7 @@ export function SkillListItem({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{confirmDelete ? "Click again to confirm" : "Delete"}
|
||||
{confirmDelete ? t('skills.deleteConfirm') : t('common.delete')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -10,10 +10,12 @@ import { PlusSignIcon, Search01Icon, ZapIcon, Loading02Icon } from "@hugeicons/c
|
||||
import { SkillListItem } from "./SkillListItem";
|
||||
import { SkillEditor } from "./SkillEditor";
|
||||
import { CreateSkillDialog } from "./CreateSkillDialog";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { SkillItem } from "./SkillListItem";
|
||||
|
||||
export function SkillsManager() {
|
||||
const { workingDirectory } = usePanel();
|
||||
const { t } = useTranslation();
|
||||
const [skills, setSkills] = useState<SkillItem[]>([]);
|
||||
const [selected, setSelected] = useState<SkillItem | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -138,7 +140,7 @@ export function SkillsManager() {
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
Loading skills...
|
||||
{t('skills.loadingSkills')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -148,10 +150,10 @@ export function SkillsManager() {
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<h3 className="text-lg font-semibold flex-1">Skills</h3>
|
||||
<h3 className="text-lg font-semibold flex-1">{t('extensions.skills')}</h3>
|
||||
<Button size="sm" onClick={() => setShowCreate(true)} className="gap-1">
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
New Skill
|
||||
{t('skills.newSkill')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +165,7 @@ export function SkillsManager() {
|
||||
<div className="relative">
|
||||
<HugeiconsIcon icon={Search01Icon} className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search skills..."
|
||||
placeholder={t('skills.searchSkills')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-7 h-8 text-sm"
|
||||
@@ -236,7 +238,7 @@ export function SkillsManager() {
|
||||
<div className="flex flex-col items-center gap-2 py-8 text-muted-foreground">
|
||||
<HugeiconsIcon icon={ZapIcon} className="h-8 w-8 opacity-40" />
|
||||
<p className="text-xs">
|
||||
{search ? "No skills match your search" : "No skills yet"}
|
||||
{search ? t('skills.noSkillsFound') : t('skills.noSkillsFound')}
|
||||
</p>
|
||||
{!search && (
|
||||
<Button
|
||||
@@ -268,9 +270,9 @@ export function SkillsManager() {
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
||||
<HugeiconsIcon icon={ZapIcon} className="h-12 w-12 opacity-30" />
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium">No skill selected</p>
|
||||
<p className="text-sm font-medium">{t('skills.noSelected')}</p>
|
||||
<p className="text-xs">
|
||||
Select a skill from the list or create a new one
|
||||
{t('skills.selectOrCreate')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -280,7 +282,7 @@ export function SkillsManager() {
|
||||
className="gap-1"
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
New Skill
|
||||
{t('skills.newSkill')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { I18nContext } from '@/components/layout/I18nProvider';
|
||||
|
||||
export function useTranslation() {
|
||||
return useContext(I18nContext);
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* English translations — source of truth.
|
||||
* Every key defined here becomes part of the `TranslationKey` type.
|
||||
* Other locale files must implement `Record<TranslationKey, string>`.
|
||||
*/
|
||||
const en = {
|
||||
// ── Navigation ──────────────────────────────────────────────
|
||||
'nav.chats': 'Chats',
|
||||
'nav.extensions': 'Extensions',
|
||||
'nav.settings': 'Settings',
|
||||
'nav.autoApproveOn': 'Auto-approve is ON',
|
||||
'nav.lightMode': 'Light mode',
|
||||
'nav.darkMode': 'Dark mode',
|
||||
'nav.toggleTheme': 'Toggle theme',
|
||||
|
||||
// ── Chat list panel ─────────────────────────────────────────
|
||||
'chatList.justNow': 'just now',
|
||||
'chatList.minutesAgo': '{n}m',
|
||||
'chatList.hoursAgo': '{n}h',
|
||||
'chatList.daysAgo': '{n}d',
|
||||
'chatList.newConversation': 'New Conversation',
|
||||
'chatList.delete': 'Delete',
|
||||
'chatList.searchSessions': 'Search sessions...',
|
||||
'chatList.noSessions': 'No sessions yet',
|
||||
'chatList.importFromCli': 'Import from Claude CLI',
|
||||
'chatList.addProjectFolder': 'Add project folder',
|
||||
|
||||
// ── Message list ────────────────────────────────────────────
|
||||
'messageList.claudeChat': 'Claude Chat',
|
||||
'messageList.emptyDescription': 'Start a conversation with Claude. Ask questions, get help with code, or explore ideas.',
|
||||
'messageList.loadEarlier': 'Load earlier messages',
|
||||
'messageList.loading': 'Loading...',
|
||||
|
||||
// ── Message input ───────────────────────────────────────────
|
||||
'messageInput.attachFiles': 'Attach files',
|
||||
'messageInput.helpDesc': 'Show available commands and tips',
|
||||
'messageInput.clearDesc': 'Clear conversation history',
|
||||
'messageInput.costDesc': 'Show token usage statistics',
|
||||
'messageInput.compactDesc': 'Compress conversation context',
|
||||
'messageInput.doctorDesc': 'Diagnose project health',
|
||||
'messageInput.initDesc': 'Initialize CLAUDE.md for project',
|
||||
'messageInput.reviewDesc': 'Review code quality',
|
||||
'messageInput.terminalSetupDesc': 'Configure terminal settings',
|
||||
'messageInput.memoryDesc': 'Edit project memory file',
|
||||
'messageInput.modeCode': 'Code',
|
||||
'messageInput.modePlan': 'Plan',
|
||||
|
||||
// ── Streaming message ───────────────────────────────────────
|
||||
'streaming.thinking': 'Thinking...',
|
||||
'streaming.allowForSession': 'Allow for Session',
|
||||
'streaming.allowed': 'Allowed',
|
||||
'streaming.denied': 'Denied',
|
||||
|
||||
// ── Chat view / session page ────────────────────────────────
|
||||
'chat.newConversation': 'New Conversation',
|
||||
|
||||
// ── Settings: General ───────────────────────────────────────
|
||||
'settings.title': 'Settings',
|
||||
'settings.description': 'Manage CodePilot and Claude CLI settings',
|
||||
'settings.general': 'General',
|
||||
'settings.providers': 'Providers',
|
||||
'settings.claudeCli': 'Claude CLI',
|
||||
'settings.codepilot': 'CodePilot',
|
||||
'settings.version': 'Version {version}',
|
||||
'settings.checkForUpdates': 'Check for Updates',
|
||||
'settings.checking': 'Checking...',
|
||||
'settings.updateAvailable': 'Update available: v{version}',
|
||||
'settings.viewRelease': 'View Release',
|
||||
'settings.latestVersion': "You're on the latest version.",
|
||||
'settings.autoApproveTitle': 'Auto-approve All Actions',
|
||||
'settings.autoApproveDesc': 'Skip all permission checks and auto-approve every tool action. This is dangerous and should only be used for trusted tasks.',
|
||||
'settings.autoApproveWarning': 'All tool actions will be auto-approved without confirmation. Use with caution.',
|
||||
'settings.autoApproveDialogTitle': 'Enable Auto-approve All Actions?',
|
||||
'settings.autoApproveDialogDesc': 'This will bypass all permission checks. Claude will be able to execute any tool action without asking for your confirmation, including:',
|
||||
'settings.autoApproveShellCommands': 'Running arbitrary shell commands',
|
||||
'settings.autoApproveFileOps': 'Reading, writing, and deleting files',
|
||||
'settings.autoApproveNetwork': 'Making network requests',
|
||||
'settings.autoApproveTrustWarning': 'Only enable this if you fully trust the task at hand. This setting applies to all new chat sessions.',
|
||||
'settings.cancel': 'Cancel',
|
||||
'settings.enableAutoApprove': 'Enable Auto-approve',
|
||||
'settings.language': 'Language',
|
||||
'settings.languageDesc': 'Choose the display language for the interface',
|
||||
|
||||
// ── Settings: CLI ───────────────────────────────────────────
|
||||
'cli.permissions': 'Permissions',
|
||||
'cli.permissionsDesc': 'Configure permission settings for Claude CLI',
|
||||
'cli.envVars': 'Environment Variables',
|
||||
'cli.envVarsDesc': 'Environment variables passed to Claude',
|
||||
'cli.form': 'Form',
|
||||
'cli.json': 'JSON',
|
||||
'cli.save': 'Save',
|
||||
'cli.format': 'Format',
|
||||
'cli.reset': 'Reset',
|
||||
'cli.settingsSaved': 'Settings saved successfully',
|
||||
'cli.confirmSaveTitle': 'Confirm Save',
|
||||
'cli.confirmSaveDesc': 'This will overwrite your current ~/.claude/settings.json file. Are you sure you want to continue?',
|
||||
|
||||
// ── Settings: Providers ─────────────────────────────────────
|
||||
'provider.addProvider': 'Add Provider',
|
||||
'provider.editProvider': 'Edit Provider',
|
||||
'provider.deleteProvider': 'Delete Provider',
|
||||
'provider.deleteConfirm': 'Are you sure you want to delete "{name}"? This action cannot be undone.',
|
||||
'provider.deleting': 'Deleting...',
|
||||
'provider.delete': 'Delete',
|
||||
'provider.noProviders': 'No providers configured',
|
||||
'provider.addToStart': 'Add a provider to get started',
|
||||
'provider.quickPresets': 'Quick Presets',
|
||||
'provider.customProviders': 'Custom Providers',
|
||||
'provider.active': 'Active',
|
||||
'provider.configured': 'Configured',
|
||||
'provider.name': 'Name',
|
||||
'provider.providerType': 'Provider Type',
|
||||
'provider.apiKey': 'API Key',
|
||||
'provider.baseUrl': 'Base URL',
|
||||
'provider.advancedOptions': 'Advanced Options',
|
||||
'provider.extraEnvVars': 'Extra Environment Variables',
|
||||
'provider.notes': 'Notes',
|
||||
'provider.notesPlaceholder': 'Optional notes about this provider...',
|
||||
'provider.saving': 'Saving...',
|
||||
'provider.update': 'Update',
|
||||
'provider.envDetected': 'Detected from environment',
|
||||
|
||||
// ── Right panel / Files ─────────────────────────────────────
|
||||
'panel.files': 'Files',
|
||||
'panel.openPanel': 'Open panel',
|
||||
'panel.closePanel': 'Close panel',
|
||||
|
||||
// ── File tree ───────────────────────────────────────────────
|
||||
'fileTree.filterFiles': 'Filter files...',
|
||||
'fileTree.refresh': 'Refresh',
|
||||
'fileTree.noFiles': 'No files found',
|
||||
'fileTree.selectFolder': 'Select a project folder to view files',
|
||||
|
||||
// ── File preview ────────────────────────────────────────────
|
||||
'filePreview.backToTree': 'Back to file tree',
|
||||
'filePreview.lines': '{count} lines',
|
||||
'filePreview.copyPath': 'Copy path',
|
||||
'filePreview.failedToLoad': 'Failed to load file',
|
||||
|
||||
// ── Doc preview ─────────────────────────────────────────────
|
||||
'docPreview.htmlPreview': 'HTML Preview',
|
||||
|
||||
// ── Extensions page ─────────────────────────────────────────
|
||||
'extensions.title': 'Extensions',
|
||||
'extensions.skills': 'Skills',
|
||||
'extensions.mcpServers': 'MCP Servers',
|
||||
|
||||
// ── Skills ──────────────────────────────────────────────────
|
||||
'skills.noSelected': 'No skill selected',
|
||||
'skills.selectOrCreate': 'Select a skill from the list or create a new one',
|
||||
'skills.newSkill': 'New Skill',
|
||||
'skills.loadingSkills': 'Loading skills...',
|
||||
'skills.noSkillsFound': 'No skills found',
|
||||
'skills.searchSkills': 'Search skills...',
|
||||
'skills.createSkill': 'Create Skill',
|
||||
'skills.nameRequired': 'Name is required',
|
||||
'skills.nameInvalid': 'Name can only contain letters, numbers, hyphens, and underscores',
|
||||
'skills.skillName': 'Skill Name',
|
||||
'skills.scope': 'Scope',
|
||||
'skills.global': 'Global',
|
||||
'skills.project': 'Project',
|
||||
'skills.template': 'Template',
|
||||
'skills.blank': 'Blank',
|
||||
'skills.commitHelper': 'Commit Helper',
|
||||
'skills.codeReviewer': 'Code Reviewer',
|
||||
'skills.saved': 'Saved',
|
||||
'skills.save': 'Save',
|
||||
'skills.edit': 'Edit',
|
||||
'skills.preview': 'Preview',
|
||||
'skills.splitView': 'Split view',
|
||||
'skills.deleteConfirm': 'Click again to confirm',
|
||||
'skills.placeholder': 'Write your skill prompt in Markdown...',
|
||||
|
||||
// ── MCP ─────────────────────────────────────────────────────
|
||||
'mcp.addServer': 'Add Server',
|
||||
'mcp.loadingServers': 'Loading MCP servers...',
|
||||
'mcp.serverConfig': 'MCP Server Configuration',
|
||||
'mcp.noServers': 'No MCP servers configured',
|
||||
'mcp.noServersDesc': "Add an MCP server to extend Claude's capabilities",
|
||||
'mcp.arguments': 'Arguments:',
|
||||
'mcp.environment': 'Environment:',
|
||||
'mcp.listTab': 'List',
|
||||
'mcp.jsonTab': 'JSON Config',
|
||||
'mcp.editServer': 'Edit Server',
|
||||
'mcp.serverName': 'Server Name',
|
||||
'mcp.serverType': 'Server Type',
|
||||
'mcp.command': 'Command',
|
||||
'mcp.argsLabel': 'Arguments (one per line)',
|
||||
'mcp.url': 'URL',
|
||||
'mcp.headers': 'Headers (JSON)',
|
||||
'mcp.envVars': 'Environment Variables (JSON)',
|
||||
'mcp.formTab': 'Form',
|
||||
'mcp.jsonEditTab': 'JSON',
|
||||
'mcp.saveChanges': 'Save Changes',
|
||||
|
||||
// ── Folder picker ───────────────────────────────────────────
|
||||
'folderPicker.title': 'Select a project folder',
|
||||
'folderPicker.loading': 'Loading...',
|
||||
'folderPicker.noSubdirs': 'No subdirectories',
|
||||
'folderPicker.cancel': 'Cancel',
|
||||
'folderPicker.select': 'Select This Folder',
|
||||
|
||||
// ── Import session dialog ───────────────────────────────────
|
||||
'import.title': 'Import Session from Claude CLI',
|
||||
'import.searchSessions': 'Search sessions...',
|
||||
'import.noSessions': 'No sessions found',
|
||||
'import.import': 'Import',
|
||||
'import.importing': 'Importing...',
|
||||
'import.justNow': 'just now',
|
||||
'import.minutesAgo': '{n}m ago',
|
||||
'import.hoursAgo': '{n}h ago',
|
||||
'import.daysAgo': '{n}d ago',
|
||||
'import.messages': '{n} msg',
|
||||
'import.messagesPlural': '{n} msgs',
|
||||
|
||||
// ── Connection status ───────────────────────────────────────
|
||||
'connection.notInstalled': 'Claude Code is not installed',
|
||||
'connection.installed': 'Claude Code is installed',
|
||||
'connection.version': 'Version: {version}',
|
||||
'connection.installPrompt': 'To use Claude Code features, you need to install the Claude Code CLI.',
|
||||
'connection.runCommand': 'Run the following command in your terminal:',
|
||||
'connection.installAuto': 'Install Claude Code Automatically',
|
||||
'connection.refresh': 'Refresh',
|
||||
'connection.installClaude': 'Install Claude Code',
|
||||
|
||||
// ── Install wizard ──────────────────────────────────────────
|
||||
'install.title': 'Install Claude Code',
|
||||
'install.checkingPrereqs': 'Checking prerequisites...',
|
||||
'install.alreadyInstalled': 'Claude Code is already installed',
|
||||
'install.readyToInstall': 'Ready to install',
|
||||
'install.installing': 'Installing Claude Code...',
|
||||
'install.complete': 'Installation complete',
|
||||
'install.failed': 'Installation failed',
|
||||
'install.copyLogs': 'Copy Logs',
|
||||
'install.copied': 'Copied',
|
||||
'install.install': 'Install',
|
||||
'install.cancel': 'Cancel',
|
||||
'install.retry': 'Retry',
|
||||
'install.done': 'Done',
|
||||
|
||||
// ── Task list ───────────────────────────────────────────────
|
||||
'tasks.all': 'All',
|
||||
'tasks.active': 'Active',
|
||||
'tasks.done': 'Done',
|
||||
'tasks.addPlaceholder': 'Add a task...',
|
||||
'tasks.addTask': 'Add task',
|
||||
'tasks.loading': 'Loading tasks...',
|
||||
'tasks.noTasks': 'No tasks yet',
|
||||
'tasks.noMatching': 'No matching tasks',
|
||||
|
||||
// ── Tool call block ─────────────────────────────────────────
|
||||
'tool.running': 'running',
|
||||
'tool.success': 'success',
|
||||
'tool.error': 'error',
|
||||
|
||||
// ── Common ──────────────────────────────────────────────────
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'common.delete': 'Delete',
|
||||
'common.loading': 'Loading...',
|
||||
'common.close': 'Close',
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
|
||||
// ── CLI dynamic field labels ──────────────────────────────
|
||||
'cli.loadingSettings': 'Loading settings...',
|
||||
'cli.field.skipDangerousModePermissionPrompt': 'Skip Dangerous Mode Permission Prompt',
|
||||
'cli.field.verbose': 'Verbose',
|
||||
'cli.field.theme': 'Theme',
|
||||
'cli.formatError': 'Cannot format: invalid JSON',
|
||||
} as const;
|
||||
|
||||
export type TranslationKey = keyof typeof en;
|
||||
export default en;
|
||||
@@ -0,0 +1,37 @@
|
||||
import en, { type TranslationKey } from './en';
|
||||
import zh from './zh';
|
||||
|
||||
export type { TranslationKey };
|
||||
|
||||
export type Locale = 'en' | 'zh';
|
||||
|
||||
export const SUPPORTED_LOCALES: { value: Locale; label: string }[] = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
];
|
||||
|
||||
const dictionaries: Record<Locale, Record<TranslationKey, string>> = {
|
||||
en,
|
||||
zh,
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate a key with optional parameter interpolation.
|
||||
* Fallback chain: target locale → English → raw key.
|
||||
*/
|
||||
export function translate(
|
||||
locale: Locale,
|
||||
key: TranslationKey,
|
||||
params?: Record<string, string | number>,
|
||||
): string {
|
||||
const dict = dictionaries[locale] ?? en;
|
||||
let text = dict[key] ?? en[key] ?? key;
|
||||
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
import type { TranslationKey } from './en';
|
||||
|
||||
const zh: Record<TranslationKey, string> = {
|
||||
// ── Navigation ──────────────────────────────────────────────
|
||||
'nav.chats': '对话',
|
||||
'nav.extensions': '扩展',
|
||||
'nav.settings': '设置',
|
||||
'nav.autoApproveOn': '自动批准已开启',
|
||||
'nav.lightMode': '浅色模式',
|
||||
'nav.darkMode': '深色模式',
|
||||
'nav.toggleTheme': '切换主题',
|
||||
|
||||
// ── Chat list panel ─────────────────────────────────────────
|
||||
'chatList.justNow': '刚刚',
|
||||
'chatList.minutesAgo': '{n}分钟',
|
||||
'chatList.hoursAgo': '{n}小时',
|
||||
'chatList.daysAgo': '{n}天',
|
||||
'chatList.newConversation': '新对话',
|
||||
'chatList.delete': '删除',
|
||||
'chatList.searchSessions': '搜索会话...',
|
||||
'chatList.noSessions': '暂无会话',
|
||||
'chatList.importFromCli': '从 Claude CLI 导入',
|
||||
'chatList.addProjectFolder': '添加项目文件夹',
|
||||
|
||||
// ── Message list ────────────────────────────────────────────
|
||||
'messageList.claudeChat': 'Claude 对话',
|
||||
'messageList.emptyDescription': '开始与 Claude 对话。提问、获取代码帮助或探索想法。',
|
||||
'messageList.loadEarlier': '加载更早的消息',
|
||||
'messageList.loading': '加载中...',
|
||||
|
||||
// ── Message input ───────────────────────────────────────────
|
||||
'messageInput.attachFiles': '附加文件',
|
||||
'messageInput.helpDesc': '显示可用命令和提示',
|
||||
'messageInput.clearDesc': '清除对话历史',
|
||||
'messageInput.costDesc': '显示 Token 用量统计',
|
||||
'messageInput.compactDesc': '压缩对话上下文',
|
||||
'messageInput.doctorDesc': '诊断项目健康状况',
|
||||
'messageInput.initDesc': '初始化项目 CLAUDE.md',
|
||||
'messageInput.reviewDesc': '审查代码质量',
|
||||
'messageInput.terminalSetupDesc': '配置终端设置',
|
||||
'messageInput.memoryDesc': '编辑项目记忆文件',
|
||||
'messageInput.modeCode': '代码',
|
||||
'messageInput.modePlan': '计划',
|
||||
|
||||
// ── Streaming message ───────────────────────────────────────
|
||||
'streaming.thinking': '思考中...',
|
||||
'streaming.allowForSession': '本次会话允许',
|
||||
'streaming.allowed': '已允许',
|
||||
'streaming.denied': '已拒绝',
|
||||
|
||||
// ── Chat view / session page ────────────────────────────────
|
||||
'chat.newConversation': '新对话',
|
||||
|
||||
// ── Settings: General ───────────────────────────────────────
|
||||
'settings.title': '设置',
|
||||
'settings.description': '管理 CodePilot 和 Claude CLI 设置',
|
||||
'settings.general': '通用',
|
||||
'settings.providers': '服务商',
|
||||
'settings.claudeCli': 'Claude CLI',
|
||||
'settings.codepilot': 'CodePilot',
|
||||
'settings.version': '版本 {version}',
|
||||
'settings.checkForUpdates': '检查更新',
|
||||
'settings.checking': '检查中...',
|
||||
'settings.updateAvailable': '有新版本:v{version}',
|
||||
'settings.viewRelease': '查看发布',
|
||||
'settings.latestVersion': '已是最新版本。',
|
||||
'settings.autoApproveTitle': '自动批准所有操作',
|
||||
'settings.autoApproveDesc': '跳过所有权限检查并自动批准每个工具操作。这很危险,仅应用于受信任的任务。',
|
||||
'settings.autoApproveWarning': '所有工具操作将在无确认的情况下自动批准。请谨慎使用。',
|
||||
'settings.autoApproveDialogTitle': '启用自动批准所有操作?',
|
||||
'settings.autoApproveDialogDesc': '这将绕过所有权限检查。Claude 将能够在不征求您确认的情况下执行任何工具操作,包括:',
|
||||
'settings.autoApproveShellCommands': '运行任意 Shell 命令',
|
||||
'settings.autoApproveFileOps': '读取、写入和删除文件',
|
||||
'settings.autoApproveNetwork': '发起网络请求',
|
||||
'settings.autoApproveTrustWarning': '仅在您完全信任当前任务时才启用此选项。此设置适用于所有新的聊天会话。',
|
||||
'settings.cancel': '取消',
|
||||
'settings.enableAutoApprove': '启用自动批准',
|
||||
'settings.language': '语言',
|
||||
'settings.languageDesc': '选择界面显示语言',
|
||||
|
||||
// ── Settings: CLI ───────────────────────────────────────────
|
||||
'cli.permissions': '权限',
|
||||
'cli.permissionsDesc': '配置 Claude CLI 的权限设置',
|
||||
'cli.envVars': '环境变量',
|
||||
'cli.envVarsDesc': '传递给 Claude 的环境变量',
|
||||
'cli.form': '表单',
|
||||
'cli.json': 'JSON',
|
||||
'cli.save': '保存',
|
||||
'cli.format': '格式化',
|
||||
'cli.reset': '重置',
|
||||
'cli.settingsSaved': '设置已保存',
|
||||
'cli.confirmSaveTitle': '确认保存',
|
||||
'cli.confirmSaveDesc': '这将覆盖您当前的 ~/.claude/settings.json 文件。确定要继续吗?',
|
||||
|
||||
// ── Settings: Providers ─────────────────────────────────────
|
||||
'provider.addProvider': '添加服务商',
|
||||
'provider.editProvider': '编辑服务商',
|
||||
'provider.deleteProvider': '删除服务商',
|
||||
'provider.deleteConfirm': '确定要删除"{name}"吗?此操作无法撤销。',
|
||||
'provider.deleting': '删除中...',
|
||||
'provider.delete': '删除',
|
||||
'provider.noProviders': '未配置服务商',
|
||||
'provider.addToStart': '添加服务商以开始使用',
|
||||
'provider.quickPresets': '快速预设',
|
||||
'provider.customProviders': '自定义服务商',
|
||||
'provider.active': '活跃',
|
||||
'provider.configured': '已配置',
|
||||
'provider.name': '名称',
|
||||
'provider.providerType': '服务商类型',
|
||||
'provider.apiKey': 'API 密钥',
|
||||
'provider.baseUrl': '基础 URL',
|
||||
'provider.advancedOptions': '高级选项',
|
||||
'provider.extraEnvVars': '额外环境变量',
|
||||
'provider.notes': '备注',
|
||||
'provider.notesPlaceholder': '关于此服务商的可选备注...',
|
||||
'provider.saving': '保存中...',
|
||||
'provider.update': '更新',
|
||||
'provider.envDetected': '从环境变量检测到',
|
||||
|
||||
// ── Right panel / Files ─────────────────────────────────────
|
||||
'panel.files': '文件',
|
||||
'panel.openPanel': '打开面板',
|
||||
'panel.closePanel': '关闭面板',
|
||||
|
||||
// ── File tree ───────────────────────────────────────────────
|
||||
'fileTree.filterFiles': '筛选文件...',
|
||||
'fileTree.refresh': '刷新',
|
||||
'fileTree.noFiles': '未找到文件',
|
||||
'fileTree.selectFolder': '选择项目文件夹以查看文件',
|
||||
|
||||
// ── File preview ────────────────────────────────────────────
|
||||
'filePreview.backToTree': '返回文件树',
|
||||
'filePreview.lines': '{count} 行',
|
||||
'filePreview.copyPath': '复制路径',
|
||||
'filePreview.failedToLoad': '加载文件失败',
|
||||
|
||||
// ── Doc preview ─────────────────────────────────────────────
|
||||
'docPreview.htmlPreview': 'HTML 预览',
|
||||
|
||||
// ── Extensions page ─────────────────────────────────────────
|
||||
'extensions.title': '扩展',
|
||||
'extensions.skills': '技能',
|
||||
'extensions.mcpServers': 'MCP 服务器',
|
||||
|
||||
// ── Skills ──────────────────────────────────────────────────
|
||||
'skills.noSelected': '未选择技能',
|
||||
'skills.selectOrCreate': '从列表中选择一个技能或创建新技能',
|
||||
'skills.newSkill': '新建技能',
|
||||
'skills.loadingSkills': '加载技能中...',
|
||||
'skills.noSkillsFound': '未找到技能',
|
||||
'skills.searchSkills': '搜索技能...',
|
||||
'skills.createSkill': '创建技能',
|
||||
'skills.nameRequired': '名称为必填项',
|
||||
'skills.nameInvalid': '名称只能包含字母、数字、连字符和下划线',
|
||||
'skills.skillName': '技能名称',
|
||||
'skills.scope': '作用域',
|
||||
'skills.global': '全局',
|
||||
'skills.project': '项目',
|
||||
'skills.template': '模板',
|
||||
'skills.blank': '空白',
|
||||
'skills.commitHelper': '提交助手',
|
||||
'skills.codeReviewer': '代码审查',
|
||||
'skills.saved': '已保存',
|
||||
'skills.save': '保存',
|
||||
'skills.edit': '编辑',
|
||||
'skills.preview': '预览',
|
||||
'skills.splitView': '分屏视图',
|
||||
'skills.deleteConfirm': '再次点击以确认',
|
||||
'skills.placeholder': '用 Markdown 编写技能提示词...',
|
||||
|
||||
// ── MCP ─────────────────────────────────────────────────────
|
||||
'mcp.addServer': '添加服务器',
|
||||
'mcp.loadingServers': '加载 MCP 服务器中...',
|
||||
'mcp.serverConfig': 'MCP 服务器配置',
|
||||
'mcp.noServers': '未配置 MCP 服务器',
|
||||
'mcp.noServersDesc': '添加 MCP 服务器以扩展 Claude 的能力',
|
||||
'mcp.arguments': '参数:',
|
||||
'mcp.environment': '环境变量:',
|
||||
'mcp.listTab': '列表',
|
||||
'mcp.jsonTab': 'JSON 配置',
|
||||
'mcp.editServer': '编辑服务器',
|
||||
'mcp.serverName': '服务器名称',
|
||||
'mcp.serverType': '服务器类型',
|
||||
'mcp.command': '命令',
|
||||
'mcp.argsLabel': '参数(每行一个)',
|
||||
'mcp.url': 'URL',
|
||||
'mcp.headers': '请求头(JSON)',
|
||||
'mcp.envVars': '环境变量(JSON)',
|
||||
'mcp.formTab': '表单',
|
||||
'mcp.jsonEditTab': 'JSON',
|
||||
'mcp.saveChanges': '保存更改',
|
||||
|
||||
// ── Folder picker ───────────────────────────────────────────
|
||||
'folderPicker.title': '选择项目文件夹',
|
||||
'folderPicker.loading': '加载中...',
|
||||
'folderPicker.noSubdirs': '无子目录',
|
||||
'folderPicker.cancel': '取消',
|
||||
'folderPicker.select': '选择此文件夹',
|
||||
|
||||
// ── Import session dialog ───────────────────────────────────
|
||||
'import.title': '从 Claude CLI 导入会话',
|
||||
'import.searchSessions': '搜索会话...',
|
||||
'import.noSessions': '未找到会话',
|
||||
'import.import': '导入',
|
||||
'import.importing': '导入中...',
|
||||
'import.justNow': '刚刚',
|
||||
'import.minutesAgo': '{n}分钟前',
|
||||
'import.hoursAgo': '{n}小时前',
|
||||
'import.daysAgo': '{n}天前',
|
||||
'import.messages': '{n} 条消息',
|
||||
'import.messagesPlural': '{n} 条消息',
|
||||
|
||||
// ── Connection status ───────────────────────────────────────
|
||||
'connection.notInstalled': 'Claude Code 未安装',
|
||||
'connection.installed': 'Claude Code 已安装',
|
||||
'connection.version': '版本:{version}',
|
||||
'connection.installPrompt': '要使用 Claude Code 功能,您需要安装 Claude Code CLI。',
|
||||
'connection.runCommand': '在终端中运行以下命令:',
|
||||
'connection.installAuto': '自动安装 Claude Code',
|
||||
'connection.refresh': '刷新',
|
||||
'connection.installClaude': '安装 Claude Code',
|
||||
|
||||
// ── Install wizard ──────────────────────────────────────────
|
||||
'install.title': '安装 Claude Code',
|
||||
'install.checkingPrereqs': '检查前置条件...',
|
||||
'install.alreadyInstalled': 'Claude Code 已安装',
|
||||
'install.readyToInstall': '准备安装',
|
||||
'install.installing': '正在安装 Claude Code...',
|
||||
'install.complete': '安装完成',
|
||||
'install.failed': '安装失败',
|
||||
'install.copyLogs': '复制日志',
|
||||
'install.copied': '已复制',
|
||||
'install.install': '安装',
|
||||
'install.cancel': '取消',
|
||||
'install.retry': '重试',
|
||||
'install.done': '完成',
|
||||
|
||||
// ── Task list ───────────────────────────────────────────────
|
||||
'tasks.all': '全部',
|
||||
'tasks.active': '进行中',
|
||||
'tasks.done': '已完成',
|
||||
'tasks.addPlaceholder': '添加任务...',
|
||||
'tasks.addTask': '添加任务',
|
||||
'tasks.loading': '加载任务中...',
|
||||
'tasks.noTasks': '暂无任务',
|
||||
'tasks.noMatching': '无匹配任务',
|
||||
|
||||
// ── Tool call block ─────────────────────────────────────────
|
||||
'tool.running': '运行中',
|
||||
'tool.success': '成功',
|
||||
'tool.error': '错误',
|
||||
|
||||
// ── Common ──────────────────────────────────────────────────
|
||||
'common.cancel': '取消',
|
||||
'common.save': '保存',
|
||||
'common.delete': '删除',
|
||||
'common.loading': '加载中...',
|
||||
'common.close': '关闭',
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
|
||||
// ── CLI dynamic field labels ──────────────────────────────
|
||||
'cli.loadingSettings': '加载设置中...',
|
||||
'cli.field.skipDangerousModePermissionPrompt': '跳过危险模式权限提示',
|
||||
'cli.field.verbose': '详细日志',
|
||||
'cli.field.theme': '主题',
|
||||
'cli.formatError': '无法格式化:无效的 JSON',
|
||||
};
|
||||
|
||||
export default zh;
|
||||
Reference in new issue
Block a user