UI: translate Backups page

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ДмитрийandClaude Sonnet 5.5 committed 2026-09-30 11:54:18 +05:00
1 parent 0b975a7c42
commit b5bfeb20e7
6 files changed
+274 -52

No files matched your search

+46
View File
@@ -226,6 +226,52 @@ const de: Partial<Record<keyof typeof en, string>> = {
"about.upToDate": "CoA Server Manager ist auf dem neuesten Stand.",
"about.checkFailed": "Die Suche nach einer neuen Version ist fehlgeschlagen. Prüfe deine Internetverbindung und versuche es erneut.",
"about.installFailed": "Das Update konnte nicht installiert werden. Dein Server wurde nicht angetastet.",
// Backups
"bk.title": "Sicherungen",
"bk.trigger.manual": "Manuelle Sicherung",
"bk.trigger.automatic": "Automatische Sicherung",
"bk.trigger.before-update": "Vor dem Update",
"bk.trigger.before-restore": "Sicherheitskopie vor einer Wiederherstellung",
"bk.trigger.before-migration": "Vor dem Datenbank-Update",
"bk.trigger.before-bots": "Vor der Installation der Begleiter",
"bk.trigger.before-repair": "Vor der Reparatur",
"bk.trigger.before-dangerous-change": "Vor einer riskanten Änderung",
"bk.kind.quick": "Charaktere, Konten und Einstellungen",
"bk.kind.full": "Alles, einschließlich der Weltdatenbank",
"bk.kind.config": "Nur Einstellungen",
"bk.kind.database": "Charaktere und Konten",
"bk.db.characters": "Charaktere",
"bk.db.auth": "Konten",
"bk.db.world": "Weltdaten",
"bk.today": "Heute {time}",
"bk.yesterday": "Gestern {time}",
"bk.created": "Sicherung erstellt.",
"bk.last": "Letzte Sicherung: {when}",
"bk.none": "Noch keine Sicherungen",
"bk.keptNext": "Sicherungen liegen neben deinem Server und überschreiben nie deine Live-Daten.",
"bk.backUpNow": "Jetzt sichern",
"bk.full": "Vollsicherung",
"bk.fullHint": "Sichert auch die große Weltdatenbank",
"bk.settingsOnly": "Nur Einstellungen",
"bk.working": "Wird ausgeführt …",
"bk.failed": "Das hat nicht funktioniert",
"bk.restoreFrom": "Wiederherstellen aus „{what}“, {when}",
"bk.restoreNote": "Es wird nichts gelöscht. Deine aktuellen Daten werden zuerst gesichert, und eine ersetzte Datenbank bleibt unter neuem Namen erhalten.",
"bk.stopFirst": "Stoppe zuerst den Server – Datenbanken können nur bei gestopptem Server wiederhergestellt werden.",
"bk.settingsRestored": "Einstellungen wiederhergestellt. Eine Sicherheitskopie der vorherigen Einstellungen wurde gespeichert.",
"bk.restoreSettings": "Einstellungen wiederherstellen",
"bk.confirmDb": "Datenbank „{db}“ aus dieser Sicherung wiederherstellen? Fortschritt seit {when} wird beiseitegelegt (nicht gelöscht).",
"bk.dbRestored": "{n} Tabellen wiederhergestellt. Die vorherigen Daten bleiben als {schema} erhalten.",
"bk.restoreDb": "{db} wiederherstellen",
"bk.loading": "Wird geladen …",
"bk.empty": "Deine Sicherungen erscheinen hier.",
"bk.restore": "Wiederherstellen",
"bk.verify": "Prüfen",
"bk.intact": "Diese Sicherung ist unversehrt.",
"bk.problem": "Problem gefunden: {details}",
"bk.delete": "Löschen",
"bk.confirmDelete": "Diese Sicherung löschen? Nur diese Sicherung wird entfernt; dein Server bleibt unberührt.",
};
export default de;
+46
View File
@@ -226,6 +226,52 @@ const en = {
"about.upToDate": "CoA Server Manager is up to date.",
"about.checkFailed": "Could not check for a new version. Check your internet connection and try again.",
"about.installFailed": "The update could not be installed. Your server was not touched.",
// Backups
"bk.title": "Backups",
"bk.trigger.manual": "Manual backup",
"bk.trigger.automatic": "Automatic backup",
"bk.trigger.before-update": "Before update",
"bk.trigger.before-restore": "Safety copy before a restore",
"bk.trigger.before-migration": "Before database update",
"bk.trigger.before-bots": "Before installing companions",
"bk.trigger.before-repair": "Before repair",
"bk.trigger.before-dangerous-change": "Before a risky change",
"bk.kind.quick": "Characters, accounts and settings",
"bk.kind.full": "Everything, including the world database",
"bk.kind.config": "Settings only",
"bk.kind.database": "Characters and accounts",
"bk.db.characters": "Characters",
"bk.db.auth": "Accounts",
"bk.db.world": "World data",
"bk.today": "Today {time}",
"bk.yesterday": "Yesterday {time}",
"bk.created": "Backup created.",
"bk.last": "Last backup: {when}",
"bk.none": "No backups yet",
"bk.keptNext": "Backups are kept next to your server and never overwrite your live data.",
"bk.backUpNow": "Back up now",
"bk.full": "Full backup",
"bk.fullHint": "Also saves the large world database",
"bk.settingsOnly": "Settings only",
"bk.working": "Working…",
"bk.failed": "That did not work",
"bk.restoreFrom": "Restore from {what}, {when}",
"bk.restoreNote": "Nothing is deleted. Your current data is saved first, and a replaced database is kept alongside under a new name.",
"bk.stopFirst": "Stop the server first — databases can only be restored while it is stopped.",
"bk.settingsRestored": "Settings restored. A safety copy of the previous settings was saved.",
"bk.restoreSettings": "Restore settings",
"bk.confirmDb": "Restore the {db} database from this backup? Progress made since {when} will be set aside (not deleted).",
"bk.dbRestored": "Restored {n} tables. The previous data is kept as {schema}.",
"bk.restoreDb": "Restore {db}",
"bk.loading": "Loading…",
"bk.empty": "Your backups will appear here.",
"bk.restore": "Restore",
"bk.verify": "Verify",
"bk.intact": "This backup is intact.",
"bk.problem": "Problem found: {details}",
"bk.delete": "Delete",
"bk.confirmDelete": "Delete this backup? Only this backup is removed; your server is not affected.",
} as const;
export default en;
+46
View File
@@ -226,6 +226,52 @@ const es: Partial<Record<keyof typeof en, string>> = {
"about.upToDate": "CoA Server Manager está actualizado.",
"about.checkFailed": "No se pudo buscar una versión nueva. Comprueba tu conexión a internet e inténtalo de nuevo.",
"about.installFailed": "No se pudo instalar la actualización. Tu servidor no se ha tocado.",
// Backups
"bk.title": "Copias de seguridad",
"bk.trigger.manual": "Copia manual",
"bk.trigger.automatic": "Copia automática",
"bk.trigger.before-update": "Antes de actualizar",
"bk.trigger.before-restore": "Copia de seguridad antes de restaurar",
"bk.trigger.before-migration": "Antes de actualizar la base de datos",
"bk.trigger.before-bots": "Antes de instalar compañeros",
"bk.trigger.before-repair": "Antes de reparar",
"bk.trigger.before-dangerous-change": "Antes de un cambio arriesgado",
"bk.kind.quick": "Personajes, cuentas y ajustes",
"bk.kind.full": "Todo, incluida la base de datos del mundo",
"bk.kind.config": "Solo ajustes",
"bk.kind.database": "Personajes y cuentas",
"bk.db.characters": "Personajes",
"bk.db.auth": "Cuentas",
"bk.db.world": "Datos del mundo",
"bk.today": "Hoy {time}",
"bk.yesterday": "Ayer {time}",
"bk.created": "Copia creada.",
"bk.last": "Última copia: {when}",
"bk.none": "Aún no hay copias",
"bk.keptNext": "Las copias se guardan junto a tu servidor y nunca sobrescriben tus datos activos.",
"bk.backUpNow": "Copiar ahora",
"bk.full": "Copia completa",
"bk.fullHint": "También guarda la gran base de datos del mundo",
"bk.settingsOnly": "Solo ajustes",
"bk.working": "Trabajando…",
"bk.failed": "Eso no funcionó",
"bk.restoreFrom": "Restaurar desde «{what}», {when}",
"bk.restoreNote": "No se borra nada. Primero se guardan tus datos actuales y la base de datos reemplazada se conserva con otro nombre.",
"bk.stopFirst": "Detén primero el servidor: las bases de datos solo se pueden restaurar con el servidor detenido.",
"bk.settingsRestored": "Ajustes restaurados. Se guardó una copia de seguridad de los ajustes anteriores.",
"bk.restoreSettings": "Restaurar ajustes",
"bk.confirmDb": "¿Restaurar la base de datos «{db}» desde esta copia? El progreso desde {when} se apartará (no se borra).",
"bk.dbRestored": "Se restauraron {n} tablas. Los datos anteriores se conservan como {schema}.",
"bk.restoreDb": "Restaurar: {db}",
"bk.loading": "Cargando…",
"bk.empty": "Tus copias aparecerán aquí.",
"bk.restore": "Restaurar",
"bk.verify": "Verificar",
"bk.intact": "Esta copia está intacta.",
"bk.problem": "Problema detectado: {details}",
"bk.delete": "Eliminar",
"bk.confirmDelete": "¿Eliminar esta copia? Solo se elimina esta copia; tu servidor no se ve afectado.",
};
export default es;
+46
View File
@@ -226,6 +226,52 @@ const fr: Partial<Record<keyof typeof en, string>> = {
"about.upToDate": "CoA Server Manager est à jour.",
"about.checkFailed": "Impossible de rechercher une nouvelle version. Vérifiez votre connexion internet et réessayez.",
"about.installFailed": "La mise à jour n’a pas pu être installée. Votre serveur n’a pas été touché.",
// Backups
"bk.title": "Sauvegardes",
"bk.trigger.manual": "Sauvegarde manuelle",
"bk.trigger.automatic": "Sauvegarde automatique",
"bk.trigger.before-update": "Avant la mise à jour",
"bk.trigger.before-restore": "Copie de sécurité avant une restauration",
"bk.trigger.before-migration": "Avant la mise à jour de la base de données",
"bk.trigger.before-bots": "Avant l’installation des compagnons",
"bk.trigger.before-repair": "Avant la réparation",
"bk.trigger.before-dangerous-change": "Avant un changement risqué",
"bk.kind.quick": "Personnages, comptes et paramètres",
"bk.kind.full": "Tout, y compris la base de données du monde",
"bk.kind.config": "Paramètres uniquement",
"bk.kind.database": "Personnages et comptes",
"bk.db.characters": "Personnages",
"bk.db.auth": "Comptes",
"bk.db.world": "Données du monde",
"bk.today": "Aujourd’hui {time}",
"bk.yesterday": "Hier {time}",
"bk.created": "Sauvegarde créée.",
"bk.last": "Dernière sauvegarde : {when}",
"bk.none": "Aucune sauvegarde pour l’instant",
"bk.keptNext": "Les sauvegardes sont conservées à côté de votre serveur et n’écrasent jamais vos données actives.",
"bk.backUpNow": "Sauvegarder maintenant",
"bk.full": "Sauvegarde complète",
"bk.fullHint": "Enregistre aussi la grande base de données du monde",
"bk.settingsOnly": "Paramètres uniquement",
"bk.working": "En cours…",
"bk.failed": "Cela n’a pas fonctionné",
"bk.restoreFrom": "Restaurer depuis « {what} », {when}",
"bk.restoreNote": "Rien n’est supprimé. Vos données actuelles sont d’abord sauvegardées, et une base remplacée est conservée sous un nouveau nom.",
"bk.stopFirst": "Arrêtez d’abord le serveur : les bases de données ne peuvent être restaurées que serveur arrêté.",
"bk.settingsRestored": "Paramètres restaurés. Une copie de sécurité des paramètres précédents a été enregistrée.",
"bk.restoreSettings": "Restaurer les paramètres",
"bk.confirmDb": "Restaurer la base « {db} » depuis cette sauvegarde ? La progression depuis {when} sera mise de côté (pas supprimée).",
"bk.dbRestored": "{n} tables restaurées. Les données précédentes sont conservées sous {schema}.",
"bk.restoreDb": "Restaurer : {db}",
"bk.loading": "Chargement…",
"bk.empty": "Vos sauvegardes apparaîtront ici.",
"bk.restore": "Restaurer",
"bk.verify": "Vérifier",
"bk.intact": "Cette sauvegarde est intacte.",
"bk.problem": "Problème détecté : {details}",
"bk.delete": "Supprimer",
"bk.confirmDelete": "Supprimer cette sauvegarde ? Seule cette sauvegarde est supprimée ; votre serveur n’est pas affecté.",
};
export default fr;
+46
View File
@@ -226,6 +226,52 @@ const ru: Partial<Record<keyof typeof en, string>> = {
"about.upToDate": "У вас последняя версия CoA Server Manager.",
"about.checkFailed": "Не удалось проверить наличие новой версии. Проверьте подключение к интернету и повторите попытку.",
"about.installFailed": "Не удалось установить обновление. Ваш сервер не затронут.",
// Backups
"bk.title": "Резервные копии",
"bk.trigger.manual": "Копия, сделанная вручную",
"bk.trigger.automatic": "Автоматическая копия",
"bk.trigger.before-update": "Перед обновлением",
"bk.trigger.before-restore": "Страховочная копия перед восстановлением",
"bk.trigger.before-migration": "Перед обновлением базы данных",
"bk.trigger.before-bots": "Перед установкой компаньонов",
"bk.trigger.before-repair": "Перед восстановлением файлов",
"bk.trigger.before-dangerous-change": "Перед рискованным изменением",
"bk.kind.quick": "Персонажи, учётные записи и настройки",
"bk.kind.full": "Всё, включая базу данных мира",
"bk.kind.config": "Только настройки",
"bk.kind.database": "Персонажи и учётные записи",
"bk.db.characters": "Персонажи",
"bk.db.auth": "Учётные записи",
"bk.db.world": "Данные мира",
"bk.today": "Сегодня, {time}",
"bk.yesterday": "Вчера, {time}",
"bk.created": "Резервная копия создана.",
"bk.last": "Последняя копия: {when}",
"bk.none": "Резервных копий пока нет",
"bk.keptNext": "Копии хранятся рядом с сервером и никогда не перезаписывают ваши рабочие данные.",
"bk.backUpNow": "Сделать копию",
"bk.full": "Полная копия",
"bk.fullHint": "Также сохраняет большую базу данных мира",
"bk.settingsOnly": "Только настройки",
"bk.working": "Работаю…",
"bk.failed": "Не получилось",
"bk.restoreFrom": "Восстановить из копии «{what}», {when}",
"bk.restoreNote": "Ничего не удаляется. Сначала сохраняются ваши текущие данные, а заменённая база остаётся рядом под новым именем.",
"bk.stopFirst": "Сначала остановите сервер — базы данных можно восстановить только при остановленном сервере.",
"bk.settingsRestored": "Настройки восстановлены. Страховочная копия прежних настроек сохранена.",
"bk.restoreSettings": "Восстановить настройки",
"bk.confirmDb": "Восстановить базу данных «{db}» из этой копии? Всё, что произошло после {when}, будет отложено в сторону (не удалено).",
"bk.dbRestored": "Восстановлено таблиц: {n}. Прежние данные сохранены как {schema}.",
"bk.restoreDb": "Восстановить: {db}",
"bk.loading": "Загрузка…",
"bk.empty": "Ваши резервные копии появятся здесь.",
"bk.restore": "Восстановить",
"bk.verify": "Проверить",
"bk.intact": "Эта копия цела.",
"bk.problem": "Обнаружена проблема: {details}",
"bk.delete": "Удалить",
"bk.confirmDelete": "Удалить эту копию? Удаляется только она; ваш сервер не затрагивается.",
};
export default ru;
+44 -52
View File
@@ -4,26 +4,13 @@ import { Loader2, ShieldCheck } from "lucide-react";
import { api, asUiError, type BackupKind, type RecoveryPoint, type UiError } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { useHuman, useI18n, type Key, type Locale } from "@/i18n";
const TRIGGER: Record<string, string> = {
manual: "Manual backup",
automatic: "Automatic backup",
"before-update": "Before update",
"before-restore": "Safety copy before a restore",
"before-migration": "Before database update",
"before-bots": "Before installing companions",
"before-repair": "Before repair",
"before-dangerous-change": "Before a risky change",
};
const TRIGGER_KEYS = ["manual", "automatic", "before-update", "before-restore", "before-migration", "before-bots", "before-repair", "before-dangerous-change"];
const KIND: Record<BackupKind, string> = {
quick: "Characters, accounts and settings",
full: "Everything, including the world database",
config: "Settings only",
database: "Characters and accounts",
};
const DB_KEYS = ["characters", "auth", "world"];
const DB_LABEL: Record<string, string> = { characters: "Characters", auth: "Accounts", world: "World data" };
type T = (k: Key, v?: Record<string, string | number>) => string;
function size(bytes: number): string {
if (bytes >= 1 << 30) return `${(bytes / (1 << 30)).toFixed(1)} GB`;
@@ -31,17 +18,22 @@ function size(bytes: number): string {
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
}
function when(iso: string): string {
function when(iso: string, t: T, locale: Locale): string {
const d = new Date(iso);
const today = new Date();
const time = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
if (d.toDateString() === today.toDateString()) return `Today ${time}`;
const time = d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
if (d.toDateString() === today.toDateString()) return t("bk.today", { time });
const y = new Date(today.getTime() - 86400000);
if (d.toDateString() === y.toDateString()) return `Yesterday ${time}`;
return `${d.toLocaleDateString([], { day: "numeric", month: "short", year: "numeric" })} ${time}`;
if (d.toDateString() === y.toDateString()) return t("bk.yesterday", { time });
return `${d.toLocaleDateString(locale, { day: "numeric", month: "short", year: "numeric" })} ${time}`;
}
export function BackupsPage({ serverId }: { serverId: string }) {
const { t, locale } = useI18n();
const human = useHuman();
const w = (iso: string) => when(iso, t, locale);
const trigger = (x: string) => (TRIGGER_KEYS.includes(x) ? t(`bk.trigger.${x}` as Key) : x);
const dbLabel = (x: string) => (DB_KEYS.includes(x) ? t(`bk.db.${x}` as Key) : x);
const [points, setPoints] = useState<RecoveryPoint[] | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [step, setStep] = useState<string | null>(null);
@@ -84,54 +76,54 @@ export function BackupsPage({ serverId }: { serverId: string }) {
const create = (kind: BackupKind) =>
run("create", async () => {
await api.createBackup(serverId, kind);
return "Backup created.";
return t("bk.created");
});
const last = points?.[0];
return (
<div className="max-w-3xl">
<h1 className="text-2xl font-semibold">Backups</h1>
<p className="mt-1 text-muted">Is my data safe?</p>
<h1 className="text-2xl font-semibold">{t("bk.title")}</h1>
<p className="mt-1 text-muted">{t("q.backups")}</p>
<Card className="mt-6 p-6">
<div className="flex items-center gap-3">
<ShieldCheck className={last ? "h-6 w-6 text-ok" : "h-6 w-6 text-muted"} aria-hidden />
<div>
<p className="font-medium">{last ? `Last backup: ${when(last.created_at)}` : "No backups yet"}</p>
<p className="text-sm text-muted">Backups are kept next to your server and never overwrite your live data.</p>
<p className="font-medium">{last ? t("bk.last", { when: w(last.created_at) }) : t("bk.none")}</p>
<p className="text-sm text-muted">{t("bk.keptNext")}</p>
</div>
</div>
<div className="mt-5 flex flex-wrap items-center gap-3">
<Button variant="primary" disabled={!!busy} onClick={() => void create("quick")}>
{busy === "create" && <Loader2 className="h-4 w-4 animate-spin" aria-hidden />}
Back up now
{t("bk.backUpNow")}
</Button>
<Button disabled={!!busy} onClick={() => void create("full")} title="Also saves the large world database">
Full backup
<Button disabled={!!busy} onClick={() => void create("full")} title={t("bk.fullHint")}>
{t("bk.full")}
</Button>
<Button variant="ghost" disabled={!!busy} onClick={() => void create("config")}>
Settings only
{t("bk.settingsOnly")}
</Button>
{busy === "create" && <span className="text-sm text-muted" role="status">{step ?? "Working…"}</span>}
{busy === "create" && <span className="text-sm text-muted" role="status">{step ?? t("bk.working")}</span>}
</div>
</Card>
{notice && <p className="mt-3 text-sm text-ok" role="status">{notice}</p>}
{error && (
<Card className="mt-4 border-bad/40 p-4" role="alert">
<p className="font-medium text-bad">{error.human.code === "unknown" ? "That did not work" : error.human.title}</p>
<p className="mt-1 text-sm text-muted">{error.human.code === "unknown" ? error.technical : error.human.message}</p>
<p className="font-medium text-bad">{error.human.code === "unknown" ? t("bk.failed") : human(error.human).title}</p>
<p className="mt-1 text-sm text-muted">{error.human.code === "unknown" ? error.technical : human(error.human).message}</p>
</Card>
)}
{restoring && (
<Card className="mt-4 border-gold/40 p-5">
<p className="font-medium">Restore from {TRIGGER[restoring.trigger] ?? restoring.trigger}, {when(restoring.created_at)}</p>
<p className="font-medium">{t("bk.restoreFrom", { what: trigger(restoring.trigger), when: w(restoring.created_at) })}</p>
<p className="mt-1 text-sm text-muted">
Nothing is deleted. Your current data is saved first, and a replaced database is kept alongside under a new name.
{t("bk.restoreNote")}
</p>
{running && <p className="mt-2 text-sm text-warn">Stop the server first — databases can only be restored while it is stopped.</p>}
{running && <p className="mt-2 text-sm text-warn">{t("bk.stopFirst")}</p>}
<div className="mt-4 flex flex-wrap gap-2">
{restoring.components.map((c) =>
c.name === "configs" ? (
@@ -143,11 +135,11 @@ export function BackupsPage({ serverId }: { serverId: string }) {
void run("restore", async () => {
await api.restoreConfigs(serverId, restoring.id);
setRestoring(null);
return "Settings restored. A safety copy of the previous settings was saved.";
return t("bk.settingsRestored");
})
}
>
Restore settings
{t("bk.restoreSettings")}
</Button>
) : (
<Button
@@ -155,39 +147,39 @@ export function BackupsPage({ serverId }: { serverId: string }) {
size="sm"
disabled={!!busy || running}
onClick={() => {
if (!window.confirm(`Restore the ${DB_LABEL[c.name] ?? c.name} database from this backup? Progress made since ${when(restoring.created_at)} will be set aside (not deleted).`)) return;
if (!window.confirm(t("bk.confirmDb", { db: dbLabel(c.name), when: w(restoring.created_at) }))) return;
void run("restore", async () => {
const r = await api.restoreDatabase(serverId, restoring.id, c.name);
setRestoring(null);
return `Restored ${r.tables_restored} tables. The previous data is kept as ${r.previous_schema}.`;
return t("bk.dbRestored", { n: r.tables_restored, schema: r.previous_schema });
});
}}
>
Restore {DB_LABEL[c.name]?.toLowerCase() ?? c.name}
{t("bk.restoreDb", { db: dbLabel(c.name) })}
</Button>
),
)}
<Button size="sm" variant="ghost" onClick={() => setRestoring(null)}>
Cancel
{t("common.cancel")}
</Button>
</div>
</Card>
)}
<div className="mt-6 divide-y divide-line rounded-card border border-line bg-card/60">
{points === null && <p className="p-4 text-muted">Loading…</p>}
{points?.length === 0 && <p className="p-4 text-muted">Your backups will appear here.</p>}
{points === null && <p className="p-4 text-muted">{t("bk.loading")}</p>}
{points?.length === 0 && <p className="p-4 text-muted">{t("bk.empty")}</p>}
{points?.map((p) => (
<div key={p.id} className="flex items-center justify-between gap-4 p-4">
<div>
<p className="font-medium">{p.label && p.trigger === "manual" && p.label !== "e2e" ? p.label : (TRIGGER[p.trigger] ?? p.trigger)}</p>
<p className="font-medium">{p.label && p.trigger === "manual" && p.label !== "e2e" ? p.label : trigger(p.trigger)}</p>
<p className="text-sm text-muted">
{when(p.created_at)} · {KIND[p.kind]} · {size(p.components.reduce((a, c) => a + c.bytes, 0))}
{w(p.created_at)} · {t(`bk.kind.${p.kind}` as Key)} · {size(p.components.reduce((a, c) => a + c.bytes, 0))}
</p>
</div>
<div className="flex shrink-0 gap-1">
<Button size="sm" disabled={!!busy} onClick={() => setRestoring(p)}>
Restore
{t("bk.restore")}
</Button>
<Button
size="sm"
@@ -196,25 +188,25 @@ export function BackupsPage({ serverId }: { serverId: string }) {
onClick={() =>
void run("verify", async () => {
const v = await api.verifyBackup(serverId, p.id);
return v.ok ? "This backup is intact." : `Problem found: ${v.problems.join("; ")}`;
return v.ok ? t("bk.intact") : t("bk.problem", { details: v.problems.join("; ") });
})
}
>
Verify
{t("bk.verify")}
</Button>
<Button
size="sm"
variant="ghost"
disabled={!!busy}
onClick={() => {
if (window.confirm("Delete this backup? Only this backup is removed; your server is not affected.")) {
if (window.confirm(t("bk.confirmDelete"))) {
void run("delete", async () => {
await api.deleteBackup(serverId, p.id);
});
}
}}
>
Delete
{t("bk.delete")}
</Button>
</div>
</div>