From 654ac3e6937b049e1bed790496840a40275432b0 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Tue, 2 Sep 2025 23:45:25 -0500 Subject: [PATCH] Add more translations, fix user delete failing --- public/locales/en/translation.json | 26 ++++++++++++++++--- public/locales/zh/translation.json | 24 ++++++++++++++--- src/backend/database/routes/users.ts | 19 ++++++++++---- src/ui/Apps/File Manager/FileManager.tsx | 10 +++---- .../File Manager/FileManagerLeftSidebar.tsx | 12 ++++----- .../FileManagerLeftSidebarFileViewer.tsx | 7 +++-- src/ui/Homepage/HomepageAlertCard.tsx | 11 +++++--- src/ui/Homepage/HomepageAlertManager.tsx | 6 +++-- src/ui/Navigation/LeftSidebar.tsx | 12 ++++----- src/ui/Navigation/Tabs/TabContext.tsx | 6 +++-- 10 files changed, 94 insertions(+), 39 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index bdef64d9..d1d55c02 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -14,7 +14,9 @@ }, "homepage": { "loggedInTitle": "Logged in!", - "loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar." + "loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.", + "failedToLoadAlerts": "Failed to load alerts", + "failedToDismissAlert": "Failed to dismiss alert" }, "common": { "close": "Close", @@ -34,7 +36,14 @@ "sidebar": "Sidebar", "home": "Home", "expired": "Expired", + "expiresToday": "Expires today", + "expiresTomorrow": "Expires tomorrow", + "expiresInDays": "Expires in {{days}} days", "updateAvailable": "Update Available", + "sshPath": "SSH Path", + "localPath": "Local Path", + "loading": "Loading...", + "noAuthCredentials": "No authentication credentials available for this SSH host", "noReleases": "No Releases", "updatesAndReleases": "Updates & Releases", "newVersionAvailable": "A new version ({{version}}) is available.", @@ -402,7 +411,15 @@ "enterFolderPath": "Enter folder path", "noShortcuts": "No shortcuts.", "searchFilesAndFolders": "Search files and folders...", - "noFilesOrFoldersFound": "No files or folders found." + "noFilesOrFoldersFound": "No files or folders found.", + "failedToConnectSSH": "Failed to connect to SSH", + "failedToReconnectSSH": "Failed to reconnect SSH session", + "failedToListFiles": "Failed to list files", + "fetchHomeDataTimeout": "Fetch home data timed out", + "sshStatusCheckTimeout": "SSH status check timed out", + "sshReconnectionTimeout": "SSH reconnection timed out", + "saveOperationTimeout": "Save operation timed out", + "cannotSaveFile": "Cannot save file" }, "tunnels": { "title": "SSH Tunnels", @@ -457,7 +474,8 @@ "portMapping": "Port {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", "disconnect": "Disconnect", "connect": "Connect", - "canceling": "Canceling..." + "canceling": "Canceling...", + "endpointHostNotFound": "Endpoint host not found" }, "serverStats": { "title": "Server Statistics", @@ -509,7 +527,7 @@ "passwordReset": "Password reset link sent", "twoFactorAuth": "Two-Factor Authentication", "enterCode": "Enter verification code", - "backupCode": "Use backup code", + "backupCode": "Or use backup code", "verifyCode": "Verify Code", "enableTwoFactor": "Enable Two-Factor Authentication", "disableTwoFactor": "Disable Two-Factor Authentication", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 89584731..c8f299dc 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -14,7 +14,9 @@ }, "homepage": { "loggedInTitle": "登录成功!", - "loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。" + "loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。", + "failedToLoadAlerts": "加载警报失败", + "failedToDismissAlert": "关闭警报失败" }, "common": { "close": "关闭", @@ -34,7 +36,14 @@ "sidebar": "侧边栏", "home": "首页", "expired": "已过期", + "expiresToday": "今天过期", + "expiresTomorrow": "明天过期", + "expiresInDays": "{{days}} 天后过期", "updateAvailable": "有可用更新", + "sshPath": "SSH 路径", + "localPath": "本地路径", + "loading": "加载中...", + "noAuthCredentials": "此 SSH 主机没有可用的身份验证凭据", "noReleases": "没有发布版本", "updatesAndReleases": "更新与发布", "newVersionAvailable": "有新版本 ({{version}}) 可用。", @@ -439,7 +448,15 @@ "enterFolderPath": "输入文件夹路径", "noShortcuts": "没有快捷方式。", "searchFilesAndFolders": "搜索文件和文件夹...", - "noFilesOrFoldersFound": "没有找到文件或文件夹。" + "noFilesOrFoldersFound": "没有找到文件或文件夹。", + "failedToConnectSSH": "连接 SSH 失败", + "failedToReconnectSSH": "重新连接 SSH 会话失败", + "failedToListFiles": "列出文件失败", + "fetchHomeDataTimeout": "获取主页数据超时", + "sshStatusCheckTimeout": "SSH 状态检查超时", + "sshReconnectionTimeout": "SSH 重新连接超时", + "saveOperationTimeout": "保存操作超时", + "cannotSaveFile": "无法保存文件" }, "tunnels": { "title": "SSH 隧道", @@ -494,7 +511,8 @@ "portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", "disconnect": "断开连接", "connect": "连接", - "canceling": "取消中..." + "canceling": "取消中...", + "endpointHostNotFound": "未找到端点主机" }, "serverStats": { "title": "服务器统计", diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index f0a892e8..39140c76 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1311,17 +1311,26 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => { const targetUserId = targetUser[0].id; try { + // Delete from tables that have foreign keys to both users and ssh_data first + // These must be deleted before ssh_data because they reference host_id + await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId)); + await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId)); + await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, targetUserId)); + + // Delete from tables that only reference users + await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, targetUserId)); + + // Delete from ssh_data (which references users) + await db.delete(sshData).where(eq(sshData.userId, targetUserId)); + + // Delete from any additional tables that might exist db.$client.prepare('DELETE FROM config_editor_recent WHERE user_id = ?').run(targetUserId); db.$client.prepare('DELETE FROM config_editor_pinned WHERE user_id = ?').run(targetUserId); db.$client.prepare('DELETE FROM config_editor_shortcuts WHERE user_id = ?').run(targetUserId); db.$client.prepare('DELETE FROM shared_hosts WHERE original_user_id = ? OR shared_with_user_id = ?').run(targetUserId, targetUserId); - await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId)); - await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId)); - await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, targetUserId)); - await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, targetUserId)); - await db.delete(sshData).where(eq(sshData.userId, targetUserId)); } catch (cleanupError) { logger.error(`Cleanup failed for user ${username}:`, cleanupError); + throw cleanupError; // Re-throw to prevent user deletion if cleanup fails } await db.delete(users).where(eq(users.id, targetUserId)); diff --git a/src/ui/Apps/File Manager/FileManager.tsx b/src/ui/Apps/File Manager/FileManager.tsx index 6f58bfb4..85e33c00 100644 --- a/src/ui/Apps/File Manager/FileManager.tsx +++ b/src/ui/Apps/File Manager/FileManager.tsx @@ -136,7 +136,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null} ]); const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Fetch home data timed out')), 15000) + setTimeout(() => reject(new Error(t('fileManager.fetchHomeDataTimeout'))), 15000) ); const [recentRes, pinnedRes, shortcutsRes] = await Promise.race([homeDataPromise, timeoutPromise]) as [any, any, any]; @@ -371,7 +371,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null} try { const statusPromise = getSSHStatus(tab.sshSessionId); const statusTimeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('SSH status check timed out')), 10000) + setTimeout(() => reject(new Error(t('fileManager.sshStatusCheckTimeout'))), 10000) ); const status = await Promise.race([statusPromise, statusTimeoutPromise]) as { connected: boolean }; @@ -386,7 +386,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null} keyPassword: currentHost.keyPassword }); const connectTimeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('SSH reconnection timed out')), 15000) + setTimeout(() => reject(new Error(t('fileManager.sshReconnectionTimeout'))), 15000) ); await Promise.race([connectPromise, connectTimeoutPromise]); @@ -397,7 +397,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null} const savePromise = writeSSHFile(tab.sshSessionId, tab.filePath, tab.content); const timeoutPromise = new Promise((_, reject) => setTimeout(() => { - reject(new Error('Save operation timed out')); + reject(new Error(t('fileManager.saveOperationTimeout'))); }, 30000) ); @@ -432,7 +432,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null} }); } catch (err) { - let errorMessage = formatErrorMessage(err, 'Cannot save file'); + let errorMessage = formatErrorMessage(err, t('fileManager.cannotSaveFile')); if (errorMessage.includes('timed out') || errorMessage.includes('timeout')) { errorMessage = t('fileManager.saveTimeout'); diff --git a/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx b/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx index 0e38d8b1..290c4ab7 100644 --- a/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx +++ b/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx @@ -128,7 +128,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( try { if (!server.password && !server.key) { - toast.error('No authentication credentials available for this SSH host'); + toast.error(t('common.noAuthCredentials')); return null; } @@ -152,7 +152,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( return sessionId; } catch (err: any) { - toast.error(err?.response?.data?.error || 'Failed to connect to SSH'); + toast.error(err?.response?.data?.error || t('fileManager.failedToConnectSSH')); setSshSessionId(null); return null; } finally { @@ -189,7 +189,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( setSshSessionId(newSessionId); res = await listSSHFiles(newSessionId, currentPath); } else { - throw new Error('Failed to reconnect SSH session'); + throw new Error(t('fileManager.failedToReconnectSSH')); } } else { res = await listSSHFiles(sshSessionId, currentPath); @@ -220,7 +220,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( } } catch (err: any) { setFiles([]); - toast.error(err?.response?.data?.error || err?.message || 'Failed to list files'); + toast.error(err?.response?.data?.error || err?.message || t('fileManager.failedToListFiles')); } finally { setFilesLoading(false); setFetchingFiles(false); @@ -334,7 +334,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( fetchFiles(); } } catch (error: any) { - toast.error(error?.response?.data?.error || 'Failed to rename item'); + toast.error(error?.response?.data?.error || t('fileManager.failedToRenameItem')); } }; @@ -350,7 +350,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( fetchFiles(); } } catch (error: any) { - toast.error(error?.response?.data?.error || 'Failed to delete item'); + toast.error(error?.response?.data?.error || t('fileManager.failedToDeleteItem')); } }; diff --git a/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx b/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx index 59bac696..c7392b46 100644 --- a/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx +++ b/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx @@ -3,6 +3,7 @@ import {Button} from '@/components/ui/button.tsx'; import {Card} from '@/components/ui/card.tsx'; import {Separator} from '@/components/ui/separator.tsx'; import {Plus, Folder, File, Star, Trash2, Edit, Link2, Server, Pin} from 'lucide-react'; +import {useTranslation} from 'react-i18next'; interface SSHConnection { id: string; @@ -61,16 +62,18 @@ export function FileManagerLeftSidebarFileViewer({ onSwitchToSSH, currentSSH, }: FileManagerLeftSidebarVileViewerProps) { + const {t} = useTranslation(); + return (
{isSSHMode ? 'SSH Path' : 'Local Path'} + className="text-xs text-muted-foreground font-semibold">{isSSHMode ? t('common.sshPath') : t('common.localPath')} {currentPath}
{isLoading ? ( -
Loading...
+
{t('common.loading')}
) : error ? (
{error}
) : ( diff --git a/src/ui/Homepage/HomepageAlertCard.tsx b/src/ui/Homepage/HomepageAlertCard.tsx index d2f34722..e2f774fa 100644 --- a/src/ui/Homepage/HomepageAlertCard.tsx +++ b/src/ui/Homepage/HomepageAlertCard.tsx @@ -3,6 +3,7 @@ import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components import {Button} from "@/components/ui/button.tsx"; import {Badge} from "@/components/ui/badge.tsx"; import {X, ExternalLink, AlertTriangle, Info, CheckCircle, AlertCircle} from "lucide-react"; +import {useTranslation} from "react-i18next"; interface TermixAlert { id: string; @@ -64,6 +65,8 @@ const getTypeBadgeVariant = (type?: string) => { }; export function HomepageAlertCard({alert, onDismiss, onClose}: AlertCardProps): React.ReactElement { + const {t} = useTranslation(); + if (!alert) { return null; } @@ -79,10 +82,10 @@ export function HomepageAlertCard({alert, onDismiss, onClose}: AlertCardProps): const diffTime = expiryDate.getTime() - now.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - if (diffDays < 0) return 'Expired'; - if (diffDays === 0) return 'Expires today'; - if (diffDays === 1) return 'Expires tomorrow'; - return `Expires in ${diffDays} days`; + if (diffDays < 0) return t('common.expired'); + if (diffDays === 0) return t('common.expiresToday'); + if (diffDays === 1) return t('common.expiresTomorrow'); + return t('common.expiresInDays', {days: diffDays}); }; return ( diff --git a/src/ui/Homepage/HomepageAlertManager.tsx b/src/ui/Homepage/HomepageAlertManager.tsx index 4aa8dd70..6e68b941 100644 --- a/src/ui/Homepage/HomepageAlertManager.tsx +++ b/src/ui/Homepage/HomepageAlertManager.tsx @@ -2,6 +2,7 @@ import React, {useEffect, useState} from "react"; import {HomepageAlertCard} from "./HomepageAlertCard.tsx"; import {Button} from "@/components/ui/button.tsx"; import { getUserAlerts, dismissAlert } from "@/ui/main-axios.ts"; +import {useTranslation} from "react-i18next"; interface TermixAlert { id: string; @@ -20,6 +21,7 @@ interface AlertManagerProps { } export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): React.ReactElement { + const {t} = useTranslation(); const [alerts, setAlerts] = useState([]); const [currentAlertIndex, setCurrentAlertIndex] = useState(0); const [loading, setLoading] = useState(false); @@ -57,7 +59,7 @@ export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): Rea setAlerts(sortedAlerts); setCurrentAlertIndex(0); } catch (err) { - setError('Failed to load alerts'); + setError(t('homepage.failedToLoadAlerts')); } finally { setLoading(false); } @@ -81,7 +83,7 @@ export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): Rea return prevIndex; }); } catch (err) { - setError('Failed to dismiss alert'); + setError(t('homepage.failedToDismissAlert')); } }; diff --git a/src/ui/Navigation/LeftSidebar.tsx b/src/ui/Navigation/LeftSidebar.tsx index 1c4828b6..cdc25b26 100644 --- a/src/ui/Navigation/LeftSidebar.tsx +++ b/src/ui/Navigation/LeftSidebar.tsx @@ -120,7 +120,7 @@ export function LeftSidebar({ const sshManagerTab = tabList.find((t) => t.type === 'ssh_manager'); const openSshManagerTab = () => { if (sshManagerTab || isSplitScreenActive) return; - const id = addTab({type: 'ssh_manager', title: 'SSH Manager'} as any); + const id = addTab({type: 'ssh_manager'} as any); setCurrentTab(id); }; const adminTab = tabList.find((t) => t.type === 'admin'); @@ -130,7 +130,7 @@ export function LeftSidebar({ setCurrentTab(adminTab.id); return; } - const id = addTab({type: 'admin', title: 'Admin'} as any); + const id = addTab({type: 'admin'} as any); setCurrentTab(id); }; @@ -186,7 +186,7 @@ export function LeftSidebar({ }, 50); } } catch (err: any) { - setHostsError('Failed to load hosts'); + setHostsError(t('leftSidebar.failedToLoadHosts')); } }, []); @@ -229,7 +229,7 @@ export function LeftSidebar({ const hostsByFolder = React.useMemo(() => { const map: Record = {}; filteredHosts.forEach(h => { - const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder'; + const folder = h.folder && h.folder.trim() ? h.folder : t('leftSidebar.noFolder'); if (!map[folder]) map[folder] = []; map[folder].push(h); }); @@ -239,8 +239,8 @@ export function LeftSidebar({ const sortedFolders = React.useMemo(() => { const folders = Object.keys(hostsByFolder); folders.sort((a, b) => { - if (a === 'No Folder') return -1; - if (b === 'No Folder') return 1; + if (a === t('leftSidebar.noFolder')) return -1; + if (b === t('leftSidebar.noFolder')) return 1; return a.localeCompare(b); }); return folders; diff --git a/src/ui/Navigation/Tabs/TabContext.tsx b/src/ui/Navigation/Tabs/TabContext.tsx index 5d8104bc..47bca744 100644 --- a/src/ui/Navigation/Tabs/TabContext.tsx +++ b/src/ui/Navigation/Tabs/TabContext.tsx @@ -1,4 +1,5 @@ import React, {createContext, useContext, useState, useRef, type ReactNode} from 'react'; +import {useTranslation} from 'react-i18next'; export interface Tab { id: number; @@ -34,15 +35,16 @@ interface TabProviderProps { } export function TabProvider({children}: TabProviderProps) { + const {t} = useTranslation(); const [tabs, setTabs] = useState([ - {id: 1, type: 'home', title: 'Home'} + {id: 1, type: 'home', title: t('nav.home')} ]); const [currentTab, setCurrentTab] = useState(1); const [allSplitScreenTab, setAllSplitScreenTab] = useState([]); const nextTabId = useRef(2); function computeUniqueTitle(tabType: Tab['type'], desiredTitle: string | undefined): string { - const defaultTitle = tabType === 'server' ? 'Server' : (tabType === 'file_manager' ? 'File Manager' : 'Terminal'); + const defaultTitle = tabType === 'server' ? t('nav.serverStats') : (tabType === 'file_manager' ? t('nav.fileManager') : t('nav.terminal')); const baseTitle = (desiredTitle || defaultTitle).trim(); const match = baseTitle.match(/^(.*) \((\d+)\)$/); const root = match ? match[1] : baseTitle;