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

+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">