fix(i18n): translate backend errors at UI boundaries
This commit is contained in:
parent
4422ac8e01
commit
2f44d36018
|
|
@ -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<string> {
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<span>{{ run.connectionName || connectionName(run.connectionId) }}</span>
|
||||
<span>{{ formatDate(run.startedAt) }}</span>
|
||||
<span>{{ t("databaseBackup.fileCount", { count: run.files.length }) }}</span>
|
||||
<span v-if="run.error" class="break-all text-destructive">{{ run.error }}</span>
|
||||
<span v-if="run.error" class="break-all text-destructive">{{ translateBackendError(t, run.error) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<PopoverContent class="w-auto p-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="color" :value="form.color" @input="handleCustomColorPicked(($event.target as HTMLInputElement).value)" class="h-6 w-6 cursor-pointer rounded border-0 p-0" />
|
||||
<Input type="text" :value="customColorInput || form.color" @input="handleCustomColorInput(($event.target as HTMLInputElement).value)" class="w-28 h-7 text-xs font-mono" :placeholder="'#ff0000 或 rgba(…)'" />
|
||||
<Input type="text" :value="customColorInput || form.color" @input="handleCustomColorInput(($event.target as HTMLInputElement).value)" class="w-28 h-7 text-xs font-mono" :placeholder="t('connection.customColorPlaceholder')" />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
|
@ -5104,11 +5104,11 @@ function openExternalUrl(url: string) {
|
|||
</div>
|
||||
<template v-if="form.db_type === 'h2' || form.db_type === 'access'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.user") }}{{ form.db_type === "access" ? "(可选)" : "" }}</Label>
|
||||
<Label :class="connectionLabelClass">{{ t("connection.user") }}{{ form.db_type === "access" ? t("connection.optionalSuffix") : "" }}</Label>
|
||||
<Input v-model="form.username" class="col-span-3" :placeholder="form.db_type === 'access' ? '' : 'sa'" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}{{ form.db_type === "access" ? "(可选)" : "" }}</Label>
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}{{ form.db_type === "access" ? t("connection.optionalSuffix") : "" }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -5817,12 +5817,12 @@ function openExternalUrl(url: string) {
|
|||
<template v-else-if="form.db_type === 'turso'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.host") }}</Label>
|
||||
<Input v-model="form.host" class="col-span-3" placeholder="your-database.turso.io 或 libsql://your-database.turso.io" />
|
||||
<Input v-model="form.host" class="col-span-3" :placeholder="t('connection.tursoHostPlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 text-xs text-muted-foreground">支持 libsql:// 或 https:// 协议,也可以只填主机名(自动使用 HTTPS)</p>
|
||||
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.tursoHostHint") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
|
|
@ -5832,12 +5832,12 @@ function openExternalUrl(url: string) {
|
|||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 text-xs text-muted-foreground">使用 <code class="px-1 py-0.5 rounded bg-muted text-xs">turso db tokens create <database-name></code> 创建 token</p>
|
||||
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.tursoTokenHint") }} <code class="px-1 py-0.5 rounded bg-muted text-xs">turso db tokens create <database-name></code></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.urlParams") }}</Label>
|
||||
<Input v-model="form.url_params" class="col-span-3" placeholder="authToken=xxx(可选,优先使用上面的 Token 字段)" />
|
||||
<Input v-model="form.url_params" class="col-span-3" :placeholder="t('connection.tursoUrlParamsPlaceholder')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -6893,7 +6893,7 @@ function openExternalUrl(url: string) {
|
|||
<Dialog :open="showAgentInstallDialog" @update:open="setAgentInstallDialogOpen">
|
||||
<DialogContent class="sm:max-w-[520px]" @interact-outside.prevent @escape-key-down.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ agentInstallError ? "驱动安装失败" : "正在安装驱动" }}</DialogTitle>
|
||||
<DialogTitle>{{ agentInstallError ? t("connection.driverInstall.failedTitle") : t("connection.driverInstall.installingTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
|
@ -6911,7 +6911,7 @@ function openExternalUrl(url: string) {
|
|||
</div>
|
||||
|
||||
<div v-if="agentInstallError" class="space-y-2">
|
||||
<div class="text-sm font-medium text-destructive">完整错误</div>
|
||||
<div class="text-sm font-medium text-destructive">{{ t("connection.driverInstall.fullError") }}</div>
|
||||
<pre class="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md border bg-muted/30 p-3 text-xs leading-5 text-destructive">{{ agentInstallError }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -6919,10 +6919,10 @@ function openExternalUrl(url: string) {
|
|||
<DialogFooter class="gap-2">
|
||||
<Button v-if="agentInstallError" variant="outline" @click="copyAgentInstallError">
|
||||
<Copy class="mr-1.5 h-3.5 w-3.5" />
|
||||
复制错误
|
||||
{{ t("connection.copyError") }}
|
||||
</Button>
|
||||
<Button :disabled="!canCloseAgentInstallDialog" @click="showAgentInstallDialog = false">
|
||||
{{ agentInstallError ? "关闭" : "安装中..." }}
|
||||
{{ agentInstallError ? t("common.close") : t("connection.driverInstall.installingButton") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -6931,11 +6931,11 @@ function openExternalUrl(url: string) {
|
|||
<Dialog v-model:open="showConnectionErrorDialog">
|
||||
<DialogContent class="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>连接失败</DialogTitle>
|
||||
<DialogTitle>{{ t("connection.connectFailedTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm text-muted-foreground">完整错误信息</div>
|
||||
<div class="text-sm text-muted-foreground">{{ t("connection.fullErrorMessage") }}</div>
|
||||
<pre class="max-h-72 overflow-auto whitespace-pre-wrap break-words rounded-md border bg-muted/30 p-3 text-xs leading-5 text-destructive">{{ connectionErrorDetail }}</pre>
|
||||
</div>
|
||||
|
||||
|
|
@ -6946,9 +6946,9 @@ function openExternalUrl(url: string) {
|
|||
</Button>
|
||||
<Button variant="outline" @click="copyConnectionErrorDetail">
|
||||
<Copy class="mr-1.5 h-3.5 w-3.5" />
|
||||
复制错误
|
||||
{{ t("connection.copyError") }}
|
||||
</Button>
|
||||
<Button @click="showConnectionErrorDialog = false">关闭</Button>
|
||||
<Button @click="showConnectionErrorDialog = false">{{ t("common.close") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -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);
|
|||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="github">GitHub Gist</SelectItem>
|
||||
<SelectItem value="gitee">Gitee 代码片段</SelectItem>
|
||||
<SelectItem value="gitee">{{ t("settings.syncSnippetProviderGitee") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</template>
|
||||
<template v-else-if="status === 'Error'">
|
||||
<XCircle class="h-4 w-4 text-destructive" />
|
||||
<span class="text-destructive">{{ errorMessage || t("exportProgress.error") }}</span>
|
||||
<span class="text-destructive">{{ translatedErrorMessage || t("exportProgress.error") }}</span>
|
||||
</template>
|
||||
<template v-else-if="status === 'Cancelled'">
|
||||
<AlertCircle class="h-4 w-4 text-yellow-500" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, CheckCircle2, XCircle, AlertCircle, X, FileDown, DatabaseBackup, FileCode2, ArrowRightLeft, ChevronRight } from "@lucide/vue";
|
||||
import { formatDataTransferDuration, useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { tasks, activeCount, hasActive, clearFinished, cancelTask, removeTask } = useExportTracker();
|
||||
|
|
@ -185,8 +186,8 @@ function failureDetailCount(task: ExportTask) {
|
|||
|
||||
<div class="min-w-0 text-muted-foreground">
|
||||
<span class="break-words tabular-nums">{{ rowsText(task) }}</span>
|
||||
<span v-if="task.status === 'Error' && task.errorMessage" class="mt-1 block whitespace-normal break-words text-destructive" :title="task.errorMessage">
|
||||
{{ task.errorMessage }}
|
||||
<span v-if="task.status === 'Error' && task.errorMessage" class="mt-1 block whitespace-normal break-words text-destructive" :title="translateBackendError(t, task.errorMessage)">
|
||||
{{ translateBackendError(t, task.errorMessage) }}
|
||||
</span>
|
||||
<template v-if="task.kind === 'data-transfer' && failureDetailCount(task) > 0">
|
||||
<button class="mt-1.5 flex items-center gap-1 text-xs font-medium text-foreground hover:text-primary" :aria-expanded="failureDetailsExpanded(task.exportId)" @click="toggleFailureDetails(task.exportId)">
|
||||
|
|
|
|||
|
|
@ -925,12 +925,12 @@ async function onFileSelected(event: Event) {
|
|||
<template v-else>
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b bg-muted/10">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">{{ t("dataGenerate.target") }}:</span>
|
||||
<span class="text-xs text-muted-foreground">{{ t("dataGenerate.target") }}:</span>
|
||||
<select v-if="generatedResults.length > 1" v-model="previewTableIndex" class="h-7 rounded border bg-background px-2 text-xs">
|
||||
<option v-for="(r, i) in generatedResults" :key="i" :value="i">{{ r.tableName }}</option>
|
||||
</select>
|
||||
<span v-else class="text-sm font-medium">{{ generatedResults[0].tableName }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ currentPreview.rows.length }} 行</span>
|
||||
<span class="text-xs text-muted-foreground">{{ t("dataGenerate.previewRowCount", { count: currentPreview.rows.length }) }}</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="regenerate">{{ t("dataGenerate.regenerate") }}</Button>
|
||||
</div>
|
||||
|
|
@ -1041,7 +1041,7 @@ async function onFileSelected(event: Event) {
|
|||
<Dialog v-model:open="optionsDialogOpen">
|
||||
<DialogContent class="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="text-sm">生成选项</DialogTitle>
|
||||
<DialogTitle class="text-sm">{{ t("dataGenerate.generateOptions") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3 py-2">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const addressTypes = [
|
||||
{ value: "line1", label: "第1行地址" },
|
||||
{ value: "line2", label: "第2行地址" },
|
||||
{ value: "full", label: "完整地址" },
|
||||
];
|
||||
const { t } = useI18n();
|
||||
|
||||
const regions = [
|
||||
{ value: "us", label: "美国" },
|
||||
{ value: "uk", label: "英国" },
|
||||
{ value: "cn", label: "中国 (English)" },
|
||||
{ value: "jp", label: "日本 (English)" },
|
||||
];
|
||||
const addressTypes = computed(() => [
|
||||
{ value: "line1", label: t("dataGenerate.addressTypes.line1") },
|
||||
{ value: "line2", label: t("dataGenerate.addressTypes.line2") },
|
||||
{ value: "full", label: t("dataGenerate.addressTypes.full") },
|
||||
]);
|
||||
|
||||
const regions = computed(() => [
|
||||
{ value: "us", label: t("dataGenerate.countries.us") },
|
||||
{ value: "uk", label: t("dataGenerate.countries.uk") },
|
||||
{ value: "cn", label: t("dataGenerate.countries.cnEnglish") },
|
||||
{ value: "jp", label: t("dataGenerate.countries.jpEnglish") },
|
||||
]);
|
||||
|
||||
if (!props.params.addressType) props.params.addressType = "line1";
|
||||
if (!props.params.regions) props.params.regions = ["us"];
|
||||
|
|
@ -90,14 +93,14 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="t in addressTypes" :key="t.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.addressType || 'line1') === t.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.addressType = t.value">{{ t.label }}</button>
|
||||
<button v-for="a in addressTypes" :key="a.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.addressType || 'line1') === a.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.addressType = a.value">{{ a.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">地区</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.regionLabel") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="r in regions" :key="r.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.regions ?? []).includes(r.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleRegion(r.value)">{{ r.label }}</button>
|
||||
</div>
|
||||
|
|
@ -105,7 +108,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const barcodeTypes = [
|
||||
{ value: "ean8", label: "EAN8", regex: "[0-9]{8}" },
|
||||
{ value: "ean13", label: "EAN13", regex: "[0-9]{13}" },
|
||||
|
|
@ -78,18 +81,18 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="b in barcodeTypes" :key="b.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.barcodeTypes ?? []).includes(b.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleBarcode(b.value)">{{ b.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
|
||||
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
|
||||
<div class="font-mono text-xs bg-background rounded border px-2 py-1 break-all">{{ currentRegex }}</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,20 +4,23 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const regions = [
|
||||
{ value: "us", label: "美国" },
|
||||
{ value: "uk", label: "英国" },
|
||||
{ value: "cn", label: "中国" },
|
||||
{ value: "jp", label: "日本" },
|
||||
];
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "native", label: "本地语言" },
|
||||
];
|
||||
const regions = computed(() => [
|
||||
{ value: "us", label: t("dataGenerate.countries.us") },
|
||||
{ value: "uk", label: t("dataGenerate.countries.uk") },
|
||||
{ value: "cn", label: t("dataGenerate.countries.cn") },
|
||||
{ value: "jp", label: t("dataGenerate.countries.jp") },
|
||||
]);
|
||||
|
||||
const languages = computed(() => [
|
||||
{ value: "en", label: t("dataGenerate.cityLanguages.english") },
|
||||
{ value: "native", label: t("dataGenerate.cityLanguages.native") },
|
||||
]);
|
||||
|
||||
if (!props.params.regions) props.params.regions = ["us"];
|
||||
if (!props.params.languages) props.params.languages = ["en"];
|
||||
|
|
@ -96,14 +99,14 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">地区</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.regionLabel") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="r in regions" :key="r.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.regions ?? []).includes(r.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleRegion(r.value)">{{ r.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -111,7 +114,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dataMap: Record<string, string[]> = {
|
||||
en: ["Red", "Blue", "Green", "Black", "White", "Yellow", "Purple", "Orange", "Pink", "Brown", "Gray", "Cyan", "Jasmine", "Teal", "Maroon", "Navy", "Olive", "Coral"],
|
||||
zh: ["红色", "蓝色", "绿色", "黑色", "白色", "黄色", "紫色", "橙色", "粉色", "棕色", "灰色", "青色", "茉莉色", "蓝绿色", "褐红色", "藏青", "橄榄", "珊瑚色"],
|
||||
|
|
@ -33,7 +36,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="l in [
|
||||
|
|
@ -54,7 +57,7 @@ function refresh() {
|
|||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languageOptions = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh_pinyin", label: "Chinese (Pinyin)" },
|
||||
|
|
@ -75,7 +78,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="l in languageOptions" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -83,7 +86,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dateFormats = [
|
||||
{ value: "MM/YY", label: "MM/YY" },
|
||||
{ value: "MM/YYYY", label: "MM/YYYY" },
|
||||
|
|
@ -59,25 +62,25 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">日期类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.dateType") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="f in dateFormats" :key="f.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.ccDateFormat || 'MM/YY') === f.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.ccDateFormat = f.value">{{ f.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">年份范围(相对于当前年的偏移量)</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.yearOffsetRange") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">最小</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.min") }}</Label>
|
||||
<Input v-model.number="params.ccYearOffsetMin" type="number" class="h-6 w-20 text-xs" />
|
||||
<Label class="text-muted-foreground">最大</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.max") }}</Label>
|
||||
<Input v-model.number="params.ccYearOffsetMax" type="number" class="h-6 w-20 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const cardTypes = [
|
||||
{ value: "american_express", label: "American Express" },
|
||||
{ value: "jcb", label: "JCB" },
|
||||
|
|
@ -95,7 +98,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">类型选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="c in cardTypes" :key="c.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.cardTypes ?? []).includes(c.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleCardType(c.value)">{{ c.label }}</button>
|
||||
</div>
|
||||
|
|
@ -103,7 +106,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const cardTypes = [
|
||||
{ value: "american_express", label: "American Express" },
|
||||
{ value: "jcb", label: "JCB" },
|
||||
|
|
@ -47,7 +50,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">类型选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="c in cardTypes" :key="c.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.cardTypes ?? []).includes(c.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleCardType(c.value)">{{ c.label }}</button>
|
||||
</div>
|
||||
|
|
@ -55,7 +58,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const previewVal = computed(() => {
|
|||
return `${year}-${month}-${day}`;
|
||||
});
|
||||
|
||||
const weekdayLabels = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
const weekdayLabels = computed(() => (["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const).map((d) => t(`dataGenerate.weekdayShort.${d}`)));
|
||||
|
||||
function toggleWeekday(d: number) {
|
||||
if (!props.params.weekdays) props.params.weekdays = [];
|
||||
|
|
@ -77,11 +77,11 @@ function refresh() {
|
|||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">星期</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.weekday") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="!params.weekdayMode || params.weekdayMode === 'all' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'all'">全部</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'weekday' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'weekday'">工作日</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'custom' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'custom'">自定义</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="!params.weekdayMode || params.weekdayMode === 'all' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'all'">{{ t("dataGenerate.all") }}</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'weekday' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'weekday'">{{ t("dataGenerate.weekdayOnly") }}</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'custom' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'custom'">{{ t("dataGenerate.custom") }}</button>
|
||||
</div>
|
||||
<div v-if="params.weekdayMode === 'custom'" class="flex gap-1 mt-2">
|
||||
<button v-for="(_, i) in 7" :key="i" type="button" class="w-7 h-7 rounded text-xs border" :class="(params.weekdays ?? []).includes(i) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleWeekday(i)">{{ weekdayLabels[i] }}</button>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const previewVal = computed(() => {
|
|||
return `${formatLocalDate(d)} ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
||||
});
|
||||
|
||||
const weekdayLabels = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
const weekdayLabels = computed(() => (["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const).map((d) => t(`dataGenerate.weekdayShort.${d}`)));
|
||||
|
||||
function toggleWeekday(d: number) {
|
||||
if (!props.params.weekdays) props.params.weekdays = [];
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh", label: "简体中文" },
|
||||
|
|
@ -50,7 +53,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -58,7 +61,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const defaultDomains = "gmail.com\noutlook.com\nyahoo.com\nexample.com\ntest.org";
|
||||
|
||||
function pick<T>(arr: T[]): T {
|
||||
|
|
@ -27,7 +30,7 @@ const previewVal = computed(() => {
|
|||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (domains.length === 0) return "(无域名)";
|
||||
if (domains.length === 0) return t("dataGenerate.noDomain");
|
||||
return `${genName()}@${pick(domains)}`;
|
||||
});
|
||||
|
||||
|
|
@ -39,13 +42,13 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">域(Domain)列表</Label>
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.domainList") }}</Label>
|
||||
<textarea v-model="params.emailDomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="defaultDomains" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function refresh() {
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.values") }}</Label>
|
||||
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个值 first second third" />
|
||||
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.enumValues')" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const extensionCategoryMap: Record<string, string[]> = {
|
||||
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
|
||||
document: [".txt", ".pdf", ".doc", ".docx", ".rtf", ".odt"],
|
||||
|
|
@ -20,18 +23,7 @@ const extensionCategoryMap: Record<string, string[]> = {
|
|||
database: [".db", ".sqlite", ".sql", ".mdb", ".accdb"],
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
image: "图像",
|
||||
document: "文档",
|
||||
spreadsheet: "表格",
|
||||
presentation: "演示文稿",
|
||||
audio: "音频",
|
||||
video: "视频",
|
||||
code: "代码",
|
||||
archive: "压缩包",
|
||||
web: "网页",
|
||||
database: "数据库",
|
||||
};
|
||||
const categoryLabels = computed<Record<string, string>>(() => Object.fromEntries((["image", "document", "spreadsheet", "presentation", "audio", "video", "code", "archive", "web", "database"] as const).map((key) => [key, t(`dataGenerate.extensionCategories.${key}`)])));
|
||||
|
||||
if (!props.params.extensionCategory) {
|
||||
props.params.extensionCategory = "image";
|
||||
|
|
@ -56,18 +48,18 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">扩展名类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.extensionType") }}</div>
|
||||
<select v-model="params.extensionCategory" class="w-full rounded border bg-background px-2 py-1 text-xs">
|
||||
<option v-for="(label, key) in categoryLabels" :key="key" :value="key">{{ label }}</option>
|
||||
</select>
|
||||
<div class="mt-2 text-xs text-muted-foreground">扩展名列表:</div>
|
||||
<div class="mt-2 text-xs text-muted-foreground">{{ t("dataGenerate.extensionList") }}</div>
|
||||
<div class="mt-1 flex flex-wrap gap-1">
|
||||
<span v-for="ext in extensionCategoryMap[params.extensionCategory || 'image'] || []" :key="ext" class="px-1.5 py-0.5 rounded bg-background border text-xs font-mono">{{ ext }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const extensionCategoryMap: Record<string, string[]> = {
|
||||
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
|
||||
document: [".txt", ".pdf", ".doc", ".docx", ".rtf", ".odt"],
|
||||
|
|
@ -20,18 +23,7 @@ const extensionCategoryMap: Record<string, string[]> = {
|
|||
database: [".db", ".sqlite", ".sql", ".mdb", ".accdb"],
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
image: "图像",
|
||||
document: "文档",
|
||||
spreadsheet: "表格",
|
||||
presentation: "演示文稿",
|
||||
audio: "音频",
|
||||
video: "视频",
|
||||
code: "代码",
|
||||
archive: "压缩包",
|
||||
web: "网页",
|
||||
database: "数据库",
|
||||
};
|
||||
const categoryLabels = computed<Record<string, string>>(() => Object.fromEntries((["image", "document", "spreadsheet", "presentation", "audio", "video", "code", "archive", "web", "database"] as const).map((key) => [key, t(`dataGenerate.extensionCategories.${key}`)])));
|
||||
|
||||
if (!props.params.includeExtension) {
|
||||
props.params.includeExtension = true;
|
||||
|
|
@ -66,22 +58,22 @@ function refresh() {
|
|||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<label class="text-xs text-muted-foreground flex items-center gap-2">
|
||||
<input type="checkbox" v-model="params.includeExtension" class="rounded" />
|
||||
包含扩展名
|
||||
{{ t("dataGenerate.includeExtension") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3" v-if="params.includeExtension">
|
||||
<div class="text-xs text-muted-foreground mb-2">扩展名类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.extensionType") }}</div>
|
||||
<select v-model="params.extensionCategory" class="w-full rounded border bg-background px-2 py-1 text-xs">
|
||||
<option v-for="(label, key) in categoryLabels" :key="key" :value="key">{{ label }}</option>
|
||||
</select>
|
||||
<div class="mt-2 text-xs text-muted-foreground">可用扩展名:</div>
|
||||
<div class="mt-2 text-xs text-muted-foreground">{{ t("dataGenerate.availableExtensions") }}</div>
|
||||
<div class="mt-1 flex flex-wrap gap-1">
|
||||
<span v-for="ext in extensionCategoryMap[params.extensionCategory || 'image'] || []" :key="ext" class="px-1.5 py-0.5 rounded bg-background border text-xs font-mono">{{ ext }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const extensionCategoryMap: Record<string, string[]> = {
|
||||
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
|
||||
document: [".txt", ".pdf", ".doc", ".docx", ".rtf", ".odt"],
|
||||
|
|
@ -20,18 +23,7 @@ const extensionCategoryMap: Record<string, string[]> = {
|
|||
database: [".db", ".sqlite", ".sql", ".mdb", ".accdb"],
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
image: "图像",
|
||||
document: "文档",
|
||||
spreadsheet: "表格",
|
||||
presentation: "演示文稿",
|
||||
audio: "音频",
|
||||
video: "视频",
|
||||
code: "代码",
|
||||
archive: "压缩包",
|
||||
web: "网页",
|
||||
database: "数据库",
|
||||
};
|
||||
const categoryLabels = computed<Record<string, string>>(() => Object.fromEntries((["image", "document", "spreadsheet", "presentation", "audio", "video", "code", "archive", "web", "database"] as const).map((key) => [key, t(`dataGenerate.extensionCategories.${key}`)])));
|
||||
|
||||
if (!props.params.pathTypes) {
|
||||
props.params.pathTypes = ["windows", "macos", "linux"];
|
||||
|
|
@ -104,7 +96,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">路径类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.pathType") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="pt in [
|
||||
|
|
@ -125,22 +117,22 @@ function refresh() {
|
|||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<label class="text-xs text-muted-foreground flex items-center gap-2">
|
||||
<input type="checkbox" v-model="params.includeFileName" class="rounded" />
|
||||
包含文件名称
|
||||
{{ t("dataGenerate.includeFileName") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">扩展名类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.extensionType") }}</div>
|
||||
<select v-model="params.extensionCategory" class="w-full rounded border bg-background px-2 py-1 text-xs">
|
||||
<option v-for="(label, key) in categoryLabels" :key="key" :value="key">{{ label }}</option>
|
||||
</select>
|
||||
<div class="mt-2 text-xs text-muted-foreground">可用扩展名:</div>
|
||||
<div class="mt-2 text-xs text-muted-foreground">{{ t("dataGenerate.availableExtensions") }}</div>
|
||||
<div class="mt-1 flex flex-wrap gap-1">
|
||||
<span v-for="ext in extensionCategoryMap[params.extensionCategory || 'image'] || []" :key="ext" class="px-1.5 py-0.5 rounded bg-background border text-xs font-mono">{{ ext }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams; config: any; connectionId?: string; database?: string }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const schemas = ref<string[]>([]);
|
||||
const tables = ref<{ name: string }[]>([]);
|
||||
const columns = ref<{ name: string }[]>([]);
|
||||
|
|
@ -13,11 +16,11 @@ const loadingSchemas = ref(false);
|
|||
const loadingTables = ref(false);
|
||||
const loadingColumns = ref(false);
|
||||
|
||||
const fkModes = [
|
||||
{ value: "random", label: "随机" },
|
||||
{ value: "unique", label: "不重复" },
|
||||
{ value: "repeat", label: "重复每个值" },
|
||||
];
|
||||
const fkModes = computed(() => [
|
||||
{ value: "random", label: t("dataGenerate.genModes.random") },
|
||||
{ value: "unique", label: t("dataGenerate.genModes.unique") },
|
||||
{ value: "repeat", label: t("dataGenerate.genModes.repeat") },
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSchemas();
|
||||
|
|
@ -84,30 +87,30 @@ function onTableChange(val: string) {
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3 space-y-2">
|
||||
<div class="grid grid-cols-[60px_1fr] items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">模式</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.schema") }}</Label>
|
||||
<select v-model="props.params.fkSchema" class="h-7 rounded border bg-background px-2 text-xs" @change="onSchemaChange(($event.target as HTMLSelectElement).value)">
|
||||
<option v-if="loadingSchemas" disabled>加载中...</option>
|
||||
<option v-if="loadingSchemas" disabled>{{ t("dataGenerate.loading") }}</option>
|
||||
<option v-for="s in schemas" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-[60px_1fr] items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">表</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.tableLabel") }}</Label>
|
||||
<select v-model="props.params.fkTable" class="h-7 rounded border bg-background px-2 text-xs" @change="onTableChange(($event.target as HTMLSelectElement).value)">
|
||||
<option v-if="loadingTables" disabled>加载中...</option>
|
||||
<option v-for="t in tables" :key="t.name" :value="t.name">{{ t.name }}</option>
|
||||
<option v-if="loadingTables" disabled>{{ t("dataGenerate.loading") }}</option>
|
||||
<option v-for="tbl in tables" :key="tbl.name" :value="tbl.name">{{ tbl.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-[60px_1fr] items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">字段</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.columnLabel") }}</Label>
|
||||
<select v-model="props.params.fkField" class="h-7 rounded border bg-background px-2 text-xs">
|
||||
<option v-if="loadingColumns" disabled>加载中...</option>
|
||||
<option v-if="loadingColumns" disabled>{{ t("dataGenerate.loading") }}</option>
|
||||
<option v-for="c in columns" :key="c.name" :value="c.name">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">生成模式</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.genMode") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs flex-wrap">
|
||||
<button v-for="m in fkModes" :key="m.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.fkMode || 'random') === m.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.fkMode = m.value as 'random' | 'unique' | 'repeat'">
|
||||
{{ m.label }}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const nameFormats: Array<{ value: "full" | "last" | "first"; label: string }> = [
|
||||
{ value: "full", label: "全名" },
|
||||
{ value: "last", label: "仅姓氏" },
|
||||
{ value: "first", label: "仅名字" },
|
||||
];
|
||||
const { t } = useI18n();
|
||||
|
||||
const nameFormats = computed<Array<{ value: "full" | "last" | "first"; label: string }>>(() => [
|
||||
{ value: "full", label: t("dataGenerate.nameFormats.full") },
|
||||
{ value: "last", label: t("dataGenerate.nameFormats.last") },
|
||||
{ value: "first", label: t("dataGenerate.nameFormats.first") },
|
||||
]);
|
||||
|
||||
const languageOptions = [
|
||||
{ value: "en", label: "English" },
|
||||
|
|
@ -78,14 +81,14 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">格式类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.nameFormat") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="f in nameFormats" :key="f.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.nameFormat || 'full') === f.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.nameFormat = f.value">{{ f.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="l in languageOptions" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -93,7 +96,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh", label: "简体中文" },
|
||||
|
|
@ -43,7 +46,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -51,7 +54,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.urlSubdomains) {
|
||||
props.params.urlSubdomains = "auth.\ndrive.\nimage.\nwww.\napi.\nmail.\nshop.\nblog.";
|
||||
}
|
||||
|
|
@ -47,16 +50,16 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">子域</Label>
|
||||
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个子域 auth. www. api." />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.subdomains") }}</Label>
|
||||
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.subdomains')" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">顶级域</Label>
|
||||
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个TLD .com .cn .io" />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.tlds") }}</Label>
|
||||
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.tlds')" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const isGenerate = computed(() => props.params.imageMode !== "folder");
|
|||
<Input v-model="params.folderPath" type="text" placeholder="/path/to/images" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-2 mb-1">{{ t("dataGenerate.filterByExtension") }}</div>
|
||||
<textarea v-model="params.fileExtensions" rows="3" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个扩展名 png jpg txt" />
|
||||
<textarea v-model="params.fileExtensions" rows="3" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.fileExtensions')" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh", label: "简体中文" },
|
||||
|
|
@ -50,7 +53,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -58,7 +61,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ipTypes = [
|
||||
{ value: "ipv4", label: "IPv4", regex: "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" },
|
||||
{ value: "ipv6", label: "IPv6", regex: "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$" },
|
||||
|
|
@ -47,18 +50,18 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">IP地址类型</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.ipType") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="t in ipTypes" :key="t.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.ipType || 'ipv4') === t.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.ipType = t.value">{{ t.label }}</button>
|
||||
<button v-for="x in ipTypes" :key="x.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.ipType || 'ipv4') === x.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.ipType = x.value">{{ x.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
|
||||
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
|
||||
<div class="font-mono text-xs bg-background rounded border px-2 py-1 break-all">{{ currentRegex }}</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh", label: "简体中文" },
|
||||
|
|
@ -43,7 +46,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -51,7 +54,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.pattern) {
|
||||
props.params.pattern = "([0-9a-f]{2}[:]){5}([0-9a-f]{2})";
|
||||
}
|
||||
|
|
@ -33,12 +36,12 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
|
||||
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
|
||||
<input v-model="params.pattern" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh", label: "简体中文" },
|
||||
|
|
@ -43,7 +46,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -51,7 +54,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<Label class="text-muted-foreground shrink-0">数字类型</Label>
|
||||
<Label class="text-muted-foreground shrink-0">{{ t("dataGenerate.numberType") }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType !== 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('integer')">整数</button>
|
||||
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType === 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('decimal')">小数</button>
|
||||
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType !== 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('integer')">{{ t("dataGenerate.integer") }}</button>
|
||||
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType === 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('decimal')">{{ t("dataGenerate.decimal") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isDecimal" class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs mt-2">
|
||||
<Label class="text-muted-foreground">小数位数</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.decimalPlaces") }}</Label>
|
||||
<Input v-model.number="params.decimalPlaces" type="number" min="0" max="10" class="h-7 w-20 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.values) {
|
||||
props.params.values = "Credit Card\nPayPal\nApple Pay\nGoogle Pay\nBank Transfer\nCash\nCryptocurrency\nAlipay\nWeChat Pay";
|
||||
}
|
||||
|
|
@ -18,7 +21,7 @@ const previewVal = computed(() => {
|
|||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (vals.length === 0) return "(无)";
|
||||
if (vals.length === 0) return t("dataGenerate.noValue");
|
||||
return vals[Math.floor(Math.random() * vals.length)];
|
||||
});
|
||||
|
||||
|
|
@ -31,13 +34,13 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">值列表</Label>
|
||||
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个支付方式 Credit Card PayPal Apple Pay" />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.valueList") }}</Label>
|
||||
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.paymentMethods')" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ const props = defineProps<{ params: GeneratorParams }>();
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
const regions = [
|
||||
{ value: "us", label: "美国" },
|
||||
{ value: "uk", label: "英国" },
|
||||
{ value: "cn", label: "中国" },
|
||||
{ value: "jp", label: "日本" },
|
||||
{ value: "other", label: "其他" },
|
||||
];
|
||||
const regions = computed(() => [
|
||||
{ value: "us", label: t("dataGenerate.countries.us") },
|
||||
{ value: "uk", label: t("dataGenerate.countries.uk") },
|
||||
{ value: "cn", label: t("dataGenerate.countries.cn") },
|
||||
{ value: "jp", label: t("dataGenerate.countries.jp") },
|
||||
{ value: "other", label: t("dataGenerate.countries.other") },
|
||||
]);
|
||||
|
||||
function randInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
|
@ -77,22 +77,22 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">格式</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.formatLabel") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="(params.phoneFormat || 'domestic') === 'domestic' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'domestic'">国内</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.phoneFormat === 'international' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'international'">国际</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="(params.phoneFormat || 'domestic') === 'domestic' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'domestic'">{{ t("dataGenerate.phoneDomestic") }}</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.phoneFormat === 'international' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'international'">{{ t("dataGenerate.phoneInternational") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<input id="phone-sep" type="checkbox" class="h-3.5 w-3.5 accent-primary" :checked="!!params.phoneSeparator" @change="params.phoneSeparator = !params.phoneSeparator" />
|
||||
<Label for="phone-sep" class="text-muted-foreground">包含分隔符</Label>
|
||||
<Label for="phone-sep" class="text-muted-foreground">{{ t("dataGenerate.includeSeparators") }}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">地区选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.regionLabel") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="r in regions" :key="r.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.phoneRegions ?? []).includes(r.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleRegion(r.value)">{{ r.label }}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dataMap: Record<string, string[]> = {
|
||||
en: ["Electronics", "Clothing", "Food", "Books", "Home Goods", "Sports", "Toys", "Beauty", "Furniture", "Automotive", "Garden", "Pet Supplies", "Video Games", "Music Instruments", "Office Supplies"],
|
||||
zh: ["电子", "服装", "食品", "图书", "家居", "运动", "玩具", "美妆", "家具", "汽车用品", "园艺", "宠物用品", "视频游戏", "乐器", "办公用品"],
|
||||
|
|
@ -33,7 +36,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="l in [
|
||||
|
|
@ -54,7 +57,7 @@ function refresh() {
|
|||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.productKeywords) {
|
||||
props.params.productKeywords = "Apple\nCherry\nOrange\nMango\nBanana\nLemon\nGrape\nPeach\nPear\nMelon";
|
||||
}
|
||||
|
|
@ -32,7 +35,7 @@ const previewVal = computed(() => {
|
|||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (keywords.length === 0) return "(无)";
|
||||
if (keywords.length === 0) return t("dataGenerate.noValue");
|
||||
const mode = Math.random();
|
||||
if (mode < 0.4) {
|
||||
return pick(keywords);
|
||||
|
|
@ -51,12 +54,12 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">使用关键字生成</Label>
|
||||
<textarea v-model="params.productKeywords" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个关键字 Apple Cherry Orange" />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.useKeywords") }}</Label>
|
||||
<textarea v-model="params.productKeywords" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.keywords')" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const previewKey = ref(0);
|
||||
const previewVal = computed(() => {
|
||||
void previewKey.value;
|
||||
const p = props.params.pattern ?? "";
|
||||
if (!p) return "(未设置)";
|
||||
if (!p) return t("dataGenerate.notSet");
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
try {
|
||||
const regex = new RegExp("^" + p + "$");
|
||||
|
|
@ -23,9 +26,9 @@ const previewVal = computed(() => {
|
|||
attempt = Array.from({ length: Math.min(len, 50) }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
||||
if (regex.test(attempt)) return attempt;
|
||||
}
|
||||
return attempt || "(无法匹配)";
|
||||
return attempt || t("dataGenerate.noMatch");
|
||||
} catch {
|
||||
return "(无效正则)";
|
||||
return t("dataGenerate.invalidRegex");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -38,18 +41,18 @@ function refresh() {
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">正则表达式</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.regex") }}</Label>
|
||||
<Input v-model="params.pattern" type="text" placeholder="[A-Za-z0-9]{10}" class="h-7 text-xs font-mono" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs mt-2">
|
||||
<input id="regex-raw" type="checkbox" class="h-3.5 w-3.5 accent-primary" :checked="!!params.rawPattern" @change="params.rawPattern = !params.rawPattern" />
|
||||
<Label for="regex-raw" class="text-muted-foreground">原始数据模式</Label>
|
||||
<Label for="regex-raw" class="text-muted-foreground">{{ t("dataGenerate.rawPattern") }}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const formats = [
|
||||
{ value: "name", label: "名称" },
|
||||
{ value: "code", label: "代码" },
|
||||
{ value: "code_name", label: "代码+名称" },
|
||||
];
|
||||
const { t } = useI18n();
|
||||
|
||||
const textTransforms = [
|
||||
{ value: "none", label: "原样" },
|
||||
{ value: "upper", label: "全大写" },
|
||||
{ value: "lower", label: "全小写" },
|
||||
{ value: "title", label: "每个单词首字母大写" },
|
||||
];
|
||||
const formats = computed(() => [
|
||||
{ value: "name", label: t("dataGenerate.regionFormats.name") },
|
||||
{ value: "code", label: t("dataGenerate.regionFormats.code") },
|
||||
{ value: "code_name", label: t("dataGenerate.regionFormats.codeName") },
|
||||
]);
|
||||
|
||||
const textTransforms = computed(() => [
|
||||
{ value: "none", label: t("dataGenerate.transforms.none") },
|
||||
{ value: "upper", label: t("dataGenerate.transforms.upper") },
|
||||
{ value: "lower", label: t("dataGenerate.transforms.lower") },
|
||||
{ value: "title", label: t("dataGenerate.transforms.title") },
|
||||
]);
|
||||
|
||||
const regionData = [
|
||||
{ code: "US", en: "United States", zh: "美国", zh_hant: "美國", ja: "アメリカ合衆国" },
|
||||
|
|
@ -71,13 +74,13 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">格式</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.formatLabel") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="f in formats" :key="f.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.regionFormat || 'name') === f.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.regionFormat = f.value">{{ f.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="l in [
|
||||
|
|
@ -97,16 +100,16 @@ function refresh() {
|
|||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">将值转换为</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.transformTo") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="t in textTransforms" :key="t.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.textTransform || 'none') === t.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.textTransform = t.value">
|
||||
{{ t.label }}
|
||||
<button v-for="x in textTransforms" :key="x.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.textTransform || 'none') === x.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.textTransform = x.value">
|
||||
{{ x.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function refresh() {
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">开始</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.start") }}</Label>
|
||||
<Input v-model.number="params.startValue" type="number" placeholder="1" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs mt-2">
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dataMap: Record<string, string[]> = {
|
||||
en: ["XS", "S", "M", "L", "XL", "XXL", "XXXL", "One Size"],
|
||||
zh: ["特小", "小", "中", "大", "加大", "特大", "超特大", "均码"],
|
||||
|
|
@ -33,7 +36,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="l in [
|
||||
|
|
@ -54,7 +57,7 @@ function refresh() {
|
|||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.pattern) {
|
||||
props.params.pattern = "([A-F]{2}[-]){2}([0-9]{4}[-])([A-Z])";
|
||||
}
|
||||
|
|
@ -72,12 +75,12 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">正则表达式</Label>
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.regex") }}</Label>
|
||||
<input v-model="params.pattern" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono" placeholder="([A-F]{2}[-]){2}([0-9]{4}[-])([A-Z])" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789_";
|
||||
|
||||
const previewKey = ref(0);
|
||||
|
|
@ -26,7 +29,7 @@ function refresh() {
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Label } from "@/components/ui/label";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const lorem =
|
||||
"Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum";
|
||||
|
||||
|
|
@ -33,7 +36,7 @@ function refresh() {
|
|||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs">
|
||||
<Label class="text-muted-foreground">字符数</Label>
|
||||
<Label class="text-muted-foreground">{{ t("dataGenerate.charCount") }}</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input v-model.number="params.minLength" type="number" min="1" placeholder="50" class="h-7 w-20 text-xs" />
|
||||
<span class="text-muted-foreground">-</span>
|
||||
|
|
@ -44,7 +47,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-start gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0 mt-0.5">预览</span>
|
||||
<span class="text-muted-foreground shrink-0 mt-0.5">{{ t("dataGenerate.preview") }}</span>
|
||||
<p :key="previewKey" class="font-mono text-xs leading-relaxed break-all">{{ previewVal }}</p>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0 mt-0.5" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const languages = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "zh", label: "简体中文" },
|
||||
|
|
@ -43,7 +46,7 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
|
||||
</div>
|
||||
|
|
@ -51,7 +54,7 @@ function refresh() {
|
|||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.urlSubdomains) {
|
||||
props.params.urlSubdomains = "auth.\ndrive.\nimage.\nwww.\napi.\nmail.\nshop.\nblog.";
|
||||
}
|
||||
|
|
@ -49,16 +52,16 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">子域</Label>
|
||||
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个子域 auth. www. api." />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.subdomains") }}</Label>
|
||||
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.subdomains')" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">顶级域</Label>
|
||||
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个TLD .com .cn .io" />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.tlds") }}</Label>
|
||||
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.tlds')" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { computed, ref } from "vue";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function genUuid(hyphens: boolean): string {
|
||||
const hex = "0123456789abcdef";
|
||||
const parts = [8, 4, 4, 4, 12];
|
||||
|
|
@ -32,21 +35,21 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">UUID格式</div>
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.uuidFormat") }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens !== false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = true">包含连字符</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens === false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = false">无格式</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens !== false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = true">{{ t("dataGenerate.uuidWithHyphens") }}</button>
|
||||
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens === false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = false">{{ t("dataGenerate.uuidNoHyphens") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
|
||||
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
|
||||
<code class="block text-xs font-mono bg-background rounded px-2 py-1 break-all">{{ regex }}</code>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!props.params.values) {
|
||||
props.params.values = "g\nkg\nlb\noz\nt\nmg\nct";
|
||||
}
|
||||
|
|
@ -23,7 +26,7 @@ const previewVal = computed(() => {
|
|||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (vals.length === 0) return "(无)";
|
||||
if (vals.length === 0) return t("dataGenerate.noValue");
|
||||
return pick(vals);
|
||||
});
|
||||
|
||||
|
|
@ -35,12 +38,12 @@ function refresh() {
|
|||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">值</Label>
|
||||
<textarea v-model="params.values" rows="5" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个单位 g kg lb" />
|
||||
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.values") }}</Label>
|
||||
<textarea v-model="params.values" rows="5" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.weightUnits')" />
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">预览</span>
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ onBeforeUnmount(() => cleanupMap());
|
|||
|
||||
<!-- Label property selector -->
|
||||
<select v-if="labelProperties.length" v-model="labelProperty" class="h-6 shrink-0 rounded border bg-background px-1.5 text-[11px] outline-none" @change="onLabelPropertyChange">
|
||||
<option value="">— 标签 —</option>
|
||||
<option value="">{{ t("grid.layerPreviewLabelNone") }}</option>
|
||||
<option v-for="p in labelProperties" :key="p" :value="p">
|
||||
{{ p }}
|
||||
</option>
|
||||
|
|
@ -462,13 +462,13 @@ onBeforeUnmount(() => cleanupMap());
|
|||
</select>
|
||||
|
||||
<!-- Save as image -->
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" title="导出图片" :disabled="isExporting" @click="saveAsImage">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" :title="t('grid.layerPreviewExportImage')" :disabled="isExporting" @click="saveAsImage">
|
||||
<Camera v-if="!isExporting" class="h-3.5 w-3.5" />
|
||||
<Loader2 v-else class="h-3.5 w-3.5 animate-spin" />
|
||||
</Button>
|
||||
|
||||
<!-- Maximise -->
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" :title="isMaximized ? '还原' : '最大化'" @click="toggleMaximize">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" :title="isMaximized ? t('grid.layerPreviewRestore') : t('grid.layerPreviewMaximize')" @click="toggleMaximize">
|
||||
<Maximize2 v-if="!isMaximized" class="h-3.5 w-3.5" />
|
||||
<Minimize2 v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useQueryStore } from "@/stores/queryStore";
|
|||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { externalSqlFileOpenErrorMessage, formatSqlFileSize, isExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
|
||||
|
|
@ -156,7 +157,7 @@ async function revealInFileManager(path: string) {
|
|||
try {
|
||||
await api.revealPathInFileManager(path);
|
||||
} catch (e: any) {
|
||||
toast(t("sqlFileTree.revealFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("sqlFileTree.revealFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { formatError } from "@/lib/backend/errorUtils";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { ConsumerInfo, ProducerInfo, SubscriptionInfo, TopicInfo, TopicRef, TopicStats, MqSystemKind } from "@/types/mq";
|
||||
|
|
@ -147,7 +148,7 @@ async function loadTopics(force = false) {
|
|||
try {
|
||||
await topicSelectRef.value?.loadTopics();
|
||||
} catch (e: unknown) {
|
||||
error.value = formatError(e);
|
||||
error.value = translateBackendError(t, formatError(e));
|
||||
} finally {
|
||||
topicsLoading.value = false;
|
||||
}
|
||||
|
|
@ -260,7 +261,7 @@ async function loadRuntimeClients() {
|
|||
}
|
||||
} catch (e: unknown) {
|
||||
if (isRuntimeLoadCurrent(loadSeq, currentKey)) {
|
||||
error.value = formatError(e) || String(e);
|
||||
error.value = translateBackendError(t, formatError(e)) || String(e);
|
||||
}
|
||||
} finally {
|
||||
if (loadSeq === runtimeLoadSeq) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { shallowRef } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
const toastMock = vi.hoisted(() => vi.fn());
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
buildTableSelectSql: vi.fn(async () => 'SELECT * FROM "main"."users" LIMIT 10000'),
|
||||
executeQuery: vi.fn(),
|
||||
exportQueryResultJson: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: toastMock }) }));
|
||||
vi.mock("@/composables/useExportTracker", () => ({ useExportTracker: () => ({ addTask: vi.fn() }) }));
|
||||
vi.mock("vue-i18n", () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string, params?: Record<string, unknown>) => {
|
||||
if (key === "editor.duckdbDraining") return "上一个 DuckDB 查询仍在停止中,请稍后重试。";
|
||||
if (key === "grid.exportFailed") return `导出失败:${params?.message}`;
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import { useSidebarTreeExportRuntime } from "@/composables/useSidebarTreeExportRuntime";
|
||||
|
||||
describe("useSidebarTreeExportRuntime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("translates direct executeQuery errors for sidebar JSON export", async () => {
|
||||
apiMock.executeQuery.mockRejectedValueOnce(new Error("The previous DuckDB query is still stopping. Please try again shortly."));
|
||||
const activeNode = shallowRef({ id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "main", children: [] } as TreeNode);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "duckdb" })),
|
||||
connectionIdentifierQuote: vi.fn(() => '"'),
|
||||
treeNodes: [],
|
||||
selectedTreeNodeIds: [],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: {} as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportData("json");
|
||||
|
||||
expect(apiMock.executeQuery).toHaveBeenCalledOnce();
|
||||
expect(toastMock).toHaveBeenCalledWith("导出失败:上一个 DuckDB 查询仍在停止中,请稍后重试。", 5000);
|
||||
});
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ import { assessProductionSql, productionContextForDatabase } from "@/lib/databas
|
|||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
interface RowItem {
|
||||
id: number;
|
||||
|
|
@ -1273,7 +1274,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
if (!confirmed) return;
|
||||
}
|
||||
if (customHandler && snapshot.newRows.length > 0 && customHandler.supportsInsert !== true && customHandler.canInsert !== true) {
|
||||
saveError.value = "当前保存目标不支持新增行。";
|
||||
saveError.value = i18n.global.t("grid.insertRowsNotSupported");
|
||||
return;
|
||||
}
|
||||
saveError.value = "";
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type { QueryResultExportRequest } from "@/lib/backend/api";
|
|||
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { buildXlsxSqlWorksheet } from "@/lib/export/xlsxSqlSheet";
|
||||
import { formatTemporalRowsForExport } from "@/lib/dataGrid/columnFormatter";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
|
||||
/**
|
||||
* Format metadata for backend table exports. Each entry maps a format key
|
||||
|
|
@ -571,7 +572,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
errorMessage: e?.message || String(e),
|
||||
};
|
||||
}
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -593,7 +594,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -617,7 +618,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -639,7 +640,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -663,7 +664,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await api.exportQueryResultMarkdown(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -685,7 +686,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await api.exportQueryResultMarkdown(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -700,7 +701,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await saveTextFile(content, exportFileName(tableMeta.value?.tableName || "export", "txt", { preferFallback: true }), "Text", "txt");
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -713,7 +714,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await saveTextFile(content, exportFileName("export-page", "txt", { page: true }), "Text", "txt");
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -784,7 +785,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
errorMessage: e?.message || String(e),
|
||||
};
|
||||
}
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -814,7 +815,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await writeXlsxResult(outputPath, result, includeSqlSheet);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -857,7 +858,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await api.exportQueryResultsXlsx(outputPath, sqlWorksheet ? [...worksheets, sqlWorksheet] : worksheets);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1113,7 +1114,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await saveTextFile(content, exportFileName(tableMeta.value?.tableName || "export", "sql", { preferFallback: true }), "SQL", "sql");
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1134,7 +1135,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
|||
await saveTextFile(content, exportFileName("export-page", "sql", { page: true }), "SQL", "sql");
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
|
|||
try {
|
||||
await revealPathInFileManager(path);
|
||||
} catch (error: any) {
|
||||
toast(typeof error === "string" ? error : error?.message || String(error), 5000);
|
||||
toast(translateBackendError(t, typeof error === "string" ? error : error?.message || String(error)), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
|
|||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
import { formatSqlInsert } from "@/lib/export/exportFormats";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import { isLoadingStructurePreview, showStructureDocCopyDialog, showStructurePreviewDialog, structureDocCopyText, structureDocCopyTitle, structurePreviewDefaultFileName, structurePreviewError, structurePreviewSql, structurePreviewTitle } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
|
|
@ -246,7 +247,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
|||
await saveFileContent(content, `${node.label}.sql`, "SQL", "sql");
|
||||
toast(t("grid.exported"));
|
||||
} catch (error: any) {
|
||||
toast(t("grid.exportFailed", { message: error?.message || String(error) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,14 +298,14 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
|||
currentTask.status = progress.status;
|
||||
currentTask.errorMessage = progress.errorMessage || null;
|
||||
if (progress.status === "Done") toast(t("grid.exported"));
|
||||
else if (progress.status === "Error") toast(t("grid.exportFailed", { message: progress.errorMessage || "" }), 5000);
|
||||
else if (progress.status === "Error") toast(t("grid.exportFailed", { message: translateBackendError(t, progress.errorMessage || "") }), 5000);
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (task) {
|
||||
task.status = "Error";
|
||||
task.errorMessage = error?.message || String(error);
|
||||
}
|
||||
toast(t("grid.exportFailed", { message: error?.message || String(error) }), 5000);
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it as test } from "vitest";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { translateBackendError, type BackendErrorTranslate } from "@/i18n/backend-errors";
|
||||
import en from "@/i18n/locales/en";
|
||||
import es from "@/i18n/locales/es";
|
||||
import it from "@/i18n/locales/it";
|
||||
import ja from "@/i18n/locales/ja";
|
||||
import ptBR from "@/i18n/locales/pt-BR";
|
||||
import zhCN from "@/i18n/locales/zh-CN";
|
||||
import zhTW from "@/i18n/locales/zh-TW";
|
||||
|
||||
const LOCALES = {
|
||||
en,
|
||||
es,
|
||||
it,
|
||||
ja,
|
||||
"pt-BR": ptBR,
|
||||
"zh-CN": zhCN,
|
||||
"zh-TW": zhTW,
|
||||
} as const;
|
||||
|
||||
type LocaleKey = keyof typeof LOCALES;
|
||||
|
||||
// Reproduces the exact string crates/dbx-core/src/agent_service.rs builds on
|
||||
// Windows: `\` line continuations strip the newline plus the following indent.
|
||||
const WINDOWS_JRE_REMOVE_ERROR = [
|
||||
"Failed to remove the old JRE directory: C:\\dbx\\jre21",
|
||||
"Possible causes:",
|
||||
" - a dbx Agent / java process still holds the directory",
|
||||
" - antivirus software is scanning it",
|
||||
"Close any process that may hold the directory, or restart dbx and try again.",
|
||||
"(original error: Access is denied. (os error 5))",
|
||||
].join("\n");
|
||||
|
||||
// Every backend message changed away from hardcoded Chinese, paired with the
|
||||
// key and params it must resolve to.
|
||||
const CASES: { name: string; message: string; key: string; params?: Record<string, string> }[] = [
|
||||
{
|
||||
name: "XLSX row limit",
|
||||
message: "XLSX supports at most 1,048,575 data rows. Use CSV export for the full result.",
|
||||
key: "exportProgress.xlsxRowLimit",
|
||||
params: { limit: "1,048,575" },
|
||||
},
|
||||
{
|
||||
name: "streaming export unsupported",
|
||||
message: "Streaming export is unsupported for this query. Simplify it or use a supported driver.",
|
||||
key: "exportProgress.streamingUnsupported",
|
||||
},
|
||||
{
|
||||
name: "agent session missing",
|
||||
message: "Streaming export needs a result-set session, but this driver returned no session_id.",
|
||||
key: "exportProgress.agentSessionMissing",
|
||||
},
|
||||
{
|
||||
name: "DuckDB draining",
|
||||
message: "The previous DuckDB query is still stopping. Please try again shortly.",
|
||||
key: "editor.duckdbDraining",
|
||||
},
|
||||
{
|
||||
name: "JRE directory remove failure (Windows)",
|
||||
message: WINDOWS_JRE_REMOVE_ERROR,
|
||||
key: "driverStore.jreDirRemoveFailedWindows",
|
||||
params: { path: "C:\\dbx\\jre21", error: "Access is denied. (os error 5)" },
|
||||
},
|
||||
{
|
||||
name: "JRE directory remove failure (POSIX)",
|
||||
message: "Failed to remove the old JRE directory: /home/u/.dbx/jre21 (original error: Permission denied (os error 13))",
|
||||
key: "driverStore.jreDirRemoveFailed",
|
||||
params: { path: "/home/u/.dbx/jre21", error: "Permission denied (os error 13)" },
|
||||
},
|
||||
{
|
||||
name: "JRE still in use",
|
||||
message: "JRE jre21 is in use by drivers: MySQL, PostgreSQL. Uninstall them first.",
|
||||
key: "driverStore.jreInUseByDrivers",
|
||||
params: { jre: "jre21", drivers: "MySQL, PostgreSQL" },
|
||||
},
|
||||
{
|
||||
name: "offline package missing registry",
|
||||
message: "agent-registry.json not found in the ZIP; not a valid offline driver package.",
|
||||
key: "driverStore.offlinePackageRegistryMissing",
|
||||
},
|
||||
{
|
||||
name: "driver update blocked by open connections",
|
||||
message: "Close these database connections before updating drivers: Prod MySQL, Stage PG",
|
||||
key: "driverStore.driverUpdateBlocked",
|
||||
params: { labels: "Prod MySQL, Stage PG" },
|
||||
},
|
||||
{
|
||||
name: "Kafka topic unload unsupported",
|
||||
message: "Kafka does not support unloading topics",
|
||||
key: "mqClients.unloadTopicUnsupportedKafka",
|
||||
},
|
||||
{
|
||||
name: "file does not exist",
|
||||
message: "file does not exist: /tmp/missing.sqlite",
|
||||
key: "common.fileNotFound",
|
||||
params: { path: "/tmp/missing.sqlite" },
|
||||
},
|
||||
{
|
||||
name: "login rate limited",
|
||||
message: "Please try again in 42s",
|
||||
key: "auth.rateLimited",
|
||||
params: { seconds: "42" },
|
||||
},
|
||||
];
|
||||
|
||||
function translatorFor(locale: LocaleKey): BackendErrorTranslate {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale,
|
||||
fallbackLocale: "en",
|
||||
messages: LOCALES as unknown as Record<string, Record<string, unknown>>,
|
||||
});
|
||||
return i18n.global.t as unknown as BackendErrorTranslate;
|
||||
}
|
||||
|
||||
function lookup(messages: Record<string, unknown>, key: string): unknown {
|
||||
return key.split(".").reduce<unknown>((node, part) => (node as Record<string, unknown> | undefined)?.[part], messages);
|
||||
}
|
||||
|
||||
describe("backend error translation", () => {
|
||||
test.each(CASES)("$name resolves to a defined English key", ({ key }) => {
|
||||
expect(lookup(en as unknown as Record<string, unknown>, key)).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
// zh-CN is the locale that regressed when these messages were switched from
|
||||
// hardcoded Chinese to hardcoded English, so it is covered explicitly
|
||||
// alongside the other non-English locales.
|
||||
const localeKeys = Object.keys(LOCALES) as LocaleKey[];
|
||||
|
||||
describe.each(localeKeys)("in %s", (locale) => {
|
||||
const t = translatorFor(locale);
|
||||
|
||||
test.each(CASES)("$name maps to its key and interpolates params", ({ message, key, params }) => {
|
||||
const translated = translateBackendError(t, message);
|
||||
|
||||
expect(translated).toBe(params ? t(key, params) : t(key));
|
||||
// A missed placeholder would leak `{path}` style tokens to the user.
|
||||
expect(translated).not.toMatch(/\{[A-Za-z]+\}/);
|
||||
// The raw message must not survive untranslated.
|
||||
if (locale !== "en") expect(translated).not.toBe(message);
|
||||
});
|
||||
|
||||
test.each(CASES.filter((entry) => entry.params))("$name keeps captured values in the output", ({ message, params }) => {
|
||||
const translated = translateBackendError(t, message);
|
||||
for (const value of Object.values(params!)) expect(translated).toContain(value);
|
||||
});
|
||||
});
|
||||
|
||||
test("unknown backend messages are passed through untouched", () => {
|
||||
const t = translatorFor("zh-CN");
|
||||
expect(translateBackendError(t, "some driver specific failure")).toBe("some driver specific failure");
|
||||
});
|
||||
|
||||
test("normalizes Error and structural message objects before translation", () => {
|
||||
const t = translatorFor("zh-CN");
|
||||
const message = "file does not exist: /tmp/missing.sqlite";
|
||||
const expected = t("common.fileNotFound", { path: "/tmp/missing.sqlite" });
|
||||
|
||||
expect(translateBackendError(t, new Error(message))).toBe(expected);
|
||||
expect(translateBackendError(t, { message })).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// Matching on message text only works while both sides agree on the wording, so
|
||||
// pin the literals that the patterns above depend on to their Rust source.
|
||||
describe("backend error wording is pinned to the Rust sources", () => {
|
||||
const rust = (path: string) => readFileSync(new URL(`../../../../../${path}`, import.meta.url), "utf8");
|
||||
|
||||
test.each([
|
||||
["crates/dbx-core/src/query_result_export.rs", "XLSX supports at most 1,048,575 data rows. Use CSV export for the full result."],
|
||||
["crates/dbx-core/src/query_result_export.rs", "Streaming export is unsupported for this query. Simplify it or use a supported driver."],
|
||||
["crates/dbx-core/src/query_result_export.rs", "Streaming export needs a result-set session, but this driver returned no session_id."],
|
||||
["crates/dbx-core/src/query.rs", "The previous DuckDB query is still stopping. Please try again shortly."],
|
||||
["crates/dbx-core/src/agent_service.rs", "Failed to remove the old JRE directory: "],
|
||||
["crates/dbx-core/src/agent_service.rs", "is in use by drivers: "],
|
||||
["crates/dbx-core/src/agent_service.rs", "agent-registry.json not found in the ZIP; not a valid offline driver package."],
|
||||
["crates/dbx-core/src/mq/adapters/kafka.rs", "Kafka does not support unloading topics"],
|
||||
["crates/dbx-web/src/auth.rs", "Please try again in {remaining}s"],
|
||||
["crates/dbx-web/src/routes/agents.rs", "Close these database connections before updating drivers: "],
|
||||
["src-tauri/src/commands/agents.rs", "Close these database connections before updating drivers: "],
|
||||
["src-tauri/src/commands/fs_open.rs", "file does not exist: "],
|
||||
])("%s still emits %j", (path, fragment) => {
|
||||
expect(rust(path)).toContain(fragment);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,13 @@
|
|||
import type { ComposerTranslation } from "vue-i18n";
|
||||
/**
|
||||
* Minimal shape of a translate function, satisfied by both `useI18n().t` inside
|
||||
* components and `i18n.global.t` in stores and composables. Using the full
|
||||
* `ComposerTranslation` type here would reject the latter, because the global
|
||||
* composer is typed against the concrete message schema.
|
||||
*/
|
||||
export type BackendErrorTranslate = {
|
||||
(key: string): string;
|
||||
(key: string, named: Record<string, unknown>): string;
|
||||
};
|
||||
|
||||
const taggedAiCliErrorKeys: Record<string, string> = {
|
||||
claudeCodeNotInstalled: "ai.cliErrors.claudeCodeNotInstalled",
|
||||
|
|
@ -53,9 +62,36 @@ const patterns: [RegExp, string][] = [
|
|||
[/^Proxy host too long for SOCKS5 domain address$/, "settings.tunnelsSocksHostTooLong"],
|
||||
[/^SOCKS proxy connect rejected \(code (\d+)\)$/, "settings.tunnelsSocksConnectRejected"],
|
||||
[/^Unsupported SOCKS bound address type: (\d+)$/, "settings.tunnelsSocksUnsupportedAddrType"],
|
||||
|
||||
// Query result export limits (crates/dbx-core/src/query_result_export.rs)
|
||||
[/^XLSX supports at most ([\d,]+) data rows\. Use CSV export for the full result\.$/, "exportProgress.xlsxRowLimit"],
|
||||
[/^Streaming export is unsupported for this query\. Simplify it or use a supported driver\.$/, "exportProgress.streamingUnsupported"],
|
||||
[/^Streaming export needs a result-set session, but this driver returned no session_id\.$/, "exportProgress.agentSessionMissing"],
|
||||
|
||||
// Query execution (crates/dbx-core/src/query.rs)
|
||||
[/^The previous DuckDB query is still stopping\. Please try again shortly\.$/, "editor.duckdbDraining"],
|
||||
|
||||
// Driver / JRE management (crates/dbx-core/src/agent_service.rs, routes/agents.rs)
|
||||
// The Windows variant is multi-line, so it must be tried before the single-line one.
|
||||
[/^Failed to remove the old JRE directory: (.+)\nPossible causes:[\s\S]*\(original error: ([\s\S]+)\)$/, "driverStore.jreDirRemoveFailedWindows"],
|
||||
[/^Failed to remove the old JRE directory: (.+) \(original error: ([\s\S]+)\)$/, "driverStore.jreDirRemoveFailed"],
|
||||
[/^JRE (.+?) is in use by drivers: (.+)\. Uninstall them first\.$/, "driverStore.jreInUseByDrivers"],
|
||||
[/^agent-registry\.json not found in the ZIP; not a valid offline driver package\.$/, "driverStore.offlinePackageRegistryMissing"],
|
||||
[/^Close these database connections before updating drivers: (.+)$/, "driverStore.driverUpdateBlocked"],
|
||||
|
||||
// Message queues (crates/dbx-core/src/mq/adapters/kafka.rs)
|
||||
[/^Kafka does not support unloading topics$/, "mqClients.unloadTopicUnsupportedKafka"],
|
||||
|
||||
// Filesystem (src-tauri/src/commands/fs_open.rs)
|
||||
[/^file does not exist: (.+)$/, "common.fileNotFound"],
|
||||
|
||||
// Web auth rate limiting (crates/dbx-web/src/auth.rs)
|
||||
[/^Please try again in (\d+)s$/, "auth.rateLimited"],
|
||||
];
|
||||
|
||||
const paramNames: Record<string, string> = {
|
||||
// Named placeholders for each pattern's capture groups, in capture order.
|
||||
// A bare string is shorthand for a single capture group.
|
||||
const paramNames: Record<string, string | string[]> = {
|
||||
"connection.driverNotInstalled": "driver",
|
||||
"connection.jreNotInstalled": "jre",
|
||||
"ai.configNameExists": "name",
|
||||
|
|
@ -69,9 +105,23 @@ const paramNames: Record<string, string> = {
|
|||
"settings.tunnelsSocksUnsupportedAuth": "method",
|
||||
"settings.tunnelsSocksConnectRejected": "code",
|
||||
"settings.tunnelsSocksUnsupportedAddrType": "type",
|
||||
"exportProgress.xlsxRowLimit": "limit",
|
||||
"driverStore.jreDirRemoveFailedWindows": ["path", "error"],
|
||||
"driverStore.jreDirRemoveFailed": ["path", "error"],
|
||||
"driverStore.jreInUseByDrivers": ["jre", "drivers"],
|
||||
"driverStore.driverUpdateBlocked": "labels",
|
||||
"common.fileNotFound": "path",
|
||||
"auth.rateLimited": "seconds",
|
||||
};
|
||||
|
||||
export function translateBackendError(t: ComposerTranslation, message: string): string {
|
||||
function backendErrorMessage(error: unknown): string {
|
||||
if (typeof error === "string") return error;
|
||||
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export function translateBackendError(t: BackendErrorTranslate, error: unknown): string {
|
||||
const message = backendErrorMessage(error);
|
||||
const tagged = message.match(/^\[([A-Za-z][A-Za-z0-9]+)\]\s*([\s\S]*)$/);
|
||||
if (tagged) {
|
||||
const [, code, rawDetail] = tagged;
|
||||
|
|
@ -85,9 +135,15 @@ export function translateBackendError(t: ComposerTranslation, message: string):
|
|||
for (const [regex, key] of patterns) {
|
||||
const match = message.match(regex);
|
||||
if (match) {
|
||||
const name = paramNames[key];
|
||||
if (name && match[1]) {
|
||||
return t(key, { [name]: match[1] });
|
||||
const names = paramNames[key];
|
||||
if (names) {
|
||||
const ordered = Array.isArray(names) ? names : [names];
|
||||
const params: Record<string, string> = {};
|
||||
ordered.forEach((name, index) => {
|
||||
const captured = match[index + 1];
|
||||
if (captured !== undefined) params[name] = captured;
|
||||
});
|
||||
if (Object.keys(params).length > 0) return t(key, params);
|
||||
}
|
||||
return t(key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export default {
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "Please try again in {seconds}s",
|
||||
setupTitle: "Set up access password",
|
||||
setupDescription: "Set a password to protect your instance",
|
||||
loginDescription: "Database management tool",
|
||||
|
|
@ -536,6 +537,28 @@ export default {
|
|||
agentJavaTooOld: "This driver requires Java 21. Use DBX managed JRE 21 or select a Java 21 executable in Driver Manager.",
|
||||
agentDriverUpdateConnectionHint: "A built-in driver update is available for this connection. The connection failure may be related to an outdated local driver. Update the corresponding driver in Driver Manager, then retry.",
|
||||
jdbcPluginNotInstalled: "JDBC plugin is not installed. Install the optional JDBC plugin to use this connection.",
|
||||
connectFailedTitle: "Connection failed",
|
||||
fullErrorMessage: "Full error message",
|
||||
copyError: "Copy error",
|
||||
optionalSuffix: " (optional)",
|
||||
customColorPlaceholder: "#ff0000 or rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io or libsql://your-database.turso.io",
|
||||
tursoHostHint: "Supports the libsql:// and https:// protocols. You can also enter just the host name (HTTPS is used automatically).",
|
||||
tursoTokenHint: "Create a token with:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx (optional, the Auth Token field above takes precedence)",
|
||||
driverInstall: {
|
||||
installingTitle: "Installing driver",
|
||||
failedTitle: "Driver installation failed",
|
||||
fullError: "Full error",
|
||||
statusFailed: "Installation failed",
|
||||
statusWaiting: "Waiting to install",
|
||||
statusPreparing: "Preparing driver installation...",
|
||||
statusExtractingJre: "Extracting JRE...",
|
||||
stepJre: "Downloading JRE",
|
||||
stepDriver: "Downloading driver",
|
||||
stepDefault: "Installing driver",
|
||||
installingButton: "Installing...",
|
||||
},
|
||||
lastError: "Connection error",
|
||||
errorIndicatorHint: "Connection error. Click to view details",
|
||||
clearError: "Clear connection error",
|
||||
|
|
@ -671,6 +694,7 @@ export default {
|
|||
connectCancelled: "Connection cancelled",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "The previous DuckDB query is still stopping. Please try again shortly.",
|
||||
statementExecutionSucceeded: "{count} statement succeeded | {count} statements succeeded",
|
||||
statementExecutionFailed: "{count} statement failed | {count} statements failed",
|
||||
pressToExecute: "Press {mod}+Enter to execute",
|
||||
|
|
@ -1256,6 +1280,11 @@ export default {
|
|||
imageLoadFailed: "Image failed to load",
|
||||
geometryPreview: "Geometry Preview",
|
||||
layerPreview: "Layer Preview",
|
||||
layerPreviewLabelNone: "— Label —",
|
||||
layerPreviewExportImage: "Export image",
|
||||
layerPreviewMaximize: "Maximize",
|
||||
layerPreviewRestore: "Restore",
|
||||
insertRowsNotSupported: "The current save target does not support adding rows.",
|
||||
zoomIn: "Zoom In",
|
||||
zoomOut: "Zoom Out",
|
||||
fitImage: "Fit to View",
|
||||
|
|
@ -1357,6 +1386,9 @@ export default {
|
|||
truncatedHint: "Results were truncated after loading {count} rows. Use the footer pagination to browse loaded data; exporting the full result reruns the database query.",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "XLSX supports at most {limit} data rows. Use CSV export for the full result.",
|
||||
streamingUnsupported: "Streaming export is unsupported for this query. Simplify it or use a supported driver.",
|
||||
agentSessionMissing: "Streaming export needs a result-set session, but this driver returned no session_id.",
|
||||
title: "Exporting Table Data",
|
||||
fetching: "Fetching data from database...",
|
||||
writing: "Writing to file...",
|
||||
|
|
@ -1413,6 +1445,7 @@ export default {
|
|||
mcpLearnMore: "Learn more",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "File does not exist: {path}",
|
||||
language: "Language",
|
||||
loading: "Loading...",
|
||||
stopping: "Stopping...",
|
||||
|
|
@ -1734,6 +1767,7 @@ export default {
|
|||
enableThinkingOff: "Disabled",
|
||||
enableThinkingHint: "This option only takes effect on /chat/completions APIs and supported models. When disabled, it can significantly reduce token usage, but the quality of generated results may decrease slightly.",
|
||||
anthropicMessagesHint: "Anthropic Messages compatible APIs usually use /v1/messages.",
|
||||
openAiCompatibleEndpointHint: "Most OpenAI-compatible APIs require the /v1 path prefix.",
|
||||
contextWindow: "Context Window",
|
||||
contextWindowAuto: "Auto (detect from model name)",
|
||||
contextWindowHint: "Tokens. Leave empty to auto-detect. Set manually for local/custom models.",
|
||||
|
|
@ -3593,6 +3627,121 @@ export default {
|
|||
ok: "OK",
|
||||
noTablesSelected: "No tables selected",
|
||||
orderHint: "Use arrow buttons to reorder",
|
||||
language: "Language",
|
||||
regionLabel: "Region",
|
||||
formatLabel: "Format",
|
||||
formatType: "Format Type",
|
||||
typeLabel: "Type",
|
||||
loading: "Loading...",
|
||||
regex: "Regex",
|
||||
rawPattern: "Raw pattern mode",
|
||||
notSet: "(not set)",
|
||||
noMatch: "(no match)",
|
||||
invalidRegex: "(invalid regex)",
|
||||
noDomain: "(no domain)",
|
||||
schema: "Schema",
|
||||
tableLabel: "Table",
|
||||
columnLabel: "Column",
|
||||
genMode: "Generation Mode",
|
||||
dateType: "Date Type",
|
||||
yearOffsetRange: "Year range (offset from the current year)",
|
||||
numberType: "Number Type",
|
||||
integer: "Integer",
|
||||
decimal: "Decimal",
|
||||
decimalPlaces: "Decimal places",
|
||||
uuidFormat: "UUID Format",
|
||||
uuidWithHyphens: "With hyphens",
|
||||
uuidNoHyphens: "No hyphens",
|
||||
includeSeparators: "Include separators",
|
||||
phoneDomestic: "Domestic",
|
||||
phoneInternational: "International",
|
||||
charCount: "Characters",
|
||||
start: "Start",
|
||||
valueList: "Value List",
|
||||
domainList: "Domain list",
|
||||
subdomains: "Subdomains",
|
||||
tlds: "Top-level domains",
|
||||
extensionType: "Extension Type",
|
||||
extensionList: "Extension list:",
|
||||
availableExtensions: "Available extensions:",
|
||||
includeExtension: "Include extension",
|
||||
includeFileName: "Include file name",
|
||||
pathType: "Path Type",
|
||||
ipType: "IP Address Type",
|
||||
nameFormat: "Name Format",
|
||||
transformTo: "Transform values to",
|
||||
generateOptions: "Generation Options",
|
||||
previewRowCount: "{count} rows",
|
||||
useKeywords: "Generate from keywords",
|
||||
weekdayShort: {
|
||||
sun: "Sun",
|
||||
mon: "Mon",
|
||||
tue: "Tue",
|
||||
wed: "Wed",
|
||||
thu: "Thu",
|
||||
fri: "Fri",
|
||||
sat: "Sat",
|
||||
},
|
||||
countries: {
|
||||
us: "United States",
|
||||
uk: "United Kingdom",
|
||||
cn: "China",
|
||||
jp: "Japan",
|
||||
other: "Other",
|
||||
cnEnglish: "China (English)",
|
||||
jpEnglish: "Japan (English)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "Native language",
|
||||
english: "English",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "Address line 1",
|
||||
line2: "Address line 2",
|
||||
full: "Full address",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "Full name",
|
||||
last: "Last name only",
|
||||
first: "First name only",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "Name",
|
||||
code: "Code",
|
||||
codeName: "Code + Name",
|
||||
},
|
||||
transforms: {
|
||||
none: "As is",
|
||||
upper: "UPPERCASE",
|
||||
lower: "lowercase",
|
||||
title: "Title Case",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "Image",
|
||||
document: "Document",
|
||||
spreadsheet: "Spreadsheet",
|
||||
presentation: "Presentation",
|
||||
audio: "Audio",
|
||||
video: "Video",
|
||||
code: "Code",
|
||||
archive: "Archive",
|
||||
web: "Web",
|
||||
database: "Database",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "One value per line\nfirst\nsecond\nthird",
|
||||
subdomains: "One subdomain per line\nauth.\nwww.\napi.",
|
||||
tlds: "One TLD per line\n.com\n.cn\n.io",
|
||||
fileExtensions: "One extension per line\npng\njpg\ntxt",
|
||||
paymentMethods: "One payment method per line\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "One keyword per line\nApple\nCherry\nOrange",
|
||||
weightUnits: "One unit per line\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "Random",
|
||||
unique: "No duplicates",
|
||||
repeat: "Repeat each value",
|
||||
},
|
||||
gen: {
|
||||
general: "General",
|
||||
personal: "Personal",
|
||||
|
|
@ -4289,6 +4438,7 @@ export default {
|
|||
syncSnippetTitle: "GitHub / Gitee Snippet Sync",
|
||||
syncSnippetDescription: "Store the same DBX snapshot in a private code snippet. The first upload creates a snippet and saves its ID.",
|
||||
syncSnippetProvider: "Provider",
|
||||
syncSnippetProviderGitee: "Gitee Snippets",
|
||||
syncSnippetId: "Snippet ID",
|
||||
syncSnippetIdPlaceholder: "Leave empty for the first upload",
|
||||
syncSnippetToken: "Access token",
|
||||
|
|
@ -4617,6 +4767,10 @@ export default {
|
|||
shortcutExPasteSqlInCondition: "ExPaste: paste as IN condition",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Failed to remove the old JRE directory: {path} (original error: {error})",
|
||||
jreDirRemoveFailedWindows: "Failed to remove the old JRE directory: {path}\nPossible causes:\n - a dbx Agent / java process still holds the directory\n - antivirus software is scanning it\nClose any process that may hold the directory, or restart dbx and try again.\n(original error: {error})",
|
||||
jreInUseByDrivers: "JRE {jre} is in use by drivers: {drivers}. Uninstall them first.",
|
||||
offlinePackageRegistryMissing: "agent-registry.json not found in the ZIP; not a valid offline driver package.",
|
||||
progressJreExtract: "Extracting JRE...",
|
||||
progressDownloadJre: "Downloading JRE",
|
||||
progressDownloadDriver: "Downloading driver",
|
||||
|
|
@ -5537,6 +5691,7 @@ export default {
|
|||
peekMessageKey: "key={key}",
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "Kafka does not support unloading topics",
|
||||
title: "Producers / Consumers",
|
||||
unloadTopic: "Unload topic",
|
||||
unloading: "Unloading...",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default withEnglishFallback({
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "Vuelva a intentarlo en {seconds} s",
|
||||
setupTitle: "Configurar contraseña de acceso",
|
||||
setupDescription: "Establece una contraseña para proteger tu instancia",
|
||||
loginDescription: "Herramienta de administración de bases de datos",
|
||||
|
|
@ -413,6 +414,28 @@ export default withEnglishFallback({
|
|||
agentDriverUpdateConnectionHint:
|
||||
"Hay una actualización de controlador integrada disponible para esta conexión. El error de conexión puede estar relacionado con un controlador local desactualizado. Actualiza el controlador correspondiente en el Administrador de controladores y vuelve a intentarlo.",
|
||||
jdbcPluginNotInstalled: "El plugin JDBC no está instalado. Instale el plugin JDBC opcional para usar esta conexión.",
|
||||
connectFailedTitle: "Conexión fallida",
|
||||
fullErrorMessage: "Mensaje de error completo",
|
||||
copyError: "Copiar error",
|
||||
optionalSuffix: " (opcional)",
|
||||
customColorPlaceholder: "#ff0000 o rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io o libsql://your-database.turso.io",
|
||||
tursoHostHint: "Admite los protocolos libsql:// y https://. También puede introducir solo el nombre del host (se usa HTTPS automáticamente).",
|
||||
tursoTokenHint: "Cree un token con:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx (opcional, el campo Auth Token de arriba tiene prioridad)",
|
||||
driverInstall: {
|
||||
installingTitle: "Instalando controlador",
|
||||
failedTitle: "Error al instalar el controlador",
|
||||
fullError: "Error completo",
|
||||
statusFailed: "Error de instalación",
|
||||
statusWaiting: "Esperando instalación",
|
||||
statusPreparing: "Preparando la instalación del controlador...",
|
||||
statusExtractingJre: "Extrayendo JRE...",
|
||||
stepJre: "Descargando JRE",
|
||||
stepDriver: "Descargando controlador",
|
||||
stepDefault: "Instalando controlador",
|
||||
installingButton: "Instalando...",
|
||||
},
|
||||
lastError: "Error de conexión",
|
||||
errorIndicatorHint: "Error de conexión. Haz clic para ver los detalles",
|
||||
clearError: "Limpiar error de conexión",
|
||||
|
|
@ -653,6 +676,7 @@ export default withEnglishFallback({
|
|||
damengJvmOptionsInvalid: "La línea {line} debe ser una propiedad -Dkey o -Dkey=value sin comillas del shell.",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "La consulta anterior de DuckDB aún se está deteniendo. Vuelva a intentarlo en breve.",
|
||||
pressToExecute: "Presiona {mod}+Enter para ejecutar",
|
||||
pressToSaveSql: "Presiona {mod}+S para guardar el SQL",
|
||||
queryTimeoutError: "La consulta agotó el tiempo ({seconds}s). Comprueba la conexión a la base de datos.",
|
||||
|
|
@ -1153,6 +1177,11 @@ export default withEnglishFallback({
|
|||
imageLoadFailed: "No se pudo cargar la imagen",
|
||||
geometryPreview: "Vista previa de geometría",
|
||||
layerPreview: "Vista previa de capa",
|
||||
layerPreviewLabelNone: "— Etiqueta —",
|
||||
layerPreviewExportImage: "Exportar imagen",
|
||||
layerPreviewMaximize: "Maximizar",
|
||||
layerPreviewRestore: "Restaurar",
|
||||
insertRowsNotSupported: "El destino de guardado actual no admite añadir filas.",
|
||||
zoomIn: "Acercar",
|
||||
zoomOut: "Alejar",
|
||||
fitImage: "Ajustar a la vista",
|
||||
|
|
@ -1300,6 +1329,9 @@ export default withEnglishFallback({
|
|||
numericColumnAlignRight: "Alineación derecha",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "XLSX admite como máximo {limit} filas de datos. Use la exportación CSV para obtener el resultado completo.",
|
||||
streamingUnsupported: "La exportación en streaming no es compatible con esta consulta. Simplifíquela o use un controlador compatible.",
|
||||
agentSessionMissing: "La exportación en streaming requiere una sesión de conjunto de resultados, pero este controlador no devolvió session_id.",
|
||||
title: "Exportando datos de la tabla",
|
||||
fetching: "Obteniendo datos de la base de datos...",
|
||||
writing: "Escribiendo en el archivo...",
|
||||
|
|
@ -1356,6 +1388,7 @@ export default withEnglishFallback({
|
|||
mcpLearnMore: "Más información",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "El archivo no existe: {path}",
|
||||
language: "Idioma",
|
||||
loading: "Cargando...",
|
||||
stopping: "Deteniendo...",
|
||||
|
|
@ -1773,6 +1806,7 @@ export default withEnglishFallback({
|
|||
enableThinkingOff: "Desactivado",
|
||||
enableThinkingHint: "Esta opción solo tiene efecto en APIs /chat/completions y modelos compatibles. Cuando está desactivado, puede reducir significativamente el uso de tokens, pero la calidad de los resultados generados puede disminuir ligeramente.",
|
||||
anthropicMessagesHint: "Las API compatibles con Anthropic Messages suelen usar /v1/messages.",
|
||||
openAiCompatibleEndpointHint: "La mayoría de las API compatibles con OpenAI requieren el prefijo de ruta /v1.",
|
||||
contextWindow: "Ventana de contexto",
|
||||
contextWindowAuto: "Auto (detectar desde el nombre del modelo)",
|
||||
contextWindowHint: "Tokens. Déjalo vacío para detección automática. Configúralo manualmente para modelos locales/personalizados.",
|
||||
|
|
@ -3399,6 +3433,121 @@ export default withEnglishFallback({
|
|||
ok: "OK",
|
||||
noTablesSelected: "No tables selected",
|
||||
orderHint: "Use arrow buttons to reorder",
|
||||
language: "Idioma",
|
||||
regionLabel: "Región",
|
||||
formatLabel: "Formato",
|
||||
formatType: "Tipo de formato",
|
||||
typeLabel: "Tipo",
|
||||
loading: "Cargando...",
|
||||
regex: "Expresión regular",
|
||||
rawPattern: "Modo de patrón sin procesar",
|
||||
notSet: "(sin definir)",
|
||||
noMatch: "(sin coincidencia)",
|
||||
invalidRegex: "(expresión regular no válida)",
|
||||
noDomain: "(sin dominio)",
|
||||
schema: "Esquema",
|
||||
tableLabel: "Tabla",
|
||||
columnLabel: "Columna",
|
||||
genMode: "Modo de generación",
|
||||
dateType: "Tipo de fecha",
|
||||
yearOffsetRange: "Rango de años (desplazamiento respecto al año actual)",
|
||||
numberType: "Tipo de número",
|
||||
integer: "Entero",
|
||||
decimal: "Decimal",
|
||||
decimalPlaces: "Decimales",
|
||||
uuidFormat: "Formato UUID",
|
||||
uuidWithHyphens: "Con guiones",
|
||||
uuidNoHyphens: "Sin guiones",
|
||||
includeSeparators: "Incluir separadores",
|
||||
phoneDomestic: "Nacional",
|
||||
phoneInternational: "Internacional",
|
||||
charCount: "Caracteres",
|
||||
start: "Inicio",
|
||||
valueList: "Lista de valores",
|
||||
domainList: "Lista de dominios",
|
||||
subdomains: "Subdominios",
|
||||
tlds: "Dominios de nivel superior",
|
||||
extensionType: "Tipo de extensión",
|
||||
extensionList: "Lista de extensiones:",
|
||||
availableExtensions: "Extensiones disponibles:",
|
||||
includeExtension: "Incluir extensión",
|
||||
includeFileName: "Incluir nombre de archivo",
|
||||
pathType: "Tipo de ruta",
|
||||
ipType: "Tipo de dirección IP",
|
||||
nameFormat: "Formato de nombre",
|
||||
transformTo: "Transformar valores a",
|
||||
generateOptions: "Opciones de generación",
|
||||
previewRowCount: "{count} filas",
|
||||
useKeywords: "Generar a partir de palabras clave",
|
||||
weekdayShort: {
|
||||
sun: "Dom",
|
||||
mon: "Lun",
|
||||
tue: "Mar",
|
||||
wed: "Mié",
|
||||
thu: "Jue",
|
||||
fri: "Vie",
|
||||
sat: "Sáb",
|
||||
},
|
||||
countries: {
|
||||
us: "Estados Unidos",
|
||||
uk: "Reino Unido",
|
||||
cn: "China",
|
||||
jp: "Japón",
|
||||
other: "Otro",
|
||||
cnEnglish: "China (inglés)",
|
||||
jpEnglish: "Japón (inglés)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "Idioma local",
|
||||
english: "Inglés",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "Dirección línea 1",
|
||||
line2: "Dirección línea 2",
|
||||
full: "Dirección completa",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "Nombre completo",
|
||||
last: "Solo apellido",
|
||||
first: "Solo nombre",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "Nombre",
|
||||
code: "Código",
|
||||
codeName: "Código + Nombre",
|
||||
},
|
||||
transforms: {
|
||||
none: "Sin cambios",
|
||||
upper: "MAYÚSCULAS",
|
||||
lower: "minúsculas",
|
||||
title: "Tipo Título",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "Imagen",
|
||||
document: "Documento",
|
||||
spreadsheet: "Hoja de cálculo",
|
||||
presentation: "Presentación",
|
||||
audio: "Audio",
|
||||
video: "Vídeo",
|
||||
code: "Código",
|
||||
archive: "Archivo comprimido",
|
||||
web: "Web",
|
||||
database: "Base de datos",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "Un valor por línea\nfirst\nsecond\nthird",
|
||||
subdomains: "Un subdominio por línea\nauth.\nwww.\napi.",
|
||||
tlds: "Un TLD por línea\n.com\n.cn\n.io",
|
||||
fileExtensions: "Una extensión por línea\npng\njpg\ntxt",
|
||||
paymentMethods: "Un método de pago por línea\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "Una palabra clave por línea\nApple\nCherry\nOrange",
|
||||
weightUnits: "Una unidad por línea\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "Aleatorio",
|
||||
unique: "Sin duplicados",
|
||||
repeat: "Repetir cada valor",
|
||||
},
|
||||
gen: {
|
||||
general: "General",
|
||||
personal: "Personal",
|
||||
|
|
@ -4021,6 +4170,7 @@ export default withEnglishFallback({
|
|||
sidebarAllowHorizontalScroll: "Permitir desplazamiento horizontal lateral",
|
||||
sidebarAllowHorizontalScrollDescription: "Muestra completos los nombres largos de tablas, vistas y colecciones permitiendo desplazamiento horizontal en la barra lateral.",
|
||||
snippetsDescription: "Personaliza plantillas SQL activadas en el editor.",
|
||||
syncSnippetProviderGitee: "Fragmentos de Gitee",
|
||||
snippetsAdd: "Agregar fragmento",
|
||||
snippetsLabel: "Etiqueta",
|
||||
snippetsPrefix: "Prefijo",
|
||||
|
|
@ -4384,6 +4534,11 @@ export default withEnglishFallback({
|
|||
dateTimeFormatEmpty: "Ingresar formato de fecha y hora personalizado",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "No se pudo eliminar el directorio JRE antiguo: {path} (error original: {error})",
|
||||
jreDirRemoveFailedWindows:
|
||||
"No se pudo eliminar el directorio JRE antiguo: {path}\nCausas posibles:\n - un proceso dbx Agent / java sigue usando el directorio\n - el antivirus lo está analizando\nCierre cualquier proceso que pueda estar usando el directorio, o reinicie dbx e inténtelo de nuevo.\n(error original: {error})",
|
||||
jreInUseByDrivers: "El JRE {jre} está en uso por los controladores: {drivers}. Desinstálelos primero.",
|
||||
offlinePackageRegistryMissing: "No se encontró agent-registry.json en el ZIP; no es un paquete de controladores offline válido.",
|
||||
progressJreExtract: "Extrayendo JRE...",
|
||||
progressDownloadJre: "Descargando JRE",
|
||||
progressDownloadDriver: "Descargando driver",
|
||||
|
|
@ -5399,6 +5554,7 @@ export default withEnglishFallback({
|
|||
},
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "Kafka no admite la descarga de temas",
|
||||
title: "Productores / Consumidores",
|
||||
unloadTopic: "Descargar tema",
|
||||
unloading: "Descargando...",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export default withEnglishFallback({
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "Riprova tra {seconds} s",
|
||||
setupTitle: "Imposta la password di accesso",
|
||||
setupDescription: "Imposta una password per proteggere la tua istanza",
|
||||
loginDescription: "Strumento di gestione del database",
|
||||
|
|
@ -411,6 +412,28 @@ export default withEnglishFallback({
|
|||
agentJavaTooOld: "Questo driver richiede Java 21. Usa il JRE 21 gestito da DBX o seleziona un eseguibile Java 21 in Gestione Driver.",
|
||||
agentDriverUpdateConnectionHint: "È disponibile un aggiornamento del driver integrato per questa connessione. Il problema di connessione potrebbe essere correlato a un driver locale obsoleto. Aggiorna il driver corrispondente in Gestione Driver e riprova.",
|
||||
jdbcPluginNotInstalled: "Il plugin JDBC non è installato. Installa il plugin JDBC opzionale per utilizzare questa connessione.",
|
||||
connectFailedTitle: "Connessione non riuscita",
|
||||
fullErrorMessage: "Messaggio di errore completo",
|
||||
copyError: "Copia errore",
|
||||
optionalSuffix: " (opzionale)",
|
||||
customColorPlaceholder: "#ff0000 o rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io o libsql://your-database.turso.io",
|
||||
tursoHostHint: "Supporta i protocolli libsql:// e https://. È possibile inserire anche solo il nome host (HTTPS viene usato automaticamente).",
|
||||
tursoTokenHint: "Crea un token con:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx (opzionale, il campo Auth Token sopra ha la precedenza)",
|
||||
driverInstall: {
|
||||
installingTitle: "Installazione del driver",
|
||||
failedTitle: "Installazione del driver non riuscita",
|
||||
fullError: "Errore completo",
|
||||
statusFailed: "Installazione non riuscita",
|
||||
statusWaiting: "In attesa di installazione",
|
||||
statusPreparing: "Preparazione dell'installazione del driver...",
|
||||
statusExtractingJre: "Estrazione JRE...",
|
||||
stepJre: "Download JRE",
|
||||
stepDriver: "Download driver",
|
||||
stepDefault: "Installazione driver",
|
||||
installingButton: "Installazione...",
|
||||
},
|
||||
lastError: "Errore di connessione",
|
||||
errorIndicatorHint: "Errore di connessione. Fai clic per visualizzare i dettagli",
|
||||
clearError: "Cancella errore di connessione",
|
||||
|
|
@ -651,6 +674,7 @@ export default withEnglishFallback({
|
|||
damengJvmOptionsInvalid: "La riga {line} deve essere una proprietà -Dkey o -Dkey=value senza virgolette della shell.",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "La query DuckDB precedente è ancora in fase di arresto. Riprova a breve.",
|
||||
pressToExecute: "Premi {mod}+Enter per eseguire",
|
||||
pressToSaveSql: "Premi {mod}+S per salvare SQL",
|
||||
queryTimeoutError: "Timeout della query ({seconds}s). Verifica se la connessione al database è integra.",
|
||||
|
|
@ -1151,6 +1175,11 @@ export default withEnglishFallback({
|
|||
imageLoadFailed: "Caricamento immagine non riuscito",
|
||||
geometryPreview: "Anteprima Geometria",
|
||||
layerPreview: "Anteprima Layer",
|
||||
layerPreviewLabelNone: "— Etichetta —",
|
||||
layerPreviewExportImage: "Esporta immagine",
|
||||
layerPreviewMaximize: "Ingrandisci",
|
||||
layerPreviewRestore: "Ripristina",
|
||||
insertRowsNotSupported: "La destinazione di salvataggio corrente non supporta l'aggiunta di righe.",
|
||||
zoomIn: "Ingrandisci",
|
||||
zoomOut: "Rimpicciolisci",
|
||||
fitImage: "Adatta alla Vista",
|
||||
|
|
@ -1298,6 +1327,9 @@ export default withEnglishFallback({
|
|||
numericColumnAlignRight: "Allineamento a destra",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "XLSX supporta al massimo {limit} righe di dati. Usa l'esportazione CSV per il risultato completo.",
|
||||
streamingUnsupported: "L'esportazione in streaming non è supportata per questa query. Semplificala o usa un driver supportato.",
|
||||
agentSessionMissing: "L'esportazione in streaming richiede una sessione del set di risultati, ma questo driver non ha restituito session_id.",
|
||||
title: "Esportazione Dati Tabella",
|
||||
fetching: "Recupero dati dal database...",
|
||||
writing: "Scrittura su file...",
|
||||
|
|
@ -1354,6 +1386,7 @@ export default withEnglishFallback({
|
|||
mcpLearnMore: "Scopri di più",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "Il file non esiste: {path}",
|
||||
language: "Lingua",
|
||||
loading: "Caricamento...",
|
||||
stopping: "Interruzione...",
|
||||
|
|
@ -1713,6 +1746,7 @@ export default withEnglishFallback({
|
|||
enableThinkingOff: "Disabilitato",
|
||||
enableThinkingHint: "Questa opzione ha effetto solo sulle API /chat/completions e sui modelli supportati. Quando è disabilitata, può ridurre notevolmente l'uso dei token, ma la qualità dei risultati generati potrebbe diminuire leggermente.",
|
||||
anthropicMessagesHint: "Le API compatibili con Anthropic Messages di solito usano /v1/messages.",
|
||||
openAiCompatibleEndpointHint: "La maggior parte delle API compatibili con OpenAI richiede il prefisso di percorso /v1.",
|
||||
contextWindow: "Finestra di Contesto",
|
||||
contextWindowAuto: "Auto (rileva dal nome del modello)",
|
||||
contextWindowHint: "Token. Lascia vuoto per il rilevamento automatico. Imposta manualmente per modelli locali/personalizzati.",
|
||||
|
|
@ -3397,6 +3431,121 @@ export default withEnglishFallback({
|
|||
ok: "OK",
|
||||
noTablesSelected: "No tables selected",
|
||||
orderHint: "Use arrow buttons to reorder",
|
||||
language: "Lingua",
|
||||
regionLabel: "Regione",
|
||||
formatLabel: "Formato",
|
||||
formatType: "Tipo di formato",
|
||||
typeLabel: "Tipo",
|
||||
loading: "Caricamento...",
|
||||
regex: "Espressione regolare",
|
||||
rawPattern: "Modalità pattern grezzo",
|
||||
notSet: "(non impostato)",
|
||||
noMatch: "(nessuna corrispondenza)",
|
||||
invalidRegex: "(espressione regolare non valida)",
|
||||
noDomain: "(nessun dominio)",
|
||||
schema: "Schema",
|
||||
tableLabel: "Tabella",
|
||||
columnLabel: "Colonna",
|
||||
genMode: "Modalità di generazione",
|
||||
dateType: "Tipo di data",
|
||||
yearOffsetRange: "Intervallo di anni (scostamento dall'anno corrente)",
|
||||
numberType: "Tipo di numero",
|
||||
integer: "Intero",
|
||||
decimal: "Decimale",
|
||||
decimalPlaces: "Cifre decimali",
|
||||
uuidFormat: "Formato UUID",
|
||||
uuidWithHyphens: "Con trattini",
|
||||
uuidNoHyphens: "Senza trattini",
|
||||
includeSeparators: "Includi separatori",
|
||||
phoneDomestic: "Nazionale",
|
||||
phoneInternational: "Internazionale",
|
||||
charCount: "Caratteri",
|
||||
start: "Inizio",
|
||||
valueList: "Elenco valori",
|
||||
domainList: "Elenco domini",
|
||||
subdomains: "Sottodomini",
|
||||
tlds: "Domini di primo livello",
|
||||
extensionType: "Tipo di estensione",
|
||||
extensionList: "Elenco estensioni:",
|
||||
availableExtensions: "Estensioni disponibili:",
|
||||
includeExtension: "Includi estensione",
|
||||
includeFileName: "Includi nome file",
|
||||
pathType: "Tipo di percorso",
|
||||
ipType: "Tipo di indirizzo IP",
|
||||
nameFormat: "Formato nome",
|
||||
transformTo: "Trasforma i valori in",
|
||||
generateOptions: "Opzioni di generazione",
|
||||
previewRowCount: "{count} righe",
|
||||
useKeywords: "Genera da parole chiave",
|
||||
weekdayShort: {
|
||||
sun: "Dom",
|
||||
mon: "Lun",
|
||||
tue: "Mar",
|
||||
wed: "Mer",
|
||||
thu: "Gio",
|
||||
fri: "Ven",
|
||||
sat: "Sab",
|
||||
},
|
||||
countries: {
|
||||
us: "Stati Uniti",
|
||||
uk: "Regno Unito",
|
||||
cn: "Cina",
|
||||
jp: "Giappone",
|
||||
other: "Altro",
|
||||
cnEnglish: "Cina (inglese)",
|
||||
jpEnglish: "Giappone (inglese)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "Lingua locale",
|
||||
english: "Inglese",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "Indirizzo riga 1",
|
||||
line2: "Indirizzo riga 2",
|
||||
full: "Indirizzo completo",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "Nome completo",
|
||||
last: "Solo cognome",
|
||||
first: "Solo nome",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "Nome",
|
||||
code: "Codice",
|
||||
codeName: "Codice + Nome",
|
||||
},
|
||||
transforms: {
|
||||
none: "Inalterato",
|
||||
upper: "MAIUSCOLO",
|
||||
lower: "minuscolo",
|
||||
title: "Iniziali Maiuscole",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "Immagine",
|
||||
document: "Documento",
|
||||
spreadsheet: "Foglio di calcolo",
|
||||
presentation: "Presentazione",
|
||||
audio: "Audio",
|
||||
video: "Video",
|
||||
code: "Codice",
|
||||
archive: "Archivio",
|
||||
web: "Web",
|
||||
database: "Database",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "Un valore per riga\nfirst\nsecond\nthird",
|
||||
subdomains: "Un sottodominio per riga\nauth.\nwww.\napi.",
|
||||
tlds: "Un TLD per riga\n.com\n.cn\n.io",
|
||||
fileExtensions: "Un'estensione per riga\npng\njpg\ntxt",
|
||||
paymentMethods: "Un metodo di pagamento per riga\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "Una parola chiave per riga\nApple\nCherry\nOrange",
|
||||
weightUnits: "Un'unità per riga\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "Casuale",
|
||||
unique: "Senza duplicati",
|
||||
repeat: "Ripeti ogni valore",
|
||||
},
|
||||
gen: {
|
||||
general: "General",
|
||||
personal: "Personal",
|
||||
|
|
@ -4019,6 +4168,7 @@ export default withEnglishFallback({
|
|||
sidebarAllowHorizontalScroll: "Consenti scorrimento orizzontale barra laterale",
|
||||
sidebarAllowHorizontalScrollDescription: "Mostra i nomi lunghi di tabelle, viste e collezioni per intero consentendo lo scorrimento orizzontale della barra laterale.",
|
||||
snippetsDescription: "Personalizza i modelli di snippet SQL attivati nell'editor.",
|
||||
syncSnippetProviderGitee: "Snippet Gitee",
|
||||
snippetsAdd: "Aggiungi Snippet",
|
||||
snippetsLabel: "Etichetta",
|
||||
snippetsPrefix: "Prefisso",
|
||||
|
|
@ -4382,6 +4532,11 @@ export default withEnglishFallback({
|
|||
dateTimeFormatEmpty: "Inserisci un formato data/ora personalizzato",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Impossibile rimuovere la vecchia directory JRE: {path} (errore originale: {error})",
|
||||
jreDirRemoveFailedWindows:
|
||||
"Impossibile rimuovere la vecchia directory JRE: {path}\nCause possibili:\n - un processo dbx Agent / java sta ancora usando la directory\n - l'antivirus la sta analizzando\nChiudi ogni processo che potrebbe usare la directory, oppure riavvia dbx e riprova.\n(errore originale: {error})",
|
||||
jreInUseByDrivers: "Il JRE {jre} è in uso dai driver: {drivers}. Disinstallali prima.",
|
||||
offlinePackageRegistryMissing: "agent-registry.json non trovato nello ZIP; non è un pacchetto driver offline valido.",
|
||||
progressJreExtract: "Estrazione JRE...",
|
||||
progressDownloadJre: "Download JRE in corso",
|
||||
progressDownloadDriver: "Download driver in corso",
|
||||
|
|
@ -5397,6 +5552,7 @@ export default withEnglishFallback({
|
|||
},
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "Kafka non supporta lo scaricamento dei topic",
|
||||
title: "Produttori / Consumatori",
|
||||
unloadTopic: "Scarica topic",
|
||||
unloading: "Scaricamento in corso...",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default withEnglishFallback({
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "{seconds} 秒後に再試行してください",
|
||||
setupTitle: "アクセスパスワードを設定",
|
||||
setupDescription: "インスタンスを保護するためのパスワードを設定してください",
|
||||
loginDescription: "データベース管理ツール",
|
||||
|
|
@ -405,6 +406,28 @@ export default withEnglishFallback({
|
|||
agentJavaTooOld: "このドライバーにはJava 21が必要です。ドライバーマネージャーでDBX管理JRE 21を使用するか、Java 21実行ファイルを選択してください。",
|
||||
agentDriverUpdateConnectionHint: "この接続で使用中の内蔵ドライバに更新が利用可能です。接続失敗はローカルドライバの旧版が原因の可能性があります。ドライバマネージャーで該当ドライバを更新してから再試行してください。",
|
||||
jdbcPluginNotInstalled: "JDBCプラグインがインストールされていません。この接続を使用するにはオプションのJDBCプラグインをインストールしてください。",
|
||||
connectFailedTitle: "接続に失敗しました",
|
||||
fullErrorMessage: "エラーの詳細",
|
||||
copyError: "エラーをコピー",
|
||||
optionalSuffix: "(任意)",
|
||||
customColorPlaceholder: "#ff0000 または rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io または libsql://your-database.turso.io",
|
||||
tursoHostHint: "libsql:// および https:// プロトコルに対応しています。ホスト名のみの入力も可能です(自動的に HTTPS を使用します)。",
|
||||
tursoTokenHint: "次のコマンドでトークンを作成します:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx(任意、上の Auth Token 欄が優先されます)",
|
||||
driverInstall: {
|
||||
installingTitle: "ドライバーをインストール中",
|
||||
failedTitle: "ドライバーのインストールに失敗しました",
|
||||
fullError: "エラーの全文",
|
||||
statusFailed: "インストール失敗",
|
||||
statusWaiting: "インストール待機中",
|
||||
statusPreparing: "ドライバーのインストールを準備中...",
|
||||
statusExtractingJre: "JRE を展開中...",
|
||||
stepJre: "JRE をダウンロード中",
|
||||
stepDriver: "ドライバーをダウンロード中",
|
||||
stepDefault: "ドライバーをインストール中",
|
||||
installingButton: "インストール中...",
|
||||
},
|
||||
lastError: "接続エラー",
|
||||
errorIndicatorHint: "接続エラー。クリックして詳細を表示",
|
||||
clearError: "接続エラーをクリア",
|
||||
|
|
@ -651,6 +674,7 @@ export default withEnglishFallback({
|
|||
damengJvmOptionsInvalid: "{line} 行目は、シェル引用符を使わない -Dkey または -Dkey=value 形式で入力してください。",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "前回の DuckDB クエリはまだ停止処理中です。しばらくしてから再試行してください。",
|
||||
pressToExecute: "{mod}+Enter で実行",
|
||||
pressToSaveSql: "{mod}+S でSQLを保存",
|
||||
queryTimeoutError: "クエリがタイムアウトしました({seconds}秒)。データベース接続が正常か確認してください。",
|
||||
|
|
@ -1148,6 +1172,11 @@ export default withEnglishFallback({
|
|||
imageLoadFailed: "画像の読み込みに失敗しました",
|
||||
geometryPreview: "ジオメトリプレビュー",
|
||||
layerPreview: "レイヤープレビュー",
|
||||
layerPreviewLabelNone: "— ラベル —",
|
||||
layerPreviewExportImage: "画像をエクスポート",
|
||||
layerPreviewMaximize: "最大化",
|
||||
layerPreviewRestore: "元に戻す",
|
||||
insertRowsNotSupported: "現在の保存先は行の追加に対応していません。",
|
||||
zoomIn: "拡大",
|
||||
zoomOut: "縮小",
|
||||
fitImage: "表示領域に合わせる",
|
||||
|
|
@ -1299,6 +1328,9 @@ export default withEnglishFallback({
|
|||
numericColumnAlignRight: "右揃え",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "XLSX は最大 {limit} 行のデータに対応しています。完全な結果を得るには CSV エクスポートを使用してください。",
|
||||
streamingUnsupported: "このクエリはストリーミングエクスポートに対応していません。クエリを簡略化するか、対応しているドライバーを使用してください。",
|
||||
agentSessionMissing: "ストリーミングエクスポートには結果セットのセッションが必要ですが、このドライバーは session_id を返しませんでした。",
|
||||
title: "テーブルデータをエクスポート中",
|
||||
fetching: "データベースからデータを取得中...",
|
||||
writing: "ファイルに書き込み中...",
|
||||
|
|
@ -1355,6 +1387,7 @@ export default withEnglishFallback({
|
|||
mcpLearnMore: "詳細を見る",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "ファイルが存在しません: {path}",
|
||||
language: "言語",
|
||||
loading: "読み込み中...",
|
||||
stopping: "停止中...",
|
||||
|
|
@ -1777,6 +1810,7 @@ export default withEnglishFallback({
|
|||
enableThinkingOff: "無効",
|
||||
enableThinkingHint: "このオプションは/chat/completions APIとサポートされているモデルでのみ有効です。無効にするとトークン使用量を大幅に削減できますが、生成結果の品質が若干低下する可能性があります。",
|
||||
anthropicMessagesHint: "Anthropic Messages 互換 API は通常 /v1/messages を使用します。",
|
||||
openAiCompatibleEndpointHint: "ほとんどの OpenAI 互換 API は /v1 パスプレフィックスが必要です。",
|
||||
actions: {
|
||||
general: "一般的な質問",
|
||||
generate: "SQLを生成",
|
||||
|
|
@ -3398,6 +3432,121 @@ export default withEnglishFallback({
|
|||
ok: "OK",
|
||||
noTablesSelected: "テーブルが選択されていません",
|
||||
orderHint: "矢印ボタンで並び替え",
|
||||
language: "言語",
|
||||
regionLabel: "地域",
|
||||
formatLabel: "形式",
|
||||
formatType: "形式の種類",
|
||||
typeLabel: "種類",
|
||||
loading: "読み込み中...",
|
||||
regex: "正規表現",
|
||||
rawPattern: "生パターンモード",
|
||||
notSet: "(未設定)",
|
||||
noMatch: "(一致なし)",
|
||||
invalidRegex: "(無効な正規表現)",
|
||||
noDomain: "(ドメインなし)",
|
||||
schema: "スキーマ",
|
||||
tableLabel: "テーブル",
|
||||
columnLabel: "カラム",
|
||||
genMode: "生成モード",
|
||||
dateType: "日付の種類",
|
||||
yearOffsetRange: "年の範囲(現在の年からのオフセット)",
|
||||
numberType: "数値の種類",
|
||||
integer: "整数",
|
||||
decimal: "小数",
|
||||
decimalPlaces: "小数点以下の桁数",
|
||||
uuidFormat: "UUID 形式",
|
||||
uuidWithHyphens: "ハイフンあり",
|
||||
uuidNoHyphens: "ハイフンなし",
|
||||
includeSeparators: "区切り文字を含める",
|
||||
phoneDomestic: "国内",
|
||||
phoneInternational: "国際",
|
||||
charCount: "文字数",
|
||||
start: "開始",
|
||||
valueList: "値のリスト",
|
||||
domainList: "ドメインのリスト",
|
||||
subdomains: "サブドメイン",
|
||||
tlds: "トップレベルドメイン",
|
||||
extensionType: "拡張子の種類",
|
||||
extensionList: "拡張子のリスト:",
|
||||
availableExtensions: "利用可能な拡張子:",
|
||||
includeExtension: "拡張子を含める",
|
||||
includeFileName: "ファイル名を含める",
|
||||
pathType: "パスの種類",
|
||||
ipType: "IP アドレスの種類",
|
||||
nameFormat: "名前の形式",
|
||||
transformTo: "値を次に変換",
|
||||
generateOptions: "生成オプション",
|
||||
previewRowCount: "{count} 行",
|
||||
useKeywords: "キーワードから生成",
|
||||
weekdayShort: {
|
||||
sun: "日",
|
||||
mon: "月",
|
||||
tue: "火",
|
||||
wed: "水",
|
||||
thu: "木",
|
||||
fri: "金",
|
||||
sat: "土",
|
||||
},
|
||||
countries: {
|
||||
us: "アメリカ合衆国",
|
||||
uk: "イギリス",
|
||||
cn: "中国",
|
||||
jp: "日本",
|
||||
other: "その他",
|
||||
cnEnglish: "中国 (English)",
|
||||
jpEnglish: "日本 (English)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "現地の言語",
|
||||
english: "English",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "住所 1 行目",
|
||||
line2: "住所 2 行目",
|
||||
full: "完全な住所",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "氏名",
|
||||
last: "姓のみ",
|
||||
first: "名のみ",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "名称",
|
||||
code: "コード",
|
||||
codeName: "コード + 名称",
|
||||
},
|
||||
transforms: {
|
||||
none: "そのまま",
|
||||
upper: "すべて大文字",
|
||||
lower: "すべて小文字",
|
||||
title: "各単語の先頭を大文字",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "画像",
|
||||
document: "ドキュメント",
|
||||
spreadsheet: "表計算",
|
||||
presentation: "プレゼンテーション",
|
||||
audio: "音声",
|
||||
video: "動画",
|
||||
code: "コード",
|
||||
archive: "アーカイブ",
|
||||
web: "Web",
|
||||
database: "データベース",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "1 行に 1 つの値\nfirst\nsecond\nthird",
|
||||
subdomains: "1 行に 1 つのサブドメイン\nauth.\nwww.\napi.",
|
||||
tlds: "1 行に 1 つの TLD\n.com\n.cn\n.io",
|
||||
fileExtensions: "1 行に 1 つの拡張子\npng\njpg\ntxt",
|
||||
paymentMethods: "1 行に 1 つの支払い方法\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "1 行に 1 つのキーワード\nApple\nCherry\nOrange",
|
||||
weightUnits: "1 行に 1 つの単位\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "ランダム",
|
||||
unique: "重複なし",
|
||||
repeat: "各値を繰り返す",
|
||||
},
|
||||
gen: {
|
||||
general: "一般",
|
||||
personal: "個人",
|
||||
|
|
@ -4010,6 +4159,7 @@ export default withEnglishFallback({
|
|||
sidebarAllowHorizontalScroll: "サイドバーの横スクロールを許可",
|
||||
sidebarAllowHorizontalScrollDescription: "サイドバーの横スクロールを許可して、長いテーブル、ビュー、コレクション名を完全に表示します。",
|
||||
snippetsDescription: "エディタでトリガーされるSQLスニペットテンプレートをカスタマイズします。",
|
||||
syncSnippetProviderGitee: "Gitee コードスニペット",
|
||||
snippetsAdd: "スニペットを追加",
|
||||
snippetsLabel: "ラベル",
|
||||
snippetsPrefix: "プレフィックス",
|
||||
|
|
@ -4382,6 +4532,11 @@ export default withEnglishFallback({
|
|||
dateTimeFormatEmpty: "カスタム日時形式を入力",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "古い JRE ディレクトリを削除できませんでした: {path}(元のエラー: {error})",
|
||||
jreDirRemoveFailedWindows:
|
||||
"古い JRE ディレクトリを削除できませんでした: {path}\n考えられる原因:\n - dbx Agent / java プロセスがディレクトリを使用中です\n - ウイルス対策ソフトがスキャン中です\nディレクトリを使用している可能性のあるプロセスを終了するか、dbx を再起動して再試行してください。\n(元のエラー: {error})",
|
||||
jreInUseByDrivers: "JRE {jre} は次のドライバーが使用中です: {drivers}。先にアンインストールしてください。",
|
||||
offlinePackageRegistryMissing: "ZIP 内に agent-registry.json が見つかりません。有効なオフラインドライバーパッケージではありません。",
|
||||
progressJreExtract: "JREを展開中...",
|
||||
progressDownloadJre: "JREをダウンロード中",
|
||||
progressDownloadDriver: "ドライバーをダウンロード中",
|
||||
|
|
@ -5397,6 +5552,7 @@ export default withEnglishFallback({
|
|||
},
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "Kafka はトピックのアンロードに対応していません",
|
||||
title: "プロデューサー / コンシューマー",
|
||||
unloadTopic: "トピックをアンロード",
|
||||
unloading: "アンロード中...",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default withEnglishFallback({
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "Tente novamente em {seconds}s",
|
||||
setupTitle: "Configurar senha de acesso",
|
||||
setupDescription: "Defina uma senha para proteger sua instância",
|
||||
loginDescription: "Ferramenta de gerenciamento de banco de dados",
|
||||
|
|
@ -412,6 +413,28 @@ export default withEnglishFallback({
|
|||
agentJavaTooOld: "Este driver requer Java 21. Use o JRE 21 gerenciado pelo DBX ou selecione um executável Java 21 no Gerenciador de Drivers.",
|
||||
agentDriverUpdateConnectionHint: "Há uma atualização de driver integrada disponível para esta conexão. A falha de conexão pode estar relacionada a um driver local desatualizado. Atualize o driver correspondente no Gerenciador de Drivers e tente novamente.",
|
||||
jdbcPluginNotInstalled: "O plugin JDBC não está instalado. Instale o plugin JDBC opcional para usar esta conexão.",
|
||||
connectFailedTitle: "Falha na conexão",
|
||||
fullErrorMessage: "Mensagem de erro completa",
|
||||
copyError: "Copiar erro",
|
||||
optionalSuffix: " (opcional)",
|
||||
customColorPlaceholder: "#ff0000 ou rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io ou libsql://your-database.turso.io",
|
||||
tursoHostHint: "Suporta os protocolos libsql:// e https://. Você também pode informar apenas o nome do host (HTTPS é usado automaticamente).",
|
||||
tursoTokenHint: "Crie um token com:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx (opcional, o campo Auth Token acima tem prioridade)",
|
||||
driverInstall: {
|
||||
installingTitle: "Instalando driver",
|
||||
failedTitle: "Falha na instalação do driver",
|
||||
fullError: "Erro completo",
|
||||
statusFailed: "Falha na instalação",
|
||||
statusWaiting: "Aguardando instalação",
|
||||
statusPreparing: "Preparando a instalação do driver...",
|
||||
statusExtractingJre: "Extraindo JRE...",
|
||||
stepJre: "Baixando JRE",
|
||||
stepDriver: "Baixando driver",
|
||||
stepDefault: "Instalando driver",
|
||||
installingButton: "Instalando...",
|
||||
},
|
||||
lastError: "Erro de conexão",
|
||||
errorIndicatorHint: "Erro de conexão. Clique para ver os detalhes",
|
||||
clearError: "Limpar erro de conexão",
|
||||
|
|
@ -652,6 +675,7 @@ export default withEnglishFallback({
|
|||
damengJvmOptionsInvalid: "A linha {line} deve ser uma propriedade -Dkey ou -Dkey=value sem aspas do shell.",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "A consulta anterior do DuckDB ainda está sendo interrompida. Tente novamente em breve.",
|
||||
pressToExecute: "Pressione {mod}+Enter para executar",
|
||||
pressToSaveSql: "Pressione {mod}+S para salvar o SQL",
|
||||
queryTimeoutError: "A consulta expirou ({seconds}s). Verifique se a conexão com o banco de dados está saudável.",
|
||||
|
|
@ -1153,6 +1177,11 @@ export default withEnglishFallback({
|
|||
imageLoadFailed: "Falha ao carregar a imagem",
|
||||
geometryPreview: "Visualização de Geometria",
|
||||
layerPreview: "Visualização de Camada",
|
||||
layerPreviewLabelNone: "— Rótulo —",
|
||||
layerPreviewExportImage: "Exportar imagem",
|
||||
layerPreviewMaximize: "Maximizar",
|
||||
layerPreviewRestore: "Restaurar",
|
||||
insertRowsNotSupported: "O destino de salvamento atual não suporta a adição de linhas.",
|
||||
zoomIn: "Aproximar",
|
||||
zoomOut: "Afastar",
|
||||
fitImage: "Ajustar à Visualização",
|
||||
|
|
@ -1300,6 +1329,9 @@ export default withEnglishFallback({
|
|||
numericColumnAlignRight: "Alinhamento à direita",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "O XLSX suporta no máximo {limit} linhas de dados. Use a exportação CSV para obter o resultado completo.",
|
||||
streamingUnsupported: "A exportação em streaming não é compatível com esta consulta. Simplifique-a ou use um driver compatível.",
|
||||
agentSessionMissing: "A exportação em streaming requer uma sessão de conjunto de resultados, mas este driver não retornou session_id.",
|
||||
title: "Exportando Dados da Tabela",
|
||||
fetching: "Buscando dados do banco de dados...",
|
||||
writing: "Gravando no arquivo...",
|
||||
|
|
@ -1356,6 +1388,7 @@ export default withEnglishFallback({
|
|||
mcpLearnMore: "Saiba mais",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "O arquivo não existe: {path}",
|
||||
language: "Idioma",
|
||||
loading: "Carregando...",
|
||||
stopping: "Parando...",
|
||||
|
|
@ -1771,6 +1804,7 @@ export default withEnglishFallback({
|
|||
proxyEnable: "Enviar requisições de AI através do proxy",
|
||||
proxyUrl: "URL do Proxy",
|
||||
anthropicMessagesHint: "APIs compatíveis com Anthropic Messages geralmente usam /v1/messages.",
|
||||
openAiCompatibleEndpointHint: "A maioria das APIs compatíveis com OpenAI requer o prefixo de caminho /v1.",
|
||||
contextWindow: "Janela de Contexto",
|
||||
contextWindowAuto: "Automático (detectar pelo nome do modelo)",
|
||||
contextWindowHint: "Tokens. Deixe vazio para detecção automática. Defina manualmente para modelos locais/personalizados.",
|
||||
|
|
@ -3399,6 +3433,121 @@ export default withEnglishFallback({
|
|||
ok: "OK",
|
||||
noTablesSelected: "No tables selected",
|
||||
orderHint: "Use arrow buttons to reorder",
|
||||
language: "Idioma",
|
||||
regionLabel: "Região",
|
||||
formatLabel: "Formato",
|
||||
formatType: "Tipo de formato",
|
||||
typeLabel: "Tipo",
|
||||
loading: "Carregando...",
|
||||
regex: "Expressão regular",
|
||||
rawPattern: "Modo de padrão bruto",
|
||||
notSet: "(não definido)",
|
||||
noMatch: "(sem correspondência)",
|
||||
invalidRegex: "(expressão regular inválida)",
|
||||
noDomain: "(sem domínio)",
|
||||
schema: "Esquema",
|
||||
tableLabel: "Tabela",
|
||||
columnLabel: "Coluna",
|
||||
genMode: "Modo de geração",
|
||||
dateType: "Tipo de data",
|
||||
yearOffsetRange: "Intervalo de anos (deslocamento em relação ao ano atual)",
|
||||
numberType: "Tipo de número",
|
||||
integer: "Inteiro",
|
||||
decimal: "Decimal",
|
||||
decimalPlaces: "Casas decimais",
|
||||
uuidFormat: "Formato UUID",
|
||||
uuidWithHyphens: "Com hifens",
|
||||
uuidNoHyphens: "Sem hifens",
|
||||
includeSeparators: "Incluir separadores",
|
||||
phoneDomestic: "Nacional",
|
||||
phoneInternational: "Internacional",
|
||||
charCount: "Caracteres",
|
||||
start: "Início",
|
||||
valueList: "Lista de valores",
|
||||
domainList: "Lista de domínios",
|
||||
subdomains: "Subdomínios",
|
||||
tlds: "Domínios de topo",
|
||||
extensionType: "Tipo de extensão",
|
||||
extensionList: "Lista de extensões:",
|
||||
availableExtensions: "Extensões disponíveis:",
|
||||
includeExtension: "Incluir extensão",
|
||||
includeFileName: "Incluir nome do arquivo",
|
||||
pathType: "Tipo de caminho",
|
||||
ipType: "Tipo de endereço IP",
|
||||
nameFormat: "Formato do nome",
|
||||
transformTo: "Transformar valores em",
|
||||
generateOptions: "Opções de geração",
|
||||
previewRowCount: "{count} linhas",
|
||||
useKeywords: "Gerar a partir de palavras-chave",
|
||||
weekdayShort: {
|
||||
sun: "Dom",
|
||||
mon: "Seg",
|
||||
tue: "Ter",
|
||||
wed: "Qua",
|
||||
thu: "Qui",
|
||||
fri: "Sex",
|
||||
sat: "Sáb",
|
||||
},
|
||||
countries: {
|
||||
us: "Estados Unidos",
|
||||
uk: "Reino Unido",
|
||||
cn: "China",
|
||||
jp: "Japão",
|
||||
other: "Outro",
|
||||
cnEnglish: "China (inglês)",
|
||||
jpEnglish: "Japão (inglês)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "Idioma local",
|
||||
english: "Inglês",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "Endereço linha 1",
|
||||
line2: "Endereço linha 2",
|
||||
full: "Endereço completo",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "Nome completo",
|
||||
last: "Somente sobrenome",
|
||||
first: "Somente nome",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "Nome",
|
||||
code: "Código",
|
||||
codeName: "Código + Nome",
|
||||
},
|
||||
transforms: {
|
||||
none: "Como está",
|
||||
upper: "MAIÚSCULAS",
|
||||
lower: "minúsculas",
|
||||
title: "Iniciais Maiúsculas",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "Imagem",
|
||||
document: "Documento",
|
||||
spreadsheet: "Planilha",
|
||||
presentation: "Apresentação",
|
||||
audio: "Áudio",
|
||||
video: "Vídeo",
|
||||
code: "Código",
|
||||
archive: "Arquivo compactado",
|
||||
web: "Web",
|
||||
database: "Banco de dados",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "Um valor por linha\nfirst\nsecond\nthird",
|
||||
subdomains: "Um subdomínio por linha\nauth.\nwww.\napi.",
|
||||
tlds: "Um TLD por linha\n.com\n.cn\n.io",
|
||||
fileExtensions: "Uma extensão por linha\npng\njpg\ntxt",
|
||||
paymentMethods: "Um método de pagamento por linha\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "Uma palavra-chave por linha\nApple\nCherry\nOrange",
|
||||
weightUnits: "Uma unidade por linha\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "Aleatório",
|
||||
unique: "Sem duplicatas",
|
||||
repeat: "Repetir cada valor",
|
||||
},
|
||||
gen: {
|
||||
general: "General",
|
||||
personal: "Personal",
|
||||
|
|
@ -4021,6 +4170,7 @@ export default withEnglishFallback({
|
|||
sidebarAllowHorizontalScroll: "Permitir rolagem horizontal da barra lateral",
|
||||
sidebarAllowHorizontalScrollDescription: "Mostrar nomes longos de tabelas, views e coleções por completo, permitindo a rolagem horizontal da barra lateral.",
|
||||
snippetsDescription: "Personalize os modelos de snippets SQL acionados no editor.",
|
||||
syncSnippetProviderGitee: "Snippets do Gitee",
|
||||
snippetsAdd: "Adicionar snippet",
|
||||
snippetsLabel: "Rótulo",
|
||||
snippetsPrefix: "Prefixo",
|
||||
|
|
@ -4384,6 +4534,11 @@ export default withEnglishFallback({
|
|||
dateTimeFormatEmpty: "Insira um formato de data e hora personalizado",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Não foi possível remover o diretório JRE antigo: {path} (erro original: {error})",
|
||||
jreDirRemoveFailedWindows:
|
||||
"Não foi possível remover o diretório JRE antigo: {path}\nCausas possíveis:\n - um processo dbx Agent / java ainda está usando o diretório\n - o antivírus está analisando o diretório\nFeche qualquer processo que possa estar usando o diretório, ou reinicie o dbx e tente novamente.\n(erro original: {error})",
|
||||
jreInUseByDrivers: "O JRE {jre} está em uso pelos drivers: {drivers}. Desinstale-os primeiro.",
|
||||
offlinePackageRegistryMissing: "agent-registry.json não encontrado no ZIP; não é um pacote de drivers offline válido.",
|
||||
progressJreExtract: "Extraindo JRE...",
|
||||
progressDownloadJre: "Baixando JRE",
|
||||
progressDownloadDriver: "Baixando driver",
|
||||
|
|
@ -5399,6 +5554,7 @@ export default withEnglishFallback({
|
|||
},
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "O Kafka não suporta o descarregamento de tópicos",
|
||||
title: "Produtores / Consumidores",
|
||||
unloadTopic: "Descarregar tópico",
|
||||
unloading: "Descarregando...",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default withEnglishFallback({
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "请 {seconds} 秒后再试",
|
||||
setupTitle: "设置访问密码",
|
||||
setupDescription: "设置密码以保护您的实例",
|
||||
loginDescription: "数据库管理工具",
|
||||
|
|
@ -539,6 +540,28 @@ export default withEnglishFallback({
|
|||
agentJavaTooOld: "该驱动需要 Java 21。请在驱动管理器中使用 DBX 托管 JRE 21,或选择 Java 21 可执行文件。",
|
||||
agentDriverUpdateConnectionHint: "当前连接使用的内置驱动有可用更新,连接失败可能与本地驱动版本过旧有关。请在「驱动管理」中更新对应驱动后重试。",
|
||||
jdbcPluginNotInstalled: "JDBC 插件未安装,请先安装 JDBC 插件再使用此连接。",
|
||||
connectFailedTitle: "连接失败",
|
||||
fullErrorMessage: "完整错误信息",
|
||||
copyError: "复制错误",
|
||||
optionalSuffix: "(可选)",
|
||||
customColorPlaceholder: "#ff0000 或 rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io 或 libsql://your-database.turso.io",
|
||||
tursoHostHint: "支持 libsql:// 或 https:// 协议,也可以只填主机名(自动使用 HTTPS)",
|
||||
tursoTokenHint: "使用以下命令创建 token:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx(可选,优先使用上面的 Auth Token 字段)",
|
||||
driverInstall: {
|
||||
installingTitle: "正在安装驱动",
|
||||
failedTitle: "驱动安装失败",
|
||||
fullError: "完整错误",
|
||||
statusFailed: "安装失败",
|
||||
statusWaiting: "等待安装",
|
||||
statusPreparing: "准备安装驱动...",
|
||||
statusExtractingJre: "解压 JRE...",
|
||||
stepJre: "下载 JRE",
|
||||
stepDriver: "下载驱动",
|
||||
stepDefault: "安装驱动",
|
||||
installingButton: "安装中...",
|
||||
},
|
||||
lastError: "连接错误",
|
||||
errorIndicatorHint: "连接错误,点击查看详情",
|
||||
clearError: "清除连接错误",
|
||||
|
|
@ -672,6 +695,7 @@ export default withEnglishFallback({
|
|||
colorCustom: "自定义颜色",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "上一条 DuckDB 查询仍在停止,请稍后重试。",
|
||||
statementExecutionSucceeded: "{count} 条语句成功",
|
||||
statementExecutionFailed: "{count} 条语句失败",
|
||||
pressToExecute: "按 {mod}+Enter 执行查询",
|
||||
|
|
@ -1257,6 +1281,11 @@ export default withEnglishFallback({
|
|||
imageLoadFailed: "图片加载失败",
|
||||
geometryPreview: "图形预览",
|
||||
layerPreview: "图层预览",
|
||||
layerPreviewLabelNone: "— 标签 —",
|
||||
layerPreviewExportImage: "导出图片",
|
||||
layerPreviewMaximize: "最大化",
|
||||
layerPreviewRestore: "还原",
|
||||
insertRowsNotSupported: "当前保存目标不支持新增行。",
|
||||
zoomIn: "放大",
|
||||
zoomOut: "缩小",
|
||||
fitImage: "适合显示区域",
|
||||
|
|
@ -1358,6 +1387,9 @@ export default withEnglishFallback({
|
|||
truncatedHint: "结果已截断,已加载前 {count} 行。可通过底部分页浏览已加载数据;导出完整结果时会重新查询数据库。",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "XLSX 最多支持 {limit} 行数据,请改用 CSV 导出完整结果。",
|
||||
streamingUnsupported: "当前查询暂不支持流式导出,请简化查询或使用受支持的驱动。",
|
||||
agentSessionMissing: "流式导出需要结果集会话,但当前驱动未返回 session_id。",
|
||||
title: "导出表数据",
|
||||
fetching: "正在从数据库获取数据...",
|
||||
writing: "正在写入文件...",
|
||||
|
|
@ -1414,6 +1446,7 @@ export default withEnglishFallback({
|
|||
mcpLearnMore: "了解更多",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "文件不存在:{path}",
|
||||
language: "语言",
|
||||
loading: "加载中...",
|
||||
stopping: "正在停止...",
|
||||
|
|
@ -1735,6 +1768,7 @@ export default withEnglishFallback({
|
|||
enableThinkingOff: "已禁用",
|
||||
enableThinkingHint: "此选项仅对 /chat/completions API 且部分支持的模型生效。设为禁用后可大幅节省 token,但生成结果质量可能会略微下降。",
|
||||
anthropicMessagesHint: "Anthropic Messages 兼容 API 通常使用 /v1/messages。",
|
||||
openAiCompatibleEndpointHint: "大多数 OpenAI 兼容 API 需要 /v1 路径前缀",
|
||||
contextWindow: "上下文窗口",
|
||||
contextWindowAuto: "自动(根据模型名称推断)",
|
||||
contextWindowHint: "单位:token。留空自动推断,本地/自定义模型建议手动设置。",
|
||||
|
|
@ -3593,6 +3627,121 @@ export default withEnglishFallback({
|
|||
ok: "确定",
|
||||
noTablesSelected: "暂无已选中的表",
|
||||
orderHint: "使用上下箭头调整顺序",
|
||||
language: "语言",
|
||||
regionLabel: "地区",
|
||||
formatLabel: "格式",
|
||||
formatType: "格式类型",
|
||||
typeLabel: "类型",
|
||||
loading: "加载中...",
|
||||
regex: "正则表达式",
|
||||
rawPattern: "原始正则模式",
|
||||
notSet: "(未设置)",
|
||||
noMatch: "(无法匹配)",
|
||||
invalidRegex: "(无效正则)",
|
||||
noDomain: "(无域名)",
|
||||
schema: "模式",
|
||||
tableLabel: "表",
|
||||
columnLabel: "字段",
|
||||
genMode: "生成模式",
|
||||
dateType: "日期类型",
|
||||
yearOffsetRange: "年份范围(相对于当前年的偏移量)",
|
||||
numberType: "数字类型",
|
||||
integer: "整数",
|
||||
decimal: "小数",
|
||||
decimalPlaces: "小数位数",
|
||||
uuidFormat: "UUID 格式",
|
||||
uuidWithHyphens: "包含连字符",
|
||||
uuidNoHyphens: "不含连字符",
|
||||
includeSeparators: "包含分隔符",
|
||||
phoneDomestic: "国内",
|
||||
phoneInternational: "国际",
|
||||
charCount: "字符数",
|
||||
start: "开始",
|
||||
valueList: "值列表",
|
||||
domainList: "域(Domain)列表",
|
||||
subdomains: "子域",
|
||||
tlds: "顶级域",
|
||||
extensionType: "扩展名类型",
|
||||
extensionList: "扩展名列表:",
|
||||
availableExtensions: "可用扩展名:",
|
||||
includeExtension: "包含扩展名",
|
||||
includeFileName: "包含文件名称",
|
||||
pathType: "路径类型",
|
||||
ipType: "IP 地址类型",
|
||||
nameFormat: "姓名格式",
|
||||
transformTo: "将值转换为",
|
||||
generateOptions: "生成选项",
|
||||
previewRowCount: "{count} 行",
|
||||
useKeywords: "使用关键字生成",
|
||||
weekdayShort: {
|
||||
sun: "日",
|
||||
mon: "一",
|
||||
tue: "二",
|
||||
wed: "三",
|
||||
thu: "四",
|
||||
fri: "五",
|
||||
sat: "六",
|
||||
},
|
||||
countries: {
|
||||
us: "美国",
|
||||
uk: "英国",
|
||||
cn: "中国",
|
||||
jp: "日本",
|
||||
other: "其他",
|
||||
cnEnglish: "中国 (English)",
|
||||
jpEnglish: "日本 (English)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "本地语言",
|
||||
english: "English",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "第1行地址",
|
||||
line2: "第2行地址",
|
||||
full: "完整地址",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "全名",
|
||||
last: "仅姓氏",
|
||||
first: "仅名字",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "名称",
|
||||
code: "代码",
|
||||
codeName: "代码+名称",
|
||||
},
|
||||
transforms: {
|
||||
none: "原样",
|
||||
upper: "全大写",
|
||||
lower: "全小写",
|
||||
title: "每个单词首字母大写",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "图像",
|
||||
document: "文档",
|
||||
spreadsheet: "表格",
|
||||
presentation: "演示文稿",
|
||||
audio: "音频",
|
||||
video: "视频",
|
||||
code: "代码",
|
||||
archive: "压缩包",
|
||||
web: "网页",
|
||||
database: "数据库",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "每行一个值\nfirst\nsecond\nthird",
|
||||
subdomains: "每行一个子域\nauth.\nwww.\napi.",
|
||||
tlds: "每行一个 TLD\n.com\n.cn\n.io",
|
||||
fileExtensions: "每行一个扩展名\npng\njpg\ntxt",
|
||||
paymentMethods: "每行一个支付方式\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "每行一个关键字\nApple\nCherry\nOrange",
|
||||
weightUnits: "每行一个单位\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "随机",
|
||||
unique: "不重复",
|
||||
repeat: "重复每个值",
|
||||
},
|
||||
gen: {
|
||||
general: "通用",
|
||||
personal: "个人",
|
||||
|
|
@ -4288,6 +4437,7 @@ export default withEnglishFallback({
|
|||
syncSnippetTitle: "GitHub / Gitee 代码片段同步",
|
||||
syncSnippetDescription: "将同一份 DBX 快照保存到私有代码片段。首次上传会自动创建代码片段并保存其 ID。",
|
||||
syncSnippetProvider: "平台",
|
||||
syncSnippetProviderGitee: "Gitee 代码片段",
|
||||
syncSnippetId: "代码片段 ID",
|
||||
syncSnippetIdPlaceholder: "首次上传可留空",
|
||||
syncSnippetToken: "访问令牌",
|
||||
|
|
@ -4616,6 +4766,10 @@ export default withEnglishFallback({
|
|||
changelogSectionRemoved: "移除",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "无法删除旧的 JRE 目录:{path}(原始错误:{error})",
|
||||
jreDirRemoveFailedWindows: "无法删除旧的 JRE 目录:{path}\n可能的原因:\n - 仍有 dbx Agent / java 进程占用该目录\n - 防病毒软件正在扫描\n请关闭可能持有该目录的进程,或重启 dbx 后重试。\n(原始错误:{error})",
|
||||
jreInUseByDrivers: "JRE {jre} 正在被以下驱动使用:{drivers},请先卸载这些驱动。",
|
||||
offlinePackageRegistryMissing: "ZIP 文件中未找到 agent-registry.json,请确认这是有效的离线驱动包。",
|
||||
progressJreExtract: "解压 JRE...",
|
||||
progressDownloadJre: "下载 JRE",
|
||||
progressDownloadDriver: "下载驱动",
|
||||
|
|
@ -5536,6 +5690,7 @@ export default withEnglishFallback({
|
|||
peekMessageKey: "key={key}",
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "Kafka 不支持卸载主题",
|
||||
title: "生产者 / 消费者",
|
||||
unloadTopic: "卸载主题",
|
||||
unloading: "卸载中...",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default withEnglishFallback({
|
|||
name: "DBX",
|
||||
},
|
||||
auth: {
|
||||
rateLimited: "請 {seconds} 秒後再試",
|
||||
setupTitle: "設定存取密碼",
|
||||
setupDescription: "設定密碼以保護您的實例",
|
||||
loginDescription: "資料庫管理工具",
|
||||
|
|
@ -411,6 +412,28 @@ export default withEnglishFallback({
|
|||
agentJavaTooOld: "此驅動程式需要 Java 21。請在驅動程式管理器中使用 DBX 託管 JRE 21,或選擇 Java 21 可執行檔。",
|
||||
agentDriverUpdateConnectionHint: "目前連線使用的內建驅動有可用更新,連線失敗可能與本機驅動版本過舊有關。請在「驅動管理」中更新對應驅動後重試。",
|
||||
jdbcPluginNotInstalled: "JDBC 外掛程式未安裝,請先安裝 JDBC 外掛程式再使用此連線。",
|
||||
connectFailedTitle: "連線失敗",
|
||||
fullErrorMessage: "完整錯誤訊息",
|
||||
copyError: "複製錯誤",
|
||||
optionalSuffix: "(可選)",
|
||||
customColorPlaceholder: "#ff0000 或 rgba(…)",
|
||||
tursoHostPlaceholder: "your-database.turso.io 或 libsql://your-database.turso.io",
|
||||
tursoHostHint: "支援 libsql:// 或 https:// 協定,也可以只填主機名稱(自動使用 HTTPS)",
|
||||
tursoTokenHint: "使用以下指令建立 token:",
|
||||
tursoUrlParamsPlaceholder: "authToken=xxx(可選,優先使用上方的 Auth Token 欄位)",
|
||||
driverInstall: {
|
||||
installingTitle: "正在安裝驅動程式",
|
||||
failedTitle: "驅動程式安裝失敗",
|
||||
fullError: "完整錯誤",
|
||||
statusFailed: "安裝失敗",
|
||||
statusWaiting: "等待安裝",
|
||||
statusPreparing: "準備安裝驅動程式...",
|
||||
statusExtractingJre: "解壓 JRE...",
|
||||
stepJre: "下載 JRE",
|
||||
stepDriver: "下載驅動程式",
|
||||
stepDefault: "安裝驅動程式",
|
||||
installingButton: "安裝中...",
|
||||
},
|
||||
lastError: "連線錯誤",
|
||||
errorIndicatorHint: "連線錯誤,點擊檢視詳情",
|
||||
clearError: "清除連線錯誤",
|
||||
|
|
@ -651,6 +674,7 @@ export default withEnglishFallback({
|
|||
damengJvmOptionsInvalid: "第 {line} 行必須是未使用 shell 引號的 -Dkey 或 -Dkey=value 系統屬性。",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "上一筆 DuckDB 查詢仍在停止,請稍後重試。",
|
||||
pressToExecute: "按 {mod}+Enter 執行查詢",
|
||||
pressToSaveSql: "按 {mod}+S 儲存 SQL",
|
||||
queryTimeoutError: "查詢逾時 ({seconds}s),請檢查資料庫連線是否正常",
|
||||
|
|
@ -1152,6 +1176,11 @@ export default withEnglishFallback({
|
|||
imageLoadFailed: "圖片載入失敗",
|
||||
geometryPreview: "圖形預覽",
|
||||
layerPreview: "圖層預覽",
|
||||
layerPreviewLabelNone: "— 標籤 —",
|
||||
layerPreviewExportImage: "匯出圖片",
|
||||
layerPreviewMaximize: "最大化",
|
||||
layerPreviewRestore: "還原",
|
||||
insertRowsNotSupported: "目前的儲存目標不支援新增資料列。",
|
||||
zoomIn: "放大",
|
||||
zoomOut: "縮小",
|
||||
fitImage: "適合顯示區域",
|
||||
|
|
@ -1299,6 +1328,9 @@ export default withEnglishFallback({
|
|||
numericColumnAlignRight: "右對齊",
|
||||
},
|
||||
exportProgress: {
|
||||
xlsxRowLimit: "XLSX 最多支援 {limit} 列資料,請改用 CSV 匯出完整結果。",
|
||||
streamingUnsupported: "目前查詢暫不支援串流匯出,請簡化查詢或使用支援的驅動程式。",
|
||||
agentSessionMissing: "串流匯出需要結果集工作階段,但目前驅動程式未回傳 session_id。",
|
||||
title: "匯出資料表資料",
|
||||
fetching: "正在從資料庫擷取資料...",
|
||||
writing: "正在寫入檔案...",
|
||||
|
|
@ -1355,6 +1387,7 @@ export default withEnglishFallback({
|
|||
mcpLearnMore: "了解更多",
|
||||
},
|
||||
common: {
|
||||
fileNotFound: "檔案不存在:{path}",
|
||||
language: "語言",
|
||||
loading: "載入中……",
|
||||
stopping: "正在停止……",
|
||||
|
|
@ -1714,6 +1747,7 @@ export default withEnglishFallback({
|
|||
enableThinkingOff: "已停用",
|
||||
enableThinkingHint: "此選項僅對 /chat/completions API 且部分支援的模型生效。設為停用後可大幅節省 token,但產生結果品質可能會略微下降。",
|
||||
anthropicMessagesHint: "Anthropic Messages 相容 API 通常使用 /v1/messages。",
|
||||
openAiCompatibleEndpointHint: "大多數 OpenAI 相容 API 需要 /v1 路徑前綴",
|
||||
contextWindow: "Context Window",
|
||||
contextWindowAuto: "自動(從模型名稱偵測)",
|
||||
contextWindowHint: "Token 數。留空以自動偵測。本機/自訂模型請手動設定。",
|
||||
|
|
@ -3070,6 +3104,121 @@ export default withEnglishFallback({
|
|||
ok: "確定",
|
||||
noTablesSelected: "暫無已選中的表",
|
||||
orderHint: "使用上下箭頭調整順序",
|
||||
language: "語言",
|
||||
regionLabel: "地區",
|
||||
formatLabel: "格式",
|
||||
formatType: "格式類型",
|
||||
typeLabel: "類型",
|
||||
loading: "載入中...",
|
||||
regex: "正規表示式",
|
||||
rawPattern: "原始正規表示式模式",
|
||||
notSet: "(未設定)",
|
||||
noMatch: "(無法比對)",
|
||||
invalidRegex: "(無效的正規表示式)",
|
||||
noDomain: "(無網域)",
|
||||
schema: "結構描述",
|
||||
tableLabel: "資料表",
|
||||
columnLabel: "欄位",
|
||||
genMode: "產生模式",
|
||||
dateType: "日期類型",
|
||||
yearOffsetRange: "年份範圍(相對於目前年份的偏移量)",
|
||||
numberType: "數字類型",
|
||||
integer: "整數",
|
||||
decimal: "小數",
|
||||
decimalPlaces: "小數位數",
|
||||
uuidFormat: "UUID 格式",
|
||||
uuidWithHyphens: "包含連字號",
|
||||
uuidNoHyphens: "不含連字號",
|
||||
includeSeparators: "包含分隔符號",
|
||||
phoneDomestic: "國內",
|
||||
phoneInternational: "國際",
|
||||
charCount: "字元數",
|
||||
start: "開始",
|
||||
valueList: "值清單",
|
||||
domainList: "網域(Domain)清單",
|
||||
subdomains: "子網域",
|
||||
tlds: "頂級網域",
|
||||
extensionType: "副檔名類型",
|
||||
extensionList: "副檔名清單:",
|
||||
availableExtensions: "可用副檔名:",
|
||||
includeExtension: "包含副檔名",
|
||||
includeFileName: "包含檔案名稱",
|
||||
pathType: "路徑類型",
|
||||
ipType: "IP 位址類型",
|
||||
nameFormat: "姓名格式",
|
||||
transformTo: "將值轉換為",
|
||||
generateOptions: "產生選項",
|
||||
previewRowCount: "{count} 列",
|
||||
useKeywords: "使用關鍵字產生",
|
||||
weekdayShort: {
|
||||
sun: "日",
|
||||
mon: "一",
|
||||
tue: "二",
|
||||
wed: "三",
|
||||
thu: "四",
|
||||
fri: "五",
|
||||
sat: "六",
|
||||
},
|
||||
countries: {
|
||||
us: "美國",
|
||||
uk: "英國",
|
||||
cn: "中國",
|
||||
jp: "日本",
|
||||
other: "其他",
|
||||
cnEnglish: "中國 (English)",
|
||||
jpEnglish: "日本 (English)",
|
||||
},
|
||||
cityLanguages: {
|
||||
native: "本地語言",
|
||||
english: "English",
|
||||
},
|
||||
addressTypes: {
|
||||
line1: "地址第 1 行",
|
||||
line2: "地址第 2 行",
|
||||
full: "完整地址",
|
||||
},
|
||||
nameFormats: {
|
||||
full: "全名",
|
||||
last: "僅姓氏",
|
||||
first: "僅名字",
|
||||
},
|
||||
regionFormats: {
|
||||
name: "名稱",
|
||||
code: "代碼",
|
||||
codeName: "代碼+名稱",
|
||||
},
|
||||
transforms: {
|
||||
none: "原樣",
|
||||
upper: "全大寫",
|
||||
lower: "全小寫",
|
||||
title: "每個單字首字母大寫",
|
||||
},
|
||||
extensionCategories: {
|
||||
image: "圖片",
|
||||
document: "文件",
|
||||
spreadsheet: "試算表",
|
||||
presentation: "簡報",
|
||||
audio: "音訊",
|
||||
video: "影片",
|
||||
code: "程式碼",
|
||||
archive: "壓縮檔",
|
||||
web: "網頁",
|
||||
database: "資料庫",
|
||||
},
|
||||
placeholders: {
|
||||
enumValues: "每行一個值\nfirst\nsecond\nthird",
|
||||
subdomains: "每行一個子網域\nauth.\nwww.\napi.",
|
||||
tlds: "每行一個 TLD\n.com\n.cn\n.io",
|
||||
fileExtensions: "每行一個副檔名\npng\njpg\ntxt",
|
||||
paymentMethods: "每行一個付款方式\nCredit Card\nPayPal\nApple Pay",
|
||||
keywords: "每行一個關鍵字\nApple\nCherry\nOrange",
|
||||
weightUnits: "每行一個單位\ng\nkg\nlb",
|
||||
},
|
||||
genModes: {
|
||||
random: "隨機",
|
||||
unique: "不重複",
|
||||
repeat: "重複每個值",
|
||||
},
|
||||
gen: {
|
||||
general: "通用",
|
||||
personal: "個人",
|
||||
|
|
@ -3686,6 +3835,7 @@ export default withEnglishFallback({
|
|||
sidebarAllowHorizontalScroll: "允許側邊欄水平捲動",
|
||||
sidebarAllowHorizontalScrollDescription: "透過啟用側邊欄的水平捲動功能,完整顯示長表格、檢視和集合的名稱",
|
||||
snippetsDescription: "自訂編輯器中觸發的 SQL 程式碼片段範本。",
|
||||
syncSnippetProviderGitee: "Gitee 程式碼片段",
|
||||
snippetsAdd: "新增片段",
|
||||
snippetsLabel: "顯示名",
|
||||
snippetsPrefix: "觸發鍵",
|
||||
|
|
@ -4053,6 +4203,10 @@ export default withEnglishFallback({
|
|||
shortcutExPasteSqlInCondition: "ExPaste:貼上為 IN 條件",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "無法刪除舊的 JRE 目錄:{path}(原始錯誤:{error})",
|
||||
jreDirRemoveFailedWindows: "無法刪除舊的 JRE 目錄:{path}\n可能的原因:\n - 仍有 dbx Agent / java 程序占用該目錄\n - 防毒軟體正在掃描\n請關閉可能持有該目錄的程序,或重新啟動 dbx 後重試。\n(原始錯誤:{error})",
|
||||
jreInUseByDrivers: "JRE {jre} 正在被以下驅動程式使用:{drivers},請先卸載這些驅動程式。",
|
||||
offlinePackageRegistryMissing: "ZIP 檔案中未找到 agent-registry.json,請確認這是有效的離線驅動程式套件。",
|
||||
progressJreExtract: "解壓縮 JRE……",
|
||||
progressDownloadJre: "下載 JRE",
|
||||
progressDownloadDriver: "下載驅動程式",
|
||||
|
|
@ -5395,6 +5549,7 @@ export default withEnglishFallback({
|
|||
},
|
||||
},
|
||||
mqClients: {
|
||||
unloadTopicUnsupportedKafka: "Kafka 不支援卸載主題",
|
||||
title: "生產者 / 消費者",
|
||||
unloadTopic: "卸載主題",
|
||||
unloading: "卸載中...",
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ export function canRollbackHistoryEntry(entry: Pick<HistoryAiAnalysisEntry, "con
|
|||
|
||||
export function buildHistoryAiAnalysisPrompt(entry: HistoryAiAnalysisEntry): string {
|
||||
const details = [
|
||||
"请分析这条 DBX 历史记录,重点说明:",
|
||||
"1. 这次操作做了什么,以及可能影响哪些数据或结构。",
|
||||
"2. 是否有风险,例如无 WHERE 更新、删除、DDL、锁表、性能或权限问题。",
|
||||
"3. 如果有 Rollback SQL,请评估它是否足够安全,执行前还应该确认什么。",
|
||||
"4. 如果没有 Rollback SQL,请明确说明无法直接回滚,并给出可行的人工恢复建议。",
|
||||
"Analyse this DBX history entry and focus on:",
|
||||
"1. What this operation did, and which data or structures it may have affected.",
|
||||
"2. Whether it carries risk, e.g. an UPDATE without WHERE, deletes, DDL, table locks, performance or permission problems.",
|
||||
"3. If rollback SQL is present, judge whether it is safe enough and what should be confirmed before running it.",
|
||||
"4. If no rollback SQL is present, state clearly that it cannot be rolled back directly and suggest a workable manual recovery path.",
|
||||
"",
|
||||
"History metadata:",
|
||||
`Connection: ${entry.connection_name || "(unknown)"}`,
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ async function parseConnection(node: ParsedNode): Promise<ConnectionConfig | nul
|
|||
const profile = inferProfile(rawType, node.tag, port);
|
||||
if (!profile) {
|
||||
const name = getAny(node.values, ["name", "connectionName", "connName", "caption", "title"]) || "(unnamed)";
|
||||
console.warn(`[Navicat Import] 跳过无法识别类型的连接: "${name}" (type="${rawType}", tag="${node.tag}", port=${port ?? "N/A"})`);
|
||||
console.warn(`[Navicat Import] Skipped connection with unrecognised type: "${name}" (type="${rawType}", tag="${node.tag}", port=${port ?? "N/A"})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName";
|
|||
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
|
||||
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
|
||||
import type { SavedSqlFile } from "@/types/database";
|
||||
import i18n from "@/i18n";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
|
||||
const ORACLE_LIKE_METADATA_TYPES = new Set<string>(["oracle", "dameng", "oceanbase-oracle"]);
|
||||
const HIDDEN_QUERY_KEY_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle"]);
|
||||
|
|
@ -2506,7 +2508,10 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
|
||||
function toErrorResult(e: any): NonNullable<QueryTab["result"]> {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
const raw = e instanceof Error ? e.message : String(e);
|
||||
// Single funnel for every query execution failure, so backend messages DBX
|
||||
// knows about are shown in the active locale rather than as raw English.
|
||||
const message = translateBackendError(i18n.global.t, raw);
|
||||
return markQueryResultRowsRaw({
|
||||
columns: ["Error"],
|
||||
execution_error: true,
|
||||
|
|
|
|||
|
|
@ -54,23 +54,23 @@ fn remove_jre_dir_with_retry(path: &Path) -> std::io::Result<()> {
|
|||
Err(last_err.unwrap_or_else(|| std::io::Error::other("remove_dir_all failed without an error")))
|
||||
}
|
||||
|
||||
/// Render a friendly Chinese error message when the old JRE directory cannot
|
||||
/// be replaced. On Windows, lists likely culprits (process holding java.exe,
|
||||
/// Render a friendly error message when the old JRE directory cannot be
|
||||
/// replaced. On Windows, lists likely culprits (process holding java.exe,
|
||||
/// AV scanning) and suggests restarting dbx; on POSIX returns a concise
|
||||
/// message. The original OS error is appended in parentheses for support.
|
||||
fn format_jre_dir_remove_error(path: &Path, os_err: &std::io::Error) -> String {
|
||||
if cfg!(windows) {
|
||||
format!(
|
||||
"无法删除旧的 JRE 目录:{}\n\
|
||||
可能的原因:\n \
|
||||
- 仍有 dbx Agent / java 进程占用该目录\n \
|
||||
- 防病毒软件正在扫描\n\
|
||||
请关闭可能持有该目录的进程,或重启 dbx 后重试。\n\
|
||||
(原始错误:{os_err})",
|
||||
"Failed to remove the old JRE directory: {}\n\
|
||||
Possible causes:\n \
|
||||
- a dbx Agent / java process still holds the directory\n \
|
||||
- antivirus software is scanning it\n\
|
||||
Close any process that may hold the directory, or restart dbx and try again.\n\
|
||||
(original error: {os_err})",
|
||||
path.display()
|
||||
)
|
||||
} else {
|
||||
format!("无法删除旧的 JRE 目录:{}(原始错误:{os_err})", path.display())
|
||||
format!("Failed to remove the old JRE directory: {} (original error: {os_err})", path.display())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -538,7 +538,7 @@ pub async fn uninstall_agent_jre(am: &AgentManager, jre_key: &str) -> Result<(),
|
|||
.map(|(k, _)| k.as_str())
|
||||
.collect();
|
||||
if !dependents.is_empty() {
|
||||
return Err(format!("JRE {} 正在被以下驱动使用: {},请先卸载这些驱动", jre_key, dependents.join(", ")));
|
||||
return Err(format!("JRE {jre_key} is in use by drivers: {}. Uninstall them first.", dependents.join(", ")));
|
||||
}
|
||||
// Stop daemons first so any java.exe holding the JRE files exits before
|
||||
// we try to remove the directory (Windows ERROR_ACCESS_DENIED otherwise).
|
||||
|
|
@ -1679,7 +1679,7 @@ fn validate_offline_identifier(value: &str, kind: &str) -> Result<(), String> {
|
|||
fn read_registry_from_zip(archive: &mut zip::ZipArchive<std::fs::File>) -> Result<AgentRegistry, String> {
|
||||
let mut entry = archive
|
||||
.by_name("agent-registry.json")
|
||||
.map_err(|_| "ZIP 文件中未找到 agent-registry.json,请确认这是有效的离线驱动包".to_string())?;
|
||||
.map_err(|_| "agent-registry.json not found in the ZIP; not a valid offline driver package.".to_string())?;
|
||||
let mut buf = String::new();
|
||||
entry.read_to_string(&mut buf).map_err(|e| format!("Failed to read agent-registry.json: {e}"))?;
|
||||
serde_json::from_str(&buf).map_err(|e| format!("Invalid agent-registry.json: {e}"))
|
||||
|
|
@ -2627,16 +2627,16 @@ mod jre_dir_remove_tests {
|
|||
let err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "拒绝访问。 (os error 5)");
|
||||
let rendered = format_jre_dir_remove_error(&path, &err);
|
||||
assert!(rendered.contains(&path.display().to_string()), "missing path: {rendered}");
|
||||
assert!(rendered.contains("(原始错误:"), "missing original error wrapper: {rendered}");
|
||||
assert!(rendered.contains("(original error:"), "missing original error wrapper: {rendered}");
|
||||
assert!(rendered.contains("拒绝访问"), "missing original error text: {rendered}");
|
||||
if cfg!(windows) {
|
||||
assert!(rendered.starts_with("无法删除旧的 JRE 目录:"), "wrong prefix: {rendered}");
|
||||
assert!(rendered.contains("Agent / java 进程占用"), "missing process advice: {rendered}");
|
||||
assert!(rendered.contains("重启 dbx 后重试"), "missing restart advice: {rendered}");
|
||||
assert!(rendered.starts_with("Failed to remove the old JRE directory:"), "wrong prefix: {rendered}");
|
||||
assert!(rendered.contains("java process still holds the directory"), "missing process advice: {rendered}");
|
||||
assert!(rendered.contains("restart dbx and try again"), "missing restart advice: {rendered}");
|
||||
} else {
|
||||
// POSIX path: short form, no Windows-specific advice.
|
||||
assert!(rendered.contains("无法删除旧的 JRE 目录"));
|
||||
assert!(!rendered.contains("防病毒"));
|
||||
assert!(rendered.contains("Failed to remove the old JRE directory"));
|
||||
assert!(!rendered.contains("antivirus"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ impl MessageQueueAdmin for KafkaAdmin {
|
|||
}
|
||||
|
||||
async fn unload_topic(&self, _topic: &TopicRef) -> Result<(), String> {
|
||||
Err("Kafka 不支持卸载主题".to_string())
|
||||
Err("Kafka does not support unloading topics".to_string())
|
||||
}
|
||||
|
||||
// ---- Rate limits / quotas / retention ----
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const SQL_OMITTED_ERROR_CONTEXT: &str =
|
|||
#[cfg(feature = "duckdb-bundled")]
|
||||
const DUCKDB_INTERRUPT_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
const DUCKDB_DRAINING_MESSAGE: &str = "上一条 DuckDB 查询仍在停止,请稍后重试。";
|
||||
const DUCKDB_DRAINING_MESSAGE: &str = "The previous DuckDB query is still stopping. Please try again shortly.";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PoolErrorAction {
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@ use tokio_util::sync::CancellationToken;
|
|||
|
||||
const AGENT_UNBOUNDED_ROW_LIMIT: usize = i32::MAX as usize;
|
||||
pub const XLSX_MAX_DATA_ROWS: usize = 1_048_575;
|
||||
const XLSX_ROW_LIMIT_ERROR: &str = "XLSX 最多支持 1,048,575 行数据,请改用 CSV 导出完整结果。";
|
||||
const STREAMING_PAGINATION_UNSUPPORTED_ERROR: &str = "当前查询暂不支持流式导出,请简化查询或使用受支持的驱动。";
|
||||
const AGENT_SESSION_MISSING_ERROR: &str = "查询结果流式导出需要驱动返回结果集会话,但当前驱动未返回 session_id。";
|
||||
const XLSX_ROW_LIMIT_ERROR: &str = "XLSX supports at most 1,048,575 data rows. Use CSV export for the full result.";
|
||||
const STREAMING_PAGINATION_UNSUPPORTED_ERROR: &str =
|
||||
"Streaming export is unsupported for this query. Simplify it or use a supported driver.";
|
||||
const AGENT_SESSION_MISSING_ERROR: &str =
|
||||
"Streaming export needs a result-set session, but this driver returned no session_id.";
|
||||
const STREAM_PROGRESS_TIME_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const EXCEL_CELL_CHARACTER_LIMIT: usize = 32_767;
|
||||
const SQL_INSERT_BATCH_SIZE: usize = 100;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ pub async fn login(State(state): State<Arc<WebState>>, Json(body): Json<LoginReq
|
|||
let remaining = (locked_until - std::time::Instant::now()).as_secs();
|
||||
return Ok((
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
Json(serde_json::json!({"error": format!("请 {remaining} 秒后再试")})),
|
||||
Json(serde_json::json!({"error": format!("Please try again in {remaining}s")})),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ async fn ensure_no_agent_update_blockers(
|
|||
if blockers.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("请先关闭以下数据库连接后再更新驱动: {}", blockers.join(", ")))
|
||||
Err(format!("Close these database connections before updating drivers: {}", blockers.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1825,7 +1825,7 @@ test("custom save handler without insert support keeps pending new rows", async
|
|||
await editor.saveChanges();
|
||||
|
||||
assert.equal(saveCalls, 0);
|
||||
assert.equal(editor.saveError.value, "当前保存目标不支持新增行。");
|
||||
assert.equal(editor.saveError.value, "The current save target does not support adding rows.");
|
||||
assert.deepEqual(editor.newRows.value, [[null, "Grace"]]);
|
||||
assert.equal(editor.hasPendingChanges.value, true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const baseEntry: HistoryAiAnalysisEntry = {
|
|||
test("buildHistoryAiAnalysisPrompt includes operation details and rollback SQL", () => {
|
||||
const prompt = buildHistoryAiAnalysisPrompt(baseEntry);
|
||||
|
||||
assert.match(prompt, /分析这条 DBX 历史记录/);
|
||||
assert.match(prompt, /Analyse this DBX history entry/);
|
||||
assert.match(prompt, /Connection: Local MySQL/);
|
||||
assert.match(prompt, /Operation: UPDATE/);
|
||||
assert.match(prompt, /Affected rows: 1/);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
databaseBackupFilePath,
|
||||
|
|
@ -203,3 +204,10 @@ test("retention never selects failed backup runs", () => {
|
|||
["old"],
|
||||
);
|
||||
});
|
||||
|
||||
test("scheduled backup history translates stable backend errors inline", () => {
|
||||
const source = readFileSync("apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue", "utf8");
|
||||
|
||||
assert.match(source, /\{\{ translateBackendError\(t, run\.error\) \}\}/);
|
||||
assert.doesNotMatch(source, /\{\{ run\.error \}\}/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,13 +25,21 @@ const clipboardMock = vi.hoisted(() => ({
|
|||
}));
|
||||
const runtimeMock = vi.hoisted(() => ({ isTauri: false }));
|
||||
const dialogMock = vi.hoisted(() => ({ save: vi.fn() }));
|
||||
const toastMock = vi.hoisted(() => vi.fn());
|
||||
const translateMock = vi.hoisted(() =>
|
||||
vi.fn((key: string, params?: Record<string, unknown>) => {
|
||||
if (key === "exportProgress.xlsxRowLimit") return `XLSX 最多支持 ${params?.limit} 行数据,请使用 CSV 导出完整结果。`;
|
||||
if (key === "grid.exportFailed") return `导出失败:${params?.message}`;
|
||||
return key;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
vi.mock("@/lib/common/clipboard", () => clipboardMock);
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => runtimeMock.isTauri }));
|
||||
vi.mock("@tauri-apps/plugin-dialog", () => ({ save: dialogMock.save }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: toastMock }) }));
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: translateMock }) }));
|
||||
|
||||
const { defaultDataGridExportFileName, useDataGridExport } = await import("../../apps/desktop/src/composables/useDataGridExport.ts");
|
||||
|
||||
|
|
@ -380,6 +388,20 @@ test("full query result CSV export streams through the backend without loading a
|
|||
assert.equal(exportProgressState.value.filePath, apiMock.startQueryResultExport.mock.calls[0][0].filePath);
|
||||
});
|
||||
|
||||
test("streaming query result export translates terminal backend errors before the toast", async () => {
|
||||
const rawMessage = "XLSX supports at most 1,048,575 data rows. Use CSV export for the full result.";
|
||||
apiMock.startQueryResultExport.mockImplementationOnce(async (request, onProgress) => {
|
||||
onProgress({ exportId: request.exportId, tableName: "", rowsExported: 0, totalRows: 1_048_576, status: "Error", errorMessage: rawMessage });
|
||||
throw new Error(rawMessage);
|
||||
});
|
||||
const { composable, exportProgressState } = buildExportHarness();
|
||||
|
||||
await composable.exportXlsx();
|
||||
|
||||
assert.equal(exportProgressState.value.errorMessage, rawMessage);
|
||||
assert.deepEqual(toastMock.mock.calls.at(-1), ["导出失败:XLSX 最多支持 1,048,575 行数据,请使用 CSV 导出完整结果。", 5000]);
|
||||
});
|
||||
|
||||
test("complete local query result XLSX export does not re-execute the query", async () => {
|
||||
const completeLocalResult: QueryResult = {
|
||||
columns: ["id", "name"],
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ async fn ensure_no_agent_update_blockers(state: &AppState, db_types: &[String])
|
|||
return Ok(());
|
||||
}
|
||||
let labels = blockers.into_iter().map(|blocker| blocker.label).collect::<Vec<_>>().join(", ");
|
||||
Err(format!("请先关闭以下数据库连接后再更新驱动: {labels}"))
|
||||
Err(format!("Close these database connections before updating drivers: {labels}"))
|
||||
}
|
||||
|
||||
async fn ensure_no_offline_import_blockers(state: &AppState, plan: &OfflineImportPlan) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ fn validate_path(raw: &str) -> Result<PathBuf, String> {
|
|||
return Err(format!("path is not absolute: {expanded}"));
|
||||
}
|
||||
if !path.exists() {
|
||||
return Err(format!("文件不存在: {expanded}"));
|
||||
return Err(format!("file does not exist: {expanded}"));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ mod tests {
|
|||
"/__dbx_definitely_missing__/foo.sqlite".to_string()
|
||||
};
|
||||
let err = validate_path(&probe).unwrap_err();
|
||||
assert!(err.contains("文件不存在"));
|
||||
assert!(err.contains("file does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue