diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 677a2c6..1d933f1 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -226,6 +226,52 @@ const de: Partial> = { "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; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index cbfd05b..7c1dce1 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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; diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 7c452b7..8371aaa 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -226,6 +226,52 @@ const es: Partial> = { "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; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 0941cd6..459c3a3 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -226,6 +226,52 @@ const fr: Partial> = { "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; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index cd2810b..3410582 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -226,6 +226,52 @@ const ru: Partial> = { "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; diff --git a/src/screens/BackupsPage.tsx b/src/screens/BackupsPage.tsx index eafdab9..288ac9e 100644 --- a/src/screens/BackupsPage.tsx +++ b/src/screens/BackupsPage.tsx @@ -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 = { - 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 = { - 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 = { characters: "Characters", auth: "Accounts", world: "World data" }; +type T = (k: Key, v?: Record) => 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(null); const [busy, setBusy] = useState(null); const [step, setStep] = useState(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 (
-

Backups

-

Is my data safe?

+

{t("bk.title")}

+

{t("q.backups")}

-

{last ? `Last backup: ${when(last.created_at)}` : "No backups yet"}

-

Backups are kept next to your server and never overwrite your live data.

+

{last ? t("bk.last", { when: w(last.created_at) }) : t("bk.none")}

+

{t("bk.keptNext")}

- - {busy === "create" && {step ?? "Working…"}} + {busy === "create" && {step ?? t("bk.working")}}
{notice &&

{notice}

} {error && ( -

{error.human.code === "unknown" ? "That did not work" : error.human.title}

-

{error.human.code === "unknown" ? error.technical : error.human.message}

+

{error.human.code === "unknown" ? t("bk.failed") : human(error.human).title}

+

{error.human.code === "unknown" ? error.technical : human(error.human).message}

)} {restoring && ( -

Restore from {TRIGGER[restoring.trigger] ?? restoring.trigger}, {when(restoring.created_at)}

+

{t("bk.restoreFrom", { what: trigger(restoring.trigger), when: w(restoring.created_at) })}

- Nothing is deleted. Your current data is saved first, and a replaced database is kept alongside under a new name. + {t("bk.restoreNote")}

- {running &&

Stop the server first — databases can only be restored while it is stopped.

} + {running &&

{t("bk.stopFirst")}

}
{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")} ) : ( ), )}
)}
- {points === null &&

Loading…

} - {points?.length === 0 &&

Your backups will appear here.

} + {points === null &&

{t("bk.loading")}

} + {points?.length === 0 &&

{t("bk.empty")}

} {points?.map((p) => (
-

{p.label && p.trigger === "manual" && p.label !== "e2e" ? p.label : (TRIGGER[p.trigger] ?? p.trigger)}

+

{p.label && p.trigger === "manual" && p.label !== "e2e" ? p.label : trigger(p.trigger)}

- {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))}