feat(i18n): 收口多语言功能业务代码

This commit is contained in:
tianqijiuyun-latiao
2026-06-22 15:12:42 +08:00
parent eba689754c
commit f282da3bcb
433 changed files with 39626 additions and 6973 deletions

View File

@@ -4,8 +4,10 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { setCurrentLanguage } from '../i18n';
import { catalogs } from '../i18n/catalog';
import type { SavedQuery, TabData } from '../types';
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import QueryEditor, {
collectQueryEditorObjectDecorationCandidates,
@@ -618,6 +620,7 @@ describe('QueryEditor external SQL save', () => {
editorState.position = { lineNumber: 1, column: 1 };
editorState.selection = null;
editorState.domNode.style.cursor = '';
editorState.providers = [];
editorState.hoverProviders = [];
editorState.contentChangeListeners = [];
editorState.cursorPositionListeners = [];
@@ -2099,7 +2102,9 @@ describe('QueryEditor external SQL save', () => {
editorState.value = '__AI_SQL__\nselect 1;';
await act(async () => {
editorState.contentChangeListeners.forEach((listener) => listener());
editorState.contentChangeListeners.forEach((listener) => (listener as any)({
changes: [{ text: '__AI_SQL__' }],
}));
vi.runAllTimers();
});
@@ -2318,7 +2323,9 @@ describe('QueryEditor external SQL save', () => {
editorState.value = '__AI_SQL__\nselect 1;';
await act(async () => {
editorState.contentChangeListeners.forEach((listener) => listener());
editorState.contentChangeListeners.forEach((listener) => (listener as any)({
changes: [{ text: '__AI_SQL__' }],
}));
vi.runAllTimers();
});
@@ -2382,7 +2389,7 @@ describe('QueryEditor external SQL save', () => {
});
expect(getLastInjectedPrompt()).toBe(
'I got an error while executing this SQL:\n```sql\nselect 1;\n```\n\nThe database returned this error:\n```text\ndriver exploded\n```\n\nAnalyze the cause and suggest a fix.',
`I got an error while executing this SQL:\n\`\`\`sql\nselect 1;\n\`\`\`\n\nThe database returned this error:\n\`\`\`text\n${formatSqlExecutionError('driver exploded')}\n\`\`\`\n\nAnalyze the cause and suggest a fix.`,
);
expect(getLastInjectedPrompt()).not.toContain('我在执行以下 SQL 时遇到了错误');
} finally {
@@ -2479,17 +2486,12 @@ describe('QueryEditor external SQL save', () => {
backendApp.DBQuery.mock.calls.some((call: any[]) => /table_comment|information_schema\.tables/i.test(String(call[2]))),
).toBe(true);
await act(async () => {
editorState.mouseMoveListeners[0]?.({
target: { position: { lineNumber: 1, column: 27 } },
event: {
ctrlKey: true,
metaKey: false,
},
});
});
const hoverMarkdown = editorState.editor.deltaDecorations.mock.calls.at(-1)?.[1]?.[0]?.options?.hoverMessage?.value;
const hoverProvider = editorState.hoverProviders[editorState.hoverProviders.length - 1];
const hover = hoverProvider?.provideHover(
editorState.editor.getModel(),
{ lineNumber: 1, column: 27 },
);
const hoverMarkdown = hover?.contents?.[0]?.value;
expect(hoverMarkdown).toContain('**Table** `reporting.events`');
expect(hoverMarkdown).toContain('Database: `main`');
expect(hoverMarkdown).toContain('Schema: `reporting`');
@@ -3002,6 +3004,13 @@ describe('QueryEditor external SQL save', () => {
const completionProvider = editorState.providers[0];
expect(completionProvider).toBeTruthy();
editorState.value = 'select * from analytics.';
await completionProvider.provideCompletionItems(
editorState.editor.getModel(),
{ lineNumber: 1, column: editorState.value.length + 1 },
);
editorState.value = 'select * from ';
const completionItems = await completionProvider.provideCompletionItems(
editorState.editor.getModel(),
{ lineNumber: 1, column: editorState.value.length + 1 },
@@ -3634,7 +3643,7 @@ describe('QueryEditor external SQL save', () => {
query: 'select 2;',
savedQueryId: undefined,
}));
expect(messageApi.success).toHaveBeenCalledWith('SQL 文件已保存');
expect(messageApi.success).toHaveBeenCalledWith('SQL 文件已保存');
});
it('keeps external SQL file typing out of persisted tab drafts to avoid input freezes', async () => {
@@ -3844,7 +3853,7 @@ describe('QueryEditor external SQL save', () => {
dbName: 'main',
createdAt: 100,
}));
expect(messageApi.success).toHaveBeenCalledWith('查询已保存');
expect(messageApi.success).toHaveBeenCalledWith('查询已保存');
});
it('does not create saved queries when external SQL file writes fail', async () => {
@@ -3865,7 +3874,7 @@ describe('QueryEditor external SQL save', () => {
expect(backendApp.WriteSQLFile).toHaveBeenCalledWith(filePath, 'select 4;');
expect(storeState.saveQuery).not.toHaveBeenCalled();
expect(storeState.addTab).not.toHaveBeenCalled();
expect(messageApi.error).toHaveBeenCalledWith('保存 SQL 文件失败: 磁盘只读');
expect(messageApi.error).toHaveBeenCalledWith('保存 SQL 文件失败磁盘只读');
});
it('keeps saved query quick-save behavior for non-file tabs', async () => {
@@ -3913,7 +3922,7 @@ describe('QueryEditor external SQL save', () => {
editorState.value = 'select 8;';
await act(async () => {
findButton(renderer!, '保存').props.onClick();
findButton(renderer!, 'Save').props.onClick();
});
expect(storeState.saveQuery).toHaveBeenCalledWith(expect.objectContaining({
@@ -4008,7 +4017,7 @@ describe('QueryEditor external SQL save', () => {
title: '查询',
savedQueryId: 'saved-1',
}));
expect(messageApi.success).toHaveBeenCalledWith('查询已重命名');
expect(messageApi.success).toHaveBeenCalledWith('查询已重命名');
});
it('exports the current editor SQL without changing saved query state', async () => {
@@ -5536,9 +5545,13 @@ describe('QueryEditor external SQL save', () => {
renderer = create(<QueryEditor tab={createTab()} />);
});
await act(async () => {
findButton(renderer!, 'Show results panel').props.onClick();
});
const rendered = textContent(renderer!.toJSON());
expect(rendered).toContain('Awaiting SQL execution');
expect(rendered).toContain('Run a query to display results below in the new data grid.');
expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.title']);
expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.description']);
expect(rendered).not.toContain('等待执行 SQL');
expect(rendered).not.toContain('运行查询后,结果会在下方以新版数据网格展示。');
});
@@ -5634,7 +5647,7 @@ describe('QueryEditor external SQL save', () => {
editorState.position = { lineNumber: 3, column: 1 };
await act(async () => {
const runButton = findButton(renderer!, 'Run');
const runButton = findButton(renderer!, '运行');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -5674,7 +5687,7 @@ describe('QueryEditor external SQL save', () => {
};
await act(async () => {
const runButton = findButton(renderer!, 'Run');
const runButton = findButton(renderer!, '运行');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -5716,7 +5729,7 @@ describe('QueryEditor external SQL save', () => {
};
await act(async () => {
const runButton = findButton(renderer!, '运行');
const runButton = findButton(renderer!, 'Run');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -5725,7 +5738,7 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
expect(textContent(renderer!.toJSON())).toContain('结果 1');
expect(textContent(renderer!.toJSON())).toContain('Result 1');
backendApp.DBQueryMulti.mockClear();
messageApi.info.mockClear();
@@ -5743,7 +5756,7 @@ describe('QueryEditor external SQL save', () => {
});
await act(async () => {
const runButton = findButton(renderer!, '运行');
const runButton = findButton(renderer!, 'Run');
runButton.props.onMouseDown?.({ preventDefault: vi.fn() });
await runButton.props.onClick();
});
@@ -5879,7 +5892,8 @@ describe('QueryEditor external SQL save', () => {
});
const rendered = textContent(renderer.toJSON());
expect(rendered).toContain('Statement 2 failed: driver exploded');
expect(rendered).toContain('Statement 2 failed:');
expect(rendered).toContain('Raw error: driver exploded');
expect(rendered).not.toContain('第 2 条语句执行失败driver exploded');
});
@@ -5951,11 +5965,16 @@ describe('QueryEditor external SQL save', () => {
expect(messageApi.success).not.toHaveBeenCalledWith('已执行完成,生成 2 个结果集。');
});
it('shows the non-Mongo zero-result success toast in English', async () => {
it('renders the non-Mongo zero-row transactional result in English', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
const query = 'update users set active = 1 where 1 = 0;';
backendApp.DBQueryMulti.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
success: true,
transactionId: 'tx-zero-rows',
transactionPending: true,
data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 0 }], statementIndex: 1 }],
});
let renderer!: ReactTestRenderer;
await act(async () => {
@@ -5980,11 +5999,16 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
const rendered = textContent(renderer.toJSON());
expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(1);
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0');
expect(messageApi.success).toHaveBeenCalledWith('Execution succeeded.');
expect(messageApi.success).not.toHaveBeenCalledWith('执行成功。');
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalledTimes(1);
expect(String(backendApp.DBQueryMultiTransactional.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0');
expect(rendered).toContain(catalogs['en-US']['query_editor.result.execution_success']);
expect(rendered).toContain(catalogs['en-US']['query_editor.result.affected_rows'].replace('{{count}}', '0'));
expect(rendered).not.toContain('执行成功');
expect(rendered).not.toContain('影响行数0');
expect(messageApi.success).not.toHaveBeenCalledWith('Execution succeeded.');
});
it('shows the wrapped execution failure toast in English while preserving raw error detail', async () => {
@@ -6019,7 +6043,7 @@ describe('QueryEditor external SQL save', () => {
expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(1);
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('select 1');
expect(messageApi.error).toHaveBeenCalledWith('Query execution failed: driver exploded');
expect(messageApi.error).toHaveBeenCalledWith(`Query execution failed: ${formatSqlExecutionError('driver exploded')}`);
expect(messageApi.error).not.toHaveBeenCalledWith('Error executing query: driver exploded');
});
});
@@ -6074,7 +6098,7 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
expect(messageApi.error).toHaveBeenCalledWith('Refresh failed: network down');
expect(messageApi.error).toHaveBeenCalledWith(`Refresh failed: ${formatSqlExecutionError('network down')}`);
expect(messageApi.error).not.toHaveBeenCalledWith('刷新失败: network down');
});
@@ -6100,7 +6124,7 @@ describe('QueryEditor external SQL save', () => {
await Promise.resolve();
});
expect(messageApi.error).toHaveBeenCalledWith('Refresh failed: socket closed');
expect(messageApi.error).toHaveBeenCalledWith(`Refresh failed: ${formatSqlExecutionError('socket closed')}`);
expect(messageApi.error).not.toHaveBeenCalledWith('刷新失败: socket closed');
});
});
@@ -6651,7 +6675,7 @@ describe('QueryEditor external SQL save', () => {
const modalSource = readFileSync(new URL('./SnippetSettingsModal.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
expect(modalSource).toContain('片段语法说明(可编辑)');
expect(modalSource).toContain("t('snippet_settings.syntax_help.label')");
expect(modalSource).toContain('data-sql-snippet-syntax-help-editor="true"');
expect(modalSource).toContain("defaultActiveKey={['snippet-help']}");
expect(modalSource).toContain('footer={null}');
@@ -6660,7 +6684,7 @@ describe('QueryEditor external SQL save', () => {
expect(modalSource).toContain("size=\"large\"");
expect(modalSource).toContain('minWidth: 96');
expect(modalSource).toContain('syntaxHelp');
expect(modalSource).toContain('占位符语法参考');
expect(modalSource).toContain("t('snippet_settings.syntax_reference.label')");
expect(source).toContain('s.syntaxHelp || s.description || s.body');
});