feat: v0.13.0 — add i18n support with Chinese/English translations
Implements a zero-dependency i18n framework with TypeScript type safety. Thanks to @gy212 for the original i18n infrastructure design (PR #89) and comprehensive translation files (PR #90-96) that inspired this implementation. ## New files (5) - src/i18n/en.ts: English translations (source of truth, ~270 keys), exports TranslationKey type for compile-time key completeness - src/i18n/zh.ts: Chinese translations, typed as Record<TranslationKey, string> - src/i18n/index.ts: translate() function with {param} interpolation, Locale type, SUPPORTED_LOCALES array - src/components/layout/I18nProvider.tsx: React Context provider, loads/persists locale preference via /api/settings/app - src/hooks/useTranslation.ts: useTranslation() hook returning { locale, setLocale, t } ## Infrastructure wiring - src/app/layout.tsx: I18nProvider wrapped inside ThemeProvider - src/app/api/settings/app/route.ts: added "locale" to ALLOWED_KEYS for persistence ## Language picker - src/components/settings/GeneralSection.tsx: added language selection dropdown (Select component) in Settings > General, using SUPPORTED_LOCALES and setLocale() ## String replacements (33 component files) All hardcoded user-facing strings replaced with t() calls: High priority (chat): - ChatListPanel.tsx: session list, search, empty state, relative time - NavRail.tsx: nav labels, theme toggle, auto-approve indicator - MessageInput.tsx: command descriptions, mode labels, attach tooltip - MessageList.tsx: empty state, load earlier, loading indicator - StreamingMessage.tsx: thinking, permission actions - chat/[id]/page.tsx: session title fallback Settings: - GeneralSection.tsx: update card, auto-approve section, warning dialog - SettingsLayout.tsx: title, description, sidebar labels - CliSettingsSection.tsx: form/json tabs, buttons, dynamic field labels (skipDangerousModePermissionPrompt etc.), Enabled/Disabled states - ProviderForm.tsx: dialog title, form labels, buttons - ProviderManager.tsx: status badges, delete dialog, section headers Extensions: - extensions/page.tsx: title, tab labels - SkillsManager.tsx: empty state, loading, search - SkillEditor.tsx: toolbar buttons, placeholder - SkillListItem.tsx: delete confirmation - CreateSkillDialog.tsx: form labels, scope, templates, validation Plugins: - McpManager.tsx: header, tabs, loading state - McpServerList.tsx: empty state, badges, field labels - McpServerEditor.tsx: dialog title, form labels, buttons - ConfigEditor.tsx: save/format buttons Layout: - RightPanel.tsx: panel header, open/close tooltips - FileTree.tsx: search placeholder, empty states - FilePreview.tsx: back button, line count, copy path - DocPreview.tsx: iframe title - ConnectionStatus.tsx: install prompts, version display - InstallWizard.tsx: phase descriptions, button labels - ImportSessionDialog.tsx: search, relative time, import buttons - FolderPicker.tsx: dialog title, loading, buttons - TaskList.tsx: filter tabs, input placeholder, empty states
This commit is contained in:
1 parent
85940dfde3
commit
c7c3ba5302
38 files changed
+980
-217
No files matched your search
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface ConfigEditorProps {
|
||||
value: string;
|
||||
@@ -12,6 +13,7 @@ interface ConfigEditorProps {
|
||||
}
|
||||
|
||||
export function ConfigEditor({ value, onSave, label }: ConfigEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [text, setText] = useState(value);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -52,7 +54,7 @@ export function ConfigEditor({ value, onSave, label }: ConfigEditorProps) {
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleFormat}>
|
||||
Format
|
||||
|
||||
@@ -8,9 +8,11 @@ import { PlusSignIcon, ListViewIcon, CodeIcon, Loading02Icon } from "@hugeicons/
|
||||
import { McpServerList } from "@/components/plugins/McpServerList";
|
||||
import { McpServerEditor } from "@/components/plugins/McpServerEditor";
|
||||
import { ConfigEditor } from "@/components/plugins/ConfigEditor";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { MCPServer } from "@/types";
|
||||
|
||||
export function McpManager() {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<Record<string, MCPServer>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
@@ -140,7 +142,7 @@ export function McpManager() {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold">MCP Servers</h3>
|
||||
<h3 className="text-lg font-semibold">{t('extensions.mcpServers')}</h3>
|
||||
{serverCount > 0 && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({serverCount})
|
||||
@@ -153,7 +155,7 @@ export function McpManager() {
|
||||
</div>
|
||||
<Button size="sm" className="gap-1" onClick={handleAdd}>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
Add Server
|
||||
{t('mcp.addServer')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -167,11 +169,11 @@ export function McpManager() {
|
||||
<TabsList>
|
||||
<TabsTrigger value="list" className="gap-1.5">
|
||||
<HugeiconsIcon icon={ListViewIcon} className="h-3.5 w-3.5" />
|
||||
Servers
|
||||
{t('mcp.listTab')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="json" className="gap-1.5">
|
||||
<HugeiconsIcon icon={CodeIcon} className="h-3.5 w-3.5" />
|
||||
JSON Config
|
||||
{t('mcp.jsonTab')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -179,7 +181,7 @@ export function McpManager() {
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-muted-foreground">
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
|
||||
<p className="text-sm">Loading MCP servers...</p>
|
||||
<p className="text-sm">{t('mcp.loadingServers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<McpServerList
|
||||
@@ -194,7 +196,7 @@ export function McpManager() {
|
||||
<ConfigEditor
|
||||
value={JSON.stringify(servers, null, 2)}
|
||||
onSave={handleJsonSave}
|
||||
label="MCP Server Configuration"
|
||||
label={t('mcp.serverConfig')}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ServerStack01Icon, Wifi01Icon, GlobeIcon, CodeIcon } from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { MCPServer } from '@/types';
|
||||
|
||||
type ServerType = 'stdio' | 'sse' | 'http';
|
||||
@@ -35,6 +36,7 @@ export function McpServerEditor({
|
||||
onSave,
|
||||
}: McpServerEditorProps) {
|
||||
const isEditing = !!initialName;
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(initialName || '');
|
||||
const [serverType, setServerType] = useState<ServerType>(
|
||||
initialServer?.type || 'stdio'
|
||||
@@ -177,13 +179,13 @@ export function McpServerEditor({
|
||||
<DialogContent className="sm:max-w-[550px] max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing ? `Edit MCP Server: ${initialName}` : 'Add MCP Server'}
|
||||
{isEditing ? `${t('mcp.editServer')}: ${initialName}` : t('mcp.addServer')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-name">Server Name</Label>
|
||||
<Label htmlFor="server-name">{t('mcp.serverName')}</Label>
|
||||
<Input
|
||||
id="server-name"
|
||||
value={name}
|
||||
@@ -206,7 +208,7 @@ export function McpServerEditor({
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
Form
|
||||
{t('mcp.formTab')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={jsonMode ? 'default' : 'outline'}
|
||||
@@ -237,7 +239,7 @@ export function McpServerEditor({
|
||||
}}
|
||||
>
|
||||
<HugeiconsIcon icon={CodeIcon} className="h-3.5 w-3.5" />
|
||||
JSON
|
||||
{t('mcp.jsonEditTab')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -257,7 +259,7 @@ export function McpServerEditor({
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Server Type</Label>
|
||||
<Label>{t('mcp.serverType')}</Label>
|
||||
<Tabs
|
||||
value={serverType}
|
||||
onValueChange={(v) => {
|
||||
@@ -285,7 +287,7 @@ export function McpServerEditor({
|
||||
{serverType === 'stdio' ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-command">Command</Label>
|
||||
<Label htmlFor="server-command">{t('mcp.command')}</Label>
|
||||
<Input
|
||||
id="server-command"
|
||||
value={command}
|
||||
@@ -299,7 +301,7 @@ export function McpServerEditor({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-args">Arguments (one per line)</Label>
|
||||
<Label htmlFor="server-args">{t('mcp.argsLabel')}</Label>
|
||||
<Textarea
|
||||
id="server-args"
|
||||
value={args}
|
||||
@@ -312,7 +314,7 @@ export function McpServerEditor({
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-url">URL</Label>
|
||||
<Label htmlFor="server-url">{t('mcp.url')}</Label>
|
||||
<Input
|
||||
id="server-url"
|
||||
value={url}
|
||||
@@ -330,7 +332,7 @@ export function McpServerEditor({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-headers">Headers (JSON)</Label>
|
||||
<Label htmlFor="server-headers">{t('mcp.headers')}</Label>
|
||||
<Textarea
|
||||
id="server-headers"
|
||||
value={headersText}
|
||||
@@ -346,7 +348,7 @@ export function McpServerEditor({
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-env">Environment Variables (JSON)</Label>
|
||||
<Label htmlFor="server-env">{t('mcp.envVars')}</Label>
|
||||
<Textarea
|
||||
id="server-env"
|
||||
value={envText}
|
||||
@@ -366,10 +368,10 @@ export function McpServerEditor({
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{isEditing ? 'Save Changes' : 'Add Server'}
|
||||
{isEditing ? t('mcp.saveChanges') : t('mcp.addServer')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { IconSvgElement } from "@hugeicons/react";
|
||||
import { Delete02Icon, PencilIcon, ServerStack01Icon, Wifi01Icon, GlobeIcon } from "@hugeicons/core-free-icons";
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import type { MCPServer } from '@/types';
|
||||
|
||||
interface McpServerListProps {
|
||||
@@ -27,15 +28,16 @@ function getServerTypeInfo(server: MCPServer) {
|
||||
}
|
||||
|
||||
export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps) {
|
||||
const { t } = useTranslation();
|
||||
const entries = Object.entries(servers);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<HugeiconsIcon icon={ServerStack01Icon} className="h-10 w-10 mb-3 opacity-50" />
|
||||
<p className="text-sm">No MCP servers configured</p>
|
||||
<p className="text-sm">{t('mcp.noServers')}</p>
|
||||
<p className="text-xs mt-1">
|
||||
Add an MCP server to extend Claude's capabilities
|
||||
{t('mcp.noServersDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -56,7 +58,7 @@ export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps)
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
Configured
|
||||
{t('provider.configured')}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs mt-1 font-mono">
|
||||
@@ -89,7 +91,7 @@ export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps)
|
||||
<CardContent className="pt-0">
|
||||
{server.args && server.args.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<p className="text-xs text-muted-foreground mb-1">Arguments:</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">{t('mcp.arguments')}</p>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{server.args.map((arg, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs font-mono">
|
||||
@@ -101,7 +103,7 @@ export function McpServerList({ servers, onEdit, onDelete }: McpServerListProps)
|
||||
)}
|
||||
{server.env && Object.keys(server.env).length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Environment:</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">{t('mcp.environment')}</p>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{Object.keys(server.env).map((key) => (
|
||||
<Badge key={key} variant="outline" className="text-xs font-mono">
|
||||
|
||||
Reference in new issue
Block a user