From c7c3ba5302ac3defa4f1bd329dba2298e9a01816 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Tue, 24 Feb 2026 16:50:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.13.0=20=E2=80=94=20add=20i18n=20supp?= =?UTF-8?q?ort=20with=20Chinese/English=20translations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 - 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 --- package-lock.json | 4 +- package.json | 2 +- src/app/api/settings/app/route.ts | 1 + src/app/chat/[id]/page.tsx | 6 +- src/app/extensions/page.tsx | 8 +- src/app/layout.tsx | 5 +- src/components/chat/FolderPicker.tsx | 12 +- src/components/chat/MessageInput.tsx | 31 +- src/components/chat/MessageList.tsx | 6 +- src/components/chat/StreamingMessage.tsx | 10 +- src/components/layout/ChatListPanel.tsx | 30 +- src/components/layout/ConnectionStatus.tsx | 12 +- src/components/layout/DocPreview.tsx | 4 +- src/components/layout/I18nProvider.tsx | 61 ++++ src/components/layout/ImportSessionDialog.tsx | 31 +- src/components/layout/InstallWizard.tsx | 20 +- src/components/layout/NavRail.tsx | 20 +- src/components/layout/RightPanel.tsx | 12 +- src/components/plugins/ConfigEditor.tsx | 4 +- src/components/plugins/McpManager.tsx | 14 +- src/components/plugins/McpServerEditor.tsx | 26 +- src/components/plugins/McpServerList.tsx | 12 +- src/components/project/FilePreview.tsx | 14 +- src/components/project/FileTree.tsx | 8 +- src/components/project/TaskList.tsx | 16 +- .../settings/CliSettingsSection.tsx | 53 ++-- src/components/settings/GeneralSection.tsx | 63 ++-- src/components/settings/ProviderForm.tsx | 24 +- src/components/settings/ProviderManager.tsx | 20 +- src/components/settings/SettingsLayout.tsx | 15 +- src/components/skills/CreateSkillDialog.tsx | 28 +- src/components/skills/SkillEditor.tsx | 14 +- src/components/skills/SkillListItem.tsx | 4 +- src/components/skills/SkillsManager.tsx | 18 +- src/hooks/useTranslation.ts | 8 + src/i18n/en.ts | 274 ++++++++++++++++++ src/i18n/index.ts | 37 +++ src/i18n/zh.ts | 270 +++++++++++++++++ 38 files changed, 980 insertions(+), 217 deletions(-) create mode 100644 src/components/layout/I18nProvider.tsx create mode 100644 src/hooks/useTranslation.ts create mode 100644 src/i18n/en.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/zh.ts diff --git a/package-lock.json b/package-lock.json index 59f3dbb..4ef35e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codepilot", - "version": "0.12.0", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codepilot", - "version": "0.12.0", + "version": "0.13.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.33", "@hugeicons/core-free-icons": "^3.1.1", diff --git a/package.json b/package.json index f766700..7689fb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codepilot", - "version": "0.12.0", + "version": "0.13.0", "private": true, "author": { "name": "op7418", diff --git a/src/app/api/settings/app/route.ts b/src/app/api/settings/app/route.ts index 5d89ac0..65aca03 100644 --- a/src/app/api/settings/app/route.ts +++ b/src/app/api/settings/app/route.ts @@ -10,6 +10,7 @@ const ALLOWED_KEYS = [ 'anthropic_auth_token', 'anthropic_base_url', 'dangerously_skip_permissions', + 'locale', ]; export async function GET() { diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 33f7e8b..474a8fa 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -9,6 +9,7 @@ import { Loading02Icon, PencilEdit01Icon } from "@hugeicons/core-free-icons"; import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { usePanel } from '@/hooks/usePanel'; +import { useTranslation } from '@/hooks/useTranslation'; interface ChatSessionPageProps { params: Promise<{ id: string }>; @@ -29,9 +30,10 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { const [editTitle, setEditTitle] = useState(''); const titleInputRef = useRef(null); const { setWorkingDirectory, setSessionId, setSessionTitle: setPanelSessionTitle, setPanelOpen } = usePanel(); + const { t } = useTranslation(); const handleStartEditTitle = useCallback(() => { - setEditTitle(sessionTitle || 'New Conversation'); + setEditTitle(sessionTitle || t('chat.newConversation')); setIsEditingTitle(true); }, [sessionTitle]); @@ -88,7 +90,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { } setSessionId(id); setPanelOpen(true); - const title = data.session.title || 'New Conversation'; + const title = data.session.title || t('chat.newConversation'); setSessionTitle(title); setPanelSessionTitle(title); setSessionModel(data.session.model || ''); diff --git a/src/app/extensions/page.tsx b/src/app/extensions/page.tsx index b92179b..7beac41 100644 --- a/src/app/extensions/page.tsx +++ b/src/app/extensions/page.tsx @@ -7,6 +7,7 @@ import { HugeiconsIcon } from "@hugeicons/react"; import { Loading02Icon } from "@hugeicons/core-free-icons"; import { SkillsManager } from "@/components/skills/SkillsManager"; import { McpManager } from "@/components/plugins/McpManager"; +import { useTranslation } from "@/hooks/useTranslation"; type ExtTab = "skills" | "mcp"; @@ -28,15 +29,16 @@ function ExtensionsPageInner() { const searchParams = useSearchParams(); const initialTab = (searchParams.get("tab") as ExtTab) || "skills"; const [tab, setTab] = useState(initialTab); + const { t } = useTranslation(); return (
-

