From 3ce85617dac0e5a4a10c199f05f54a44181f3e3b Mon Sep 17 00:00:00 2001 From: tianqijiuyun-latiao <69459608+tianqijiuyun-latiao@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:14:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E6=94=B6=E5=8F=A3=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=89=8D=E7=AB=AF=E5=A4=9A=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/DataExportDialog.tsx | 25 +- .../components/DataExportFlow.i18n.test.ts | 57 +++ .../src/components/DataGrid.layout.test.tsx | 88 ++--- .../src/components/ExportProgressModal.tsx | 25 +- .../components/TableExportWorkbench.test.tsx | 6 +- .../src/components/TableExportWorkbench.tsx | 331 ++++++++++-------- shared/i18n/de-DE.json | 114 +++++- shared/i18n/en-US.json | 114 +++++- shared/i18n/ja-JP.json | 114 +++++- shared/i18n/ru-RU.json | 114 +++++- shared/i18n/zh-CN.json | 114 +++++- shared/i18n/zh-TW.json | 114 +++++- 12 files changed, 1010 insertions(+), 206 deletions(-) create mode 100644 frontend/src/components/DataExportFlow.i18n.test.ts diff --git a/frontend/src/components/DataExportDialog.tsx b/frontend/src/components/DataExportDialog.tsx index 1e5995f..0625262 100644 --- a/frontend/src/components/DataExportDialog.tsx +++ b/frontend/src/components/DataExportDialog.tsx @@ -2,6 +2,7 @@ import Modal from './common/ResizableDraggableModal'; import React, { useEffect, useMemo, useState } from 'react'; import { Form, InputNumber, Select, message } from 'antd'; import { ExportOutlined } from '@ant-design/icons'; +import { t } from '../i18n'; export type DataExportFormat = 'csv' | 'xlsx' | 'json' | 'md' | 'html'; export type DataExportScope = 'selected' | 'page' | 'all' | 'filteredAll'; @@ -69,21 +70,23 @@ const validateDialogValues = ( scopeOptions: DataExportScopeOption[], ): string | null => { if (!DATA_EXPORT_FORMAT_OPTIONS.some((item) => item.value === values.format)) { - return '请选择导出格式'; + return t('data_export.dialog.validation.format_required'); } if (scopeOptions.length > 0) { const matchedScope = scopeOptions.find((item) => String(item.value) === String(values.scope)); if (!matchedScope || matchedScope.disabled) { - return '请选择可用的导出范围'; + return t('data_export.dialog.validation.scope_required'); } } if (values.format === 'xlsx') { const rows = Math.trunc(Number(values.xlsxMaxRowsPerSheet) || 0); if (!Number.isFinite(rows) || rows <= 0) { - return '请输入有效的每个工作表最大行数'; + return t('data_export.dialog.validation.xlsx_max_rows_required'); } if (rows > MAX_XLSX_ROWS_PER_SHEET) { - return `每个工作表最大行数不能超过 ${MAX_XLSX_ROWS_PER_SHEET.toLocaleString()}`; + return t('data_export.dialog.validation.xlsx_max_rows_limit', { + maxRows: MAX_XLSX_ROWS_PER_SHEET.toLocaleString(), + }); } } return null; @@ -108,7 +111,7 @@ const DataExportDialogContent: React.FC<{ return (
- + readFileSync(new URL(file, import.meta.url), 'utf8')); +const combinedSource = sources.join('\n'); +const catalogs = Object.fromEntries(localeFiles.map((locale) => [ + locale, + JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record, +])) as Record>; + +const extractKeys = (source: string): string[] => ( + Array.from(new Set(source.match(/data_export(?:\.[a-z0-9_]+)+/g) || [])).sort() +); + +const placeholdersOf = (value: string): string[] => ( + Array.from(value.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g), (match) => match[1]).sort() +); + +describe('data export i18n', () => { + it('routes dialog, progress modal, and workbench copy through translation keys instead of inline Han literals', () => { + expect(sources[0]).toContain("t('data_export.dialog.field.format')"); + expect(sources[1]).toContain("t('data_export.progress.title.error')"); + expect(sources[2]).toContain("t('data_export.workbench.title')"); + expect(combinedSource).not.toMatch(/\p{Script=Han}/u); + }); + + it('keeps all extracted data_export keys present in every supported locale with matching placeholders', () => { + const keys = extractKeys(combinedSource); + const baseline = catalogs['zh-CN']; + + expect(keys.length).toBeGreaterThan(0); + + keys.forEach((key) => { + expect(baseline, `zh-CN:${key}`).toHaveProperty(key); + const expectedPlaceholders = placeholdersOf(baseline[key]); + localeFiles.forEach((locale) => { + expect(catalogs[locale], `${locale}:${key}`).toHaveProperty(key); + expect(placeholdersOf(catalogs[locale][key]), `${locale}:${key}`).toEqual(expectedPlaceholders); + }); + }); + }); +}); diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx index 92b9df2..352d3c1 100644 --- a/frontend/src/components/DataGrid.layout.test.tsx +++ b/frontend/src/components/DataGrid.layout.test.tsx @@ -1951,44 +1951,51 @@ describe('DataGrid layout', () => { }); it('renders a non-data row number column when enabled', () => { - const markup = renderToStaticMarkup( - {}} - />, - ); + const previousLanguage = getCurrentLanguage(); + setCurrentLanguage('zh-CN'); - expect(markup).toContain('aria-label="行号"'); - expect(markup).toContain('#'); - expect(markup).not.toContain('>行号<'); - expect(markup).toContain('data-grid-row-number-title="true"'); - expect(markup).toContain('data-grid-column-title-single-line="true"'); - expect(markup).toContain('justify-content:center'); - expect(markup).toContain('align-items:center'); - expect(markup).toContain('min-height:var(--gonavi-header-min-height, 40px)'); - expect(markup).toContain('text-align:center'); - expect(markup).toContain('padding-inline:0'); - expect(markup).toContain('vertical-align:middle'); - expect(markup).toContain('data-grid-row-number="true"'); - expect(markup).toContain('51'); + try { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('aria-label="行号"'); + expect(markup).toContain('#'); + expect(markup).not.toContain('>行号<'); + expect(markup).toContain('data-grid-row-number-title="true"'); + expect(markup).toContain('data-grid-column-title-single-line="true"'); + expect(markup).toContain('justify-content:center'); + expect(markup).toContain('align-items:center'); + expect(markup).toContain('min-height:var(--gonavi-header-min-height, 40px)'); + expect(markup).toContain('text-align:center'); + expect(markup).toContain('padding-inline:0'); + expect(markup).toContain('vertical-align:middle'); + expect(markup).toContain('data-grid-row-number="true"'); + expect(markup).toContain('51'); + } finally { + setCurrentLanguage(previousLanguage); + } }); it('clears modified cell markers when refreshing the grid', () => { @@ -2215,9 +2222,10 @@ describe('DataGrid layout', () => { expect(source).toContain("if (backendExportSql && connectionId) {"); expect(source).toContain("label: allRowsLabel"); expect(exportDialogSource).toContain('data-export-config-modal="true"'); - expect(exportDialogSource).toContain('label="导出格式"'); - expect(exportDialogSource).toContain('label="每个工作表最大行数"'); - expect(exportDialogSource).toContain('仅 XLSX 生效'); + expect(exportDialogSource).toContain("import { t } from '../i18n';"); + expect(exportDialogSource).toContain("label={t('data_export.dialog.field.format')}"); + expect(exportDialogSource).toContain("label={t('data_export.dialog.field.xlsx_max_rows')}"); + expect(exportDialogSource).toContain("t('data_export.dialog.field.xlsx_max_rows_help'"); expect(source).toContain('const queryResultCurrentPageRows = useMemo(() => {'); expect(source).toContain('const resolveContextMenuPosition = useCallback((x: number, y: number, estimatedWidth: number, estimatedHeight: number) => {'); expect(source).toContain('const rect = element.getBoundingClientRect();'); diff --git a/frontend/src/components/ExportProgressModal.tsx b/frontend/src/components/ExportProgressModal.tsx index c6e1f6c..483f9df 100644 --- a/frontend/src/components/ExportProgressModal.tsx +++ b/frontend/src/components/ExportProgressModal.tsx @@ -4,6 +4,7 @@ import { Button, Typography } from 'antd'; import { formatExportProgressRows, } from '../utils/exportProgress'; +import { t } from '../i18n'; import ExportProgressBar from './ExportProgressBar'; import { useExportProgressRunner } from './useExportProgressRunner'; @@ -16,7 +17,9 @@ export function useExportProgressDialog() { const modalNode = ( 关闭, + , ] : null} >
- 任务 - {state.title || state.targetName || '导出任务'} + {t('data_export.progress.label.task')} + {state.title || state.targetName || t('data_export.progress.value.task_fallback')} - 对象 - {state.targetName || '未命名对象'} + {t('data_export.label.object')} + {state.targetName || t('data_export.progress.value.target_fallback')} - 格式 + {t('data_export.label.format')} {state.format || '-'} - 状态 - {state.stage || '准备中'} + {t('data_export.label.status')} + {state.stage || t('data_export.progress.status.start')} {state.filePath ? ( <> - 文件 + {t('data_export.label.file')} {state.filePath} ) : null} @@ -59,7 +62,7 @@ export function useExportProgressDialog() {
{formatExportProgressRows(state.current, state.total, state.totalRowsKnown)} {!state.totalRowsKnown && state.status !== 'done' && state.status !== 'error' ? ( - 当前未预先统计总行数,暂不显示百分比,写入行数为实时值。 + {t('data_export.hint.rows_unknown')} ) : null} {state.message ? ( {state.message} diff --git a/frontend/src/components/TableExportWorkbench.test.tsx b/frontend/src/components/TableExportWorkbench.test.tsx index 5c240a9..7d2e5de 100644 --- a/frontend/src/components/TableExportWorkbench.test.tsx +++ b/frontend/src/components/TableExportWorkbench.test.tsx @@ -4,6 +4,7 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import TableExportWorkbench, { buildTableExportHistoryEntry } from './TableExportWorkbench'; +import { setCurrentLanguage } from '../i18n'; import type { ExportProgressState } from './useExportProgressRunner'; const mockUpsertTableExportHistory = vi.fn(); @@ -75,6 +76,7 @@ vi.mock('./useExportProgressRunner', () => ({ describe('TableExportWorkbench', () => { beforeEach(() => { + setCurrentLanguage('zh-CN'); mockUpsertTableExportHistory.mockReset(); mockStoreState = createMockStoreState(); mockProgressRunnerState = createMockProgressRunnerState(); @@ -277,8 +279,8 @@ describe('TableExportWorkbench', () => { expect(progressMatches).toHaveLength(1); expect(source).not.toContain(' { diff --git a/frontend/src/components/TableExportWorkbench.tsx b/frontend/src/components/TableExportWorkbench.tsx index 6d8de17..df65b7b 100644 --- a/frontend/src/components/TableExportWorkbench.tsx +++ b/frontend/src/components/TableExportWorkbench.tsx @@ -26,6 +26,7 @@ import { resolveExportElapsedMs, type ExportProgressStatus, } from '../utils/exportProgress'; +import { t } from '../i18n'; import { DATA_EXPORT_FORMAT_OPTIONS, DEFAULT_DATA_EXPORT_FORMAT, @@ -45,8 +46,12 @@ type BatchTableExportMode = 'schema' | 'dataOnly' | 'backup'; type BatchDatabaseExportMode = 'schema' | 'backup'; type SelectOption = { value: string; label: React.ReactNode; title: string }; -const DEFAULT_SCOPE_OPTIONS: TableExportScopeOption[] = [ - { value: 'all', label: '全表数据', description: '后台重新查询整张表并导出全部数据。' }, +const createDefaultScopeOptions = (): TableExportScopeOption[] => [ + { + value: 'all', + label: t('data_export.workbench.scope.all.label'), + description: t('data_export.workbench.scope.all.description'), + }, ]; const SELECT_ELLIPSIS_LABEL_STYLE: React.CSSProperties = { @@ -58,20 +63,40 @@ const SELECT_ELLIPSIS_LABEL_STYLE: React.CSSProperties = { whiteSpace: 'nowrap', }; -const BATCH_TABLE_EXPORT_MODE_OPTIONS: Array<{ value: BatchTableExportMode; label: string; description: string }> = [ - { value: 'schema', label: '结构', description: '导出当前数据库下所选对象的建表或定义 SQL。' }, - { value: 'dataOnly', label: '仅数据', description: '导出所选对象的数据 INSERT 语句。' }, - { value: 'backup', label: '备份', description: '导出所选对象的结构和数据 SQL。' }, +const createBatchTableExportModeOptions = (): Array<{ value: BatchTableExportMode; label: string; description: string }> => [ + { + value: 'schema', + label: t('data_export.workbench.batch_tables.mode.schema.label'), + description: t('data_export.workbench.batch_tables.mode.schema.description'), + }, + { + value: 'dataOnly', + label: t('data_export.workbench.batch_tables.mode.data_only.label'), + description: t('data_export.workbench.batch_tables.mode.data_only.description'), + }, + { + value: 'backup', + label: t('data_export.workbench.batch_tables.mode.backup.label'), + description: t('data_export.workbench.batch_tables.mode.backup.description'), + }, ]; -const BATCH_DATABASE_EXPORT_MODE_OPTIONS: Array<{ value: BatchDatabaseExportMode; label: string; description: string }> = [ - { value: 'schema', label: '导出库结构', description: '按数据库分别生成结构 SQL 文件。' }, - { value: 'backup', label: '备份库', description: '按数据库分别生成结构加数据 SQL 文件。' }, +const createBatchDatabaseExportModeOptions = (): Array<{ value: BatchDatabaseExportMode; label: string; description: string }> => [ + { + value: 'schema', + label: t('data_export.workbench.batch_databases.mode.schema.label'), + description: t('data_export.workbench.batch_databases.mode.schema.description'), + }, + { + value: 'backup', + label: t('data_export.workbench.batch_databases.mode.backup.label'), + description: t('data_export.workbench.batch_databases.mode.backup.description'), + }, ]; const normalizeScopeOptions = (input: TabData['tableExportScopeOptions']): TableExportScopeOption[] => { if (!Array.isArray(input) || input.length === 0) { - return DEFAULT_SCOPE_OPTIONS; + return createDefaultScopeOptions(); } return input; }; @@ -103,22 +128,25 @@ const formatDateTime = (timestamp: number): string => { const resolveWorkbenchMode = (tab: TabData): ExportWorkbenchMode => tab.exportWorkbenchMode || 'single'; const resolveObjectTypeLabel = (objectType?: TabData['objectType']): string => { - if (objectType === 'view') return '视图'; - if (objectType === 'materialized-view') return '物化视图'; - return '表'; + if (objectType === 'view') return t('data_export.workbench.object_type.view'); + if (objectType === 'materialized-view') return t('data_export.workbench.object_type.materialized_view'); + return t('data_export.workbench.object_type.table'); }; -const STATUS_META: Record = { - idle: { label: '待开始', border: 'rgba(148, 163, 184, 0.35)', bg: 'rgba(148, 163, 184, 0.12)', text: '#475467' }, - start: { label: '准备中', border: 'rgba(59, 130, 246, 0.3)', bg: 'rgba(59, 130, 246, 0.12)', text: '#1d4ed8' }, - running: { label: '执行中', border: 'rgba(16, 185, 129, 0.3)', bg: 'rgba(16, 185, 129, 0.14)', text: '#047857' }, - finalizing: { label: '收尾中', border: 'rgba(249, 115, 22, 0.3)', bg: 'rgba(249, 115, 22, 0.12)', text: '#c2410c' }, - done: { label: '已完成', border: 'rgba(34, 197, 94, 0.3)', bg: 'rgba(34, 197, 94, 0.14)', text: '#15803d' }, - error: { label: '失败', border: 'rgba(239, 68, 68, 0.32)', bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626' }, +const resolveStatusMeta = (status: ExportProgressStatus): { label: string; border: string; bg: string; text: string } => { + const meta: Record = { + idle: { label: t('data_export.progress.status.idle'), border: 'rgba(148, 163, 184, 0.35)', bg: 'rgba(148, 163, 184, 0.12)', text: '#475467' }, + start: { label: t('data_export.progress.status.start'), border: 'rgba(59, 130, 246, 0.3)', bg: 'rgba(59, 130, 246, 0.12)', text: '#1d4ed8' }, + running: { label: t('data_export.progress.status.running'), border: 'rgba(16, 185, 129, 0.3)', bg: 'rgba(16, 185, 129, 0.14)', text: '#047857' }, + finalizing: { label: t('data_export.progress.status.finalizing'), border: 'rgba(249, 115, 22, 0.3)', bg: 'rgba(249, 115, 22, 0.12)', text: '#c2410c' }, + done: { label: t('data_export.progress.status.done'), border: 'rgba(34, 197, 94, 0.3)', bg: 'rgba(34, 197, 94, 0.14)', text: '#15803d' }, + error: { label: t('data_export.progress.status.error'), border: 'rgba(239, 68, 68, 0.32)', bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626' }, + }; + return meta[status] || meta.idle; }; const renderStatusPill = (status: ExportProgressStatus) => { - const meta = STATUS_META[status] || STATUS_META.idle; + const meta = resolveStatusMeta(status); return ( - BATCH_TABLE_EXPORT_MODE_OPTIONS.find((item) => item.value === mode) || BATCH_TABLE_EXPORT_MODE_OPTIONS[0]; + createBatchTableExportModeOptions().find((item) => item.value === mode) || createBatchTableExportModeOptions()[0]; const resolveBatchDatabaseModeMeta = (mode: BatchDatabaseExportMode) => - BATCH_DATABASE_EXPORT_MODE_OPTIONS.find((item) => item.value === mode) || BATCH_DATABASE_EXPORT_MODE_OPTIONS[0]; + createBatchDatabaseExportModeOptions().find((item) => item.value === mode) || createBatchDatabaseExportModeOptions()[0]; const resolveBatchTablesTargetName = (dbName: string, objectCount: number): string => { - const safeDbName = String(dbName || '').trim() || '当前数据库'; - return `${safeDbName} · ${objectCount} 个对象`; + const safeDbName = String(dbName || '').trim() || t('data_export.workbench.target.current_database'); + return t('data_export.workbench.target.batch_tables', { database: safeDbName, count: objectCount }); }; -const resolveBatchDatabasesTargetName = (databaseCount: number): string => `${databaseCount} 个数据库`; +const resolveBatchDatabasesTargetName = (databaseCount: number): string => ( + t('data_export.workbench.target.batch_databases', { count: databaseCount }) +); const formatWorkbenchProgressSummary = ( mode: ExportWorkbenchMode, @@ -179,12 +209,18 @@ const formatWorkbenchProgressSummary = ( totalRowsKnown: boolean, ): string => { if (mode === 'batch-tables') { - if (!totalRowsKnown) return '批量对象导出正在执行'; - return `已完成 ${Math.min(current, total).toLocaleString()} / ${total.toLocaleString()} 个对象`; + if (!totalRowsKnown) return t('data_export.workbench.summary.batch_tables_running'); + return t('data_export.workbench.summary.batch_tables_done', { + current: Math.min(current, total).toLocaleString(), + total: total.toLocaleString(), + }); } if (mode === 'batch-databases') { - if (!totalRowsKnown) return '批量库导出正在执行'; - return `已完成 ${Math.min(current, total).toLocaleString()} / ${total.toLocaleString()} 个库`; + if (!totalRowsKnown) return t('data_export.workbench.summary.batch_databases_running'); + return t('data_export.workbench.summary.batch_databases_done', { + current: Math.min(current, total).toLocaleString(), + total: total.toLocaleString(), + }); } return formatExportProgressRows(current, total, totalRowsKnown); }; @@ -194,12 +230,14 @@ const resolveProgressHint = (mode: ExportWorkbenchMode, status: ExportProgressSt return null; } if (mode === 'single') { - return '当前未预先统计总行数,暂不显示百分比,写入行数为实时值。'; + return t('data_export.hint.rows_unknown'); } - return '当前阶段为后端执行中的过程提示,整体进度会在对象或数据库完成后推进。'; + return t('data_export.hint.batch_stage'); }; -const resolveOutputLabel = (mode: ExportWorkbenchMode): string => (mode === 'batch-databases' ? '输出目录' : '输出文件'); +const resolveOutputLabel = (mode: ExportWorkbenchMode): string => ( + mode === 'batch-databases' ? t('data_export.label.directory') : t('data_export.label.file') +); export const buildTableExportHistoryEntry = ({ progressState, @@ -219,7 +257,7 @@ export const buildTableExportHistoryEntry = ({ strategyLabel: string; }): TableExportHistoryEntry => ({ jobId: progressState.jobId, - targetName: progressState.targetName || fallbackTargetName || '未命名对象', + targetName: progressState.targetName || fallbackTargetName || t('data_export.progress.value.target_fallback'), startedAt: progressState.startedAt || existingEntry?.startedAt || 0, finishedAt: progressState.finishedAt || existingEntry?.finishedAt || 0, format: progressState.format || existingEntry?.format || fallbackFormat, @@ -353,7 +391,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { if (!res.success) { setAvailableDatabases([]); setSelectedDatabaseNames([]); - setDatabaseLoadError(res.message || '获取数据库列表失败'); + setDatabaseLoadError(res.message || t('data_export.message.load_databases_failed')); return; } const dbRows: any[] = Array.isArray(res.data) ? res.data : []; @@ -381,7 +419,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { if (!alive) return; setAvailableDatabases([]); setSelectedDatabaseNames([]); - setDatabaseLoadError(error?.message || '获取数据库列表失败'); + setDatabaseLoadError(error?.message || t('data_export.message.load_databases_failed')); }) .finally(() => { if (alive) { @@ -411,7 +449,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { if (!res.success) { setAvailableObjects([]); setSelectedObjectNames([]); - setObjectLoadError(res.message || '获取对象列表失败'); + setObjectLoadError(res.message || t('data_export.message.load_objects_failed')); return; } const tableRows: any[] = Array.isArray(res.data) ? res.data : []; @@ -428,7 +466,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { if (!alive) return; setAvailableObjects([]); setSelectedObjectNames([]); - setObjectLoadError(error?.message || '获取对象列表失败'); + setObjectLoadError(error?.message || t('data_export.message.load_objects_failed')); }) .finally(() => { if (alive) { @@ -463,15 +501,19 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { const activeScopeLabel = isSingleWorkbench ? singleScopeLabel : isBatchTablesWorkbench - ? `已选对象(${selectedObjectNames.length})` - : `已选数据库(${selectedDatabaseNames.length})`; + ? t('data_export.workbench.scope.selected_objects', { count: selectedObjectNames.length }) + : t('data_export.workbench.scope.selected_databases', { count: selectedDatabaseNames.length }); const activeScopeCount = isSingleWorkbench ? singleScopeRowCount : (isBatchTablesWorkbench ? selectedObjectNames.length : selectedDatabaseNames.length); const totalRowsKnown = isSingleWorkbench ? singleTotalRowsKnown : true; const exportStrategyLabel = isSingleWorkbench - ? (scope === 'all' && !activeScopeQuery ? '整表导出链路' : 'SQL 重放导出') - : (isBatchTablesWorkbench ? `批量对象 SQL 导出 · ${batchTableModeMeta.label}` : `批量库 SQL 导出 · ${batchDatabaseModeMeta.label}`); + ? (scope === 'all' && !activeScopeQuery + ? t('data_export.workbench.strategy.full_table') + : t('data_export.workbench.strategy.query_replay')) + : (isBatchTablesWorkbench + ? t('data_export.workbench.strategy.batch_tables', { mode: batchTableModeMeta.label }) + : t('data_export.workbench.strategy.batch_databases', { mode: batchDatabaseModeMeta.label })); const currentElapsedMs = useMemo( () => resolveExportElapsedMs(progressState.startedAt, progressState.finishedAt, nowTick), [nowTick, progressState.finishedAt, progressState.startedAt], @@ -480,7 +522,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { const currentProgressHint = resolveProgressHint(workbenchMode, progressState.status, progressState.totalRowsKnown); const progressOutputLabel = resolveOutputLabel(workbenchMode); const fallbackTargetName = isSingleWorkbench - ? (tab.tableName || '未命名对象') + ? (tab.tableName || t('data_export.progress.value.target_fallback')) : (isBatchTablesWorkbench ? resolveBatchTablesTargetName(selectedDbName, selectedObjectNames.length) : resolveBatchDatabasesTargetName(selectedDatabaseNames.length)); const fallbackFormat = isSingleWorkbench ? String(format || '').toUpperCase() : 'SQL'; @@ -575,7 +617,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { return; } await runExportWithProgress({ - title: tab.title || `导出 ${objectName}`, + title: tab.title || t('data_export.workbench.task.export_target', { name: objectName }), targetName: objectName, format, totalRows: singleScopeRowCount, @@ -684,25 +726,25 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { if (isSingleWorkbench) { return [ `${resolveObjectTypeLabel(tab.objectType)} · ${tab.tableName || '-'}`, - `数据库 · ${tab.dbName || '-'}`, - `连接 · ${connection?.name || '-'}`, - `Host · ${hostSummary || '-'}`, + `${t('data_export.label.database')} · ${tab.dbName || '-'}`, + `${t('data_export.label.connection')} · ${connection?.name || '-'}`, + `${t('data_export.label.host')} · ${hostSummary || '-'}`, ]; } if (isBatchTablesWorkbench) { return [ - '模式 · 批量对象', - `数据库 · ${selectedDbName || '-'}`, - `连接 · ${connection?.name || '-'}`, - `对象数 · ${selectedObjectNames.length}`, - `Host · ${hostSummary || '-'}`, + `${t('data_export.label.mode')} · ${t('data_export.workbench.mode.batch_tables')}`, + `${t('data_export.label.database')} · ${selectedDbName || '-'}`, + `${t('data_export.label.connection')} · ${connection?.name || '-'}`, + `${t('data_export.label.object_count')} · ${selectedObjectNames.length}`, + `${t('data_export.label.host')} · ${hostSummary || '-'}`, ]; } return [ - '模式 · 批量库', - `连接 · ${connection?.name || '-'}`, - `已选库 · ${selectedDatabaseNames.length}`, - `Host · ${hostSummary || '-'}`, + `${t('data_export.label.mode')} · ${t('data_export.workbench.mode.batch_databases')}`, + `${t('data_export.label.connection')} · ${connection?.name || '-'}`, + `${t('data_export.label.selected_databases')} · ${selectedDatabaseNames.length}`, + `${t('data_export.label.host')} · ${hostSummary || '-'}`, ]; }, [ connection?.name, @@ -735,9 +777,9 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { }} >
- 导出工作台 + {t('data_export.workbench.title')}
- 在同一页内配置导出、观察主进度,并回看最近任务摘要。 + {t('data_export.workbench.subtitle')}
@@ -789,53 +831,53 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { }} >
-
导出配置
+
{t('data_export.workbench.section.config')}
{isSingleWorkbench ? (
- 对象 + {t('data_export.label.object')} {tab.tableName || '-'} - 类型 + {t('data_export.label.type')} {resolveObjectTypeLabel(tab.objectType)} - 连接 + {t('data_export.label.connection')} {connection?.name || '-'} - 数据库 + {t('data_export.label.database')} {tab.dbName || '-'} - Host + {t('data_export.label.host')} {hostSummary || '-'}
) : isBatchTablesWorkbench ? (
- 模式 - 批量对象 + {t('data_export.label.mode')} + {t('data_export.workbench.mode.batch_tables')} - 连接 + {t('data_export.label.connection')} {connection?.name || '-'} - 数据库 + {t('data_export.label.database')} {selectedDbName || '-'} - 对象数 + {t('data_export.label.object_count')} {selectedObjectNames.length} - Host + {t('data_export.label.host')} {hostSummary || '-'}
) : (
- 模式 - 批量库 + {t('data_export.label.mode')} + {t('data_export.workbench.mode.batch_databases')} - 连接 + {t('data_export.label.connection')} {connection?.name || '-'} - 已选库 + {t('data_export.label.selected_databases')} {selectedDatabaseNames.length} - Host + {t('data_export.label.host')} {hostSummary || '-'}
)} @@ -845,8 +887,8 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { ) : null} @@ -854,7 +896,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { ) : null} @@ -863,7 +905,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { ) : null} @@ -872,7 +914,7 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { {isSingleWorkbench ? ( <>
-
导出范围
+
{t('data_export.label.export_scope')}
= ({ tab }) => { {format === 'xlsx' ? (
-
每个工作表最大行数
+
{t('data_export.label.xlsx_max_rows')}
= ({ tab }) => { }} />
- 仅 XLSX 生效,最大 {MAX_XLSX_ROWS_PER_SHEET.toLocaleString()} 行(不含表头) + {t('data_export.dialog.field.xlsx_max_rows_help', { + maxRows: MAX_XLSX_ROWS_PER_SHEET.toLocaleString(), + })}
) : null} @@ -927,13 +971,13 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { ) : isBatchTablesWorkbench ? ( <>
-
连接
+
{t('data_export.label.connection')}
= ({ tab }) => {
-
对象
+
{t('data_export.label.object')}
@@ -999,7 +1045,11 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { style={{ width: '100%' }} mode="multiple" value={selectedObjectNames} - placeholder={selectedDbName ? (loadingObjects ? '正在加载对象...' : '选择对象') : '请先选择数据库'} + placeholder={selectedDbName + ? (loadingObjects + ? t('data_export.workbench.placeholder.loading_objects') + : t('data_export.workbench.placeholder.select_object')) + : t('data_export.workbench.placeholder.select_database_first')} loading={loadingObjects} options={availableObjects} disabled={!selectedDbName} @@ -1010,16 +1060,19 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { onChange={(next) => setSelectedObjectNames((next as string[]).map((item) => String(item).trim()).filter(Boolean))} />
- 当前库可选 {availableObjects.length} 个对象,已选 {selectedObjectNames.length} 个。 + {t('data_export.workbench.helper.available_objects', { + available: availableObjects.length, + selected: selectedObjectNames.length, + })}
-
导出内容
+
{t('data_export.label.export_content')}
) : ( <>
-
连接
+
{t('data_export.label.connection')}
({ value: item.value, label: item.label }))} + options={createBatchDatabaseExportModeOptions().map((item) => ({ value: item.value, label: item.label }))} onChange={(next) => setBatchDatabaseMode(next as BatchDatabaseExportMode)} />
@@ -1115,12 +1170,12 @@ const TableExportWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => {
-
导出格式
+
{t('data_export.label.format')}