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:
7418 committed 2026-02-24 16:50:57 +08:00
1 parent 85940dfde3
commit c7c3ba5302
38 files changed
+980 -217

No files matched your search

+7 -5
View File
@@ -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>
+18 -13
View File
@@ -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>
);
})}
+4 -2
View File
@@ -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>
)}
+6 -4
View File
@@ -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>
)}
+16 -14
View File
@@ -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>
)}
+7 -5
View File
@@ -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>
+3 -1
View File
@@ -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')}
/>
);
}
+61
View File
@@ -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>
);
}
+18 -13
View File
@@ -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)}
+11 -9
View File
@@ -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>
+14 -6
View File
@@ -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>
)}
+7 -5
View File
@@ -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>
+3 -1
View File
@@ -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 -6
View File
@@ -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>
+14 -12
View File
@@ -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>
+7 -5
View File
@@ -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&apos;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">
+8 -6
View File
@@ -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 ? (
+5 -3
View File
@@ -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
+9 -7
View File
@@ -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">
+34 -19
View File
@@ -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>
+42 -21
View File
@@ -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&apos;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>
+13 -11
View File
@@ -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>
+11 -9
View File
@@ -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 &quot;{deleteTarget?.name}&quot;? 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 -3
View File
@@ -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>
+15 -13
View File
@@ -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>
+8 -6
View File
@@ -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">
+3 -1
View File
@@ -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 -8
View File
@@ -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>
)}