Extensions

+

{t('extensions.title')}

setTab(v as ExtTab)}> - Skills - MCP Servers + {t('extensions.skills')} + {t('extensions.mcpServers')}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b06958a..391e8e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/layout/ThemeProvider"; +import { I18nProvider } from "@/components/layout/I18nProvider"; import { AppShell } from "@/components/layout/AppShell"; const geistSans = Geist({ @@ -30,7 +31,9 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > - {children} + + {children} + diff --git a/src/components/chat/FolderPicker.tsx b/src/components/chat/FolderPicker.tsx index ba1b569..c51e201 100644 --- a/src/components/chat/FolderPicker.tsx +++ b/src/components/chat/FolderPicker.tsx @@ -19,6 +19,7 @@ import { } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { useTranslation } from '@/hooks/useTranslation'; interface FolderEntry { name: string; @@ -40,6 +41,7 @@ interface FolderPickerProps { } export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: FolderPickerProps) { + const { t } = useTranslation(); const [currentDir, setCurrentDir] = useState(''); const [parentDir, setParentDir] = useState(null); const [directories, setDirectories] = useState([]); @@ -99,7 +101,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold - Select Project Folder + {t('folderPicker.title')} {/* Path input */} @@ -162,11 +164,11 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold {loading ? (
- Loading... + {t('folderPicker.loading')}
) : directories.length === 0 ? (
- No subdirectories + {t('folderPicker.noSubdirs')}
) : (
@@ -188,11 +190,11 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index 7084236..732c860 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -21,6 +21,8 @@ import { StopIcon, } from "@hugeicons/core-free-icons"; import { cn } from '@/lib/utils'; +import { useTranslation } from '@/hooks/useTranslation'; +import type { TranslationKey } from '@/i18n'; import { PromptInput, PromptInputTextarea, @@ -65,6 +67,7 @@ interface PopoverItem { label: string; value: string; description?: string; + descriptionKey?: TranslationKey; builtIn?: boolean; immediate?: boolean; installedSource?: "agents" | "claude"; @@ -91,15 +94,15 @@ const COMMAND_PROMPTS: Record = { }; const BUILT_IN_COMMANDS: PopoverItem[] = [ - { label: 'help', value: '/help', description: 'Show available commands and tips', builtIn: true, immediate: true, icon: HelpCircleIcon }, - { label: 'clear', value: '/clear', description: 'Clear conversation history', builtIn: true, immediate: true, icon: Delete02Icon }, - { label: 'cost', value: '/cost', description: 'Show token usage statistics', builtIn: true, immediate: true, icon: Coins01Icon }, - { label: 'compact', value: '/compact', description: 'Compress conversation context', builtIn: true, icon: FileZipIcon }, - { label: 'doctor', value: '/doctor', description: 'Diagnose project health', builtIn: true, icon: Stethoscope02Icon }, - { label: 'init', value: '/init', description: 'Initialize CLAUDE.md for project', builtIn: true, icon: FileEditIcon }, - { label: 'review', value: '/review', description: 'Review code quality', builtIn: true, icon: SearchList01Icon }, - { label: 'terminal-setup', value: '/terminal-setup', description: 'Configure terminal settings', builtIn: true, icon: CommandLineIcon }, - { label: 'memory', value: '/memory', description: 'Edit project memory file', builtIn: true, icon: BrainIcon }, + { label: 'help', value: '/help', description: 'Show available commands and tips', descriptionKey: 'messageInput.helpDesc', builtIn: true, immediate: true, icon: HelpCircleIcon }, + { label: 'clear', value: '/clear', description: 'Clear conversation history', descriptionKey: 'messageInput.clearDesc', builtIn: true, immediate: true, icon: Delete02Icon }, + { label: 'cost', value: '/cost', description: 'Show token usage statistics', descriptionKey: 'messageInput.costDesc', builtIn: true, immediate: true, icon: Coins01Icon }, + { label: 'compact', value: '/compact', description: 'Compress conversation context', descriptionKey: 'messageInput.compactDesc', builtIn: true, icon: FileZipIcon }, + { label: 'doctor', value: '/doctor', description: 'Diagnose project health', descriptionKey: 'messageInput.doctorDesc', builtIn: true, icon: Stethoscope02Icon }, + { label: 'init', value: '/init', description: 'Initialize CLAUDE.md for project', descriptionKey: 'messageInput.initDesc', builtIn: true, icon: FileEditIcon }, + { label: 'review', value: '/review', description: 'Review code quality', descriptionKey: 'messageInput.reviewDesc', builtIn: true, icon: SearchList01Icon }, + { label: 'terminal-setup', value: '/terminal-setup', description: 'Configure terminal settings', descriptionKey: 'messageInput.terminalSetupDesc', builtIn: true, icon: CommandLineIcon }, + { label: 'memory', value: '/memory', description: 'Edit project memory file', descriptionKey: 'messageInput.memoryDesc', builtIn: true, icon: BrainIcon }, ]; interface ModeOption { @@ -183,11 +186,12 @@ function FileAwareSubmitButton({ */ function AttachFileButton() { const attachments = usePromptInputAttachments(); + const { t } = useTranslation(); return ( attachments.openFileDialog()} - tooltip="Attach files" + tooltip={t('messageInput.attachFiles')} > @@ -331,6 +335,7 @@ export function MessageInput({ mode = 'code', onModeChange, }: MessageInputProps) { + const { t } = useTranslation(); const textareaRef = useRef(null); const popoverRef = useRef(null); const searchInputRef = useRef(null); @@ -752,9 +757,9 @@ export function MessageInput({ )} {item.label} - {item.description && ( + {(item.descriptionKey || item.description) && ( - {item.description} + {item.descriptionKey ? t(item.descriptionKey) : item.description} )} {!item.builtIn && item.installedSource && ( @@ -919,7 +924,7 @@ export function MessageInput({ )} onClick={() => onModeChange?.(opt.value)} > - {opt.label} + {opt.value === 'code' ? t('messageInput.modeCode') : opt.value === 'plan' ? t('messageInput.modePlan') : opt.label} ); })} diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx index 8868391..7dc444a 100644 --- a/src/components/chat/MessageList.tsx +++ b/src/components/chat/MessageList.tsx @@ -1,6 +1,7 @@ 'use client'; import { useRef, useEffect } from 'react'; +import { useTranslation } from '@/hooks/useTranslation'; import type { Message, PermissionRequestEvent } from '@/types'; import { Conversation, @@ -57,6 +58,7 @@ export function MessageList({ loadingMore, onLoadMore, }: MessageListProps) { + const { t } = useTranslation(); // Scroll anchor: preserve position when older messages are prepended const anchorIdRef = useRef(null); const prevMessageCountRef = useRef(messages.length); @@ -86,7 +88,7 @@ export function MessageList({
} />
@@ -103,7 +105,7 @@ export function MessageList({ disabled={loadingMore} className="text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" > - {loadingMore ? 'Loading...' : 'Load earlier messages'} + {loadingMore ? t('messageList.loading') : t('messageList.loadEarlier')}
)} diff --git a/src/components/chat/StreamingMessage.tsx b/src/components/chat/StreamingMessage.tsx index b64128f..60bf06a 100644 --- a/src/components/chat/StreamingMessage.tsx +++ b/src/components/chat/StreamingMessage.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; +import { useTranslation } from '@/hooks/useTranslation'; import { Message as AIMessage, MessageContent, @@ -295,6 +296,7 @@ export function StreamingMessage({ permissionResolved, onForceStop, }: StreamingMessageProps) { + const { t } = useTranslation(); const runningTools = toolUses.filter( (tool) => !toolResults.some((r) => r.tool_use_id === tool.id) ); @@ -430,18 +432,18 @@ export function StreamingMessage({ variant="default" onClick={() => onPermissionResponse?.('allow_session')} > - Allow for Session + {t('streaming.allowForSession')} )} -

Allowed

+

{t('streaming.allowed')}

-

Denied

+

{t('streaming.denied')}

)} @@ -454,7 +456,7 @@ export function StreamingMessage({ {/* Loading indicator when no content yet */} {isStreaming && !content && toolUses.length === 0 && !pendingPermission && (
- Thinking... + {t('streaming.thinking')}
)} diff --git a/src/components/layout/ChatListPanel.tsx b/src/components/layout/ChatListPanel.tsx index c5868d9..436bb9e 100644 --- a/src/components/layout/ChatListPanel.tsx +++ b/src/components/layout/ChatListPanel.tsx @@ -25,6 +25,7 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { usePanel } from "@/hooks/usePanel"; +import { useTranslation } from "@/hooks/useTranslation"; import { ConnectionStatus } from "./ConnectionStatus"; import { ImportSessionDialog } from "./ImportSessionDialog"; import { FolderPicker } from "@/components/chat/FolderPicker"; @@ -35,7 +36,7 @@ interface ChatListPanelProps { width?: number; } -function formatRelativeTime(dateStr: string): string { +function formatRelativeTime(dateStr: string, t: (key: import('@/i18n').TranslationKey, params?: Record) => string): string { const date = new Date(dateStr.includes("T") ? dateStr : dateStr + "Z"); const now = new Date(); const diffMs = now.getTime() - date.getTime(); @@ -43,10 +44,10 @@ function formatRelativeTime(dateStr: string): string { const diffHr = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHr / 24); - if (diffMin < 1) return "just now"; - if (diffMin < 60) return `${diffMin}m`; - if (diffHr < 24) return `${diffHr}h`; - if (diffDay < 7) return `${diffDay}d`; + if (diffMin < 1) return t('chatList.justNow'); + if (diffMin < 60) return t('chatList.minutesAgo', { n: diffMin }); + if (diffHr < 24) return t('chatList.hoursAgo', { n: diffHr }); + if (diffDay < 7) return t('chatList.daysAgo', { n: diffDay }); return date.toLocaleDateString(); } @@ -117,6 +118,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { const pathname = usePathname(); const router = useRouter(); const { streamingSessionId, pendingApprovalSessionId, workingDirectory } = usePanel(); + const { t } = useTranslation(); const [sessions, setSessions] = useState([]); const [hoveredSession, setHoveredSession] = useState(null); const [deletingSession, setDeletingSession] = useState(null); @@ -331,7 +333,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { onClick={handleNewChat} > - New Chat + {t('chatList.newConversation')} @@ -342,10 +344,10 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { onClick={() => setFolderPickerOpen(true)} > - Open project folder + {t('chatList.addProjectFolder')} - Open project folder + {t('chatList.addProjectFolder')}
@@ -357,7 +359,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { className="absolute left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" /> setSearchQuery(e.target.value)} className="h-8 pl-7 text-xs" @@ -374,7 +376,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { onClick={() => setImportDialogOpen(true)} > - Import CLI Session + {t('chatList.importFromCli')} @@ -383,7 +385,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
{filteredSessions.length === 0 ? (

- {searchQuery ? "No matching threads" : "No conversations yet"} + {searchQuery ? "No matching threads" : t('chatList.noSessions')}

) : ( projectGroups.map((group) => { @@ -539,7 +541,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { {badgeCfg.label} - {formatRelativeTime(session.updated_at)} + {formatRelativeTime(session.updated_at, t)}
@@ -560,12 +562,12 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { className="h-3 w-3" /> - Delete session + {t('chatList.delete')} - Delete + {t('chatList.delete')} )} diff --git a/src/components/layout/ConnectionStatus.tsx b/src/components/layout/ConnectionStatus.tsx index 45de2d0..68b8267 100644 --- a/src/components/layout/ConnectionStatus.tsx +++ b/src/components/layout/ConnectionStatus.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { useTranslation } from "@/hooks/useTranslation"; import { InstallWizard } from "@/components/layout/InstallWizard"; interface ClaudeStatus { @@ -23,6 +24,7 @@ const BACKED_OFF_INTERVAL = 60_000; // 60s after 3 consecutive stable results const STABLE_THRESHOLD = 3; export function ConnectionStatus() { + const { t } = useTranslation(); const [status, setStatus] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [wizardOpen, setWizardOpen] = useState(false); @@ -150,7 +152,7 @@ export function ConnectionStatus() { - {connected ? "Claude Code Connected" : "Claude Code Not Connected"} + {connected ? t('connection.installed') : t('connection.notInstalled')} {connected @@ -165,7 +167,7 @@ export function ConnectionStatus() {

Active

-

Version {status?.version}

+

{t('connection.version', { version: status?.version ?? '' })}

@@ -177,7 +179,7 @@ export function ConnectionStatus() {
-

1. Install Claude Code

+

1. {t('connection.installClaude')}

npm install -g @anthropic-ai/claude-code @@ -206,7 +208,7 @@ export function ConnectionStatus() { }} className="w-full" > - Install Claude Code Automatically + {t('connection.installAuto')}
)} @@ -218,7 +220,7 @@ export function ConnectionStatus() { variant="outline" onClick={handleManualRefresh} > - Refresh + {t('connection.refresh')}
diff --git a/src/components/layout/DocPreview.tsx b/src/components/layout/DocPreview.tsx index 8419ecd..d2962e4 100644 --- a/src/components/layout/DocPreview.tsx +++ b/src/components/layout/DocPreview.tsx @@ -14,6 +14,7 @@ import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { usePanel } from "@/hooks/usePanel"; +import { useTranslation } from "@/hooks/useTranslation"; import type { FilePreview as FilePreviewType } from "@/types"; const streamdownPlugins = { cjk, code, math, mermaid }; @@ -252,13 +253,14 @@ function RenderedView({ content: string; filePath: string; }) { + const { t } = useTranslation(); if (isHtml(filePath)) { return (