fix(plugins): localize workbench and filesystem tab titles on locale change
This commit is contained in:
3 files changed
+97
-1
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user