diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 0d4af9dc..0d813751 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -32,8 +32,9 @@ import { splitSidebarQualifiedName } from '../utils/sidebarLocate'; import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata'; import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag'; import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert'; -import { SUPPORTED_LANGUAGES, t as translate } from '../i18n'; +import { t as translate } from '../i18n'; import { buildSqlAnalysisWorkbenchTab } from '../utils/sqlAnalysisTab'; +import { isLocalizedUntitledQueryTitle } from '../utils/queryTabTitle'; import { DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN, @@ -124,28 +125,6 @@ export { resolveQueryEditorNavigationTarget, } from './queryEditor/QueryEditorHelpers'; -const UNTITLED_QUERY_DATABASE_PLACEHOLDER = '__GONAVI_QUERY_DATABASE__'; - -const UNTITLED_QUERY_TITLE_PREFIXES = Array.from( - new Set( - SUPPORTED_LANGUAGES - .flatMap((language) => { - const titles = [translate('query.new', undefined, language).trim()]; - const databaseQueryTitle = translate( - 'sidebar.tab.new_query_database', - { database: UNTITLED_QUERY_DATABASE_PLACEHOLDER }, - language, - ).trim(); - const databasePrefixIndex = databaseQueryTitle.indexOf(UNTITLED_QUERY_DATABASE_PLACEHOLDER); - if (databasePrefixIndex > 0) { - titles.push(databaseQueryTitle.slice(0, databasePrefixIndex).trim()); - } - return titles; - }) - .filter(Boolean) - ) -); - const buildQueryEditorMonacoActionLabel = (key: string): string => `GoNavi: ${translate(key)}`; @@ -4048,7 +4027,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const resolveDefaultQueryName = () => { const rawTitle = String(tab.title || '').trim(); - if (!rawTitle || UNTITLED_QUERY_TITLE_PREFIXES.some((title) => rawTitle.startsWith(title))) { + if (isLocalizedUntitledQueryTitle(rawTitle)) { return translate('query_editor.save_modal.unnamed'); } return rawTitle; diff --git a/frontend/src/utils/queryTabTitle.ts b/frontend/src/utils/queryTabTitle.ts new file mode 100644 index 00000000..43509d2a --- /dev/null +++ b/frontend/src/utils/queryTabTitle.ts @@ -0,0 +1,68 @@ +import { t as translateCatalog } from '../i18n/catalog'; +import { SUPPORTED_LANGUAGES } from '../i18n/resolveLanguage'; +import type { I18nParams } from '../i18n/types'; + +const UNTITLED_QUERY_DATABASE_PLACEHOLDER = '__GONAVI_QUERY_DATABASE__'; +const UNTITLED_QUERY_TITLE_KEYS = [ + 'query.new', + 'sidebar.tab.new_query', + 'table_overview.menu.new_query', +] as const; + +export type QueryTabTitleTranslate = (key: string, params?: I18nParams) => string; + +const UNTITLED_QUERY_TITLES = new Set( + SUPPORTED_LANGUAGES.flatMap((language) => UNTITLED_QUERY_TITLE_KEYS + .map((key) => translateCatalog(language, key).trim()) + .filter(Boolean)), +); + +const UNTITLED_QUERY_DATABASE_TITLE_PREFIXES = Array.from( + new Set( + SUPPORTED_LANGUAGES + .map((language) => { + const databaseScopedTitle = translateCatalog( + language, + 'sidebar.tab.new_query_database', + { database: UNTITLED_QUERY_DATABASE_PLACEHOLDER }, + ).trim(); + const placeholderIndex = databaseScopedTitle.indexOf(UNTITLED_QUERY_DATABASE_PLACEHOLDER); + return placeholderIndex > 0 ? databaseScopedTitle.slice(0, placeholderIndex).trim() : ''; + }) + .filter(Boolean), + ), +); + +const hasUntitledDatabaseQueryPrefix = (value: string): boolean => ( + UNTITLED_QUERY_DATABASE_TITLE_PREFIXES.some((prefix) => value.startsWith(prefix)) +); + +export const isLocalizedUntitledQueryTitle = (value: unknown): boolean => { + const rawValue = String(value || '').trim(); + if (!rawValue) { + return true; + } + return UNTITLED_QUERY_TITLES.has(rawValue) || hasUntitledDatabaseQueryPrefix(rawValue); +}; + +export const resolveLocalizedUntitledQueryTitle = ( + title: unknown, + database: unknown, + translate: QueryTabTitleTranslate, +): string | null => { + const rawTitle = String(title || '').trim(); + if (!rawTitle) { + return translate('sidebar.tab.new_query'); + } + if (UNTITLED_QUERY_TITLES.has(rawTitle)) { + return translate('sidebar.tab.new_query'); + } + if (hasUntitledDatabaseQueryPrefix(rawTitle)) { + const databaseName = String(database || '').trim(); + if (databaseName) { + return translate('sidebar.tab.new_query_database', { database: databaseName }); + } + return translate('sidebar.tab.new_query'); + } + return null; +}; diff --git a/frontend/src/utils/tabDisplay.test.ts b/frontend/src/utils/tabDisplay.test.ts index 1baf06a2..3ebc4bdc 100644 --- a/frontend/src/utils/tabDisplay.test.ts +++ b/frontend/src/utils/tabDisplay.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import { t as catalogTranslate } from '../i18n/catalog'; import type { SavedConnection, TabData } from '../types'; import { applyTabDisplaySettingsPatch, @@ -14,6 +15,7 @@ import { const keyEchoTranslate = (key: string, params?: Record): string => { if (key === 'sidebar.tab.new_query') return 'T(New query)'; + if (key === 'sidebar.tab.new_query_database') return `T(New query (${params?.database}))`; if (key === 'sidebar.tab.redis_command') return `T(Command ${params?.database})`; if (key === 'sidebar.tab.redis_monitor') return `T(Monitor ${params?.database})`; return key; @@ -284,6 +286,36 @@ describe('tabDisplay', () => { expect(model.fullTitle).not.toContain('select *'); }); + it('relocalizes untitled query tab labels when the stored title came from another locale', () => { + const queryTab: TabData = { + id: 'query-1', + title: catalogTranslate('zh-CN', 'query.new'), + type: 'query', + connectionId: 'mysql-1', + dbName: 'front_end_sys', + query: 'select 1;', + }; + + const model = buildTabDisplayModel(queryTab, undefined, undefined, keyEchoTranslate); + + expect(model.primaryText).toBe('SQL T(New query)'); + }); + + it('relocalizes database-scoped untitled query labels with the current database name', () => { + const queryTab: TabData = { + id: 'query-1', + title: catalogTranslate('zh-CN', 'sidebar.tab.new_query_database', { database: 'main' }), + type: 'query', + connectionId: 'mysql-1', + dbName: 'main', + query: 'select 1;', + }; + + const model = buildTabDisplayModel(queryTab, undefined, undefined, keyEchoTranslate); + + expect(model.primaryText).toBe('SQL T(New query (main))'); + }); + it('uses SQL file names as compact query tab object labels', () => { const queryTab: TabData = { id: 'query-file-1', diff --git a/frontend/src/utils/tabDisplay.ts b/frontend/src/utils/tabDisplay.ts index 4aba000f..5c14aae5 100644 --- a/frontend/src/utils/tabDisplay.ts +++ b/frontend/src/utils/tabDisplay.ts @@ -1,6 +1,7 @@ import type { ConnectionConfig, SavedConnection, TabData } from '../types'; import { t as catalogTranslate } from '../i18n/catalog'; import type { I18nParams } from '../i18n/types'; +import { resolveLocalizedUntitledQueryTitle } from './queryTabTitle'; export const TAB_DISPLAY_ELEMENT_KEYS = ['connection', 'kind', 'object', 'database', 'schema', 'host'] as const; @@ -425,7 +426,9 @@ const compactQueryTabTitle = (tab: TabData, translate: TabDisplayTranslate = def } const rawTitle = String(tab.title || '').trim(); - const title = rawTitle && !isLikelyRawSqlTitle(rawTitle) ? rawTitle : translate('sidebar.tab.new_query'); + const resolvedUntitledTitle = resolveLocalizedUntitledQueryTitle(rawTitle, tab.dbName, translate); + const displayTitle = resolvedUntitledTitle || rawTitle; + const title = displayTitle && !isLikelyRawSqlTitle(displayTitle) ? displayTitle : translate('sidebar.tab.new_query'); if (title.length <= QUERY_TAB_TITLE_MAX_LENGTH) { return title; }