diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4778b77..48bf923 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,10 +19,13 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "dagre": "^0.8.5", + "i18next": "^25.9.0", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-grid-layout": "^2.2.2", + "react-i18next": "^16.5.8", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", "react-syntax-highlighter": "^16.1.1", @@ -6169,6 +6172,15 @@ "node": ">=16.9.0" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -6231,6 +6243,46 @@ "node": ">=18.18.0" } }, + "node_modules/i18next": { + "version": "25.9.0", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.9.0.tgz", + "integrity": "sha512-mJ4rVRNWOTkqh5xnaGR6iMFT5vEw3Y2MTJhcjinR/7u8cRv6dAfC0ofuePh5fVPxoh395p6JdrJTStCcNW66gg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -8932,6 +8984,33 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-i18next": { + "version": "16.5.8", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.5.8.tgz", + "integrity": "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", @@ -11192,6 +11271,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index a6c3057..36b219e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,10 +21,13 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "dagre": "^0.8.5", + "i18next": "^25.9.0", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-grid-layout": "^2.2.2", + "react-i18next": "^16.5.8", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", "react-syntax-highlighter": "^16.1.1", diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 3ec4fde..ae76dc7 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -1,8 +1,6 @@ import { useState, useRef, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { User, Loader2, Sparkles, ArrowUp, ChevronDown, Paperclip, Check, X, Square, Plus, Database, Wand2, Search, Zap, LayoutGrid, CheckCircle2, Table, XCircle, Settings, ExternalLink } from "lucide-react"; +import { User, Loader2, ArrowUp, ChevronDown, Check, Square, Plus, Database, Wand2, Zap, CheckCircle2, Table, XCircle, Settings, ExternalLink } from "lucide-react"; import { api } from "@/lib/api"; import { type ChartSpec } from "@/store/visualizationStore"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -12,6 +10,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { useLocation } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { InlineVisualizationCard } from "./InlineVisualizationCard"; import { useProjectStore } from "@/store/projectStore"; import { SlashCommandMenu } from "./SlashCommandMenu"; @@ -99,6 +98,7 @@ interface SessionData { } export function ChatInterface() { + const { t } = useTranslation(); const [messagesBySession, setMessagesBySession] = useState>({}); const [input, setInput] = useState(""); const [selectedDataSource, setSelectedDataSource] = useState(""); @@ -214,7 +214,7 @@ export function ChatInterface() { // File upload state const [attachedFile, setAttachedFile] = useState(null); const [activeDataFile, setActiveDataFile] = useState(null); - const [isUploading, setIsUploading] = useState(false); + const [, setIsUploading] = useState(false); const fileInputRef = useRef(null); useEffect(() => { @@ -337,7 +337,7 @@ export function ChatInterface() { const currentModel = models.find(m => m.id === selectedModelId); - const chartIntentPattern = /(图表|可视化|画图|作图|柱状图|折线图|饼图|趋势|分布|chart|plot|visuali[sz]e)/i; + const chartIntentPattern = new RegExp(t('chartIntentPattern'), 'i'); const buildMessageViz = (payload: { sql?: string; @@ -419,7 +419,7 @@ export function ChatInterface() { {selectedDataSource ? (
- {`数据源:${selectedDataSourceName}`} + {`${t('dataSource')}:${selectedDataSourceName}`}
) : null} {selectedSkills.map((skill) => ( @@ -447,7 +447,7 @@ export function ChatInterface() {
{file.filename}
-
电子表格
+
{t('spreadsheet')}
)} @@ -893,7 +893,7 @@ export function ChatInterface() { ) : (
-

暂无可用技能

+

{t('noAvailableSkills')}

)} @@ -903,7 +903,7 @@ export function ChatInterface() { onClick={() => setSelectedSkillIds([])} className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1" > - 清除已选 ({selectedSkillIds.length}) + {t('clearSelectedWithCount', { count: selectedSkillIds.length })} )} @@ -918,7 +918,7 @@ export function ChatInterface() { value={input} onChange={handleInputChange} onKeyDown={handleInputKeyDown} - placeholder="有问题,尽管问" + placeholder={t('askAnything')} className="flex-1 bg-transparent border-none focus:ring-0 text-lg px-3 py-2 text-zinc-900 placeholder:text-zinc-300 outline-none" disabled={isLoading} /> @@ -984,7 +984,7 @@ export function ChatInterface() {
- 思考过程 + {t('thinkingProcess')}
{msg.reasoningContent}
@@ -993,13 +993,13 @@ export function ChatInterface() {
{msg.awaitingFirstToken ? : } - {msg.awaitingFirstToken ? "正在处理中" : "处理完成"} + {msg.awaitingFirstToken ? t('processing') : t('processCompleted')}
{msg.progressLogs.map((log, idx, arr) => { const isLast = idx === arr.length - 1; // 如果是正在处理的会话,且当前日志是最后一条,或者是明确包含“正在”的日志,则显示 loading - const isLoadingLog = (isLast && msg.awaitingFirstToken) || log.includes("正在"); + const isLoadingLog = (isLast && msg.awaitingFirstToken) || log.includes(t('processingIndicator')); return (
{isLoadingLog && msg.awaitingFirstToken ? ( @@ -1017,7 +1017,7 @@ export function ChatInterface() { {msg.awaitingFirstToken && !msg.content ? (
- 模型思考中,请稍候... + {t('modelThinking')}
) : ( <> @@ -1047,7 +1047,7 @@ export function ChatInterface() { className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-600 hover:bg-blue-100 hover:text-blue-700 rounded-lg text-sm font-medium transition-colors" > - 在新标签页中打开分析报告 + {t('openReportInNewTab')}
) : null} @@ -1095,7 +1095,7 @@ export function ChatInterface() {
- 数据源 + {t('dataSource')}
{availableDataSources.map((ds) => ( @@ -1126,7 +1126,7 @@ export function ChatInterface() { }} className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1" > - 清除已选 + {t('clearSelected')}
)} @@ -1170,7 +1170,7 @@ export function ChatInterface() { ) : (
-

暂无可用技能

+

{t('noAvailableSkills')}

)}
@@ -1180,7 +1180,7 @@ export function ChatInterface() { onClick={() => setSelectedSkillIds([])} className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1" > - 清除已选 ({selectedSkillIds.length}) + {t('clearSelectedWithCount', { count: selectedSkillIds.length })}
)} @@ -1195,7 +1195,7 @@ export function ChatInterface() { value={input} onChange={handleInputChange} onKeyDown={handleInputKeyDown} - placeholder="有问题,尽管问" + placeholder={t('askAnything')} className="flex-1 bg-transparent border-none focus:ring-0 text-lg px-3 py-2 text-zinc-900 placeholder:text-zinc-300 outline-none" disabled={isLoading} /> @@ -1229,7 +1229,7 @@ export function ChatInterface() {

- DataClaw 可能会出错。请核查重要信息。 + {t('dataClawDisclaimer')}

diff --git a/frontend/src/components/DataSourceForm.tsx b/frontend/src/components/DataSourceForm.tsx index 849bd8a..0089dad 100644 --- a/frontend/src/components/DataSourceForm.tsx +++ b/frontend/src/components/DataSourceForm.tsx @@ -2,6 +2,7 @@ import { useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Loader2, Check, AlertTriangle, Upload } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { api } from "@/lib/api"; export interface DataSourceConfig { @@ -19,6 +20,7 @@ interface DataSourceFormProps { } export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: DataSourceFormProps) { + const { t } = useTranslation(); const [name, setName] = useState(initialData?.name || ""); const [type, setType] = useState(initialData?.type || "postgres"); const [config, setConfig] = useState>(initialData?.config || {}); @@ -52,7 +54,7 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data } } catch (error) { console.error("Upload failed", error); - alert("上传失败"); + alert(t('uploadFailed')); } finally { setIsUploading(false); // Clear input value so same file can be selected again @@ -69,12 +71,12 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data const success = await onTest(type, config); setTestResult({ success, - message: success ? "连接成功" : "连接失败", + message: success ? t('connectionSuccess') : t('connectionFailed'), }); } catch (e: any) { setTestResult({ success: false, - message: e.message || "连接失败", + message: e.message || t('connectionFailed'), }); } finally { setIsTesting(false); @@ -141,7 +143,7 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data />
- 或者直接使用 Supabase 控制台提供的 Connection String (URI): + {t('orUseSupabaseConnectionString')}
@@ -206,7 +208,7 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data />
- 或者使用连接字符串 (覆盖上述设置): + {t('orUseConnectionString')}
@@ -273,7 +275,7 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data return (
- +
-

上传文件或输入服务器路径

+

{t('uploadFileOrEnterPath')}

); @@ -300,9 +302,9 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data return (
-

暂不支持该数据源类型

+

{t('unsupportedDataSourceType')}

- 该数据源连接器正在开发中。请尝试使用 PostgreSQL, ClickHouse 或文件上传。 + {t('dataSourceConnectorInDevelopment')}

); @@ -312,18 +314,18 @@ export function DataSourceForm({ initialData, onSubmit, onTest, onCancel }: Data return (
- + setName(e.target.value)} - placeholder="我的数据源" + placeholder={t('myDataSource')} required />
{!initialData?.type && (
- + setSessionFilter(e.target.value)} - placeholder="过滤会话名称" + placeholder={t('filterSessionName')} className="pl-9 h-9 border-zinc-200 bg-white" />
- 重命名会话 + {t('renameSession')}
setNewTitle(e.target.value)} - placeholder="输入新的会话标题" + placeholder={t('enterNewSessionTitle')} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') { @@ -527,8 +530,8 @@ function SidebarBody() { />
- - + +
@@ -543,7 +546,7 @@ function SidebarBody() {
- {user?.username || 'User'} + {user?.username || t('defaultUser')}
@@ -552,7 +555,7 @@ function SidebarBody() { onClick={() => navigate("/skills")} > - 技能中心 + {t('skillCenter')} @@ -572,7 +575,7 @@ function SidebarBody() { }} > - 项目管理 + {t('projectManagement')} {user?.is_admin && ( @@ -607,29 +610,40 @@ function SidebarBody() { }} > - 模型配置 + {t('modelConfig')} )}
+ +
)} diff --git a/frontend/src/components/SlashCommandMenu.tsx b/frontend/src/components/SlashCommandMenu.tsx index 9813327..0e42f4b 100644 --- a/frontend/src/components/SlashCommandMenu.tsx +++ b/frontend/src/components/SlashCommandMenu.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { cn } from "@/lib/utils"; interface Skill { @@ -17,6 +18,7 @@ interface SlashCommandMenuProps { } export function SlashCommandMenu({ isOpen, skills, selectedIndex, onSelect, onClose }: SlashCommandMenuProps) { + const { t } = useTranslation(); const menuRef = useRef(null); const selectedRef = useRef(null); @@ -60,7 +62,7 @@ export function SlashCommandMenu({ isOpen, skills, selectedIndex, onSelect, onCl )} > /{skill.name} - {skill.description || "无描述"} + {skill.description || t('noDescription')} ))} diff --git a/frontend/src/components/VisualizationPanel.tsx b/frontend/src/components/VisualizationPanel.tsx index 8cac54d..a0189f8 100644 --- a/frontend/src/components/VisualizationPanel.tsx +++ b/frontend/src/components/VisualizationPanel.tsx @@ -8,9 +8,11 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { useDashboardStore, type ChartConfig } from "@/store/dashboardStore"; import { useVisualizationStore } from "@/store/visualizationStore"; import { useProjectStore } from "@/store/projectStore"; +import { useTranslation } from "react-i18next"; import { VegaChart } from "./VegaChart"; export function VisualizationPanel() { + const { t } = useTranslation(); const [view, setView] = useState<'table' | 'chart'>('chart'); const [confirmOpen, setConfirmOpen] = useState(false); const [pendingChart, setPendingChart] = useState | null>(null); @@ -206,9 +208,9 @@ export function VisualizationPanel() { - 确认加入 Dashboard + {t('confirmAddToDashboard')} - 将当前图表添加到 Dashboard,是否继续? + {t('confirmAddChartToDashboardDesc')} @@ -219,9 +221,9 @@ export function VisualizationPanel() { setPendingChart(null); }} > - 取消 + {t('cancel')} - + diff --git a/frontend/src/i18n/config.ts b/frontend/src/i18n/config.ts new file mode 100644 index 0000000..da06801 --- /dev/null +++ b/frontend/src/i18n/config.ts @@ -0,0 +1,22 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import en from './locales/en.json'; +import zh from './locales/zh.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + zh: { translation: zh } + }, + fallbackLng: 'zh', + interpolation: { + escapeValue: false // React already escapes by default + } + }); + +export default i18n; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json new file mode 100644 index 0000000..986e869 --- /dev/null +++ b/frontend/src/i18n/locales/en.json @@ -0,0 +1,208 @@ +{ + "selectAllOrCancel": "Select All / Cancel", + "invertSelection": "Invert Selection", + "batchDelete": "Batch Delete", + "cancel": "Cancel", + "rename": "Rename", + "pin": "Pin", + "unpin": "Unpin", + "archive": "Archive", + "unarchive": "Unarchive", + "deleteSession": "Delete Session", + "confirmDeleteSession": "Are you sure you want to delete this session?", + "confirmBatchDeleteSessions": "Are you sure you want to delete the selected {{count}} sessions?", + "filterSessionName": "Filter session name", + "renameSession": "Rename Session", + "enterNewSessionTitle": "Enter new session title", + "save": "Save", + "lobsterDataQA": "DataClaw", + "skillCenter": "Skill Center", + "projectManagement": "Project Management", + "dataSourceManagement": "Data Sources", + "personalSettings": "Settings", + "modelConfig": "Model Config", + "userManagement": "Users", + "logout": "Logout", + "searchModel": "Search model...", + "modelNotFound": "Model not found", + "availableModels": "Available Models", + "dataSource": "Data Source", + "clearSelected": "Clear Selected", + "clearSelectedWithCount": "Clear Selected ({{count}})", + "noAvailableSkills": "No available skills", + "askAnything": "Ask anything...", + "dataClawDisclaimer": "DataClaw can make mistakes. Consider verifying important information.", + "processing": "Processing...", + "processCompleted": "Completed", + "thinkingProcess": "Thinking Process", + "modelThinking": "Model is thinking, please wait...", + "openReportInNewTab": "Open report in new tab", + "outputInterrupted": "Output interrupted", + "requestSubmittedRouting": "Request submitted, preparing to route...", + "routingInfo": "Routing: {{selected}} {{reason}}", + "sqlAnalysis": "SQL Analysis", + "generalConversation": "General Conversation", + "answerGenerationCompleted": "Answer generation completed", + "noReply": "No reply", + "chartGenerationCompleted": "Chart generation completed", + "dataQueryCompleted": "Data query completed", + "streamResponseFailed": "Stream response failed", + "streamResponseError": "Stream response error", + "userUploadedFile": "User uploaded file", + "fileContentSummary": "File content summary", + "none": "None", + "dataColumns": "Data columns", + "fileDownloadLink": "File download link", + "spreadsheet": "Spreadsheet", + "name": "Name", + "myDataSource": "My Data Source", + "type": "Type", + "uploadFileOrEnterPath": "Upload file or enter server path", + "unsupportedDataSourceType": "Unsupported data source type", + "dataSourceConnectorInDevelopment": "This data source connector is under development. Please try PostgreSQL, ClickHouse, or file upload.", + "testConnection": "Test Connection", + "uploadFailed": "Upload failed", + "connectionSuccess": "Connection successful", + "connectionFailed": "Connection failed", + "orUseSupabaseConnectionString": "Or use the Connection String (URI) provided by Supabase console:", + "orUseConnectionString": "Or use connection string (overrides above settings):", + "fileUpload": "File Upload", + "noDescription": "No description", + "confirmAddToDashboard": "Confirm Add to Dashboard", + "confirmAddChartToDashboardDesc": "Add the current chart to Dashboard, continue?", + "confirmAdd": "Confirm Add", + "visualizationResult": "Visualization Result", + "sqlQueryDescription": "The data query statement used to generate the current chart.", + "copied": "Copied", + "copy": "Copy", + "resultNotSuitableForChart": "This result is not suitable for chart display.", + "noStructuredDataToRender": "No structured data to render in the current result.", + "pageRenderFailed": "Page render failed", + "chinese": "Chinese", + "chartIntentPattern": "(图表|可视化|画图|作图|柱状图|折线图|饼图|趋势|分布|chart|plot|visuali[sz]e)", + "processingIndicator": "正在", + "confirmDeleteDataSource": "Are you sure you want to delete this data source?", + "saveFailed": "Save failed: ", + "editDataSource": "Edit Data Source", + "createNewDataSourceWithType": "Create {{type}} Data Source", + "editDataSourceWithType": "Edit {{type}} Data Source", + "backToList": "Back to List", + "dataSourceConfig": "Data Source Configuration", + "manageDataSourceConnections": "Manage data source connections for Q&A", + "newDataSource": "New Data Source", + "noDataSources": "No data sources", + "clickTopRightToAddFirstDataSource": "Click the button on the top right to add your first data source", + "newProject": "New Project", + "projectList": "Project List", + "manageProjectsDesc": "Manage your projects, different projects have independent data sources", + "loading": "Loading...", + "noProjectsCreateOne": "No projects, please create one first", + "description": "Description", + "createdAt": "Created At", + "actions": "Actions", + "manageDataSources": "Manage Data Sources", + "editProject": "Edit Project", + "deleteProject": "Delete Project", + "enterProjectName": "Enter project name", + "descriptionOptional": "Description (Optional)", + "enterProjectDescription": "Enter project description", + "creating": "Creating...", + "create": "Create", + "saving": "Saving...", + "confirmDeleteProject": "Are you sure you want to delete this project? All associated data sources will be deleted.", + "selectProjectToViewDashboard": "Please select a project to view the dashboard.", + "noChartsInCurrentProject": "No charts in the current project.", + "goToChatToAddCharts": "Go to the chat page and add visualization results!", + "currentTableNoData": "Current table has no data to display", + "currentTableMissingFields": "Current table data is missing fields to display", + "previewTableRows": "Preview first {{previewLimit}} rows / Total {{rowCount}} rows, {{colCount}} columns", + "totalTableRows": "Total {{rowCount}} rows, {{colCount}} columns", + "currentChartMissingFields": "Current chart data is missing fields to plot", + "passwordsDoNotMatch": "The two passwords entered do not match", + "personalSettingsSaved": "Personal settings saved successfully!", + "personalSettingsAndPasswordSaved": "Personal settings and password modified successfully!", + "failedToSaveSettings": "Failed to save settings", + "accountInfo": "Account Information", + "modifyLoginEmailAndPassword": "Modify your login email and password", + "username": "Username", + "usernameCannotBeModified": "Username cannot be modified", + "emailAddress": "Email Address", + "newPassword": "New Password", + "leaveBlankIfNotModifying": "Leave blank if not modifying", + "confirmNewPassword": "Confirm New Password", + "saveSettings": "Save Settings", + "confirmDeleteUser": "Are you sure you want to delete this user?", + "newUserMustHavePassword": "New users must have a password", + "anErrorOccurred": "An error occurred", + "addUser": "Add User", + "editUser": "Edit User", + "addNewUser": "Add New User", + "email": "Email", + "password": "Password", + "activeStatus": "Active Status", + "adminPrivileges": "Admin Privileges", + "id": "ID", + "status": "Status", + "role": "Role", + "noUserData": "No user data", + "normal": "Normal", + "disabled": "Disabled", + "admin": "Admin", + "regularUser": "Regular User", + "fillRequiredInfoFirst": "Please fill in required information first (Provider, Model ID)", + "extraConfigMustBeValidJson": "Extra config must be valid JSON", + "connectionTestSuccessful": "Connection test successful!", + "connectionTestFailed": "Connection test failed", + "fillRequiredFields": "Please fill in required fields", + "failedToSaveConfig": "Failed to save config", + "confirmDeleteModel": "Are you sure you want to delete this model?", + "noPermissionAdminOnly": "No permission to access this page, please log in with an admin account.", + "addModel": "Add Model", + "modelName": "Model Name", + "provider": "Provider", + "modelIdentifier": "Model Identifier", + "noModelData": "No model data", + "currentDefaultModel": "Current default model", + "clickToSetDefault": "Click to set as default", + "default": "Default", + "setDefault": "Set as Default", + "editModel": "Edit Model", + "providerRequired": "Provider *", + "egGpt4": "e.g., GPT-4", + "modelIdRequired": "Model ID *", + "egGpt4Turbo": "e.g., gpt-4-turbo", + "apiDomain": "API Domain", + "egApiDomain": "e.g., https://api.openai.com/v1", + "extraConfigJson": "Extra Config (JSON)", + "unknownError": "Unknown error", + "confirmDeleteSkill": "Are you sure you want to delete this skill?", + "selectProjectToManageSkills": "Please select a project at the top first to manage its skills", + "skillsRepository": "Skills Repository - {{project}}", + "manageAiSkillsDesc": "Manage AI skills and tools for this project, supports file uploads conforming to the agentskills.io standard", + "uploadSkill": "Upload Skill", + "source": "Source", + "installationTime": "Installation Time", + "noSkillsInProjectClickImport": "No skills in this project yet, click \"Upload Skill\" to start", + "viewOrEditSkill": "View/Edit Skill", + "addNewSkill": "Add New Skill", + "skillName": "Skill Name", + "selectType": "Select Type", + "selectStatus": "Select Status", + "brieflyDescribeSkillFunction": "Briefly describe the function of the skill...", + "content": "Content", + "pythonSqlApiContentPlaceholder": "Python code, SQL query template or API specification...", + "saveSkill": "Save Skill", + "safe": "Safe", + "lowRisk": "Low Risk", + "localImport": "Local Import", + "zhipuAi": "ZhipuAI", + "dashScope": "DashScope", + "volcengine": "Volcengine", + "tableRowColDesc": "TABLE · {{rowCount}} rows · {{colCount}} columns", + "projectName": "Project Name", + "dashboardMenu": "Dashboard", + "newThread": "New Thread", + "threads": "THREADS", + "archivedThreads": "ARCHIVED THREADS", + "defaultUser": "User" +} diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json new file mode 100644 index 0000000..acd221a --- /dev/null +++ b/frontend/src/i18n/locales/zh.json @@ -0,0 +1,208 @@ +{ + "selectAllOrCancel": "全选/取消全选", + "invertSelection": "反选", + "batchDelete": "批量删除", + "cancel": "取消", + "rename": "重命名", + "pin": "置顶", + "unpin": "取消置顶", + "archive": "归档", + "unarchive": "取消归档", + "deleteSession": "删除会话", + "confirmDeleteSession": "确定要删除这个会话吗?", + "confirmBatchDeleteSessions": "确定要删除选中的 {{count}} 个会话吗?", + "filterSessionName": "过滤会话名称", + "renameSession": "重命名会话", + "enterNewSessionTitle": "输入新的会话标题", + "save": "保存", + "lobsterDataQA": "龙虾问数", + "skillCenter": "技能中心", + "projectManagement": "项目管理", + "dataSourceManagement": "数据源管理", + "personalSettings": "个人设置", + "modelConfig": "模型配置", + "userManagement": "用户管理", + "logout": "退出登录", + "searchModel": "搜索模型...", + "modelNotFound": "未找到模型", + "availableModels": "可用模型", + "dataSource": "数据源", + "clearSelected": "清除已选", + "clearSelectedWithCount": "清除已选 ({{count}})", + "noAvailableSkills": "暂无可用技能", + "askAnything": "有问题,尽管问", + "dataClawDisclaimer": "DataClaw 可能会出错。请核查重要信息。", + "processing": "正在处理中", + "processCompleted": "处理完成", + "thinkingProcess": "思考过程", + "modelThinking": "模型思考中,请稍候...", + "openReportInNewTab": "在新标签页中打开分析报告", + "outputInterrupted": "已中断输出", + "requestSubmittedRouting": "请求已提交,准备路由...", + "routingInfo": "路由:{{selected}}{{reason}}", + "sqlAnalysis": "SQL 分析", + "generalConversation": "通用对话", + "answerGenerationCompleted": "回答生成完成", + "noReply": "暂无回复", + "chartGenerationCompleted": "图表生成完成", + "dataQueryCompleted": "数据查询完成", + "streamResponseFailed": "流式响应失败", + "streamResponseError": "流式响应错误", + "userUploadedFile": "用户上传了文件", + "fileContentSummary": "文件内容摘要", + "none": "无", + "dataColumns": "数据列", + "fileDownloadLink": "文件下载链接", + "spreadsheet": "电子表格", + "name": "名称", + "myDataSource": "我的数据源", + "type": "类型", + "uploadFileOrEnterPath": "上传文件或输入服务器路径", + "unsupportedDataSourceType": "暂不支持该数据源类型", + "dataSourceConnectorInDevelopment": "该数据源连接器正在开发中。请尝试使用 PostgreSQL, ClickHouse 或文件上传。", + "testConnection": "测试连接", + "uploadFailed": "上传失败", + "connectionSuccess": "连接成功", + "connectionFailed": "连接失败", + "orUseSupabaseConnectionString": "或者直接使用 Supabase 控制台提供的 Connection String (URI):", + "orUseConnectionString": "或者使用连接字符串 (覆盖上述设置):", + "fileUpload": "文件上传", + "noDescription": "无描述", + "confirmAddToDashboard": "确认加入 Dashboard", + "confirmAddChartToDashboardDesc": "将当前图表添加到 Dashboard,是否继续?", + "confirmAdd": "确认添加", + "visualizationResult": "可视化结果", + "sqlQueryDescription": "用于生成当前图表的数据查询语句。", + "copied": "已复制", + "copy": "复制", + "resultNotSuitableForChart": "本次结果不适合图表展示。", + "noStructuredDataToRender": "当前结果没有可渲染的结构化数据。", + "pageRenderFailed": "页面渲染失败", + "chinese": "中文", + "chartIntentPattern": "(图表|可视化|画图|作图|柱状图|折线图|饼图|趋势|分布|chart|plot|visuali[sz]e)", + "processingIndicator": "正在", + "confirmDeleteDataSource": "确定要删除这个数据源吗?", + "saveFailed": "保存失败: ", + "editDataSource": "编辑数据源", + "createNewDataSourceWithType": "新建 {{type}} 数据源", + "editDataSourceWithType": "编辑 {{type}} 数据源", + "backToList": "返回列表", + "dataSourceConfig": "数据源配置", + "manageDataSourceConnections": "管理可用于问答的数据源连接", + "newDataSource": "新建数据源", + "noDataSources": "暂无数据源", + "clickTopRightToAddFirstDataSource": "点击右上角按钮添加第一个数据源", + "newProject": "新建项目", + "projectList": "项目列表", + "manageProjectsDesc": "管理您的项目,不同项目拥有独立的数据源", + "loading": "加载中...", + "noProjectsCreateOne": "暂无项目,请先创建一个", + "description": "描述", + "createdAt": "创建时间", + "actions": "操作", + "manageDataSources": "管理数据源", + "editProject": "编辑项目", + "deleteProject": "删除项目", + "enterProjectName": "输入项目名称", + "descriptionOptional": "描述 (可选)", + "enterProjectDescription": "输入项目描述", + "creating": "创建中...", + "create": "创建", + "saving": "保存中...", + "confirmDeleteProject": "确定要删除这个项目吗?所有相关的数据源都将被删除。", + "selectProjectToViewDashboard": "请选择一个项目以查看仪表板。", + "noChartsInCurrentProject": "当前项目暂无图表。", + "goToChatToAddCharts": "前往对话页并添加可视化结果!", + "currentTableNoData": "当前表格没有可展示数据", + "currentTableMissingFields": "当前表格数据缺少可展示字段", + "previewTableRows": "预览前 {{previewLimit}} 行 / 共 {{rowCount}} 行,{{colCount}} 列", + "totalTableRows": "共 {{rowCount}} 行,{{colCount}} 列", + "currentChartMissingFields": "当前图表数据缺少可绘制字段", + "passwordsDoNotMatch": "两次输入的密码不一致", + "personalSettingsSaved": "个人设置保存成功!", + "personalSettingsAndPasswordSaved": "个人设置及密码修改成功!", + "failedToSaveSettings": "保存设置失败", + "accountInfo": "账号信息", + "modifyLoginEmailAndPassword": "修改您的登录邮箱和密码", + "username": "用户名", + "usernameCannotBeModified": "用户名不可修改", + "emailAddress": "邮箱地址", + "newPassword": "新密码", + "leaveBlankIfNotModifying": "如不修改请留空", + "confirmNewPassword": "确认新密码", + "saveSettings": "保存设置", + "confirmDeleteUser": "确认删除该用户吗?", + "newUserMustHavePassword": "新建用户必须填写密码", + "anErrorOccurred": "发生错误", + "addUser": "添加用户", + "editUser": "编辑用户", + "addNewUser": "添加新用户", + "email": "邮箱", + "password": "密码", + "activeStatus": "激活状态", + "adminPrivileges": "管理员权限", + "id": "ID", + "status": "状态", + "role": "角色", + "noUserData": "暂无用户数据", + "normal": "正常", + "disabled": "禁用", + "admin": "管理员", + "regularUser": "普通用户", + "fillRequiredInfoFirst": "请先填写必要信息(供应商、模型ID)", + "extraConfigMustBeValidJson": "额外配置必须是有效的JSON", + "connectionTestSuccessful": "连接测试成功!", + "connectionTestFailed": "连接测试失败", + "fillRequiredFields": "请填写必填项", + "failedToSaveConfig": "保存配置失败", + "confirmDeleteModel": "确认删除该模型吗?", + "noPermissionAdminOnly": "无权限访问此页面,请使用管理员账号登录。", + "addModel": "添加模型", + "modelName": "模型名称", + "provider": "供应商", + "modelIdentifier": "模型标识", + "noModelData": "暂无模型数据", + "currentDefaultModel": "当前默认模型", + "clickToSetDefault": "点击设为默认", + "default": "默认", + "setDefault": "设为默认", + "editModel": "编辑模型", + "providerRequired": "供应商 *", + "egGpt4": "如:GPT-4", + "modelIdRequired": "模型ID *", + "egGpt4Turbo": "如:gpt-4-turbo", + "apiDomain": "API 域名", + "egApiDomain": "如:https://api.openai.com/v1", + "extraConfigJson": "额外配置 (JSON)", + "unknownError": "未知错误", + "confirmDeleteSkill": "确定要删除这个技能吗?", + "selectProjectToManageSkills": "请先在顶部选择一个项目以管理其技能", + "skillsRepository": "Skills 仓库 - {{project}}", + "manageAiSkillsDesc": "管理该项目的 AI 技能和工具,支持符合 agentskills.io 标准的文件上传", + "uploadSkill": "上传 Skill", + "source": "来源", + "installationTime": "安装时间", + "noSkillsInProjectClickImport": "该项目尚无技能,点击“导入 Skill”开始", + "viewOrEditSkill": "查看/编辑技能", + "addNewSkill": "添加新技能", + "skillName": "技能名称", + "selectType": "选择类型", + "selectStatus": "选择状态", + "brieflyDescribeSkillFunction": "简要描述技能的功能...", + "content": "内容", + "pythonSqlApiContentPlaceholder": "Python 代码、SQL 查询模板或 API 规范...", + "saveSkill": "保存技能", + "safe": "安全", + "lowRisk": "低风险", + "localImport": "本地导入", + "zhipuAi": "ZhipuAI (智谱)", + "dashScope": "DashScope (通义千问)", + "volcengine": "Volcengine (火山引擎)", + "tableRowColDesc": "TABLE · {{rowCount}} 行 · {{colCount}} 列", + "projectName": "项目名称", + "dashboardMenu": "仪表盘", + "newThread": "新会话", + "threads": "会话", + "archivedThreads": "已归档会话", + "defaultUser": "用户" +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 20a562b..d75325e 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App' import { ErrorBoundary } from './components/ErrorBoundary' +import './i18n/config' createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 043c357..5b80c13 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,5 @@ import { useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { Responsive, WidthProvider } from 'react-grid-layout/legacy'; import { useDashboardStore } from '../store/dashboardStore'; import { useProjectStore } from '../store/projectStore'; @@ -43,6 +44,7 @@ function inferChartKeys(data: Record[]) { } export function Dashboard() { + const { t } = useTranslation(); const { charts, removeChart, updateLayout, loadCharts } = useDashboardStore(); const { currentProject } = useProjectStore(); @@ -79,7 +81,7 @@ export function Dashboard() { if (!currentProject) { return (
-

请选择一个项目以查看仪表板。

+

{t('selectProjectToViewDashboard')}

); } @@ -87,8 +89,8 @@ export function Dashboard() { if (charts.length === 0) { return (
-

当前项目暂无图表。

-

前往对话页并添加可视化结果!

+

{t('noChartsInCurrentProject')}

+

{t('goToChatToAddCharts')}

); } @@ -119,7 +121,7 @@ export function Dashboard() { {chart.title} {chart.type === "table" - ? `TABLE · ${rows.length} 行 · ${columns.length} 列` + ? t('tableRowColDesc', { rowCount: rows.length, colCount: columns.length }) : `${chart.type.toUpperCase()} Chart`} @@ -152,7 +154,7 @@ export function Dashboard() { return (
- {isTableTruncated ? `预览前 ${TABLE_PREVIEW_LIMIT} 行 / 共 ${rows.length} 行,${columns.length} 列` : `共 ${rows.length} 行,${columns.length} 列`} + {isTableTruncated ? t('previewTableRows', { previewLimit: TABLE_PREVIEW_LIMIT, rowCount: rows.length, colCount: columns.length }) : t('totalTableRows', { rowCount: rows.length, colCount: columns.length })}
diff --git a/frontend/src/pages/DataSources.tsx b/frontend/src/pages/DataSources.tsx index 3d130cb..94a6e64 100644 --- a/frontend/src/pages/DataSources.tsx +++ b/frontend/src/pages/DataSources.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; +import { useTranslation } from 'react-i18next'; import { api } from "@/lib/api"; import { DataSourceForm, type DataSourceConfig } from "@/components/DataSourceForm"; import { Button } from "@/components/ui/button"; -import { Plus, Database, Pencil, Trash2, Loader2, FolderOpen, Info, ChevronLeft, FileText, Search, Network } from "lucide-react"; +import { Plus, Database, Pencil, Trash2, Loader2, Info, ChevronLeft, FileText, Search, Network } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { useAuthStore } from "@/store/authStore"; import { useProjectStore } from "@/store/projectStore"; import { useNavigate } from "react-router-dom"; @@ -30,13 +30,13 @@ const SOURCE_TYPES = [ ]; export function DataSources() { + const { t } = useTranslation(); const [datasources, setDatasources] = useState([]); const [isLoading, setIsLoading] = useState(false); const [view, setView] = useState<"list" | "select-type">("list"); const [isOpen, setIsOpen] = useState(false); const [editingDs, setEditingDs] = useState(null); const [selectedType, setSelectedType] = useState(null); - const { user } = useAuthStore(); const { currentProject } = useProjectStore(); const navigate = useNavigate(); @@ -77,7 +77,7 @@ export function DataSources() { }; const handleDelete = async (id: number) => { - if (!window.confirm("确定要删除这个数据源吗?")) return; + if (!window.confirm(t('confirmDeleteDataSource'))) return; try { await api.delete(`/api/v1/datasources/${id}`); fetchDataSources(); @@ -98,7 +98,7 @@ export function DataSources() { fetchDataSources(); } catch (e) { console.error("Failed to save data source", e); - alert("保存失败: " + (e as any).message); + alert(t('saveFailed') + (e as any).message); } }; @@ -121,7 +121,7 @@ export function DataSources() { className="flex items-center text-zinc-500 hover:text-zinc-800 transition-colors mb-6 group" > - 返回列表 + {t('backToList')}

Connect an external data source

@@ -158,7 +158,7 @@ export function DataSources() { - {editingDs ? "编辑数据源" : `新建 ${SOURCE_TYPES.find(t => t.id === selectedType)?.name || ""} 数据源`} + {editingDs ? t('editDataSource') : t('createNewDataSourceWithType', { type: SOURCE_TYPES.find(t => t.id === selectedType)?.name || "" })}
@@ -179,12 +179,12 @@ export function DataSources() {
-

数据源配置

-

管理可用于问答的数据源连接

+

{t('dataSourceConfig')}

+

{t('manageDataSourceConnections')}

@@ -196,8 +196,8 @@ export function DataSources() { ) : datasources.length === 0 ? (
-

暂无数据源

-

点击右上角按钮添加第一个数据源

+

{t('noDataSources')}

+

{t('clickTopRightToAddFirstDataSource')}

) : (
@@ -256,7 +256,7 @@ export function DataSources() { - {editingDs ? `编辑 ${SOURCE_TYPES.find(t => t.id === editingDs.type)?.name || editingDs.type} 数据源` : `新建 ${SOURCE_TYPES.find(t => t.id === selectedType)?.name || ""} 数据源`} + {editingDs ? t('editDataSourceWithType', { type: SOURCE_TYPES.find(t => t.id === editingDs.type)?.name || editingDs.type }) : t('createNewDataSourceWithType', { type: SOURCE_TYPES.find(t => t.id === selectedType)?.name || "" })}
diff --git a/frontend/src/pages/ModelConfigs.tsx b/frontend/src/pages/ModelConfigs.tsx index a80fd7f..6b3fcdf 100644 --- a/frontend/src/pages/ModelConfigs.tsx +++ b/frontend/src/pages/ModelConfigs.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from 'react-i18next'; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; @@ -32,6 +33,7 @@ const defaultForm: Omit = { }; export function ModelConfigs() { + const { t } = useTranslation(); const { user } = useAuthStore(); const isAdmin = !!user?.is_admin; const [configs, setConfigs] = useState([]); @@ -99,7 +101,7 @@ export function ModelConfigs() { const handleTestConnection = async () => { if (!form.model || !form.provider) { - setError("请先填写必要信息(供应商、模型ID)"); + setError(t('fillRequiredInfoFirst')); return; } setIsTesting(true); @@ -111,7 +113,7 @@ export function ModelConfigs() { const parsed = JSON.parse(extraConfigText); if (parsed && typeof parsed === "object") extraHeaders = parsed; } catch (err) { - setError("额外配置必须是有效的JSON"); + setError(t('extraConfigMustBeValidJson')); setIsTesting(false); return; } @@ -126,9 +128,9 @@ export function ModelConfigs() { }; await api.post("/api/v1/llm/test", payload); - alert("连接测试成功!"); + alert(t('connectionTestSuccessful')); } catch (e: any) { - setError(e.message || "连接测试失败"); + setError(e.message || t('connectionTestFailed')); } finally { setIsTesting(false); } @@ -137,7 +139,7 @@ export function ModelConfigs() { const handleSave = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!form.model || !form.provider) { - setError("请填写必填项"); + setError(t('fillRequiredFields')); return; } setIsSaving(true); @@ -149,7 +151,7 @@ export function ModelConfigs() { const parsed = JSON.parse(extraConfigText); if (parsed && typeof parsed === "object") extraHeaders = parsed; } catch (err) { - setError("额外配置必须是有效的JSON"); + setError(t('extraConfigMustBeValidJson')); setIsSaving(false); return; } @@ -168,14 +170,14 @@ export function ModelConfigs() { setDialogOpen(false); await fetchConfigs(); } catch (e: any) { - setError(e.message || "保存配置失败"); + setError(e.message || t('failedToSaveConfig')); } finally { setIsSaving(false); } }; const handleDelete = async (id: string) => { - if (!window.confirm("确认删除该模型吗?")) return; + if (!window.confirm(t('confirmDeleteModel'))) return; try { await api.delete(`/api/v1/llm/${id}`); await fetchConfigs(); @@ -197,7 +199,7 @@ export function ModelConfigs() { if (!isAdmin) { return (
-
无权限访问此页面,请使用管理员账号登录。
+
{t('noPermissionAdminOnly')}
); } @@ -206,21 +208,17 @@ export function ModelConfigs() {
- - 模型配置 -
+ {t('modelConfig')}
- setKeyword(e.target.value)} placeholder="搜索模型..." className="w-[200px] pl-9 h-8 text-sm" /> + setKeyword(e.target.value)} placeholder={t('searchModel')} className="w-[200px] pl-9 h-8 text-sm" />
+ {t('addModel')}
@@ -234,19 +232,17 @@ export function ModelConfigs() {
- 模型名称 - 供应商 - 模型标识 - 状态 - 操作 + {t('modelName')} + {t('provider')} + {t('modelIdentifier')} + {t('status')} + {t('actions')} {filteredConfigs.length === 0 ? ( - - 暂无模型数据 - + {t('noModelData')} ) : ( filteredConfigs.map((item) => ( @@ -260,9 +256,9 @@ export function ModelConfigs() { handleSetDefault(item)} className={`inline-flex px-2 py-1 rounded-full text-xs font-medium cursor-pointer transition-colors ${item.is_active ? 'bg-emerald-100 text-emerald-700' : 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200'}`} - title={item.is_active ? "当前默认模型" : "点击设为默认"} + title={item.is_active ? t('currentDefaultModel') : t('clickToSetDefault')} > - {item.is_active ? '默认' : '设为默认'} + {item.is_active ? t('default') : t('setDefault')} @@ -296,18 +292,18 @@ export function ModelConfigs() { - {editingId ? "编辑模型" : "添加模型"} + {editingId ? t('editModel') : t('addModel')}
{error &&
{error}
}
- - setForm((p) => ({ ...p, name: e.target.value }))} placeholder="如:GPT-4" /> + + setForm((p) => ({ ...p, name: e.target.value }))} placeholder={t('egGpt4')} />
- + setForm((p) => ({ ...p, model: e.target.value }))} placeholder="如:gpt-4-turbo" required /> + + setForm((p) => ({ ...p, model: e.target.value }))} placeholder={t('egGpt4Turbo')} required />
- - setForm((p) => ({ ...p, api_base: e.target.value }))} placeholder="如:https://api.openai.com/v1" /> + + setForm((p) => ({ ...p, api_base: e.target.value }))} placeholder={t('egApiDomain')} />
@@ -353,7 +349,7 @@ export function ModelConfigs() { value={form.api_key || ""} onChange={(e) => setForm((p) => ({ ...p, api_key: e.target.value }))} className="pr-10" - placeholder="不修改请留空" + placeholder={t('leaveBlankIfNotModifying')} />