mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-04 20:05:38 +08:00
feat(i18n): localize plugin settings for all languages (#515)
* chore(gitignore): add .worktrees/ to .gitignore
* fix(gitignore): add .worktrees/ to .gitignore
* feat(i18n): localize plugin settings
- Add missing mainTabs.plugins key in Russian locale.
- Add useTranslation to PluginSettingsTab and MobileNav.
- Add pluginSettings translations for en, ru, ja, ko, zh-CN.
- Localize the mobile navigation More button.
* fix: remove Japanese symbols in Rorean translate
* fix: fix Korean typo and localize starter plugin error
* fix(plugins): localize toggle labels and fix translation issues
* refactor(plugins): extract inline onToggle to named handleToggle
* fix(plugins): localize repo input aria-label and "tab" badge
- Replace hardcoded aria-label with t('pluginSettings.installAriaLabel')
- Replace hardcoded "tab" badge text with t('pluginSettings.tab')
- Add missing keys to all settings.json locale files
* fix(plugins): localize "running" status badge
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -135,3 +135,6 @@ tasks/
|
|||||||
!src/i18n/locales/en/tasks.json
|
!src/i18n/locales/en/tasks.json
|
||||||
!src/i18n/locales/ja/tasks.json
|
!src/i18n/locales/ja/tasks.json
|
||||||
!src/i18n/locales/ru/tasks.json
|
!src/i18n/locales/ru/tasks.json
|
||||||
|
|
||||||
|
# Git worktrees
|
||||||
|
.worktrees/
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useRef, useEffect, type Dispatch, type SetStateAction } from 'react';
|
import { useState, useRef, useEffect, type Dispatch, type SetStateAction } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -37,6 +38,7 @@ type MobileNavProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: MobileNavProps) {
|
export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: MobileNavProps) {
|
||||||
|
const { t } = useTranslation(['common', 'settings']);
|
||||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
||||||
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
||||||
const { plugins } = usePlugins();
|
const { plugins } = usePlugins();
|
||||||
@@ -126,11 +128,10 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMoreOpen((v) => !v);
|
setMoreOpen((v) => !v);
|
||||||
}}
|
}}
|
||||||
className={`relative flex w-full touch-manipulation flex-col items-center justify-center gap-0.5 rounded-xl px-3 py-2 transition-all duration-200 active:scale-95 ${
|
className={`relative flex w-full touch-manipulation flex-col items-center justify-center gap-0.5 rounded-xl px-3 py-2 transition-all duration-200 active:scale-95 ${isPluginActive || moreOpen
|
||||||
isPluginActive || moreOpen
|
|
||||||
? 'text-primary'
|
? 'text-primary'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
aria-label="More plugins"
|
aria-label="More plugins"
|
||||||
aria-expanded={moreOpen}
|
aria-expanded={moreOpen}
|
||||||
>
|
>
|
||||||
@@ -142,7 +143,7 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
strokeWidth={isPluginActive ? 2.4 : 1.8}
|
strokeWidth={isPluginActive ? 2.4 : 1.8}
|
||||||
/>
|
/>
|
||||||
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isPluginActive || moreOpen ? 'opacity-100' : 'opacity-60'}`}>
|
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isPluginActive || moreOpen ? 'opacity-100' : 'opacity-60'}`}>
|
||||||
More
|
{t('settings:pluginSettings.morePlugins')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -157,11 +158,10 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
<button
|
<button
|
||||||
key={p.name}
|
key={p.name}
|
||||||
onClick={() => selectPlugin(p.name)}
|
onClick={() => selectPlugin(p.name)}
|
||||||
className={`flex w-full items-center gap-2.5 px-3.5 py-2.5 text-sm transition-colors ${
|
className={`flex w-full items-center gap-2.5 px-3.5 py-2.5 text-sm transition-colors ${isActive
|
||||||
isActive
|
|
||||||
? 'bg-primary/8 text-primary'
|
? 'bg-primary/8 text-primary'
|
||||||
: 'text-foreground hover:bg-muted/60'
|
: 'text-foreground hover:bg-muted/60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4 flex-shrink-0" strokeWidth={isActive ? 2.2 : 1.8} />
|
<Icon className="h-4 w-4 flex-shrink-0" strokeWidth={isActive ? 2.2 : 1.8} />
|
||||||
<span className="truncate">{p.displayName}</span>
|
<span className="truncate">{p.displayName}</span>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Trash2, RefreshCw, GitBranch, Loader2, ServerCrash, ShieldAlert, ExternalLink, BookOpen, Download, BarChart3 } from 'lucide-react';
|
import { Trash2, RefreshCw, GitBranch, Loader2, ServerCrash, ShieldAlert, ExternalLink, BookOpen, Download, BarChart3 } from 'lucide-react';
|
||||||
import { usePlugins } from '../../../contexts/PluginsContext';
|
import { usePlugins } from '../../../contexts/PluginsContext';
|
||||||
import type { Plugin } from '../../../contexts/PluginsContext';
|
import type { Plugin } from '../../../contexts/PluginsContext';
|
||||||
@@ -32,7 +33,7 @@ function ToggleSwitch({ checked, onChange, ariaLabel }: { checked: boolean; onCh
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Server Dot ────────────────────────────────────────────────────────── */
|
/* ─── Server Dot ────────────────────────────────────────────────────────── */
|
||||||
function ServerDot({ running }: { running: boolean }) {
|
function ServerDot({ running, t }: { running: boolean; t: any }) {
|
||||||
if (!running) return null;
|
if (!running) return null;
|
||||||
return (
|
return (
|
||||||
<span className="relative flex items-center gap-1.5">
|
<span className="relative flex items-center gap-1.5">
|
||||||
@@ -41,7 +42,7 @@ function ServerDot({ running }: { running: boolean }) {
|
|||||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-[10px] uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
|
<span className="font-mono text-[10px] uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
|
||||||
running
|
{t('pluginSettings.runningStatus')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -71,6 +72,7 @@ function PluginCard({
|
|||||||
onCancelUninstall,
|
onCancelUninstall,
|
||||||
updateError,
|
updateError,
|
||||||
}: PluginCardProps) {
|
}: PluginCardProps) {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
const accentColor = plugin.enabled
|
const accentColor = plugin.enabled
|
||||||
? 'bg-emerald-500'
|
? 'bg-emerald-500'
|
||||||
: 'bg-muted-foreground/20';
|
: 'bg-muted-foreground/20';
|
||||||
@@ -108,7 +110,7 @@ function PluginCard({
|
|||||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
{plugin.slot}
|
{plugin.slot}
|
||||||
</span>
|
</span>
|
||||||
<ServerDot running={!!plugin.serverRunning} />
|
<ServerDot running={!!plugin.serverRunning} t={t} />
|
||||||
</div>
|
</div>
|
||||||
{plugin.description && (
|
{plugin.description && (
|
||||||
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
||||||
@@ -143,8 +145,8 @@ function PluginCard({
|
|||||||
<button
|
<button
|
||||||
onClick={onUpdate}
|
onClick={onUpdate}
|
||||||
disabled={updating || !plugin.repoUrl}
|
disabled={updating || !plugin.repoUrl}
|
||||||
title={plugin.repoUrl ? 'Pull latest from git' : 'No git remote — update not available'}
|
title={plugin.repoUrl ? t('pluginSettings.pullLatest') : t('pluginSettings.noGitRemote')}
|
||||||
aria-label={`Update ${plugin.displayName}`}
|
aria-label={t('pluginSettings.pullLatest')}
|
||||||
className="rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40"
|
className="rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{updating ? (
|
{updating ? (
|
||||||
@@ -156,18 +158,17 @@ function PluginCard({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onUninstall}
|
onClick={onUninstall}
|
||||||
title={confirmingUninstall ? 'Click again to confirm' : 'Uninstall plugin'}
|
title={confirmingUninstall ? t('pluginSettings.confirmUninstall') : t('pluginSettings.uninstallPlugin')}
|
||||||
aria-label={`Uninstall ${plugin.displayName}`}
|
aria-label={t('pluginSettings.uninstallPlugin')}
|
||||||
className={`rounded p-1.5 transition-colors ${
|
className={`rounded p-1.5 transition-colors ${confirmingUninstall
|
||||||
confirmingUninstall
|
? 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30'
|
||||||
? 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30'
|
: 'text-muted-foreground hover:bg-muted hover:text-red-500'
|
||||||
: 'text-muted-foreground hover:bg-muted hover:text-red-500'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ToggleSwitch checked={plugin.enabled} onChange={onToggle} ariaLabel={`${plugin.enabled ? 'Disable' : 'Enable'} ${plugin.displayName}`} />
|
<ToggleSwitch checked={plugin.enabled} onChange={onToggle} ariaLabel={`${plugin.enabled ? t('pluginSettings.disable') : t('pluginSettings.enable')} ${plugin.displayName}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,20 +176,20 @@ function PluginCard({
|
|||||||
{confirmingUninstall && (
|
{confirmingUninstall && (
|
||||||
<div className="mt-3 flex items-center justify-between gap-3 rounded border border-red-200 bg-red-50 px-3 py-2 dark:border-red-800/50 dark:bg-red-950/30">
|
<div className="mt-3 flex items-center justify-between gap-3 rounded border border-red-200 bg-red-50 px-3 py-2 dark:border-red-800/50 dark:bg-red-950/30">
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
Remove <span className="font-semibold">{plugin.displayName}</span>? This cannot be undone.
|
{t('pluginSettings.confirmUninstallMessage', { name: plugin.displayName })}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<button
|
<button
|
||||||
onClick={onCancelUninstall}
|
onClick={onCancelUninstall}
|
||||||
className="rounded border border-border px-2.5 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
className="rounded border border-border px-2.5 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
{t('pluginSettings.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onUninstall}
|
onClick={onUninstall}
|
||||||
className="rounded border border-red-300 px-2.5 py-1 text-sm font-medium text-red-600 transition-colors hover:bg-red-100 dark:border-red-700 dark:text-red-400 dark:hover:bg-red-900/30"
|
className="rounded border border-red-300 px-2.5 py-1 text-sm font-medium text-red-600 transition-colors hover:bg-red-100 dark:border-red-700 dark:text-red-400 dark:hover:bg-red-900/30"
|
||||||
>
|
>
|
||||||
Remove
|
{t('pluginSettings.remove')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,6 +209,8 @@ function PluginCard({
|
|||||||
|
|
||||||
/* ─── Starter Plugin Card ───────────────────────────────────────────────── */
|
/* ─── Starter Plugin Card ───────────────────────────────────────────────── */
|
||||||
function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
|
function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex overflow-hidden rounded-lg border border-dashed border-border bg-card transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500">
|
<div className="relative flex overflow-hidden rounded-lg border border-dashed border-border bg-card transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500">
|
||||||
<div className="w-[3px] flex-shrink-0 bg-blue-500/30" />
|
<div className="w-[3px] flex-shrink-0 bg-blue-500/30" />
|
||||||
@@ -220,17 +223,17 @@ function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; i
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-sm font-semibold leading-none text-foreground">
|
<span className="text-sm font-semibold leading-none text-foreground">
|
||||||
Project Stats
|
{t('pluginSettings.starterPlugin.name')}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:bg-blue-950/50 dark:text-blue-400">
|
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:bg-blue-950/50 dark:text-blue-400">
|
||||||
starter
|
{t('pluginSettings.starterPlugin.badge')}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
tab
|
{t('pluginSettings.tab')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
||||||
File counts, lines of code, file-type breakdown, and recent activity for your project.
|
{t('pluginSettings.starterPlugin.description')}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href={STARTER_PLUGIN_URL}
|
href={STARTER_PLUGIN_URL}
|
||||||
@@ -253,7 +256,7 @@ function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; i
|
|||||||
) : (
|
) : (
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
{installing ? 'Installing…' : 'Install'}
|
{installing ? t('pluginSettings.installing') : t('pluginSettings.starterPlugin.install')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,6 +266,7 @@ function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; i
|
|||||||
|
|
||||||
/* ─── Main Component ────────────────────────────────────────────────────── */
|
/* ─── Main Component ────────────────────────────────────────────────────── */
|
||||||
export default function PluginSettingsTab() {
|
export default function PluginSettingsTab() {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
const { plugins, loading, installPlugin, uninstallPlugin, updatePlugin, togglePlugin } =
|
const { plugins, loading, installPlugin, uninstallPlugin, updatePlugin, togglePlugin } =
|
||||||
usePlugins();
|
usePlugins();
|
||||||
|
|
||||||
@@ -279,7 +283,7 @@ export default function PluginSettingsTab() {
|
|||||||
setUpdateErrors((prev) => { const next = { ...prev }; delete next[name]; return next; });
|
setUpdateErrors((prev) => { const next = { ...prev }; delete next[name]; return next; });
|
||||||
const result = await updatePlugin(name);
|
const result = await updatePlugin(name);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setUpdateErrors((prev) => ({ ...prev, [name]: result.error || 'Update failed' }));
|
setUpdateErrors((prev) => ({ ...prev, [name]: result.error || t('pluginSettings.updateFailed') }));
|
||||||
}
|
}
|
||||||
setUpdatingPlugins((prev) => { const next = new Set(prev); next.delete(name); return next; });
|
setUpdatingPlugins((prev) => { const next = new Set(prev); next.delete(name); return next; });
|
||||||
};
|
};
|
||||||
@@ -292,7 +296,7 @@ export default function PluginSettingsTab() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setGitUrl('');
|
setGitUrl('');
|
||||||
} else {
|
} else {
|
||||||
setInstallError(result.error || 'Installation failed');
|
setInstallError(result.error || t('pluginSettings.installFailed'));
|
||||||
}
|
}
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
};
|
};
|
||||||
@@ -302,7 +306,7 @@ export default function PluginSettingsTab() {
|
|||||||
setInstallError(null);
|
setInstallError(null);
|
||||||
const result = await installPlugin(STARTER_PLUGIN_URL);
|
const result = await installPlugin(STARTER_PLUGIN_URL);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setInstallError(result.error || 'Installation failed');
|
setInstallError(result.error || t('pluginSettings.installFailed'));
|
||||||
}
|
}
|
||||||
setInstallingStarter(false);
|
setInstallingStarter(false);
|
||||||
};
|
};
|
||||||
@@ -316,7 +320,7 @@ export default function PluginSettingsTab() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setConfirmUninstall(null);
|
setConfirmUninstall(null);
|
||||||
} else {
|
} else {
|
||||||
setInstallError(result.error || 'Uninstall failed');
|
setInstallError(result.error || t('pluginSettings.uninstallFailed'));
|
||||||
setConfirmUninstall(null);
|
setConfirmUninstall(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -328,17 +332,10 @@ export default function PluginSettingsTab() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-1 text-base font-semibold text-foreground">
|
<h3 className="mb-1 text-base font-semibold text-foreground">
|
||||||
Plugins
|
{t('pluginSettings.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Extend the interface with custom plugins. Install from{' '}
|
{t('pluginSettings.description')}
|
||||||
<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-semibold">
|
|
||||||
git
|
|
||||||
</code>{' '}
|
|
||||||
or drop a folder in{' '}
|
|
||||||
<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-semibold">
|
|
||||||
~/.claude-code-ui/plugins/
|
|
||||||
</code>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -354,8 +351,8 @@ export default function PluginSettingsTab() {
|
|||||||
setGitUrl(e.target.value);
|
setGitUrl(e.target.value);
|
||||||
setInstallError(null);
|
setInstallError(null);
|
||||||
}}
|
}}
|
||||||
placeholder="https://github.com/user/my-plugin"
|
placeholder={t('pluginSettings.installPlaceholder')}
|
||||||
aria-label="Plugin git repository URL"
|
aria-label={t('pluginSettings.installAriaLabel')}
|
||||||
className="flex-1 bg-transparent px-2 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
|
className="flex-1 bg-transparent px-2 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') void handleInstall();
|
if (e.key === 'Enter') void handleInstall();
|
||||||
@@ -369,7 +366,7 @@ export default function PluginSettingsTab() {
|
|||||||
{installing ? (
|
{installing ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
'Install'
|
t('pluginSettings.installButton')
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,7 +378,7 @@ export default function PluginSettingsTab() {
|
|||||||
<p className="-mt-4 flex items-start gap-1.5 text-xs leading-snug text-muted-foreground/50">
|
<p className="-mt-4 flex items-start gap-1.5 text-xs leading-snug text-muted-foreground/50">
|
||||||
<ShieldAlert className="mt-px h-3 w-3 flex-shrink-0" />
|
<ShieldAlert className="mt-px h-3 w-3 flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
Only install plugins whose source code you have reviewed or from authors you trust.
|
{t('pluginSettings.securityWarning')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -394,26 +391,35 @@ export default function PluginSettingsTab() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground">
|
<div className="flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
Scanning plugins…
|
{t('pluginSettings.scanningPlugins')}
|
||||||
</div>
|
</div>
|
||||||
) : plugins.length === 0 ? (
|
) : plugins.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">No plugins installed</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">{t('pluginSettings.noPluginsInstalled')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{plugins.map((plugin, index) => (
|
{plugins.map((plugin, index) => {
|
||||||
<PluginCard
|
const handleToggle = async (enabled: boolean) => {
|
||||||
key={plugin.name}
|
const r = await togglePlugin(plugin.name, enabled);
|
||||||
plugin={plugin}
|
if (!r.success) {
|
||||||
index={index}
|
setInstallError(r.error || t('pluginSettings.toggleFailed'));
|
||||||
onToggle={(enabled) => void togglePlugin(plugin.name, enabled).then(r => { if (!r.success) setInstallError(r.error || 'Toggle failed'); })}
|
}
|
||||||
onUpdate={() => void handleUpdate(plugin.name)}
|
};
|
||||||
onUninstall={() => void handleUninstall(plugin.name)}
|
|
||||||
updating={updatingPlugins.has(plugin.name)}
|
return (
|
||||||
confirmingUninstall={confirmUninstall === plugin.name}
|
<PluginCard
|
||||||
onCancelUninstall={() => setConfirmUninstall(null)}
|
key={plugin.name}
|
||||||
updateError={updateErrors[plugin.name] ?? null}
|
plugin={plugin}
|
||||||
/>
|
index={index}
|
||||||
))}
|
onToggle={(enabled) => void handleToggle(enabled)}
|
||||||
|
onUpdate={() => void handleUpdate(plugin.name)}
|
||||||
|
onUninstall={() => void handleUninstall(plugin.name)}
|
||||||
|
updating={updatingPlugins.has(plugin.name)}
|
||||||
|
confirmingUninstall={confirmUninstall === plugin.name}
|
||||||
|
onCancelUninstall={() => setConfirmUninstall(null)}
|
||||||
|
updateError={updateErrors[plugin.name] ?? null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -422,7 +428,7 @@ export default function PluginSettingsTab() {
|
|||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
|
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
|
||||||
<span className="text-xs text-muted-foreground/60">
|
<span className="text-xs text-muted-foreground/60">
|
||||||
Build your own plugin
|
{t('pluginSettings.buildYourOwn')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 items-center gap-3">
|
<div className="flex flex-shrink-0 items-center gap-3">
|
||||||
@@ -432,7 +438,7 @@ export default function PluginSettingsTab() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
Starter <ExternalLink className="h-2.5 w-2.5" />
|
{t('pluginSettings.starter')} <ExternalLink className="h-2.5 w-2.5" />
|
||||||
</a>
|
</a>
|
||||||
<span className="text-muted-foreground/20">·</span>
|
<span className="text-muted-foreground/20">·</span>
|
||||||
<a
|
<a
|
||||||
@@ -441,7 +447,7 @@ export default function PluginSettingsTab() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
Docs <ExternalLink className="h-2.5 w-2.5" />
|
{t('pluginSettings.docs')} <ExternalLink className="h-2.5 w-2.5" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -434,5 +434,41 @@
|
|||||||
"title": "About Codex MCP",
|
"title": "About Codex MCP",
|
||||||
"description": "Codex supports stdio-based MCP servers. You can add servers that extend Codex's capabilities with additional tools and resources."
|
"description": "Codex supports stdio-based MCP servers. You can add servers that extend Codex's capabilities with additional tools and resources."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pluginSettings": {
|
||||||
|
"title": "Plugins",
|
||||||
|
"description": "Extend the interface with custom plugins. Install from git or drop a folder in ~/.claude-code-ui/plugins/",
|
||||||
|
"installPlaceholder": "https://github.com/user/my-plugin",
|
||||||
|
"installButton": "Install",
|
||||||
|
"installing": "Installing…",
|
||||||
|
"securityWarning": "Only install plugins whose source code you have reviewed or from authors you trust.",
|
||||||
|
"scanningPlugins": "Scanning plugins…",
|
||||||
|
"noPluginsInstalled": "No plugins installed",
|
||||||
|
"pullLatest": "Pull latest from git",
|
||||||
|
"noGitRemote": "No git remote — update not available",
|
||||||
|
"uninstallPlugin": "Uninstall plugin",
|
||||||
|
"confirmUninstall": "Click again to confirm",
|
||||||
|
"confirmUninstallMessage": "Remove {{name}}? This cannot be undone.",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"remove": "Remove",
|
||||||
|
"updateFailed": "Update failed",
|
||||||
|
"installFailed": "Installation failed",
|
||||||
|
"uninstallFailed": "Uninstall failed",
|
||||||
|
"toggleFailed": "Toggle failed",
|
||||||
|
"buildYourOwn": "Build your own plugin",
|
||||||
|
"starter": "Starter",
|
||||||
|
"docs": "Docs",
|
||||||
|
"starterPlugin": {
|
||||||
|
"name": "Project Stats",
|
||||||
|
"badge": "starter",
|
||||||
|
"description": "File counts, lines of code, file-type breakdown, and recent activity for your project.",
|
||||||
|
"install": "Install"
|
||||||
|
},
|
||||||
|
"morePlugins": "More",
|
||||||
|
"enable": "Enable",
|
||||||
|
"disable": "Disable",
|
||||||
|
"installAriaLabel": "Plugin git repository URL",
|
||||||
|
"tab": "tab",
|
||||||
|
"runningStatus": "running"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,5 +434,41 @@
|
|||||||
"title": "Codex MCPについて",
|
"title": "Codex MCPについて",
|
||||||
"description": "Codexはstdioベースのツールサーバーをサポートしています。追加のツールやリソースでCodexの機能を拡張するサーバーを追加できます。"
|
"description": "Codexはstdioベースのツールサーバーをサポートしています。追加のツールやリソースでCodexの機能を拡張するサーバーを追加できます。"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pluginSettings": {
|
||||||
|
"title": "プラグイン",
|
||||||
|
"description": "カスタムプラグインでインターフェースを拡張します。gitからインストールするか、~/.claude-code-ui/plugins/ にフォルダを配置してください。",
|
||||||
|
"installPlaceholder": "https://github.com/user/my-plugin",
|
||||||
|
"installButton": "インストール",
|
||||||
|
"installing": "インストール中…",
|
||||||
|
"securityWarning": "信頼できる作成者のプラグイン、またはソースコードを確認済みのプラグインのみをインストールしてください。",
|
||||||
|
"scanningPlugins": "プラグインをスキャン中…",
|
||||||
|
"noPluginsInstalled": "プラグインがインストールされていません",
|
||||||
|
"pullLatest": "gitから最新を取得",
|
||||||
|
"noGitRemote": "リモートgitリポジトリがありません — アップデート不可",
|
||||||
|
"uninstallPlugin": "プラグインを削除",
|
||||||
|
"confirmUninstall": "クリックして確定",
|
||||||
|
"confirmUninstallMessage": "{{name}} を削除しますか?この操作は取り消せません。",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"remove": "削除",
|
||||||
|
"updateFailed": "アップデートに失敗しました",
|
||||||
|
"installFailed": "インストールに失敗しました",
|
||||||
|
"uninstallFailed": "削除に失敗しました",
|
||||||
|
"toggleFailed": "切り替えに失敗しました",
|
||||||
|
"buildYourOwn": "プラグインを自作する",
|
||||||
|
"starter": "スターター",
|
||||||
|
"docs": "ドキュメント",
|
||||||
|
"starterPlugin": {
|
||||||
|
"name": "プロジェクト統計",
|
||||||
|
"badge": "スターター",
|
||||||
|
"description": "プロジェクトのファイル数、コード行数、ファイルタイプの内訳、最近のアクティビティを表示します。",
|
||||||
|
"install": "インストール"
|
||||||
|
},
|
||||||
|
"morePlugins": "詳細",
|
||||||
|
"enable": "有効にする",
|
||||||
|
"disable": "無効にする",
|
||||||
|
"installAriaLabel": "プラグインのgitリポジトリURL",
|
||||||
|
"tab": "タブ",
|
||||||
|
"runningStatus": "実行中"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,5 +434,41 @@
|
|||||||
"title": "Codex MCP 정보",
|
"title": "Codex MCP 정보",
|
||||||
"description": "Codex는 stdio 기반 MCP 서버를 지원합니다. 추가 도구와 리소스로 Codex의 기능을 확장하는 서버를 추가할 수 있습니다."
|
"description": "Codex는 stdio 기반 MCP 서버를 지원합니다. 추가 도구와 리소스로 Codex의 기능을 확장하는 서버를 추가할 수 있습니다."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pluginSettings": {
|
||||||
|
"title": "플러그인",
|
||||||
|
"description": "커스텀 플러그인으로 인터페이스를 확장하세요. git에서 설치하거나 ~/.claude-code-ui/plugins/ 폴더에 직접 추가할 수 있습니다.",
|
||||||
|
"installPlaceholder": "https://github.com/user/my-plugin",
|
||||||
|
"installButton": "설치",
|
||||||
|
"installing": "설치 중…",
|
||||||
|
"securityWarning": "소스 코드를 검토했거나 신뢰할 수 있는 작성자의 플러그인만 설치하세요.",
|
||||||
|
"scanningPlugins": "플러그인 스캔 중…",
|
||||||
|
"noPluginsInstalled": "설치된 플러그인이 없습니다",
|
||||||
|
"pullLatest": "git에서 최신 버전 가져오기",
|
||||||
|
"noGitRemote": "git 리모트가 없음 — 업데이트 불가",
|
||||||
|
"uninstallPlugin": "플러그인 삭제",
|
||||||
|
"confirmUninstall": "다시 클릭하여 확인",
|
||||||
|
"confirmUninstallMessage": "{{name}} 플러그인을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||||
|
"cancel": "취소",
|
||||||
|
"remove": "삭제",
|
||||||
|
"updateFailed": "업데이트 실패",
|
||||||
|
"installFailed": "설치 실패",
|
||||||
|
"uninstallFailed": "삭제 실패",
|
||||||
|
"toggleFailed": "토글 실패",
|
||||||
|
"buildYourOwn": "나만의 플러그인 만들기",
|
||||||
|
"starter": "스타터",
|
||||||
|
"docs": "문서",
|
||||||
|
"starterPlugin": {
|
||||||
|
"name": "프로젝트 통계",
|
||||||
|
"badge": "스타터",
|
||||||
|
"description": "프로젝트의 파일 수, 코드 라인 수, 파일 유형별 분석 및 최근 활동을 확인합니다.",
|
||||||
|
"install": "설치"
|
||||||
|
},
|
||||||
|
"morePlugins": "더 보기",
|
||||||
|
"enable": "활성화",
|
||||||
|
"disable": "비활성화",
|
||||||
|
"installAriaLabel": "플러그인 git 저장소 URL",
|
||||||
|
"tab": "탭",
|
||||||
|
"runningStatus": "실행 중"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,8 @@
|
|||||||
"appearance": "Внешний вид",
|
"appearance": "Внешний вид",
|
||||||
"git": "Git",
|
"git": "Git",
|
||||||
"apiTokens": "API и токены",
|
"apiTokens": "API и токены",
|
||||||
"tasks": "Задачи"
|
"tasks": "Задачи",
|
||||||
|
"plugins": "Плагины"
|
||||||
},
|
},
|
||||||
"appearanceSettings": {
|
"appearanceSettings": {
|
||||||
"darkMode": {
|
"darkMode": {
|
||||||
@@ -433,5 +434,41 @@
|
|||||||
"title": "О Codex MCP",
|
"title": "О Codex MCP",
|
||||||
"description": "Codex поддерживает MCP серверы на основе stdio. Вы можете добавлять серверы, которые расширяют возможности Codex дополнительными инструментами и ресурсами."
|
"description": "Codex поддерживает MCP серверы на основе stdio. Вы можете добавлять серверы, которые расширяют возможности Codex дополнительными инструментами и ресурсами."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pluginSettings": {
|
||||||
|
"title": "Плагины",
|
||||||
|
"description": "Расширяйте интерфейс с помощью кастомных плагинов. Установите из git или добавьте папку в ~/.claude-code-ui/plugins/",
|
||||||
|
"installPlaceholder": "https://github.com/user/my-plugin",
|
||||||
|
"installButton": "Установить",
|
||||||
|
"installing": "Установка…",
|
||||||
|
"securityWarning": "Устанавливайте только те плагины, исходный код которых вы проверили или от авторов, которым вы доверяете.",
|
||||||
|
"scanningPlugins": "Сканирование плагинов…",
|
||||||
|
"noPluginsInstalled": "Плагины не установлены",
|
||||||
|
"pullLatest": "Получить обновления из git",
|
||||||
|
"noGitRemote": "Нет удаленного git-репозитория — обновление недоступно",
|
||||||
|
"uninstallPlugin": "Удалить плагин",
|
||||||
|
"confirmUninstall": "Нажмите еще раз для подтверждения",
|
||||||
|
"confirmUninstallMessage": "Удалить {{name}}? Это действие нельзя отменить.",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"remove": "Удалить",
|
||||||
|
"updateFailed": "Ошибка обновления",
|
||||||
|
"installFailed": "Ошибка установки",
|
||||||
|
"uninstallFailed": "Ошибка удаления",
|
||||||
|
"toggleFailed": "Ошибка переключения",
|
||||||
|
"buildYourOwn": "Создайте свой плагин",
|
||||||
|
"starter": "Шаблон",
|
||||||
|
"docs": "Документация",
|
||||||
|
"starterPlugin": {
|
||||||
|
"name": "Статистика проекта",
|
||||||
|
"badge": "шаблон",
|
||||||
|
"description": "Количество файлов, строк кода, разбивка по типам файлов и недавняя активность в вашем проекте.",
|
||||||
|
"install": "Установить"
|
||||||
|
},
|
||||||
|
"morePlugins": "Ещё",
|
||||||
|
"enable": "Включить",
|
||||||
|
"disable": "Выключить",
|
||||||
|
"installAriaLabel": "URL git-репозитория плагина",
|
||||||
|
"tab": "вкладка",
|
||||||
|
"runningStatus": "запущен"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,5 +434,41 @@
|
|||||||
"title": "关于 Codex MCP",
|
"title": "关于 Codex MCP",
|
||||||
"description": "Codex 支持基于 stdio 的 MCP 服务器。您可以添加服务器,通过额外的工具和资源来扩展 Codex 的功能。"
|
"description": "Codex 支持基于 stdio 的 MCP 服务器。您可以添加服务器,通过额外的工具和资源来扩展 Codex 的功能。"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pluginSettings": {
|
||||||
|
"title": "插件",
|
||||||
|
"description": "通过自定义插件扩展界面。从 git 安装或直接将文件夹放入 ~/.claude-code-ui/plugins/",
|
||||||
|
"installPlaceholder": "https://github.com/user/my-plugin",
|
||||||
|
"installButton": "安装",
|
||||||
|
"installing": "安装中…",
|
||||||
|
"securityWarning": "仅安装您已审查过源代码或信任作者的插件。",
|
||||||
|
"scanningPlugins": "正在扫描插件…",
|
||||||
|
"noPluginsInstalled": "未安装插件",
|
||||||
|
"pullLatest": "从 git 拉取最新内容",
|
||||||
|
"noGitRemote": "无 git 远程仓库 — 无法更新",
|
||||||
|
"uninstallPlugin": "卸载插件",
|
||||||
|
"confirmUninstall": "再次点击确认",
|
||||||
|
"confirmUninstallMessage": "移除 {{name}}?此操作无法撤销。",
|
||||||
|
"cancel": "取消",
|
||||||
|
"remove": "移除",
|
||||||
|
"updateFailed": "更新失败",
|
||||||
|
"installFailed": "安装失败",
|
||||||
|
"uninstallFailed": "卸载失败",
|
||||||
|
"toggleFailed": "切换失败",
|
||||||
|
"buildYourOwn": "构建您自己的插件",
|
||||||
|
"starter": "入门模板",
|
||||||
|
"docs": "文档",
|
||||||
|
"starterPlugin": {
|
||||||
|
"name": "项目统计",
|
||||||
|
"badge": "入门",
|
||||||
|
"description": "查看项目的文件数、代码行数、文件类型分布以及最近活动。",
|
||||||
|
"install": "安装"
|
||||||
|
},
|
||||||
|
"morePlugins": "更多",
|
||||||
|
"enable": "启用",
|
||||||
|
"disable": "禁用",
|
||||||
|
"installAriaLabel": "插件 git 仓库 URL",
|
||||||
|
"tab": "标签",
|
||||||
|
"runningStatus": "运行中"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user