feat(i18n): Implement internationalization for Channels, Skills, Setup, and Models pages

This commit is contained in:
DigHuang
2026-03-10 15:51:45 +08:00
Unverified
parent d9ae0f3263
commit 17e6ab9149
20 changed files with 175 additions and 74 deletions

View File

@@ -121,10 +121,10 @@ export function Channels() {
<div className="flex flex-col md:flex-row md:items-start justify-between mb-8 shrink-0 gap-4">
<div>
<h1 className="text-5xl md:text-6xl font-serif text-foreground mb-3 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
{t('title') || 'Channels'}
{t('title')}
</h1>
<p className="text-[17px] text-foreground/80 font-medium">
{t('subtitle') || 'Connect to messaging platforms.'}
{t('subtitle')}
</p>
</div>
@@ -170,7 +170,7 @@ export function Channels() {
{safeChannels.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-serif text-foreground mb-6 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
Available Channels
{t('availableChannels')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{safeChannels.map((channel) => (
@@ -187,7 +187,7 @@ export function Channels() {
{/* Supported Channels (Not yet configured) */}
<div className="mb-8">
<h2 className="text-3xl font-serif text-foreground mb-6 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
Supported Channels
{t('supportedChannels')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
@@ -217,7 +217,7 @@ export function Channels() {
<h3 className="text-[16px] font-semibold text-foreground truncate">{meta.name}</h3>
{meta.isPlugin && (
<Badge variant="secondary" className="font-mono text-[10px] font-medium px-2 py-0.5 rounded-full bg-black/[0.04] dark:bg-white/[0.08] border-0 shadow-none text-foreground/70">
{t('pluginBadge', 'Plugin')}
{t('pluginBadge')}
</Badge>
)}
</div>

View File

@@ -72,10 +72,10 @@ export function Models() {
<div className="flex flex-col md:flex-row md:items-start justify-between mb-12 shrink-0 gap-4">
<div>
<h1 className="text-5xl md:text-6xl font-serif text-foreground mb-3 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
Models
{t('dashboard:models.title')}
</h1>
<p className="text-[17px] text-foreground/80 font-medium">
Manage your AI providers and monitor token usage.
{t('dashboard:models.subtitle')}
</p>
</div>
</div>

View File

@@ -29,6 +29,7 @@ import { cn } from '@/lib/utils';
import { useGatewayStore } from '@/stores/gateway';
import { useSettingsStore } from '@/stores/settings';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { SUPPORTED_LANGUAGES } from '@/i18n';
import { toast } from 'sonner';
import { invokeIpc } from '@/lib/api-client';
@@ -48,31 +49,31 @@ const STEP = {
COMPLETE: 4,
} as const;
const steps: SetupStep[] = [
const getSteps = (t: TFunction): SetupStep[] => [
{
id: 'welcome',
title: 'Welcome to ClawX',
description: 'Your AI assistant is ready to be configured',
title: t('steps.welcome.title'),
description: t('steps.welcome.description'),
},
{
id: 'runtime',
title: 'Environment Check',
description: 'Verifying system requirements',
title: t('steps.runtime.title'),
description: t('steps.runtime.description'),
},
{
id: 'provider',
title: 'AI Provider',
description: 'Configure your AI service',
title: t('steps.provider.title'),
description: t('steps.provider.description'),
},
{
id: 'installing',
title: 'Setting Up',
description: 'Installing essential components',
title: t('steps.installing.title'),
description: t('steps.installing.description'),
},
{
id: 'complete',
title: 'All Set!',
description: 'ClawX is ready to use',
title: t('steps.complete.title'),
description: t('steps.complete.description'),
},
];
@@ -83,12 +84,12 @@ interface DefaultSkill {
description: string;
}
const defaultSkills: DefaultSkill[] = [
{ id: 'opencode', name: 'OpenCode', description: 'AI coding assistant backend' },
{ id: 'python-env', name: 'Python Environment', description: 'Python runtime for skills' },
{ id: 'code-assist', name: 'Code Assist', description: 'Code analysis and suggestions' },
{ id: 'file-tools', name: 'File Tools', description: 'File operations and management' },
{ id: 'terminal', name: 'Terminal', description: 'Shell command execution' },
const getDefaultSkills = (t: TFunction): DefaultSkill[] => [
{ id: 'opencode', name: t('defaultSkills.opencode.name'), description: t('defaultSkills.opencode.description') },
{ id: 'python-env', name: t('defaultSkills.python-env.name'), description: t('defaultSkills.python-env.description') },
{ id: 'code-assist', name: t('defaultSkills.code-assist.name'), description: t('defaultSkills.code-assist.description') },
{ id: 'file-tools', name: t('defaultSkills.file-tools.name'), description: t('defaultSkills.file-tools.description') },
{ id: 'terminal', name: t('defaultSkills.terminal.name'), description: t('defaultSkills.terminal.description') },
];
import {
@@ -130,6 +131,7 @@ export function Setup() {
// Runtime check status
const [runtimeChecksPassed, setRuntimeChecksPassed] = useState(false);
const steps = getSteps(t);
const safeStepIndex = Number.isInteger(currentStep)
? Math.min(Math.max(currentStep, STEP.WELCOME), steps.length - 1)
: STEP.WELCOME;
@@ -255,7 +257,7 @@ export function Setup() {
)}
{safeStepIndex === STEP.INSTALLING && (
<InstallingContent
skills={defaultSkills}
skills={getDefaultSkills(t)}
onComplete={handleInstallationComplete}
onSkip={() => setCurrentStep((i) => i + 1)}
/>
@@ -634,10 +636,10 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
</div>
<div className="grid grid-cols-[1fr_auto] items-center gap-4 p-3 rounded-lg bg-muted/50">
<div className="flex items-center gap-2 text-left">
<span>Gateway Service</span>
<span>{t('runtime.gateway')}</span>
{checks.gateway.status === 'error' && (
<Button variant="outline" size="sm" onClick={handleStartGateway}>
Start Gateway
{t('runtime.startGateway')}
</Button>
)}
</div>
@@ -665,19 +667,19 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
{showLogs && (
<div className="mt-4 p-4 rounded-lg bg-black/40 border border-border">
<div className="flex items-center justify-between mb-2">
<p className="font-medium text-foreground text-sm">Application Logs</p>
<p className="font-medium text-foreground text-sm">{t('runtime.logs.title')}</p>
<div className="flex gap-2">
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={handleOpenLogDir}>
<ExternalLink className="h-3 w-3 mr-1" />
Open Log Folder
{t('runtime.logs.openFolder')}
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setShowLogs(false)}>
Close
{t('runtime.logs.close')}
</Button>
</div>
</div>
<pre className="text-xs text-slate-300 bg-black/50 p-3 rounded max-h-60 overflow-auto whitespace-pre-wrap font-mono">
{logContent || '(No logs available yet)'}
{logContent || t('runtime.logs.noLogs')}
</pre>
</div>
)}
@@ -1574,9 +1576,9 @@ function CompleteContent({ selectedProvider, installedSkills }: CompleteContentP
const gatewayStatus = useGatewayStore((state) => state.status);
const providerData = providers.find((p) => p.id === selectedProvider);
const installedSkillNames = defaultSkills
.filter((s) => installedSkills.includes(s.id))
.map((s) => s.name)
const installedSkillNames = getDefaultSkills(t)
.filter((s: DefaultSkill) => installedSkills.includes(s.id))
.map((s: DefaultSkill) => s.name)
.join(', ');
return (

View File

@@ -197,7 +197,7 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
<div className="space-y-2">
<h3 className="text-[13px] font-bold flex items-center gap-2 text-foreground/80">
<Key className="h-3.5 w-3.5 text-blue-500" />
API Key
{t('detail.apiKey')}
</h3>
<Input
placeholder={t('detail.apiKeyPlaceholder', 'Enter API Key (optional)')}
@@ -218,7 +218,7 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<h3 className="text-[13px] font-bold text-foreground/80">
Environment Variables
{t('detail.envVars')}
{envVars.length > 0 && (
<Badge variant="secondary" className="ml-2 px-1.5 py-0 text-[10px] h-5 bg-black/10 dark:bg-white/10 text-foreground">
{envVars.length}
@@ -298,7 +298,7 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
)}
disabled={isSaving}
>
{isSaving ? t('detail.saving', 'Saving...') : 'Save Configuration'}
{isSaving ? t('detail.saving') : t('detail.saveConfig')}
</Button>
)}
@@ -316,8 +316,8 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall }: Sk
}}
>
{!skill.isBundled && onUninstall
? 'Uninstall'
: (skill.enabled ? t('detail.disable', 'Disable') : t('detail.enable', 'Enable'))}
? t('detail.uninstall')
: (skill.enabled ? t('detail.disable') : t('detail.enable'))}
</Button>
)}
</div>
@@ -554,10 +554,10 @@ export function Skills() {
<div className="flex flex-col md:flex-row md:items-start justify-between mb-6 shrink-0 gap-4">
<div>
<h1 className="text-5xl md:text-6xl font-serif text-foreground mb-3 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
{t('title') || 'Skills'}
{t('title')}
</h1>
<p className="text-[17px] text-foreground/80 font-medium">
{t('subtitle') || 'Browse and manage AI capabilities.'}
{t('subtitle')}
</p>
</div>
@@ -568,7 +568,7 @@ export function Skills() {
className="hover:bg-black/5 dark:hover:bg-white/5 transition-colors shrink-0 text-[13px] font-medium px-4 h-8 rounded-full border border-black/10 dark:border-white/10 flex items-center justify-center text-foreground/80 hover:text-foreground"
>
<FolderOpen className="h-4 w-4 mr-2" />
Open Skills Folder
{t('openFolder')}
</button>
)}
</div>
@@ -611,22 +611,19 @@ export function Skills() {
onClick={() => { setActiveTab('all'); setSelectedSource('all'); }}
className={cn("font-medium transition-colors flex items-center gap-1.5", activeTab === 'all' && selectedSource === 'all' ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
All Skills
<span className="text-[12px] font-normal opacity-70">{sourceStats.all}</span>
{t('filter.all', { count: sourceStats.all })}
</button>
<button
onClick={() => { setActiveTab('all'); setSelectedSource('built-in'); }}
className={cn("font-medium transition-colors flex items-center gap-1.5", activeTab === 'all' && selectedSource === 'built-in' ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
Built-in
<span className="text-[12px] font-normal opacity-70">{sourceStats.builtIn}</span>
{t('filter.builtIn', { count: sourceStats.builtIn })}
</button>
<button
onClick={() => setActiveTab('marketplace')}
className={cn("font-medium transition-colors flex items-center gap-1.5", activeTab === 'marketplace' ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
>
Marketplace
<span className="text-[12px] font-normal opacity-70">{sourceStats.marketplace}</span>
{t('filter.marketplace', { count: sourceStats.marketplace })}
</button>
</div>
</div>
@@ -640,7 +637,7 @@ export function Skills() {
onClick={() => bulkToggleVisible(true)}
className="h-8 text-[13px] font-medium rounded-md px-3 border-black/10 dark:border-white/10 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 shadow-none"
>
Enable All
{t('actions.enableVisible')}
</Button>
<Button
variant="outline"
@@ -648,7 +645,7 @@ export function Skills() {
onClick={() => bulkToggleVisible(false)}
className="h-8 text-[13px] font-medium rounded-md px-3 border-black/10 dark:border-white/10 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 shadow-none"
>
Disable All
{t('actions.disableVisible')}
</Button>
</>
)}