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 formats 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>Shift 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>所有文本格式 文件", + "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>Shift 键并拖动鼠标可选择范围,或直接输入数值。", + "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() {