diff --git a/package-lock.json b/package-lock.json
index c07de6c..9d2fb4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,11 +12,13 @@
"autoprefixer": "^10.4.21",
"chart.js": "^4.5.0",
"chartjs-plugin-zoom": "^2.2.0",
+ "i18next": "^25.4.2",
"lucide-react": "^0.522.0",
"postcss": "^8.5.6",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0",
+ "react-i18next": "^15.7.2",
"tailwindcss": "^3.4.4"
},
"devDependencies": {
@@ -319,7 +321,6 @@
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -3471,6 +3472,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/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/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -3509,6 +3519,37 @@
"node": ">=16.17.0"
}
},
+ "node_modules/i18next": {
+ "version": "25.4.2",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz",
+ "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==",
+ "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.27.6"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -4802,6 +4843,32 @@
"react": "^19.1.0"
}
},
+ "node_modules/react-i18next": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.2.tgz",
+ "integrity": "sha512-xJxq7ibnhUlMvd82lNC4te1GxGUMoM1A05KKyqoqsBXVZtEvZg/fz/fnVzdlY/hhQ3SpP/79qCocZOtICGhd3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.27.6",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 25.4.1",
+ "react": ">= 16.8.0",
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -6762,6 +6829,15 @@
}
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/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/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/package.json b/package.json
index b8b0f28..693739c 100644
--- a/package.json
+++ b/package.json
@@ -17,11 +17,13 @@
"autoprefixer": "^10.4.21",
"chart.js": "^4.5.0",
"chartjs-plugin-zoom": "^2.2.0",
+ "i18next": "^25.4.2",
"lucide-react": "^0.522.0",
"postcss": "^8.5.6",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0",
+ "react-i18next": "^15.7.2",
"tailwindcss": "^3.4.4"
},
"devDependencies": {
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
new file mode 100644
index 0000000..1aa629c
--- /dev/null
+++ b/public/locales/en/translation.json
@@ -0,0 +1,119 @@
+{
+ "language": "Language",
+ "header.language": "Language",
+ "fileUpload.title": "📁 File Upload",
+ "fileUpload.drag": "🎯 Drag files here or click to select",
+ "fileUpload.support": "📄 Supports all text file formats",
+ "fileUpload.aria": "Select log files, supports all text formats",
+ "fileList.title": "📋 Loaded Files",
+ "fileList.empty": "📂 No files",
+ "fileList.loaded": "Loaded {{count}} files",
+ "fileList.enabled": "Enabled",
+ "fileList.disabled": "Disabled",
+ "fileList.config": "Configure file {{name}}",
+ "fileList.delete": "Remove file {{name}}",
+ "comparison.title": "⚖️ Compare Mode",
+ "comparison.select": "Select comparison mode",
+ "comparison.normal": "📊 Mean Error (normal)",
+ "comparison.normalDesc": "Mean error without absolute value",
+ "comparison.absolute": "📈 Mean Error (absolute)",
+ "comparison.absoluteDesc": "Mean of absolute differences",
+ "comparison.relativeNormal": "📉 Relative Error (normal)",
+ "comparison.relativeNormalDesc": "Relative error without absolute value",
+ "comparison.relative": "📊 Mean Relative Error (absolute)",
+ "comparison.relativeDesc": "Mean of absolute relative error",
+ "themeToggle.aria": "Toggle theme",
+ "chart.noData": "📊 No data",
+ "chart.uploadPrompt": "📁 Upload log files to begin",
+ "chart.selectPrompt": "🎯 Select charts to display",
+ "chart.diffStats": "Difference Statistics",
+ "chart": "chart",
+ "resetConfig": "Reset Config",
+ "useStepKeyword": "Use Step keyword",
+ "placeholder.step": "step:",
+ "resize.drag": "Drag to resize chart height",
+ "resize.adjust": "Adjust {{title}} chart height",
+ "exportPNG": "Export PNG",
+ "copyImage": "Copy image",
+ "exportCSV": "Export CSV",
+ "copyImageError": "Failed to copy image",
+ "language.en": "English",
+ "language.zh": "Chinese",
+ "showToolbar": "Show toolbar",
+ "hideToolbar": "Hide toolbar",
+ "intro": "📊 Analyze and visualize loss and gradient norm data from large model training logs",
+ "status.group": "Tool status and links",
+ "status.onlineAria": "Currently online version",
+ "status.online": "Online",
+ "github.aria": "Open GitHub repository (opens in new window)",
+ "display.options": "🎛️ Display Options",
+ "display.chart": "📊 Chart Display",
+ "display.chartDesc": "Automatically shows charts for all configured metrics after upload",
+ "display.baseline": "Baseline Settings",
+ "display.relativeBaseline": "Relative error baseline",
+ "display.relativeBaselineDesc": "Set baseline value for relative error comparison",
+ "display.absoluteBaseline": "Absolute error baseline",
+ "display.absoluteBaselineDesc": "Set baseline value for absolute error comparison",
+ "sidebar.controlPanel": "Control Panel",
+ "globalDrag.release": "🎯 Release files to upload",
+ "globalDrag.support": "Supports <0>all text formats0> files",
+ "globalDrag.tip": "Drag files anywhere to upload quickly",
+ "chart.area": "Chart display area",
+ "chart.actions": "Chart action buttons",
+ "chart.diffLabel": "{{title}} difference",
+ "comparison.panelTitle": "⚖️ {{key}} comparison ({{mode}})",
+ "comparison.meanNormal": "Mean error (normal): {{value}}",
+ "comparison.meanAbsolute": "Mean error (absolute): {{value}}",
+ "comparison.relativeError": "Relative error (normal): {{value}}",
+ "comparison.meanRelative": "Mean relative error (absolute): {{value}}",
+ "regex.preset": "Preset",
+ "regex.selectPreset": "Select preset",
+ "regex.mode": "Match Mode",
+ "regex.keyword": "Keyword",
+ "regex.keywordHint": "Supports fuzzy match, e.g., \"loss\" matches \"training_loss\"",
+ "regex.regex": "Regular Expression",
+ "regex.regexHint": "Use capture groups () to extract numbers",
+ "regex.dataParsing": "Data Parsing Config",
+ "regex.smartRecommend": "Smart recommend best config",
+ "regex.previewMatches": "Preview matches",
+ "regex.deleteConfig": "Remove config",
+ "regex.metricConfig": "{{title}} parsing config",
+ "regex.addMetric": "+ Add Metric",
+ "regex.xRange": "X-axis range",
+ "regex.reset": "Reset",
+ "regex.xRangeHint": "Hold <0>Shift0> and drag on the chart to select range, or input values directly.",
+ "regex.matchPreview": "Match Preview",
+ "regex.matchCount": "{{count}} matches",
+ "regex.lineNumber": "(line {{line}})",
+ "regex.featureDescAria": "Feature description",
+ "regex.featureHeading": "🎯 Enhanced parsing:",
+ "regex.featureKeywordTitle": "Keyword Match",
+ "regex.featureKeywordDesc": "Simply input a keyword to extract numbers (default)",
+ "regex.featureRegexTitle": "Regular Expression",
+ "regex.featureRegexDesc": "Advanced users can use complex patterns",
+ "regex.featureSmartTitle": "Smart Suggest",
+ "regex.featureSmartDesc": "One-click best parsing config",
+ "regex.mode.keyword": "Keyword Match",
+ "regex.mode.keywordDesc": "Enter keyword to find and extract numbers",
+ "regex.mode.regex": "Regular Expression",
+ "regex.mode.regexDesc": "Use regular expressions for advanced matching",
+ "configModal.configFile": "Configure file: {{name}}",
+ "configModal.close": "Close configuration dialog",
+ "configModal.parsingConfig": "Parsing Config",
+ "configModal.syncFromGlobalTitle": "Sync from global config",
+ "configModal.syncFromGlobal": "Sync global config",
+ "configModal.dataRange": "Data Range Config",
+ "configModal.rangeDesc": "Configure range of data points to display. Defaults to all data (first to last).",
+ "configModal.start": "Start position",
+ "configModal.startPlaceholder": "0 (defaults to first data point)",
+ "configModal.startHint": "Start index (0-based)",
+ "configModal.end": "End position",
+ "configModal.endPlaceholder": "Leave empty to show to end",
+ "configModal.endHint": "End index (exclusive)",
+ "configModal.examplesHeading": "Examples:",
+ "configModal.example1": "Start: 0, End: 100 → shows data points 1-100",
+ "configModal.example2": "Start: 50, End: empty → shows data points 51-end",
+ "configModal.example3": "Start: 0, End: empty → shows all data points (default)",
+ "configModal.cancel": "Cancel",
+ "configModal.save": "Save Config"
+}
diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json
new file mode 100644
index 0000000..8ae331d
--- /dev/null
+++ b/public/locales/zh/translation.json
@@ -0,0 +1,119 @@
+{
+ "language": "语言",
+ "header.language": "语言",
+ "fileUpload.title": "📁 文件上传",
+ "fileUpload.drag": "🎯 拖拽文件到此处或点击选择文件",
+ "fileUpload.support": "📄 支持所有文本格式文件",
+ "fileUpload.aria": "选择日志文件,支持所有文本格式",
+ "fileList.title": "📋 已加载文件",
+ "fileList.empty": "📂 暂无文件",
+ "fileList.loaded": "已加载 {{count}} 个文件",
+ "fileList.enabled": "已启用",
+ "fileList.disabled": "已禁用",
+ "fileList.config": "配置文件 {{name}}",
+ "fileList.delete": "删除文件 {{name}}",
+ "comparison.title": "⚖️ 对比模式",
+ "comparison.select": "选择数据对比模式",
+ "comparison.normal": "📊 平均误差 (normal)",
+ "comparison.normalDesc": "未取绝对值的平均误差",
+ "comparison.absolute": "📈 平均误差 (absolute)",
+ "comparison.absoluteDesc": "绝对值差值的平均",
+ "comparison.relativeNormal": "📉 相对误差 (normal)",
+ "comparison.relativeNormalDesc": "不取绝对值的相对误差",
+ "comparison.relative": "📊 平均相对误差 (absolute)",
+ "comparison.relativeDesc": "绝对相对误差的平均",
+ "themeToggle.aria": "切换主题",
+ "chart.noData": "📊 暂无数据",
+ "chart.uploadPrompt": "📁 请上传日志文件开始分析",
+ "chart.selectPrompt": "🎯 请选择要显示的图表",
+ "chart.diffStats": "差值统计",
+ "chart": "图表",
+ "resetConfig": "重置配置",
+ "useStepKeyword": "使用 Step 关键字",
+ "placeholder.step": "step:",
+ "resize.drag": "拖拽调整图表高度",
+ "resize.adjust": "调整 {{title}} 图表高度",
+ "exportPNG": "导出 PNG",
+ "copyImage": "复制图片",
+ "exportCSV": "导出 CSV",
+ "copyImageError": "复制图片失败",
+ "language.en": "English",
+ "language.zh": "中文",
+ "showToolbar": "显示工具栏",
+ "hideToolbar": "隐藏工具栏",
+ "intro": "📊 分析和可视化大模型训练日志中的损失函数和梯度范数数据",
+ "status.group": "工具状态和链接",
+ "status.onlineAria": "当前为在线版本",
+ "status.online": "在线使用",
+ "github.aria": "访问 GitHub 仓库(在新窗口中打开)",
+ "display.options": "🎛️ 显示选项",
+ "display.chart": "📊 图表显示",
+ "display.chartDesc": "上传文件后自动展示所有已配置的指标图表",
+ "display.baseline": "基准线设置",
+ "display.relativeBaseline": "相对误差 Baseline",
+ "display.relativeBaselineDesc": "设置相对误差对比的基准线数值",
+ "display.absoluteBaseline": "绝对误差 Baseline",
+ "display.absoluteBaselineDesc": "设置绝对误差对比的基准线数值",
+ "sidebar.controlPanel": "控制面板",
+ "globalDrag.release": "🎯 释放文件以上传",
+ "globalDrag.support": "支持 <0>所有文本格式0> 文件",
+ "globalDrag.tip": "拖拽到页面任意位置即可快速上传文件",
+ "chart.area": "图表显示区域",
+ "chart.actions": "图表操作按钮",
+ "chart.diffLabel": "{{title}} 差值",
+ "comparison.panelTitle": "⚖️ {{key}} 对比分析 ({{mode}})",
+ "comparison.meanNormal": "平均误差 (normal): {{value}}",
+ "comparison.meanAbsolute": "平均误差 (absolute): {{value}}",
+ "comparison.relativeError": "相对误差 (normal): {{value}}",
+ "comparison.meanRelative": "平均相对误差 (absolute): {{value}}",
+ "regex.preset": "预设",
+ "regex.selectPreset": "选择预设",
+ "regex.mode": "匹配模式",
+ "regex.keyword": "关键词",
+ "regex.keywordHint": "支持模糊匹配,如 \"loss\" 可匹配 \"training_loss\"",
+ "regex.regex": "正则表达式",
+ "regex.regexHint": "使用捕获组 () 来提取数值",
+ "regex.dataParsing": "数据解析配置",
+ "regex.smartRecommend": "智能推荐最佳配置",
+ "regex.previewMatches": "预览匹配结果",
+ "regex.deleteConfig": "删除配置",
+ "regex.metricConfig": "{{title}} 解析配置",
+ "regex.addMetric": "+ 添加指标",
+ "regex.xRange": "X轴范围",
+ "regex.reset": "复位",
+ "regex.xRangeHint": "在图表上按住 <0>Shift0> 键并拖动鼠标可选择范围,或直接输入数值。",
+ "regex.matchPreview": "匹配预览",
+ "regex.matchCount": "({{count}} 个匹配)",
+ "regex.lineNumber": "(第{{line}}行)",
+ "regex.featureDescAria": "功能说明",
+ "regex.featureHeading": "🎯 增强解析功能:",
+ "regex.featureKeywordTitle": "关键词匹配",
+ "regex.featureKeywordDesc": "简单输入关键词,自动提取数值(默认模式)",
+ "regex.featureRegexTitle": "正则表达式",
+ "regex.featureRegexDesc": "高级用户可使用复杂模式",
+ "regex.featureSmartTitle": "智能推荐",
+ "regex.featureSmartDesc": "一键获得最佳解析配置",
+ "regex.mode.keyword": "关键词匹配",
+ "regex.mode.keywordDesc": "输入关键词,自动查找并提取数值",
+ "regex.mode.regex": "正则表达式",
+ "regex.mode.regexDesc": "使用正则表达式进行高级匹配",
+ "configModal.configFile": "配置文件: {{name}}",
+ "configModal.close": "关闭配置对话框",
+ "configModal.parsingConfig": "解析配置",
+ "configModal.syncFromGlobalTitle": "从全局配置同步",
+ "configModal.syncFromGlobal": "同步全局配置",
+ "configModal.dataRange": "数据范围配置",
+ "configModal.rangeDesc": "配置要显示的数据点范围。默认显示全部数据(从第一个到最后一个数据点)。",
+ "configModal.start": "起始位置",
+ "configModal.startPlaceholder": "0(默认从第一个数据点)",
+ "configModal.startHint": "第几个数据点开始(从0开始计数)",
+ "configModal.end": "结束位置",
+ "configModal.endPlaceholder": "留空显示到最后",
+ "configModal.endHint": "第几个数据点结束(不包含该点)",
+ "configModal.examplesHeading": "示例说明:",
+ "configModal.example1": "起始: 0, 结束: 100 → 显示第1到第100个数据点",
+ "configModal.example2": "起始: 50, 结束: 留空 → 显示第51个数据点到结尾",
+ "configModal.example3": "起始: 0, 结束: 留空 → 显示全部数据点(默认)",
+ "configModal.cancel": "取消",
+ "configModal.save": "保存配置"
+}
diff --git a/src/App.jsx b/src/App.jsx
index f15f88c..961d291 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,4 +1,5 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
+import { useTranslation, Trans } from 'react-i18next';
import { FileUpload } from './components/FileUpload';
import { RegexControls } from './components/RegexControls';
import { FileList } from './components/FileList';
@@ -6,10 +7,11 @@ import ChartContainer from './components/ChartContainer';
import { ComparisonControls } from './components/ComparisonControls';
import { FileConfigModal } from './components/FileConfigModal';
import { ThemeToggle } from './components/ThemeToggle';
+import { Header } from './components/Header';
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import { mergeFilesWithReplacement } from './utils/mergeFiles.js';
-// 默认全局解析配置
+// Default global parsing configuration
export const DEFAULT_GLOBAL_PARSING_CONFIG = {
metrics: [
{
@@ -30,12 +32,13 @@ export const DEFAULT_GLOBAL_PARSING_CONFIG = {
};
function App() {
- const [uploadedFiles, setUploadedFiles] = useState(() => {
+ const { t } = useTranslation();
+ const [uploadedFiles, setUploadedFiles] = useState(() => {
const stored = localStorage.getItem('uploadedFiles');
return stored ? JSON.parse(stored) : [];
});
- // 全局解析配置状态
+ // Global parsing configuration state
const [globalParsingConfig, setGlobalParsingConfig] = useState(() => {
const stored = localStorage.getItem('globalParsingConfig');
return stored ? JSON.parse(stored) : JSON.parse(JSON.stringify(DEFAULT_GLOBAL_PARSING_CONFIG));
@@ -53,7 +56,7 @@ function App() {
const [sidebarVisible, setSidebarVisible] = useState(true);
const savingDisabledRef = useRef(false);
- // 持久化配置到 localStorage
+ // Persist configuration to localStorage
useEffect(() => {
if (savingDisabledRef.current) return;
localStorage.setItem('globalParsingConfig', JSON.stringify(globalParsingConfig));
@@ -80,12 +83,12 @@ function App() {
...file,
enabled: true,
config: {
- // 使用全局解析配置作为默认值
+ // Use global parsing config as default values
metrics: globalParsingConfig.metrics.map(m => ({ ...m })),
dataRange: {
- start: 0, // 默认从第一个数据点开始
- end: undefined, // 默认到最后一个数据点
- useRange: false // 保留这个字段用于向后兼容,但默认不启用
+ start: 0, // start from first data point by default
+ end: undefined, // default to last data point
+ useRange: false // keep for backward compatibility but disabled by default
},
useStepKeyword: globalParsingConfig.useStepKeyword,
stepKeyword: globalParsingConfig.stepKeyword
@@ -94,7 +97,7 @@ function App() {
setUploadedFiles(prev => mergeFilesWithReplacement(prev, filesWithDefaults));
}, [globalParsingConfig]);
- // 全局文件处理函数
+ // Global file processing function
const processGlobalFiles = useCallback((files) => {
const fileArray = Array.from(files);
@@ -151,11 +154,11 @@ function App() {
setConfigFile(null);
}, []);
- // 全局解析配置变更处理
+ // Handle global parsing config changes
const handleGlobalParsingConfigChange = useCallback((newConfig) => {
setGlobalParsingConfig(newConfig);
- // 同步所有文件的解析配置
+ // Sync parsing config to all files
setUploadedFiles(prev => prev.map(file => ({
...file,
config: {
@@ -167,7 +170,7 @@ function App() {
})));
}, []);
- // 重置配置
+ // Reset configuration
const handleResetConfig = useCallback(() => {
savingDisabledRef.current = true;
localStorage.removeItem('globalParsingConfig');
@@ -179,12 +182,12 @@ function App() {
}, 0);
}, []);
- // 全局拖拽事件处理
+ // Global drag event handlers
const handleGlobalDragEnter = useCallback((e) => {
e.preventDefault();
setDragCounter(prev => prev + 1);
- // 检查是否包含文件
+ // Check if files are included
if (e.dataTransfer.types.includes('Files')) {
setGlobalDragOver(true);
}
@@ -192,7 +195,7 @@ function App() {
const handleGlobalDragOver = useCallback((e) => {
e.preventDefault();
- // 设置拖拽效果
+ // Set drag effect
e.dataTransfer.dropEffect = 'copy';
}, []);
@@ -217,7 +220,7 @@ function App() {
}
}, [processGlobalFiles]);
- // 添加全局拖拽监听器
+ // Add global drag listeners
useEffect(() => {
const handleDragEnter = (e) => handleGlobalDragEnter(e);
const handleDragOver = (e) => handleGlobalDragOver(e);
@@ -239,7 +242,7 @@ function App() {
return (
- {/* 全页面拖拽覆盖层 */}
+ {/* Full-page drag overlay */}
{globalDragOver && (
- 🎯 释放文件以上传
+ {t('globalDrag.release')}
- 支持 所有文本格式 文件
+
+ Supports all text formats files
+
- 拖拽到页面任意位置即可快速上传文件
+ {t('globalDrag.tip')}
@@ -287,7 +292,7 @@ function App() {
@@ -304,9 +309,9 @@ function App() {