feat(updater): add entry to roll back to older versions

This commit is contained in:
高桥凉介 authored and GitHub committed 2026-09-24 21:32:05 +08:00
1 parent 4a67591641
commit c4754dc49f
15 files changed
+356 -18

No files matched your search

+127 -4
View File
@@ -1,16 +1,34 @@
// @vitest-environment happy-dom
import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import UpdateDialog from "@/components/layout/UpdateDialog.vue";
const runtimeState = vi.hoisted(() => ({ tauri: true }));
const updateMocks = vi.hoisted(() => ({
fetchChangelog: vi.fn(),
getAppVersion: vi.fn(),
openExternal: vi.fn(),
}));
vi.mock("@/lib/backend/tauriRuntime", () => ({
isTauriRuntime: () => runtimeState.tauri,
}));
vi.mock("@/lib/app/changelog", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/app/changelog")>();
return { ...actual, fetchChangelog: updateMocks.fetchChangelog };
});
vi.mock("@/lib/backend/api", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/backend/api")>();
return { ...actual, getAppVersion: updateMocks.getAppVersion };
});
vi.mock("@tauri-apps/plugin-shell", () => ({ open: updateMocks.openExternal }));
const mountedApps: App[] = [];
interface DialogState {
@@ -136,6 +154,13 @@ async function clickOutside() {
await flushDialog();
}
beforeEach(() => {
// 默认没有历史版本:只有专门的历史版本用例才提供发布记录
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "", releases: [] });
updateMocks.getAppVersion.mockResolvedValue("0.5.60");
updateMocks.openExternal.mockClear();
});
afterEach(() => {
runtimeState.tauri = true;
for (const app of mountedApps.splice(0)) app.unmount();
@@ -379,8 +404,105 @@ describe("UpdateDialog release notes safety", () => {
});
});
describe("UpdateDialog older versions", () => {
const release = (tag: string, date: string) => ({ tag, name: `DBX ${tag}`, date, sections: [] });
const upToDateInfo = {
current_version: "0.5.60",
latest_version: "0.5.60",
update_available: false,
portable_mode: false,
manual_update_only: false,
release_name: "",
release_url: "",
release_notes: "",
};
const historyRegion = () => document.body.querySelector<HTMLElement>("[data-update-history]");
const historyButton = (text: string) => Array.from(historyRegion()?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes(text));
it("lists only versions older than the current build and opens their release page", async () => {
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.60", "2026-09-01"), release("v0.5.59", "2026-08-20")] });
await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." });
await flushDialog();
// 默认折叠:即使没有可用更新也先收起,且列表只保留低于当前版本的版本
const region = historyRegion();
expect(region?.textContent).toContain("Older versions");
expect(region?.textContent).not.toContain("v0.5.59");
historyButton("Older versions")?.click();
await flushDialog();
expect(region?.textContent).toContain("Back up your data before rolling back");
expect(region?.textContent).toContain("v0.5.59");
expect(region?.textContent).not.toContain("v0.5.61");
historyButton("Open Release")?.click();
await flushDialog();
expect(updateMocks.openExternal).toHaveBeenCalledWith("https://github.com/t8y2/dbx/releases/tag/v0.5.59");
});
it("keeps the history collapsed until the user expands it", async () => {
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.59", "2026-08-20")] });
await mountDialog(0);
await flushDialog();
expect(historyRegion()?.textContent).toContain("Older versions");
expect(historyRegion()?.textContent).not.toContain("v0.5.59");
historyButton("Older versions")?.click();
await flushDialog();
expect(historyRegion()?.textContent).toContain("v0.5.59");
});
it("starts collapsed again after the dialog is reopened", async () => {
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.59", "2026-08-20")] });
const { state } = await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." });
await flushDialog();
historyButton("Older versions")?.click();
await flushDialog();
expect(historyRegion()?.textContent).toContain("v0.5.59");
state.open = false;
await flushDialog();
state.open = true;
await flushDialog();
expect(historyRegion()?.textContent).toContain("Older versions");
expect(historyRegion()?.textContent).not.toContain("v0.5.59");
});
it("hides the history section when no release is older than the current build", async () => {
updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.60", "2026-09-01")] });
await mountDialog(0);
await flushDialog();
expect(historyRegion()).toBeNull();
});
it("surfaces a changelog load failure instead of hiding it silently", async () => {
updateMocks.fetchChangelog.mockRejectedValue(new Error("offline"));
await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." });
await flushDialog();
expect(historyRegion()?.textContent).toContain("Older versions");
historyButton("Older versions")?.click();
await flushDialog();
expect(historyRegion()?.textContent).toContain("Failed to load older versions");
});
});
describe("UpdateDialog aggregate update center", () => {
it("hides the tab bar when no updates are available", async () => {
it("keeps only the client tab when nothing is updatable", async () => {
await mountDialog(0, {}, undefined, {
updateInfo: {
current_version: "0.5.60",
@@ -395,8 +517,9 @@ describe("UpdateDialog aggregate update center", () => {
updateCheckMessage: "DBX is up to date (0.5.60).",
});
expect(document.body.querySelector('[role="tablist"]')).toBeNull();
expect(document.body.querySelector('[data-update-tab="app"]')).toBeNull();
// 客户端页签常驻(承载回退入口),但没有可更新项时不再出现其他页签
expect(document.body.querySelector('[data-update-tab="app"]')).not.toBeNull();
expect(document.body.querySelector('[data-update-tab="drivers"]')).toBeNull();
expect(document.body.textContent).toContain("DBX is up to date (0.5.60).");
});
@@ -1,19 +1,21 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { AlertTriangle, CheckCircle2, Loader2, RefreshCw } from "@lucide/vue";
import { AlertTriangle, CheckCircle2, ChevronDown, Loader2, RefreshCw } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { UpdateInfo } from "@/lib/backend/api";
import { getAppVersion, type UpdateInfo } from "@/lib/backend/api";
import type { AgentDriverInfo, McpServerStatus, UpdateDownloadSource } from "@/lib/backend/tauri";
import type { JdbcPluginStatus } from "@/types/database";
import { pluginSourceChange, type MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
import { mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { isUpdatePreviewMockEnabled } from "@/lib/updates/updatePreviewMock";
import { canDownloadAndInstallUpdate } from "@/composables/useAppUpdater";
import { canDownloadAndInstallUpdate, isNewerRemoteVersion, resolveReleaseTagUrl } from "@/composables/useAppUpdater";
import type { ComponentUpdateCategory } from "@/composables/useComponentUpdates";
import { currentLocale } from "@/i18n";
import { changelogLangFromLocale, createLatestRequestGuard, fetchChangelog, type ChangelogRelease } from "@/lib/app/changelog";
type UpdateTab = "app" | ComponentUpdateCategory;
@@ -69,16 +71,40 @@ const emit = defineEmits<{
"update-all": [];
}>();
const { t } = useI18n();
const { t, locale } = useI18n();
const isDesktop = isTauriRuntime() || isUpdatePreviewMockEnabled();
const selectedTab = ref<UpdateTab>("app");
const renderedNotes = ref("");
const hasAppUpdate = computed(() => props.updateDownloaded || props.updateReady || props.updateInfo?.update_available === true);
const mcpAvailable = computed(() => (props.mcpUpdate ? mcpUpdateAvailability(props.mcpUpdate) === true : false));
/** 历史版本每页条数(与更新日志一致,避免一次性铺开所有版本) */
const HISTORY_PAGE_SIZE = 10;
const historyLoading = ref(false);
const historyError = ref("");
const historyReleases = ref<ChangelogRelease[]>([]);
const historyVisibleCount = ref(HISTORY_PAGE_SIZE);
const historyRequest = createLatestRequestGuard();
/** 弹窗可能在“检查更新”完成前打开,这里保存单独取到的应用版本用于比较 */
const fallbackVersion = ref("");
const changelogLang = computed(() => changelogLangFromLocale(locale.value || currentLocale()));
const currentVersion = computed(() => props.updateInfo?.current_version || fallbackVersion.value);
// 只保留低于当前版本的发布记录:新版本走“客户端更新”,这里专用于回退
const olderReleases = computed(() => {
const current = currentVersion.value;
if (!current) return [];
return historyReleases.value.filter((release) => isNewerRemoteVersion(current, release.tag));
});
const visibleOlderReleases = computed(() => olderReleases.value.slice(0, historyVisibleCount.value));
const hasMoreOlderReleases = computed(() => historyVisibleCount.value < olderReleases.value.length);
/** 历史版本默认折叠,避免挤压更新信息 */
const olderReleasesExpanded = ref(false);
const tabs = computed(() => {
const available: Array<{ id: UpdateTab; label: string; count: number }> = [];
if (hasAppUpdate.value) available.push({ id: "app", label: t("settings.updateClient"), count: 1 });
// 客户端页签常驻:既承载更新流程,也是“回退到旧版本”的入口
available.push({ id: "app", label: t("settings.updateClient"), count: hasAppUpdate.value ? 1 : 0 });
if (props.driverUpdates.length) available.push({ id: "drivers", label: t("settings.updateDrivers"), count: props.driverUpdates.length });
if (props.jdbcUpdate?.update_available) available.push({ id: "jdbc", label: t("settings.updateJdbc"), count: 1 });
if (mcpAvailable.value) available.push({ id: "mcp", label: t("settings.updateMcp"), count: 1 });
@@ -103,10 +129,29 @@ const isCloseBlocked = computed(() => props.isInstallingUpdate);
const blocksImplicitDismiss = computed(() => isCloseBlocked.value);
const canIgnoreVersion = computed(() => props.updateInfo?.update_available === true && !props.isDownloadingUpdate && !props.isInstallingUpdate && !props.updateReady && !props.isUpdatingAll);
/** 客户端页签默认收起历史版本,用户手动切过页签后不再自动切换 */
const userSelectedTab = ref(false);
function selectTab(tab: UpdateTab) {
userSelectedTab.value = true;
selectedTab.value = tab;
}
// 客户端页签常驻,但它没有更新时不应抢占默认落点:优先显示真正有待更新内容的页签
function pickDefaultTab(nextTabs: Array<{ id: UpdateTab; count: number }>): UpdateTab {
return nextTabs.find((tab) => tab.count > 0)?.id ?? "app";
}
watch(
tabs,
(nextTabs) => {
if (!nextTabs.some((tab) => tab.id === selectedTab.value)) selectedTab.value = nextTabs[0]?.id ?? "app";
if (!nextTabs.some((tab) => tab.id === selectedTab.value)) {
selectedTab.value = pickDefaultTab(nextTabs);
return;
}
if (!userSelectedTab.value && selectedTab.value === "app" && !hasAppUpdate.value && nextTabs.some((tab) => tab.count > 0)) {
selectedTab.value = pickDefaultTab(nextTabs);
}
},
{ immediate: true },
);
@@ -120,6 +165,69 @@ function handleOpenChange(nextOpen: boolean) {
open.value = false;
}
function openExternalUrl(url: string) {
if (isTauriRuntime()) {
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
} else {
window.open(url, "_blank", "noopener,noreferrer");
}
}
async function loadOlderReleases() {
const requestId = historyRequest.begin();
historyLoading.value = true;
historyError.value = "";
try {
// 弹窗可能在“检查更新”完成前打开:先取一次应用版本,否则无法判断哪些版本更低
if (!props.updateInfo?.current_version && !fallbackVersion.value) {
fallbackVersion.value = await getAppVersion().catch(() => "");
}
const data = await fetchChangelog(changelogLang.value);
if (!historyRequest.isCurrent(requestId)) return;
historyReleases.value = data.releases;
} catch (error) {
if (!historyRequest.isCurrent(requestId)) return;
historyError.value = error instanceof Error ? error.message : String(error);
} finally {
if (historyRequest.isCurrent(requestId)) historyLoading.value = false;
}
}
function showMoreOlderReleases() {
historyVisibleCount.value += HISTORY_PAGE_SIZE;
}
function toggleOlderReleases() {
olderReleasesExpanded.value = !olderReleasesExpanded.value;
}
function formatOlderReleaseDate(dateStr: string) {
if (!dateStr) return "";
const date = new Date(dateStr);
if (Number.isNaN(date.getTime())) return dateStr;
try {
return new Intl.DateTimeFormat(locale.value || currentLocale(), { year: "numeric", month: "short", day: "numeric" }).format(date);
} catch {
return dateStr;
}
}
// 回退只提供跳转:旧版本安装包由用户自行下载安装,应用不做静默降级
function openOlderReleasePage(tag: string) {
openExternalUrl(resolveReleaseTagUrl(tag, props.updateDownloadSource));
}
watch(
() => [open.value, changelogLang.value] as const,
([isOpen]) => {
if (!isOpen) return;
// 每次打开弹窗都回到折叠状态,不残留上次展开的状态
olderReleasesExpanded.value = false;
void loadOlderReleases();
},
{ immediate: true },
);
function handleReleaseNotesClick(event: MouseEvent) {
const target = event.target as HTMLElement;
const anchor = target.closest("a");
@@ -127,11 +235,7 @@ function handleReleaseNotesClick(event: MouseEvent) {
event.preventDefault();
const url = anchor.getAttribute("href");
if (!url || !/^https?:\/\//i.test(url)) return;
if (isTauriRuntime()) {
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
} else {
window.open(url, "_blank", "noopener,noreferrer");
}
openExternalUrl(url);
}
watch(
@@ -205,7 +309,7 @@ watch(
:data-update-tab="tab.id"
class="relative inline-flex h-full flex-1 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 text-sm font-medium transition-colors hover:text-foreground focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50"
:class="selectedTab === tab.id ? 'bg-background text-foreground shadow-sm' : 'text-foreground/60'"
@click="selectedTab = tab.id"
@click="selectTab(tab.id)"
>
<span>{{ tab.label }}</span>
<span v-if="tab.count > 0" class="rounded-full bg-foreground/10 px-1.5 text-[11px]">{{ tab.count }}</span>
@@ -255,6 +359,33 @@ watch(
</p>
<p v-if="updateDownloaded && !isInstallingUpdate" class="mt-3">{{ t("updates.downloadedReady", { version: updateInfo?.latest_version }) }}</p>
<p v-if="updateCheckFailed && updateInfo?.update_available" role="alert" class="mt-3 text-destructive">{{ updateCheckMessage }}</p>
<div v-if="olderReleases.length || historyError" data-update-history class="mt-4 border-t pt-3">
<button type="button" class="flex w-full items-center justify-between gap-2 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground" @click="toggleOlderReleases">
<span>{{ t("updates.historyTab") }}</span>
<ChevronDown class="h-3.5 w-3.5 shrink-0 transition-transform" :class="olderReleasesExpanded ? 'rotate-180' : ''" />
</button>
<div v-if="olderReleasesExpanded" class="mt-2 space-y-2">
<p class="text-xs text-muted-foreground">{{ t("updates.historyHint") }}</p>
<div v-if="historyLoading && !visibleOlderReleases.length" class="flex items-center justify-center gap-2 py-6 text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("updates.checking") }}
</div>
<div v-else-if="historyError" role="alert" class="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-destructive">
{{ t("updates.historyLoadFailed") }}
</div>
<template v-else>
<div v-for="release in visibleOlderReleases" :key="release.tag" class="flex items-center justify-between gap-4 rounded-md border p-3">
<div class="min-w-0">
<div class="font-medium">{{ release.tag }}</div>
<div class="mt-0.5 text-xs text-muted-foreground">{{ t("updates.historyPublishedOn", { date: formatOlderReleaseDate(release.date) }) }}</div>
</div>
<Button variant="outline" class="shrink-0" @click="openOlderReleasePage(release.tag)">{{ t("updates.openRelease") }}</Button>
</div>
<Button v-if="hasMoreOlderReleases" variant="ghost" class="w-full" @click="showMoreOlderReleases">{{ t("updates.historyLoadMore") }}</Button>
</template>
</div>
</div>
</template>
<template v-else>
@@ -363,7 +494,7 @@ watch(
</template>
</DialogFooter>
<DialogFooter v-else data-update-footer class="mx-0 mb-0 rounded-none border-t px-[22px] pb-2.5 pt-2.5 sm:items-center sm:justify-end">
<DialogFooter v-else data-update-footer class="mx-0 mb-0 min-w-0 rounded-none border-t px-[22px] pb-2.5 pt-2.5 sm:items-center sm:justify-end">
<template v-if="isAnyComponentUpdating">
<Button variant="ghost" class="shrink-0" @click="handleOpenChange(false)">{{ t("updates.updateInBackground") }}</Button>
<Button class="shrink-0" disabled>
@@ -2,7 +2,7 @@
import { createApp, defineComponent, h, reactive, nextTick, type App } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import { useAppUpdater } from "@/composables/useAppUpdater";
import { isNewerRemoteVersion, resolveReleaseTagUrl, useAppUpdater } from "@/composables/useAppUpdater";
const mocks = vi.hoisted(() => ({
checkForUpdates: vi.fn(),
downloadUpdate: vi.fn(),
@@ -71,6 +71,23 @@ afterEach(() => {
app?.unmount();
vi.useRealTimers();
});
describe("older version release pages", () => {
it("maps a historical version to the matching GitHub release page", () => {
expect(resolveReleaseTagUrl("0.5.59", "official")).toBe("https://github.com/t8y2/dbx/releases/tag/v0.5.59");
expect(resolveReleaseTagUrl("v0.5.59", "official")).toBe("https://github.com/t8y2/dbx/releases/tag/v0.5.59");
});
it("uses the CNB release page when the CNB download source is selected", () => {
expect(resolveReleaseTagUrl("v0.5.59", "cnb")).toBe("https://cnb.cool/dbxio.com/dbx/-/releases/tag/v0.5.59");
});
it("treats only strictly older releases as rollback candidates", () => {
expect(isNewerRemoteVersion("0.5.60", "v0.5.59")).toBe(true);
expect(isNewerRemoteVersion("0.5.60", "v0.5.60")).toBe(false);
expect(isNewerRemoteVersion("0.5.60", "v0.5.61")).toBe(false);
});
});
describe("silent update lifecycle", () => {
it("checks and shows a badge without downloading when automatic downloads are disabled", async () => {
settings.autoUpdateApp = false;
@@ -106,6 +106,18 @@ export function resolveUpdateReleaseUrl(info: api.UpdateInfo | null, source: unk
return info?.release_url || fallbackUrl;
}
/**
* 按版本 tag 解析“下载页”地址:CNB 源指向 CNB 的 release 页,其余指向 GitHub release 页。
* 用于历史版本回退:只做跳转,不在应用内下载/安装旧版本。
*/
export function resolveReleaseTagUrl(tag: string, source: unknown): string {
const normalizedTag = tagVersion(tag);
if (normalizeUpdateDownloadSource(source) === "cnb") {
return `https://cnb.cool/dbxio.com/dbx/-/releases/tag/${encodeURIComponent(normalizedTag)}`;
}
return `https://github.com/t8y2/dbx/releases/tag/${encodeURIComponent(normalizedTag)}`;
}
export async function resolveUpdaterProxy(): Promise<string | undefined> {
if (!isTauriRuntime()) return undefined;
try {
+5
View File
@@ -423,6 +423,11 @@ export default withEnglishFallback({
exitAndUpdate: "Çıx və yenilə",
dockerUsersRun: "Docker istifadəçiləri bu əmri işlətməlidir",
toUpdate: "yeniləmək üçün",
historyTab: "Köhnə versiyalar",
historyHint: "Köhnə versiyaya qayıtmazdan əvvəl məlumatlarınızın ehtiyat nüsxəsini çıxarın. Köhnə versiyalar yeni versiyaların yaratdığı məlumatları və ya etimad məlumatlarını oxuya bilməz.",
historyPublishedOn: "{date} tarixində yayımlandı",
historyLoadFailed: "Köhnə versiyalar yüklənə bilmədi. Zəhmət olmasa sonra yenidən cəhd edin.",
historyLoadMore: "Daha çox yüklə",
},
sidebar: {
connections: "ƏLAQƏLƏR",
+5
View File
@@ -646,6 +646,11 @@ export default {
exitAndUpdate: "Exit & Update",
dockerUsersRun: "Docker users should run",
toUpdate: "to update",
historyTab: "Older versions",
historyHint: "Back up your data before rolling back. Older versions may not be able to read data or credentials created by newer versions.",
historyPublishedOn: "Released {date}",
historyLoadFailed: "Failed to load older versions. Please try again later.",
historyLoadMore: "Load more",
},
pluginDock: {
close: "Close terminal",
+5
View File
@@ -635,6 +635,11 @@ export default withEnglishFallback({
exitAndUpdate: "Salir y actualizar",
dockerUsersRun: "Los usuarios de Docker deben ejecutar",
toUpdate: "para actualizar",
historyTab: "Versiones anteriores",
historyHint: "Haz una copia de seguridad de tus datos antes de volver a una versión anterior. Es posible que las versiones antiguas no puedan leer datos o credenciales creados por versiones más recientes.",
historyPublishedOn: "Publicado el {date}",
historyLoadFailed: "No se pudieron cargar las versiones anteriores. Inténtalo de nuevo más tarde.",
historyLoadMore: "Cargar más",
source: "Fuente de actualizaciones",
sourceOfficial: "Fuente oficial",
sourceCnb: "CNB",
+5
View File
@@ -629,6 +629,11 @@ export default withEnglishFallback({
exitAndUpdate: "Esci e Aggiorna",
dockerUsersRun: "Gli utenti Docker dovrebbero eseguire",
toUpdate: "per aggiornare",
historyTab: "Versioni precedenti",
historyHint: "Esegui un backup dei dati prima di tornare a una versione precedente. Le versioni più vecchie potrebbero non riuscire a leggere dati o credenziali creati dalle versioni più recenti.",
historyPublishedOn: "Pubblicato il {date}",
historyLoadFailed: "Impossibile caricare le versioni precedenti. Riprova più tardi.",
historyLoadMore: "Carica altro",
source: "Sorgente di aggiornamento",
sourceOfficial: "Sorgente ufficiale",
sourceCnb: "CNB",
+5
View File
@@ -630,6 +630,11 @@ export default withEnglishFallback({
exitAndUpdate: "終了してアップデート",
dockerUsersRun: "Dockerユーザーは以下を実行してください",
toUpdate: "更新するには",
historyTab: "過去のバージョン",
historyHint: "旧バージョンに戻す前にデータをバックアップしてください。新しいバージョンで作成したデータや認証情報を読み取れない場合があります。",
historyPublishedOn: "{date} 公開",
historyLoadFailed: "過去のバージョンの読み込みに失敗しました。後でもう一度お試しください。",
historyLoadMore: "さらに読み込む",
source: "更新ソース",
sourceOfficial: "公式ソース",
sourceCnb: "CNB",
+5
View File
@@ -637,6 +637,11 @@ export default withEnglishFallback({
exitAndUpdate: "종료 후 업데이트",
dockerUsersRun: "Docker 사용자는 다음 명령을 실행하세요",
toUpdate: "업데이트하려면",
historyTab: "이전 버전",
historyHint: "이전 버전으로 되돌리기 전에 데이터를 백업하세요. 최신 버전에서 만든 데이터나 자격 증명을 읽지 못할 수 있습니다.",
historyPublishedOn: "{date} 릴리스",
historyLoadFailed: "이전 버전을 불러오지 못했습니다. 잠시 후 다시 시도하세요.",
historyLoadMore: "더 보기",
source: "업데이트 소스",
sourceOfficial: "공식 소스",
sourceCnb: "CNB",
+5
View File
@@ -630,6 +630,11 @@ export default withEnglishFallback({
exitAndUpdate: "Sair e Atualizar",
dockerUsersRun: "Usuários do Docker devem executar",
toUpdate: "para atualizar",
historyTab: "Versões anteriores",
historyHint: "Faça backup dos seus dados antes de voltar para uma versão anterior. Versões antigas podem não conseguir ler dados ou credenciais criados por versões mais recentes.",
historyPublishedOn: "Publicado em {date}",
historyLoadFailed: "Falha ao carregar as versões anteriores. Tente novamente mais tarde.",
historyLoadMore: "Carregar mais",
source: "Fonte de atualização",
sourceOfficial: "Fonte oficial",
sourceCnb: "CNB",
+5
View File
@@ -592,6 +592,11 @@ export default withEnglishFallback({
exitAndUpdate: "Выйти и обновить",
dockerUsersRun: "Пользователям Docker следует выполнить",
toUpdate: "для обновления",
historyTab: "История версий",
historyHint: "Перед откатом к старой версии сначала сделайте резервную копию данных; старая версия может не поддерживать чтение данных или учётных данных, созданных новой версией.",
historyPublishedOn: "Опубликовано {date}",
historyLoadFailed: "Не удалось загрузить историю версий, повторите попытку позже.",
historyLoadMore: "Загрузить ещё",
},
pluginDock: {
close: "Закрыть терминал",
+5
View File
@@ -425,6 +425,11 @@ export default withEnglishFallback({
exitAndUpdate: "Çık ve Güncelle",
dockerUsersRun: "Docker kullanıcıları güncellemek için",
toUpdate: "komutunu çalıştırmalı",
historyTab: "Eski sürümler",
historyHint: "Eski bir sürüme dönmeden önce verilerinizi yedekleyin. Eski sürümler, yeni sürümlerle oluşturulan verileri veya kimlik bilgilerini okuyamayabilir.",
historyPublishedOn: "{date} tarihinde yayınlandı",
historyLoadFailed: "Eski sürümler yüklenemedi. Lütfen daha sonra tekrar deneyin.",
historyLoadMore: "Daha fazla yükle",
},
sidebar: {
connections: "BAĞLANTILAR",
+5
View File
@@ -562,6 +562,11 @@ export default withEnglishFallback({
exitAndUpdate: "退出并更新",
dockerUsersRun: "Docker 用户请运行",
toUpdate: "更新",
historyTab: "历史版本",
historyHint: "回退到旧版本前请先备份数据;旧版本可能无法读取新版本创建的数据或凭据。",
historyPublishedOn: "发布于 {date}",
historyLoadFailed: "加载历史版本失败,请稍后重试。",
historyLoadMore: "加载更多",
},
pluginDock: {
close: "关闭终端",
+5
View File
@@ -633,6 +633,11 @@ export default withEnglishFallback({
exitAndUpdate: "結束並更新",
dockerUsersRun: "Docker 使用者請執行",
toUpdate: "更新",
historyTab: "歷史版本",
historyHint: "回退至舊版本前請先備份資料;舊版本可能無法讀取新版本建立的資料或憑證。",
historyPublishedOn: "發佈於 {date}",
historyLoadFailed: "載入歷史版本失敗,請稍後重試。",
historyLoadMore: "載入更多",
source: "更新來源",
sourceOfficial: "官方來源",
sourceCnb: "CNB",