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
124 lines
4.8 KiB
TypeScript
124 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
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 {
|
|
servers: Record<string, MCPServer>;
|
|
onEdit: (name: string, server: MCPServer) => void;
|
|
onDelete: (name: string) => void;
|
|
}
|
|
|
|
function getServerTypeInfo(server: MCPServer) {
|
|
const type = server.type || 'stdio';
|
|
switch (type) {
|
|
case 'sse':
|
|
return { label: 'SSE', icon: Wifi01Icon, color: 'text-blue-500' };
|
|
case 'http':
|
|
return { label: 'HTTP', icon: GlobeIcon, color: 'text-green-500' };
|
|
default:
|
|
return { label: 'stdio', icon: ServerStack01Icon, color: 'text-muted-foreground' };
|
|
}
|
|
}
|
|
|
|
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">{t('mcp.noServers')}</p>
|
|
<p className="text-xs mt-1">
|
|
{t('mcp.noServersDesc')}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{entries.map(([name, server]) => {
|
|
const typeInfo = getServerTypeInfo(server);
|
|
return (
|
|
<Card key={name}>
|
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2">
|
|
<div className="flex-1 min-w-0 mr-3">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<HugeiconsIcon icon={typeInfo.icon} className={`h-4 w-4 shrink-0 ${typeInfo.color}`} />
|
|
<CardTitle className="text-sm font-medium">{name}</CardTitle>
|
|
<Badge variant="outline" className="text-xs shrink-0">
|
|
{typeInfo.label}
|
|
</Badge>
|
|
<Badge variant="secondary" className="text-xs shrink-0">
|
|
{t('provider.configured')}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription className="text-xs mt-1 font-mono">
|
|
{server.url
|
|
? server.url
|
|
: `${server.command} ${server.args?.join(' ') || ''}`}
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex gap-1 shrink-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => onEdit(name, server)}
|
|
>
|
|
<HugeiconsIcon icon={PencilIcon} className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
onClick={() => onDelete(name)}
|
|
>
|
|
<HugeiconsIcon icon={Delete02Icon} className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
{(server.env && Object.keys(server.env).length > 0) ||
|
|
(server.args && server.args.length > 0) ? (
|
|
<CardContent className="pt-0">
|
|
{server.args && server.args.length > 0 && (
|
|
<div className="mb-2">
|
|
<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">
|
|
{arg}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{server.env && Object.keys(server.env).length > 0 && (
|
|
<div>
|
|
<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">
|
|
{key}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
) : null}
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|