From 2f44d360185df5581e2145142b92d4bef4416f54 Mon Sep 17 00:00:00 2001 From: Rahul <42892759+heyrmi@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:10:45 +0530 Subject: [PATCH] fix(i18n): translate backend errors at UI boundaries --- .../desktop/src/components/auth/LoginPage.vue | 19 +- .../ScheduledDatabaseBackupSettings.vue | 7 +- .../components/config/DriverStoreDialog.vue | 32 +-- .../connection/ConnectionDialog.vue | 42 ++-- .../editor/EditorSettingsDialog.vue | 4 +- .../export/ExportProgressDialog.vue | 7 +- .../export/ExportProgressPopover.vue | 5 +- .../generate/DataGenerateDialog.vue | 6 +- .../generate/params/AddressParams.vue | 33 ++-- .../generate/params/BarcodeParams.vue | 9 +- .../components/generate/params/CityParams.vue | 29 +-- .../generate/params/ColorParams.vue | 7 +- .../generate/params/CompanyNameParams.vue | 7 +- .../generate/params/CreditCardDateParams.vue | 13 +- .../params/CreditCardNumberParams.vue | 7 +- .../generate/params/CreditCardTypeParams.vue | 7 +- .../components/generate/params/DateParams.vue | 10 +- .../generate/params/DateTimeParams.vue | 2 +- .../generate/params/DepartmentParams.vue | 7 +- .../generate/params/EmailParams.vue | 9 +- .../components/generate/params/EnumParams.vue | 2 +- .../generate/params/FileExtensionParams.vue | 22 +-- .../generate/params/FileNameParams.vue | 24 +-- .../generate/params/FilePathParams.vue | 26 +-- .../generate/params/ForeignKeyParams.vue | 31 +-- .../generate/params/FullNameParams.vue | 19 +- .../generate/params/GenderParams.vue | 7 +- .../generate/params/HostnameParams.vue | 13 +- .../generate/params/ImageBinaryParams.vue | 2 +- .../generate/params/IndustryParams.vue | 7 +- .../generate/params/IpAddressParams.vue | 11 +- .../generate/params/JobTitleParams.vue | 7 +- .../generate/params/MacAddressParams.vue | 7 +- .../generate/params/MaritalStatusParams.vue | 7 +- .../generate/params/NumberParams.vue | 8 +- .../generate/params/PaymentMethodParams.vue | 11 +- .../generate/params/PhoneParams.vue | 24 +-- .../generate/params/ProductCategoryParams.vue | 7 +- .../generate/params/ProductNameParams.vue | 11 +- .../generate/params/RegexParams.vue | 15 +- .../generate/params/RegionParams.vue | 37 ++-- .../generate/params/SequenceParams.vue | 2 +- .../components/generate/params/SizeParams.vue | 7 +- .../components/generate/params/SkuParams.vue | 7 +- .../generate/params/SocialIdParams.vue | 5 +- .../components/generate/params/TextParams.vue | 7 +- .../generate/params/TitleParams.vue | 7 +- .../components/generate/params/UrlParams.vue | 13 +- .../components/generate/params/UuidParams.vue | 13 +- .../generate/params/WeightUnitParams.vue | 11 +- .../components/grid/LayerPreviewDialog.vue | 6 +- .../src/components/layout/SqlFilePanel.vue | 3 +- .../components/mq/ProducerConsumerPanel.vue | 5 +- .../useSidebarTreeExportRuntime.spec.ts | 55 ++++++ .../src/composables/useDataGridEditor.ts | 3 +- .../src/composables/useDataGridExport.ts | 27 +-- .../useSidebarConnectionMutationRuntime.ts | 2 +- .../useSidebarTreeExportRuntime.ts | 7 +- .../src/i18n/__tests__/backendErrors.spec.ts | 187 ++++++++++++++++++ apps/desktop/src/i18n/backend-errors.ts | 68 ++++++- apps/desktop/src/i18n/locales/en.ts | 155 +++++++++++++++ apps/desktop/src/i18n/locales/es.ts | 156 +++++++++++++++ apps/desktop/src/i18n/locales/it.ts | 156 +++++++++++++++ apps/desktop/src/i18n/locales/ja.ts | 156 +++++++++++++++ apps/desktop/src/i18n/locales/pt-BR.ts | 156 +++++++++++++++ apps/desktop/src/i18n/locales/zh-CN.ts | 155 +++++++++++++++ apps/desktop/src/i18n/locales/zh-TW.ts | 155 +++++++++++++++ .../src/lib/history/historyAiAnalysis.ts | 10 +- apps/desktop/src/lib/imports/navicatImport.ts | 2 +- apps/desktop/src/stores/queryStore.ts | 7 +- crates/dbx-core/src/agent_service.rs | 34 ++-- crates/dbx-core/src/mq/adapters/kafka.rs | 2 +- crates/dbx-core/src/query.rs | 2 +- crates/dbx-core/src/query_result_export.rs | 8 +- crates/dbx-web/src/auth.rs | 2 +- crates/dbx-web/src/routes/agents.rs | 2 +- packages/app-tests/dataGridEditor.test.ts | 2 +- packages/app-tests/historyAiAnalysis.test.ts | 2 +- .../app-tests/scheduledDatabaseBackup.test.ts | 8 + packages/app-tests/useDataGridExport.test.ts | 26 ++- src-tauri/src/commands/agents.rs | 2 +- src-tauri/src/commands/fs_open.rs | 4 +- 82 files changed, 1862 insertions(+), 335 deletions(-) create mode 100644 apps/desktop/src/composables/__tests__/useSidebarTreeExportRuntime.spec.ts create mode 100644 apps/desktop/src/i18n/__tests__/backendErrors.spec.ts diff --git a/apps/desktop/src/components/auth/LoginPage.vue b/apps/desktop/src/components/auth/LoginPage.vue index a081d71c0..ccab301db 100644 --- a/apps/desktop/src/components/auth/LoginPage.vue +++ b/apps/desktop/src/components/auth/LoginPage.vue @@ -6,6 +6,7 @@ import PasswordInput from "@/components/ui/PasswordInput.vue"; import { Lock, Loader2, ShieldCheck } from "@lucide/vue"; import AppLogo from "@/components/icons/AppLogo.vue"; import { apiUrl } from "@/lib/common/webPath"; +import { translateBackendError } from "@/i18n/backend-errors"; const props = withDefaults( defineProps<{ @@ -22,6 +23,21 @@ const confirmPassword = ref(""); const error = ref(""); const loading = ref(false); +// The auth routes report failures as `{"error": "..."}`, so unwrap that before +// translating; anything else is treated as a plain-text message. +async function readAuthError(res: Response): Promise { + const text = (await res.text()).trim(); + if (!text) return t("auth.loginFailed"); + let message = text; + try { + const parsed = JSON.parse(text); + if (parsed && typeof parsed.error === "string") message = parsed.error; + } catch { + // not JSON — fall through with the raw body + } + return translateBackendError(t, message) || t("auth.loginFailed"); +} + async function submit() { if (props.setupMode && password.value !== confirmPassword.value) { error.value = t("auth.passwordMismatch"); @@ -40,8 +56,7 @@ async function submit() { if (res.ok) { emit("authenticated"); } else { - const text = await res.text(); - error.value = text || t("auth.loginFailed"); + error.value = await readAuthError(res); } } catch (e: any) { error.value = e?.message || t("auth.connectFailed"); diff --git a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue index 143b5f09d..5a7970cab 100644 --- a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue +++ b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue @@ -12,6 +12,7 @@ import { ChevronDown, ChevronRight, DatabaseBackup, FolderOpen, Loader2, Pencil, import * as api from "@/lib/backend/api"; import { useScheduledDatabaseBackups } from "@/composables/useScheduledDatabaseBackups"; import { useToast } from "@/composables/useToast"; +import { translateBackendError } from "@/i18n/backend-errors"; import { generateDatabaseExportId } from "@/lib/export/databaseExport"; import { nextDatabaseBackupRunAt, normalizeDatabaseBackupTablePatterns, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -213,7 +214,7 @@ async function runNow(schedule: DatabaseBackupSchedule) { if (!run) return; if (run.status === "success") toast(t("databaseBackup.runSuccess", { count: run.files.length }), 3000); else if (run.status === "cancelled") toast(t("databaseBackup.runCancelled"), 3000); - else toast(t("databaseBackup.runFailed", { error: run.error || t("databaseBackup.unknownError") }), 5000); + else toast(t("databaseBackup.runFailed", { error: run.error ? translateBackendError(t, run.error) : t("databaseBackup.unknownError") }), 5000); } catch (error: any) { toast(error?.message || String(error), 5000); } @@ -261,7 +262,7 @@ async function revealBackup(file: DatabaseBackupFile) { try { await api.revealPathInFileManager(file.filePath); } catch (error: any) { - toast(error?.message || String(error), 5000); + toast(translateBackendError(t, error), 5000); } } @@ -345,7 +346,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) { {{ run.connectionName || connectionName(run.connectionId) }} {{ formatDate(run.startedAt) }} {{ t("databaseBackup.fileCount", { count: run.files.length }) }} - {{ run.error }} + {{ translateBackendError(t, run.error) }}
diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 2fd54308e..b8339e304 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -32,6 +32,7 @@ import { import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver"; import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection"; +import { translateBackendError } from "@/i18n/backend-errors"; import { DRIVER_CATEGORIES, getCategoryForAgentDriver, assertAgentDriverCategoriesComplete } from "@/lib/connection/driver-category-definitions"; import { selectUpdatableDrivers, selectStableDrivers, hasAnyUpdatableDriverMatching } from "@/lib/connection/driverListFilter"; @@ -39,6 +40,13 @@ const { t } = useI18n(); const { toast } = useToast(); const isWeb = !isTauriRuntime(); +// Backend errors arrive as plain strings, so translate them before they are +// interpolated into an already-localized wrapper message. +function backendError(e: unknown): string { + const message = e instanceof Error ? e.message : ((e as { message?: string } | null)?.message ?? String(e)); + return translateBackendError(t, message); +} + const props = withDefaults( defineProps<{ updateNotificationsEnabled?: boolean; @@ -166,7 +174,7 @@ async function applyDriverStoreDir(kind: DriverStoreDirKind, newDir: string | nu const { relaunch } = await import("@tauri-apps/plugin-process"); relaunch(); } catch (e: any) { - toast(t("driverStore.driverStoreDirMigrationFailed", { error: e?.message || String(e) }), 5000); + toast(t("driverStore.driverStoreDirMigrationFailed", { error: backendError(e) }), 5000); } finally { driverStoreDirMigrating.value = null; } @@ -427,7 +435,7 @@ async function saveJavaRuntimeConfig() { customJavaPath.value = config.custom_java_path ?? ""; toast(t("driverStore.javaRuntimeSaved")); } catch (e: any) { - toast(t("driverStore.javaRuntimeSaveFailed", { error: e })); + toast(t("driverStore.javaRuntimeSaveFailed", { error: backendError(e) })); } finally { savingJavaRuntime.value = false; } @@ -480,7 +488,7 @@ async function runDriverInstall(dbType: string) { await refreshAgents(); toast(t("driverStore.driverInstallSuccess", { label })); } catch (e: any) { - toast(t("driverStore.driverInstallFailed", { label, error: e })); + toast(t("driverStore.driverInstallFailed", { label, error: backendError(e) })); } finally { installing.value = null; activeAgentOperationId.value = null; @@ -522,7 +530,7 @@ async function upgradeAll() { toast(t("driverStore.upgradeAllSuccess", { count: result.upgraded })); } } catch (e: any) { - toast(t("driverStore.upgradeAllFailed", { error: e })); + toast(t("driverStore.upgradeAllFailed", { error: backendError(e) })); } finally { upgradingAll.value = false; activeAgentOperationId.value = null; @@ -548,7 +556,7 @@ async function uninstallDriver(dbType: string) { await refreshAgents(); toast(t("driverStore.driverUninstallSuccess", { label })); } catch (e: any) { - toast(t("driverStore.driverUninstallFailed", { label, error: e })); + toast(t("driverStore.driverUninstallFailed", { label, error: backendError(e) })); } } @@ -615,7 +623,7 @@ async function importOfflineZip() { await refreshAgents(); toast(t("driverStore.offlineImportSuccess", { count })); } catch (e: any) { - toast(t("driverStore.offlineImportFailed", { error: e })); + toast(t("driverStore.offlineImportFailed", { error: backendError(e) })); } finally { importingZip.value = false; activeAgentOperationId.value = null; @@ -663,7 +671,7 @@ async function importDriverFile(driver: AgentDriverInfo) { try { await installSelectedFile(file); } catch (e: any) { - toast(t("driverStore.driverImportFailed", { label, error: e })); + toast(t("driverStore.driverImportFailed", { label, error: backendError(e) })); } return; } @@ -677,7 +685,7 @@ async function importDriverFile(driver: AgentDriverInfo) { try { await installSelectedFile(selected); } catch (e: any) { - toast(t("driverStore.driverImportFailed", { label, error: e })); + toast(t("driverStore.driverImportFailed", { label, error: backendError(e) })); } } @@ -690,7 +698,7 @@ async function reinstallJre(jreKey: string) { await refreshAgents(); toast(t("driverStore.jreReinstallSuccess", { jre: jreKey })); } catch (e: any) { - toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: e })); + toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: backendError(e) })); } finally { reinstallingJre.value = null; activeAgentOperationId.value = null; @@ -1005,7 +1013,7 @@ async function stopRuntime(runtime: DriverRuntimeInfo) { await loadDriverRuntimeSummary(false); toast(t("driverStore.runtimeStopSuccess", { label: runtime.label })); } catch (e: any) { - toast(t("driverStore.runtimeStopFailed", { label: runtime.label, error: e })); + toast(t("driverStore.runtimeStopFailed", { label: runtime.label, error: backendError(e) })); } finally { runtimeBusy.value = null; } @@ -1018,7 +1026,7 @@ async function restartRuntime(runtime: DriverRuntimeInfo) { await loadDriverRuntimeSummary(false); toast(t("driverStore.runtimeRestartSuccess", { label: runtime.label })); } catch (e: any) { - toast(t("driverStore.runtimeRestartFailed", { label: runtime.label, error: e })); + toast(t("driverStore.runtimeRestartFailed", { label: runtime.label, error: backendError(e) })); } finally { runtimeBusy.value = null; } @@ -1060,7 +1068,7 @@ async function clearDownloadCache() { await loadDriverStoreUsage(); toast(t("driverStore.downloadCacheClearSuccess")); } catch (e: any) { - toast(t("driverStore.downloadCacheClearFailed", { error: e?.message || String(e) }), 5000); + toast(t("driverStore.downloadCacheClearFailed", { error: backendError(e) }), 5000); } finally { clearingDownloadCache.value = false; } diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 0a7532747..17b72e3da 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -1475,7 +1475,7 @@ function finishAgentDriverInstall() { function failAgentDriverInstall(error: unknown) { agentInstallOperationId.value = null; agentInstallRunning.value = false; - agentInstallError.value = errorMessage(error); + agentInstallError.value = translateBackendError(t, errorMessage(error)); showAgentInstallDialog.value = true; } @@ -2780,11 +2780,11 @@ const testResultMessage = computed(() => { const agentInstallPercent = computed(() => driverInstallProgressPercent(agentInstallProgress.value)); const agentInstallProgressLabel = computed(() => { const progress = agentInstallProgress.value; - if (agentInstallError.value) return "安装失败"; - if (!agentInstallRunning.value) return "等待安装"; - if (!progress) return "准备安装驱动..."; - if (progress.step === "jre-extract") return "解压 JRE..."; - const label = progress.step === "jre" ? "下载 JRE" : progress.step === "driver" ? "下载驱动" : progress.step || "安装驱动"; + if (agentInstallError.value) return t("connection.driverInstall.statusFailed"); + if (!agentInstallRunning.value) return t("connection.driverInstall.statusWaiting"); + if (!progress) return t("connection.driverInstall.statusPreparing"); + if (progress.step === "jre-extract") return t("connection.driverInstall.statusExtractingJre"); + const label = progress.step === "jre" ? t("connection.driverInstall.stepJre") : progress.step === "driver" ? t("connection.driverInstall.stepDriver") : progress.step || t("connection.driverInstall.stepDefault"); if (!progress.total) return `${label}...`; return `${label} ${formatInstallSize(progress.downloaded ?? 0)} / ${formatInstallSize(progress.total)} (${agentInstallPercent.value ?? 0}%)`; }); @@ -4874,7 +4874,7 @@ function openExternalUrl(url: string) {
- +
@@ -5104,11 +5104,11 @@ function openExternalUrl(url: string) {
@@ -5817,12 +5817,12 @@ function openExternalUrl(url: string) { @@ -6893,7 +6893,7 @@ function openExternalUrl(url: string) { - {{ agentInstallError ? "驱动安装失败" : "正在安装驱动" }} + {{ agentInstallError ? t("connection.driverInstall.failedTitle") : t("connection.driverInstall.installingTitle") }}
@@ -6911,7 +6911,7 @@ function openExternalUrl(url: string) {
-
完整错误
+
{{ t("connection.driverInstall.fullError") }}
{{ agentInstallError }}
@@ -6919,10 +6919,10 @@ function openExternalUrl(url: string) {
@@ -6931,11 +6931,11 @@ function openExternalUrl(url: string) { - 连接失败 + {{ t("connection.connectFailedTitle") }}
-
完整错误信息
+
{{ t("connection.fullErrorMessage") }}
{{ connectionErrorDetail }}
@@ -6946,9 +6946,9 @@ function openExternalUrl(url: string) { - +
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 93861e297..7a07e0619 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -2304,7 +2304,7 @@ const aiEndpointHint = computed(() => { return t("ai.anthropicMessagesHint"); } if (aiEditProvider.value === "openai-compatible" || aiEditProvider.value === "custom") { - return "大多数 OpenAI 兼容 API 需要 /v1 路径前缀"; + return t("ai.openAiCompatibleEndpointHint"); } return ""; }); @@ -4525,7 +4525,7 @@ onUnmounted(cleanupPreviewEditor); GitHub Gist - Gitee 代码片段 + {{ t("settings.syncSnippetProviderGitee") }} diff --git a/apps/desktop/src/components/export/ExportProgressDialog.vue b/apps/desktop/src/components/export/ExportProgressDialog.vue index 16c5aba07..5edf8facc 100644 --- a/apps/desktop/src/components/export/ExportProgressDialog.vue +++ b/apps/desktop/src/components/export/ExportProgressDialog.vue @@ -7,6 +7,7 @@ import { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, X } from "@luc import { useToast } from "@/composables/useToast"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import * as api from "@/lib/backend/api"; +import { translateBackendError } from "@/i18n/backend-errors"; const { t } = useI18n(); const { toast } = useToast(); @@ -30,6 +31,7 @@ const emit = defineEmits<{ "update:open": [value: boolean]; }>(); +const translatedErrorMessage = computed(() => (props.errorMessage ? translateBackendError(t, props.errorMessage) : "")); const isActive = computed(() => props.status === "Running" || props.status === "Writing"); const isFinished = computed(() => props.status === "Done" || props.status === "Error" || props.status === "Cancelled"); const canRevealFile = computed(() => props.status === "Done" && !!props.filePath && isTauriRuntime()); @@ -53,8 +55,7 @@ async function revealExportFile() { try { await api.revealPathInFileManager(props.filePath); } catch (error) { - const message = error instanceof Error ? error.message : String(error); - toast(t("exportProgress.openFolderFailed", { message }), 5000); + toast(t("exportProgress.openFolderFailed", { message: translateBackendError(t, error) }), 5000); } finally { isRevealing.value = false; } @@ -106,7 +107,7 @@ async function revealExportFile() {