fix(plugins): localize workbench and filesystem tab titles on locale change

This commit is contained in:
AiLi authored and GitHub committed 2026-09-23 15:45:20 +08:00
1 parent 491565fdda
commit 571eaf960b
3 files changed
+97 -1

No files matched your search

+15 -1
View File
@@ -93,6 +93,7 @@ import { externalSqlFileOpenErrorMessage, externalSqlEditorMaxBytes, isSqlFilePa
import type { ConnectionConfig, DatabaseType, ObjectBrowserFilter, ObjectSourceKind, QueryTab, TabOutputView, TreeNode } from "@/types/database";
import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation";
import { parsePluginInstallDeepLink } from "@/lib/plugins/pluginInstallDeepLink";
import { createFrontendPluginRegistry } from "@/lib/plugins/frontendPlugin";
import { parseConnectionDeepLink, parseConnectionDeepLinkUpdate, type ConnectionDeepLinkDraft, type ConnectionDeepLinkUpdate } from "@/lib/connection/connectionDeepLink";
import { resolveConnectionDeepLinkUpdate } from "@/lib/connection/connectionDeepLinkUpdate";
import { parseAiConfigDeepLink, type AiConfigDeepLinkDraft } from "@/lib/ai/aiConfigDeepLink";
@@ -208,7 +209,7 @@ type AiAssistantHandle = {
type AuxiliarySearchSurface = "ai" | "history" | "sqlLibrary" | null;
const { t } = useI18n();
const { t, locale: appLocale } = useI18n();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
@@ -216,6 +217,19 @@ const { active: appBackgroundActive, backgroundObjectUrl: appBackgroundObjectUrl
const { uiFontFamilyPreview } = useUiFontFamilyPreview();
const savedSqlStore = useSavedSqlStore();
const promptTemplateStore = usePromptTemplateStore();
let pluginTitleLocaleGeneration = 0;
watch(appLocale, async (locale) => {
const generation = ++pluginTitleLocaleGeneration;
try {
const registry = createFrontendPluginRegistry(await api.listPlugins(), locale);
if (generation !== pluginTitleLocaleGeneration) return;
queryStore.localizePluginTabTitles((pluginId, contributionId, surface) =>
surface === "filesystem" ? registry.listFilesystemProviders().find((entry) => entry.plugin.manifest.id === pluginId && entry.contribution.id === contributionId)?.contribution.label : registry.findUiContribution(pluginId, contributionId)?.contribution.label,
);
} catch (error) {
console.warn("Failed to refresh localized plugin tab titles", error);
}
});
const recentConnectionIds = ref<readonly string[]>(parseRecentConnectionIds(safeLocalStorageGet(RECENT_CONNECTION_IDS_STORAGE_KEY)));
connectionStore.setBeforeConnectHandler(async (config) => {
const jdbcxRuntime = await ensureJdbcxRuntimeDrivers(config, api);
@@ -217,3 +217,62 @@ describe("queryStore openPluginWorkbench reuse", () => {
expect(bridged?.connectionId).toBe("conn-1");
});
});
describe("queryStore localizePluginTabTitles", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
vi.stubGlobal("localStorage", {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
});
vi.stubGlobal("window", { dispatchEvent: vi.fn() });
setActivePinia(createPinia());
});
it("relocalizes connectionless workbench titles on locale change, preserving session suffixes and skipping bound, renamed, or unresolvable tabs", () => {
const queryStore = useQueryStore();
// Two connectionless workbench tabs: bare title + session-suffixed title.
const bareId = queryStore.openPluginWorkbench("io.dbx.ssh", "workbench", { title: "Workbench", forceNew: true });
const suffixedId = queryStore.openPluginWorkbench("io.dbx.ssh", "workbench", { title: "Workbench", forceNew: true });
// Connection-bound session tab is titled after the connection and stays as-is.
const boundId = queryStore.openPluginWorkbench("io.dbx.ssh", "workbench", { title: "prod-server", connectionId: "conn-1", context: { connectionId: "conn-1" }, forceNew: true });
// A contribution whose label is unavailable in the new locale keeps its title.
const unresolvableId = queryStore.openPluginWorkbench("io.dbx.mystery", "workbench", { title: "Mystery", forceNew: true });
// An explicitly renamed tab: customTitle wins over the locale refresh.
// (Session numbering is plugin-wide per connection, so this third
// connectionless io.dbx.ssh tab is created as "Pinned (2)".)
const renamedId = queryStore.openPluginWorkbench("io.dbx.ssh", "result-view", { title: "Pinned", forceNew: true });
const renamedTab = queryStore.tabs.find((tab) => tab.id === renamedId);
expect(renamedTab).toBeDefined();
renamedTab!.customTitle = true;
const renamedTitleBefore = renamedTab!.title;
queryStore.localizePluginTabTitles((pluginId, contributionId, surface) => (pluginId === "io.dbx.ssh" && contributionId === "workbench" && surface === "ui" ? "工作台" : undefined));
const titles = Object.fromEntries(queryStore.tabs.map((tab) => [tab.id, tab.title]));
expect(titles[bareId]).toBe("工作台");
expect(titles[suffixedId]).toBe("工作台 (1)");
expect(titles[boundId]).toBe("prod-server");
expect(titles[unresolvableId]).toBe("Mystery");
expect(titles[renamedId]).toBe(renamedTitleBefore);
});
it("relocalizes connectionless plugin-filesystem titles on locale change, skipping connection-bound ones", () => {
const queryStore = useQueryStore();
// PluginWorkbenchTab.vue opens the filesystem browse with the localized
// provider label and no connection binding; that title must follow the
// app locale like the workbench tabs do.
const freeId = queryStore.openPluginFilesystem("io.dbx.ssh", "ssh.files", { title: "Files", forceNew: true });
const boundId = queryStore.openPluginFilesystem("io.dbx.ssh", "ssh.files", { title: "prod-server · SFTP", connectionId: "conn-1", forceNew: true });
queryStore.localizePluginTabTitles((pluginId, contributionId, surface) => (pluginId === "io.dbx.ssh" && contributionId === "ssh.files" && surface === "filesystem" ? "文件" : undefined));
const titles = Object.fromEntries(queryStore.tabs.map((tab) => [tab.id, tab.title]));
expect(titles[freeId]).toBe("文件");
expect(titles[boundId]).toBe("prod-server · SFTP");
});
});
+23
View File
@@ -3657,6 +3657,28 @@ export const useQueryStore = defineStore("query", () => {
return registerOpenTab(tab);
}
// Connectionless plugin tabs inherit their title from the localized
// contribution label (sidebar/webview workbench opens and filesystem
// browse), so a locale switch leaves them showing the previous language.
// Re-resolve them on locale change: connection-bound tabs keep the
// connection name, explicit renames (customTitle) win, and a missing
// localized label leaves the current title untouched.
function localizePluginTabTitles(resolveTitle: (pluginId: string, contributionId: string, surface: "ui" | "filesystem") => string | undefined): void {
for (const tab of tabs.value) {
const target =
tab.mode === "plugin-workbench" && tab.pluginWorkbench
? { pluginId: tab.pluginWorkbench.pluginId, contributionId: tab.pluginWorkbench.contributionId, surface: "ui" as const }
: tab.mode === "plugin-filesystem" && tab.pluginFilesystem
? { pluginId: tab.pluginFilesystem.pluginId, contributionId: tab.pluginFilesystem.providerId, surface: "filesystem" as const }
: undefined;
if (!target || tab.customTitle || pluginTabConnectionId(tab)) continue;
const localizedTitle = resolveTitle(target.pluginId, target.contributionId, target.surface)?.trim();
if (!localizedTitle) continue;
const suffix = / \((\d+)\)$/.exec(tab.title)?.[0] || "";
tab.title = `${localizedTitle}${suffix}`;
}
}
function openPluginFilesystem(pluginId: string, providerId: string, options: { title?: string; connectionId?: string; rootUri?: string; currentUri?: string; forceNew?: boolean } = {}) {
if (!options.forceNew) {
const existing = tabs.value.find((tab) => tab.mode === "plugin-filesystem" && tab.pluginFilesystem?.pluginId === pluginId && tab.pluginFilesystem?.providerId === providerId && tab.connectionId === (options.connectionId || ""));
@@ -9133,6 +9155,7 @@ export const useQueryStore = defineStore("query", () => {
openMqttAdmin,
openNacosAdmin,
openPluginWorkbench,
localizePluginTabTitles,
openPluginFilesystem,
reconnectRestoredPluginTabs,
openPluginConnection,