chore: update translations

This commit is contained in:
LukeGus
2025-12-23 16:14:44 -06:00
parent 66ca197301
commit 186ba34c66
31 changed files with 828 additions and 11916 deletions

View File

@@ -311,14 +311,14 @@ export function CredentialEditor({
const getFriendlyKeyTypeName = (keyType: string): string => {
const keyTypeMap: Record<string, string> = {
"ssh-rsa": "RSA (SSH)",
"ssh-ed25519": "Ed25519 (SSH)",
"ecdsa-sha2-nistp256": "ECDSA P-256 (SSH)",
"ecdsa-sha2-nistp384": "ECDSA P-384 (SSH)",
"ecdsa-sha2-nistp521": "ECDSA P-521 (SSH)",
"ssh-dss": "DSA (SSH)",
"rsa-sha2-256": "RSA-SHA2-256",
"rsa-sha2-512": "RSA-SHA2-512",
"ssh-rsa": t("credentials.keyTypeRSA"),
"ssh-ed25519": t("credentials.keyTypeEd25519"),
"ecdsa-sha2-nistp256": t("credentials.keyTypeEcdsaP256"),
"ecdsa-sha2-nistp384": t("credentials.keyTypeEcdsaP384"),
"ecdsa-sha2-nistp521": t("credentials.keyTypeEcdsaP521"),
"ssh-dss": t("credentials.keyTypeDsa"),
"rsa-sha2-256": t("credentials.keyTypeRsaSha256"),
"rsa-sha2-512": t("credentials.keyTypeRsaSha512"),
invalid: t("credentials.invalidKey"),
error: t("credentials.detectionError"),
unknown: t("credentials.unknown"),

View File

@@ -141,11 +141,11 @@ export function CredentialsManager({
const handleDeploy = (credential: Credential) => {
if (credential.authType !== "key") {
toast.error("Only SSH key-based credentials can be deployed");
toast.error(t("credentials.keyBasedOnlyForDeployment"));
return;
}
if (!credential.publicKey) {
toast.error("Public key is required for deployment");
toast.error(t("credentials.publicKeyRequiredForDeployment"));
return;
}
setDeployingCredential(credential);
@@ -156,7 +156,7 @@ export function CredentialsManager({
const performDeploy = async () => {
if (!deployingCredential || !selectedHostId) {
toast.error("Please select a target host");
toast.error(t("credentials.selectTargetHost"));
return;
}
@@ -173,11 +173,11 @@ export function CredentialsManager({
setDeployingCredential(null);
setSelectedHostId("");
} else {
toast.error(result.error || "Deployment failed");
toast.error(result.error || t("credentials.deploymentFailed"));
}
} catch (error) {
console.error("Deployment error:", error);
toast.error("Failed to deploy SSH key");
toast.error(t("credentials.failedToDeployKey"));
} finally {
setDeployLoading(false);
}
@@ -564,7 +564,7 @@ export function CredentialsManager({
}}
title={
folder !== t("credentials.uncategorized")
? "Click to rename folder"
? t("credentials.clickToRenameFolder")
: ""
}
>
@@ -579,7 +579,7 @@ export function CredentialsManager({
startFolderEdit(folder);
}}
className="h-4 w-4 p-0 opacity-50 hover:opacity-100 transition-opacity"
title="Rename folder"
title={t("credentials.renameFolder")}
>
<Pencil className="h-3 w-3" />
</Button>
@@ -622,7 +622,7 @@ export function CredentialsManager({
{credential.username}
</p>
<p className="text-xs text-muted-foreground truncate">
ID: {credential.id}
{t("credentials.idLabel")} {credential.id}
</p>
<p className="text-xs text-muted-foreground truncate">
{credential.authType === "password"
@@ -867,7 +867,7 @@ export function CredentialsManager({
{t("credentials.keyType")}
</div>
<div className="text-sm font-medium">
{deployingCredential.keyType || "SSH Key"}
{deployingCredential.keyType || t("credentials.sshKey")}
</div>
</div>
</div>

View File

@@ -253,8 +253,7 @@ export function DockerManager({
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Docker is not enabled for this host. Enable it in Host Settings
to use Docker features.
{t("docker.notEnabled")}
</AlertDescription>
</Alert>
</div>
@@ -284,8 +283,8 @@ export function DockerManager({
<SimpleLoader size="lg" />
<p className="text-gray-400 mt-4">
{isValidating
? "Validating Docker..."
: "Connecting to host..."}
? t("docker.validating")
: t("docker.connectingToHost")}
</p>
</div>
</div>
@@ -314,11 +313,11 @@ export function DockerManager({
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<div className="font-semibold mb-2">Docker Error</div>
<div className="font-semibold mb-2">{t("docker.error")}</div>
<div>{dockerValidation.error}</div>
{dockerValidation.code && (
<div className="mt-2 text-xs opacity-70">
Error code: {dockerValidation.code}
{t("docker.errorCode", { code: dockerValidation.code })}
</div>
)}
</AlertDescription>
@@ -340,7 +339,7 @@ export function DockerManager({
</h1>
{dockerValidation?.version && (
<p className="text-xs text-gray-400">
Docker v{dockerValidation.version}
{t("docker.version", { version: dockerValidation.version })}
</p>
)}
</div>

View File

@@ -17,6 +17,7 @@ import { toast } from "sonner";
import type { SSHHost } from "@/types/index.js";
import { getCookie, isElectron } from "@/ui/main-axios.ts";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
import { useTranslation } from "react-i18next";
interface ConsoleTerminalProps {
sessionId: string;
@@ -33,6 +34,7 @@ export function ConsoleTerminal({
containerState,
hostConfig,
}: ConsoleTerminalProps): React.ReactElement {
const { t } = useTranslation();
const { instance: terminal, ref: xtermRef } = useXTerm();
const [isConnected, setIsConnected] = React.useState(false);
const [isConnecting, setIsConnecting] = React.useState(false);
@@ -150,7 +152,7 @@ export function ConsoleTerminal({
if (terminal) {
try {
terminal.clear();
terminal.write("Disconnected from container console.\r\n");
terminal.write(`${t("docker.disconnectedFromContainer")}\r\n`);
} catch (error) {
// Terminal might be disposed
}
@@ -159,7 +161,7 @@ export function ConsoleTerminal({
const connect = React.useCallback(() => {
if (!terminal || containerState !== "running") {
toast.error("Container must be running to connect to console");
toast.error(t("docker.containerMustBeRunning"));
return;
}
@@ -170,7 +172,7 @@ export function ConsoleTerminal({
? localStorage.getItem("jwt")
: getCookie("jwt");
if (!token) {
toast.error("Authentication required");
toast.error(t("docker.authenticationRequired"));
setIsConnecting(false);
return;
}
@@ -214,7 +216,7 @@ export function ConsoleTerminal({
case "connected":
setIsConnected(true);
setIsConnecting(false);
toast.success(`Connected to ${containerName}`);
toast.success(t("docker.connectedTo", { containerName }));
// Fit terminal and send resize to ensure correct dimensions
setTimeout(() => {
@@ -238,7 +240,7 @@ export function ConsoleTerminal({
setIsConnected(false);
setIsConnecting(false);
terminal.write(
`\r\n\x1b[1;33m${msg.message || "Disconnected"}\x1b[0m\r\n`,
`\r\n\x1b[1;33m${msg.message || t("docker.disconnected")}\x1b[0m\r\n`,
);
if (wsRef.current) {
wsRef.current.close();
@@ -248,8 +250,8 @@ export function ConsoleTerminal({
case "error":
setIsConnecting(false);
toast.error(msg.message || "Console error");
terminal.write(`\r\n\x1b[1;31mError: ${msg.message}\x1b[0m\r\n`);
toast.error(msg.message || t("docker.consoleError"));
terminal.write(`\r\n\x1b[1;31m${t("docker.errorMessage", { message: msg.message })}\x1b[0m\r\n`);
break;
}
} catch (error) {
@@ -261,7 +263,7 @@ export function ConsoleTerminal({
console.error("WebSocket error:", error);
setIsConnecting(false);
setIsConnected(false);
toast.error("Failed to connect to console");
toast.error(t("docker.failedToConnect"));
};
// Set up periodic ping to keep connection alive
@@ -340,9 +342,9 @@ export function ConsoleTerminal({
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-2">
<TerminalIcon className="h-12 w-12 text-gray-600 mx-auto" />
<p className="text-gray-400 text-lg">Container is not running</p>
<p className="text-gray-400 text-lg">{t("docker.containerNotRunning")}</p>
<p className="text-gray-500 text-sm">
Start the container to access the console
{t("docker.startContainerToAccess")}
</p>
</div>
</div>
@@ -357,7 +359,7 @@ export function ConsoleTerminal({
<div className="flex flex-col sm:flex-row gap-2 items-center sm:items-center">
<div className="flex items-center gap-2 flex-1">
<TerminalIcon className="h-5 w-5" />
<span className="text-base font-medium">Console</span>
<span className="text-base font-medium">{t("docker.console")}</span>
</div>
<Select
value={selectedShell}
@@ -365,12 +367,12 @@ export function ConsoleTerminal({
disabled={isConnected}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Select shell" />
<SelectValue placeholder={t("docker.selectShell")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="bash">Bash</SelectItem>
<SelectItem value="sh">Sh</SelectItem>
<SelectItem value="ash">Ash</SelectItem>
<SelectItem value="bash">{t("docker.bash")}</SelectItem>
<SelectItem value="sh">{t("docker.sh")}</SelectItem>
<SelectItem value="ash">{t("docker.ash")}</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-2 sm:gap-2">
@@ -383,12 +385,12 @@ export function ConsoleTerminal({
{isConnecting ? (
<>
<div className="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Connecting...
{t("docker.connecting")}
</>
) : (
<>
<Power className="h-4 w-4 mr-2" />
Connect
{t("docker.connect")}
</>
)}
</Button>
@@ -399,7 +401,7 @@ export function ConsoleTerminal({
className="min-w-[120px]"
>
<PowerOff className="h-4 w-4 mr-2" />
Disconnect
{t("docker.disconnect")}
</Button>
)}
</div>
@@ -422,9 +424,9 @@ export function ConsoleTerminal({
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center space-y-2">
<TerminalIcon className="h-12 w-12 text-gray-600 mx-auto" />
<p className="text-gray-400">Not connected</p>
<p className="text-gray-400">{t("docker.notConnected")}</p>
<p className="text-gray-500 text-sm">
Click Connect to start an interactive shell
{t("docker.clickToConnect")}
</p>
</div>
</div>
@@ -436,7 +438,7 @@ export function ConsoleTerminal({
<div className="text-center">
<SimpleLoader size="lg" />
<p className="text-gray-400 mt-4">
Connecting to {containerName}...
{t("docker.connectingTo", { containerName })}
</p>
</div>
</div>

View File

@@ -116,11 +116,13 @@ export function ContainerCard({
setIsStarting(true);
try {
await startDockerContainer(sessionId, container.id);
toast.success(`Container ${container.name} started`);
toast.success(t("docker.containerStarted", { name: container.name }));
onRefresh?.();
} catch (error) {
toast.error(
`Failed to start container: ${error instanceof Error ? error.message : "Unknown error"}`,
t("docker.failedToStartContainer", {
error: error instanceof Error ? error.message : "Unknown error",
}),
);
} finally {
setIsStarting(false);
@@ -132,11 +134,13 @@ export function ContainerCard({
setIsStopping(true);
try {
await stopDockerContainer(sessionId, container.id);
toast.success(`Container ${container.name} stopped`);
toast.success(t("docker.containerStopped", { name: container.name }));
onRefresh?.();
} catch (error) {
toast.error(
`Failed to stop container: ${error instanceof Error ? error.message : "Unknown error"}`,
t("docker.failedToStopContainer", {
error: error instanceof Error ? error.message : "Unknown error",
}),
);
} finally {
setIsStopping(false);
@@ -148,11 +152,13 @@ export function ContainerCard({
setIsRestarting(true);
try {
await restartDockerContainer(sessionId, container.id);
toast.success(`Container ${container.name} restarted`);
toast.success(t("docker.containerRestarted", { name: container.name }));
onRefresh?.();
} catch (error) {
toast.error(
`Failed to restart container: ${error instanceof Error ? error.message : "Unknown error"}`,
t("docker.failedToRestartContainer", {
error: error instanceof Error ? error.message : "Unknown error",
}),
);
} finally {
setIsRestarting(false);
@@ -165,15 +171,18 @@ export function ContainerCard({
try {
if (container.state === "paused") {
await unpauseDockerContainer(sessionId, container.id);
toast.success(`Container ${container.name} unpaused`);
toast.success(t("docker.containerUnpaused", { name: container.name }));
} else {
await pauseDockerContainer(sessionId, container.id);
toast.success(`Container ${container.name} paused`);
toast.success(t("docker.containerPaused", { name: container.name }));
}
onRefresh?.();
} catch (error) {
toast.error(
`Failed to ${container.state === "paused" ? "unpause" : "pause"} container: ${error instanceof Error ? error.message : "Unknown error"}`,
t("docker.failedToTogglePauseContainer", {
action: container.state === "paused" ? "unpause" : "pause",
error: error instanceof Error ? error.message : "Unknown error",
}),
);
} finally {
setIsPausing(false);
@@ -185,12 +194,14 @@ export function ContainerCard({
try {
const force = container.state === "running";
await removeDockerContainer(sessionId, container.id, force);
toast.success(`Container ${container.name} removed`);
toast.success(t("docker.containerRemoved", { name: container.name }));
setShowRemoveDialog(false);
onRefresh?.();
} catch (error) {
toast.error(
`Failed to remove container: ${error instanceof Error ? error.message : "Unknown error"}`,
t("docker.failedToRemoveContainer", {
error: error instanceof Error ? error.message : "Unknown error",
}),
);
} finally {
setIsRemoving(false);
@@ -249,21 +260,19 @@ export function ContainerCard({
<CardContent className="space-y-3 px-4 pb-3">
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<span className="text-gray-400 min-w-[50px] text-xs">Image:</span>
<span className="text-gray-400 min-w-[50px] text-xs">{t("docker.image")}</span>
<span className="truncate text-gray-200 text-xs">
{container.image}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400 min-w-[50px] text-xs">ID:</span>
<span className="text-gray-400 min-w-[50px] text-xs">{t("docker.idLabel")}</span>
<span className="font-mono text-xs text-gray-200">
{container.id.substring(0, 12)}
</span>
</div>
<div className="flex items-start gap-2">
<span className="text-gray-400 min-w-[50px] text-xs shrink-0">
Ports:
</span>
<span className="text-gray-400 min-w-[50px] text-xs shrink-0">{t("docker.ports")}</span>
<div className="flex flex-wrap gap-1">
{portsList.length > 0 ? (
portsList.map((port, idx) => (
@@ -280,15 +289,13 @@ export function ContainerCard({
variant="outline"
className="text-xs bg-gray-500/10 text-gray-400 border-gray-500/30"
>
None
{t("docker.noPorts")}
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400 min-w-[50px] text-xs">
Created:
</span>
<span className="text-gray-400 min-w-[50px] text-xs">{t("docker.created")}</span>
<span className="text-gray-200 text-xs">
{formatCreatedDate(container.created)}
</span>
@@ -314,7 +321,7 @@ export function ContainerCard({
)}
</Button>
</TooltipTrigger>
<TooltipContent>Start</TooltipContent>
<TooltipContent>{t("docker.start")}</TooltipContent>
</Tooltip>
)}
@@ -335,7 +342,7 @@ export function ContainerCard({
)}
</Button>
</TooltipTrigger>
<TooltipContent>Stop</TooltipContent>
<TooltipContent>{t("docker.stop")}</TooltipContent>
</Tooltip>
)}
@@ -360,7 +367,9 @@ export function ContainerCard({
</Button>
</TooltipTrigger>
<TooltipContent>
{container.state === "paused" ? "Unpause" : "Pause"}
{container.state === "paused"
? t("docker.unpause")
: t("docker.pause")}
</TooltipContent>
</Tooltip>
)}
@@ -381,8 +390,7 @@ export function ContainerCard({
)}
</Button>
</TooltipTrigger>
<TooltipContent>Restart</TooltipContent>
</Tooltip>
<TooltipContent>{t("docker.restart")}</TooltipContent> </Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -399,8 +407,7 @@ export function ContainerCard({
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Remove</TooltipContent>
</Tooltip>
<TooltipContent>{t("docker.remove")}</TooltipContent> </Tooltip>
</TooltipProvider>
</div>
</CardContent>
@@ -409,25 +416,22 @@ export function ContainerCard({
<AlertDialog open={showRemoveDialog} onOpenChange={setShowRemoveDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove Container</AlertDialogTitle>
<AlertDialogTitle>{t("docker.removeContainer")}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to remove container{" "}
<span className="font-semibold">
{container.name.startsWith("/")
{t("docker.confirmRemoveContainer", {
name: container.name.startsWith("/")
? container.name.slice(1)
: container.name}
</span>
?
: container.name,
})}
{container.state === "running" && (
<div className="mt-2 text-yellow-400">
Warning: This container is currently running and will be
force-removed.
{t("docker.runningContainerWarning")}
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isRemoving}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isRemoving}>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
@@ -436,7 +440,7 @@ export function ContainerCard({
disabled={isRemoving}
className="bg-red-600 hover:bg-red-700"
>
{isRemoving ? "Removing..." : "Remove"}
{isRemoving ? t("docker.removing") : t("common.remove")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -36,10 +36,10 @@ export function ContainerDetail({
return (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-2">
<p className="text-gray-400 text-lg">Container not found</p>
<p className="text-gray-400 text-lg">{t("docker.containerNotFound")}</p>
<Button onClick={onBack} variant="outline">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to list
{t("docker.backToList")}
</Button>
</div>
</div>
@@ -52,7 +52,7 @@ export function ContainerDetail({
<div className="flex items-center gap-4 px-4 pt-3 pb-3">
<Button variant="ghost" onClick={onBack} size="sm">
<ArrowLeft className="h-4 w-4 mr-2" />
Back
{t("common.back")}
</Button>
<div className="min-w-0 flex-1">
<h2 className="font-bold text-lg truncate">{container.name}</h2>
@@ -70,9 +70,9 @@ export function ContainerDetail({
>
<div className="px-4 pt-2">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="stats">Stats</TabsTrigger>
<TabsTrigger value="console">Console</TabsTrigger>
<TabsTrigger value="logs">{t("docker.logs")}</TabsTrigger>
<TabsTrigger value="stats">{t("docker.stats")}</TabsTrigger>
<TabsTrigger value="console">{t("docker.consoleTab")}</TabsTrigger>
</TabsList>
</div>

View File

@@ -55,10 +55,8 @@ export function ContainerList({
return (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-2">
<p className="text-gray-400 text-lg">No containers found</p>
<p className="text-gray-500 text-sm">
Start by creating containers on your server
</p>
<p className="text-gray-400 text-lg">{t("docker.noContainersFound")}</p>
<p className="text-gray-500 text-sm">{t("docker.noContainersFoundHint")}</p>
</div>
</div>
);
@@ -71,7 +69,7 @@ export function ContainerList({
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search by name, image, or ID..."
placeholder={t("docker.searchPlaceholder")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
@@ -81,13 +79,13 @@ export function ContainerList({
<Filter className="h-4 w-4 text-gray-400" />
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Filter by status" />
<SelectValue placeholder={t("docker.filterByStatusPlaceholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All ({containers.length})</SelectItem>
<SelectItem value="all">{t("docker.allContainersCount", { count: containers.length })}</SelectItem>
{Object.entries(statusCounts).map(([status, count]) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)} ({count})
{t("docker.statusCount", { status: status.charAt(0).toUpperCase() + status.slice(1), count })}
</SelectItem>
))}
</SelectContent>
@@ -99,10 +97,8 @@ export function ContainerList({
{filteredContainers.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<div className="text-center space-y-2">
<p className="text-gray-400">No containers match your filters</p>
<p className="text-gray-500 text-sm">
Try adjusting your search or filter
</p>
<p className="text-gray-400">{t("docker.noContainersMatchFilters")}</p>
<p className="text-gray-500 text-sm">{t("docker.noContainersMatchFiltersHint")}</p>
</div>
</div>
) : (

View File

@@ -10,6 +10,7 @@ import { Cpu, MemoryStick, Network, HardDrive, Activity } from "lucide-react";
import type { DockerStats } from "@/types/index.js";
import { getContainerStats } from "@/ui/main-axios.ts";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
import { useTranslation } from "react-i18next";
interface ContainerStatsProps {
sessionId: string;
@@ -24,13 +25,14 @@ export function ContainerStats({
containerName,
containerState,
}: ContainerStatsProps): React.ReactElement {
const { t } = useTranslation();
const [stats, setStats] = React.useState<DockerStats | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const fetchStats = React.useCallback(async () => {
if (containerState !== "running") {
setError("Container must be running to view stats");
setError(t("docker.containerMustBeRunningToViewStats"));
return;
}
@@ -40,7 +42,7 @@ export function ContainerStats({
const data = await getContainerStats(sessionId, containerId);
setStats(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch stats");
setError(err instanceof Error ? err.message : t("docker.failedToFetchStats"));
} finally {
setIsLoading(false);
}
@@ -60,9 +62,11 @@ export function ContainerStats({
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-2">
<Activity className="h-12 w-12 text-gray-600 mx-auto" />
<p className="text-gray-400 text-lg">Container is not running</p>
<p className="text-gray-400 text-lg">
{t("docker.containerNotRunning")}
</p>
<p className="text-gray-500 text-sm">
Start the container to view statistics
{t("docker.startContainerToViewStats")}
</p>
</div>
</div>
@@ -74,7 +78,7 @@ export function ContainerStats({
<div className="flex items-center justify-center h-full">
<div className="text-center">
<SimpleLoader size="lg" />
<p className="text-gray-400 mt-4">Loading stats...</p>
<p className="text-gray-400 mt-4">{t("docker.loadingStats")}</p>
</div>
</div>
);
@@ -84,7 +88,9 @@ export function ContainerStats({
return (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-2">
<p className="text-red-400 text-lg">Error loading stats</p>
<p className="text-red-400 text-lg">
{t("docker.errorLoadingStats")}
</p>
<p className="text-gray-500 text-sm">{error}</p>
</div>
</div>
@@ -94,7 +100,7 @@ export function ContainerStats({
if (!stats) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-gray-400">No stats available</p>
<p className="text-gray-400">{t("docker.noStatsAvailable")}</p>
</div>
);
}
@@ -109,14 +115,13 @@ export function ContainerStats({
<CardHeader className="pb-2 px-4">
<CardTitle className="text-base flex items-center gap-2">
<Cpu className="h-5 w-5 text-blue-400" />
CPU Usage
{t("docker.cpuUsage")}
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Current</span>
<span className="font-mono font-semibold text-blue-300">
<span className="text-gray-400">{t("docker.current")}</span> <span className="font-mono font-semibold text-blue-300">
{stats.cpu}
</span>
</div>
@@ -130,19 +135,19 @@ export function ContainerStats({
<CardHeader className="pb-2 px-4">
<CardTitle className="text-base flex items-center gap-2">
<MemoryStick className="h-5 w-5 text-purple-400" />
Memory Usage
{t("docker.memoryUsage")}
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Used / Limit</span>
<span className="text-gray-400">{t("docker.usedLimit")}</span>
<span className="font-mono font-semibold text-purple-300">
{stats.memoryUsed} / {stats.memoryLimit}
</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Percentage</span>
<span className="text-gray-400">{t("docker.percentage")}</span>
<span className="font-mono text-purple-300">
{stats.memoryPercent}
</span>
@@ -157,17 +162,17 @@ export function ContainerStats({
<CardHeader className="pb-2 px-4">
<CardTitle className="text-base flex items-center gap-2">
<Network className="h-5 w-5 text-green-400" />
Network I/O
{t("docker.networkIo")}
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Input</span>
<span className="text-gray-400">{t("docker.input")}</span>
<span className="font-mono text-green-300">{stats.netInput}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Output</span>
<span className="text-gray-400">{t("docker.output")}</span>
<span className="font-mono text-green-300">
{stats.netOutput}
</span>
@@ -181,26 +186,26 @@ export function ContainerStats({
<CardHeader className="pb-2 px-4">
<CardTitle className="text-base flex items-center gap-2">
<HardDrive className="h-5 w-5 text-orange-400" />
Block I/O
{t("docker.blockIo")}
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Read</span>
<span className="text-gray-400">{t("docker.read")}</span>
<span className="font-mono text-orange-300">
{stats.blockRead}
</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Write</span>
<span className="text-gray-400">{t("docker.write")}</span>
<span className="font-mono text-orange-300">
{stats.blockWrite}
</span>
</div>
{stats.pids && (
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">PIDs</span>
<span className="text-gray-400">{t("docker.pids")}</span>
<span className="font-mono text-orange-300">{stats.pids}</span>
</div>
)}
@@ -213,23 +218,23 @@ export function ContainerStats({
<CardHeader className="pb-2 px-4">
<CardTitle className="text-base flex items-center gap-2">
<Activity className="h-5 w-5 text-cyan-400" />
Container Information
{t("docker.containerInformation")}
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
<div className="flex justify-between items-center">
<span className="text-gray-400">Name:</span>
<span className="text-gray-400">{t("docker.name")}</span>
<span className="font-mono text-gray-200">{containerName}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">ID:</span>
<span className="text-gray-400">{t("docker.id")}</span>
<span className="font-mono text-sm text-gray-300">
{containerId.substring(0, 12)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">State:</span>
<span className="text-gray-400">{t("docker.state")}</span>
<span className="font-semibold text-green-400 capitalize">
{containerState}
</span>

View File

@@ -348,7 +348,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
if (result?.requires_totp) {
setTotpRequired(true);
setTotpSessionId(sessionId);
setTotpPrompt(result.prompt || "Verification code:");
setTotpPrompt(result.prompt || t("fileManager.verificationCodePrompt"));
setIsLoading(false);
return;
}
@@ -586,7 +586,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
error.message?.includes("established")
) {
toast.error(
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
t("fileManager.sshConnectionFailed", { name: currentHost?.name, ip: currentHost?.ip, port: currentHost?.port }),
);
} else {
toast.error(t("fileManager.failedToUploadFile"));
@@ -633,7 +633,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
error.message?.includes("established")
) {
toast.error(
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
t("fileManager.sshConnectionFailed", { name: currentHost?.name, ip: currentHost?.ip, port: currentHost?.port }),
);
} else {
toast.error(t("fileManager.failedToDownloadFile"));
@@ -1497,7 +1497,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
if (result?.requires_totp) {
setTotpRequired(true);
setTotpSessionId(sessionId);
setTotpPrompt(result.prompt || "Verification code:");
setTotpPrompt(result.prompt || t("fileManager.verificationCodePrompt"));
setIsLoading(false);
return;
}

View File

@@ -1166,7 +1166,7 @@ export function HostManagerEditor({
<TabsTrigger value="terminal">
{t("hosts.terminal")}
</TabsTrigger>
<TabsTrigger value="docker">Docker</TabsTrigger>
<TabsTrigger value="docker">{t("hosts.docker")}</TabsTrigger>
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
<TabsTrigger value="file_manager">
{t("hosts.fileManager")}
@@ -1895,7 +1895,7 @@ export function HostManagerEditor({
</FormLabel>
<FormControl>
<Input
placeholder="proxy.example.com"
placeholder={t("placeholders.socks5Host")}
{...field}
onBlur={(e) => {
field.onChange(
@@ -1923,7 +1923,7 @@ export function HostManagerEditor({
<FormControl>
<Input
type="number"
placeholder="1080"
placeholder={t("placeholders.socks5Port")}
{...field}
onChange={(e) =>
field.onChange(
@@ -1945,8 +1945,7 @@ export function HostManagerEditor({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("hosts.socks5Username")} (
{t("hosts.optional")})
{t("hosts.socks5Username")} {t("hosts.optional")}
</FormLabel>
<FormControl>
<Input
@@ -1970,8 +1969,7 @@ export function HostManagerEditor({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("hosts.socks5Password")} (
{t("hosts.optional")})
{t("hosts.socks5Password")} {t("hosts.optional")}
</FormLabel>
<FormControl>
<PasswordInput
@@ -2059,7 +2057,7 @@ export function HostManagerEditor({
{t("hosts.socks5Host")}
</FormLabel>
<Input
placeholder="proxy.example.com"
placeholder={t("placeholders.socks5Host")}
value={node.host}
onChange={(e) => {
const currentChain =
@@ -2104,7 +2102,7 @@ export function HostManagerEditor({
</FormLabel>
<Input
type="number"
placeholder="1080"
placeholder={t("placeholders.socks5Port")}
value={node.port}
onChange={(e) => {
const currentChain =
@@ -2155,10 +2153,10 @@ export function HostManagerEditor({
</SelectTrigger>
<SelectContent>
<SelectItem value="4">
SOCKS4
{t("hosts.socks4")}
</SelectItem>
<SelectItem value="5">
SOCKS5
{t("hosts.socks5")}
</SelectItem>
</SelectContent>
</Select>
@@ -2167,8 +2165,7 @@ export function HostManagerEditor({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<FormLabel>
{t("hosts.socks5Username")} (
{t("hosts.optional")})
{t("hosts.socks5Username")} {t("hosts.optional")}
</FormLabel>
<Input
placeholder={t("hosts.username")}
@@ -2212,8 +2209,7 @@ export function HostManagerEditor({
<div className="space-y-2">
<FormLabel>
{t("hosts.socks5Password")} (
{t("hosts.optional")})
{t("hosts.socks5Password")} {t("hosts.optional")}
</FormLabel>
<PasswordInput
placeholder={t("hosts.password")}
@@ -2720,7 +2716,7 @@ export function HostManagerEditor({
</PopoverContent>
</Popover>
<FormDescription>
Execute a snippet when the terminal connects
{t("hosts.executeSnippetOnConnect")}
</FormDescription>
</FormItem>
);
@@ -2733,9 +2729,9 @@ export function HostManagerEditor({
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Auto-MOSH</FormLabel>
<FormLabel>{t("hosts.autoMosh")}</FormLabel>
<FormDescription>
Automatically run MOSH command on connect
{t("hosts.autoMoshDesc")}
</FormDescription>
</div>
<FormControl>
@@ -2754,11 +2750,10 @@ export function HostManagerEditor({
name="terminalConfig.moshCommand"
render={({ field }) => (
<FormItem>
<FormLabel>MOSH Command</FormLabel>
<FormLabel>{t("hosts.moshCommand")}</FormLabel>
<FormControl>
<Input
placeholder="mosh user@server"
{...field}
placeholder={t("placeholders.moshCommand")} {...field}
onBlur={(e) => {
field.onChange(e.target.value.trim());
field.onBlur();
@@ -2766,7 +2761,7 @@ export function HostManagerEditor({
/>
</FormControl>
<FormDescription>
The MOSH command to execute
{t("hosts.moshCommandDesc")}
</FormDescription>
</FormItem>
)}
@@ -2819,11 +2814,10 @@ export function HostManagerEditor({
<div className="space-y-2">
<label className="text-sm font-medium">
Environment Variables
{t("hosts.environmentVariables")}
</label>
<FormDescription>
Set custom environment variables for the terminal
session
{t("hosts.environmentVariablesDesc")}
</FormDescription>
{form
.watch("terminalConfig.environmentVariables")
@@ -2836,7 +2830,7 @@ export function HostManagerEditor({
<FormItem className="flex-1">
<FormControl>
<Input
placeholder="Variable name"
placeholder={t("hosts.variableName")}
{...field}
onBlur={(e) => {
field.onChange(
@@ -2856,7 +2850,7 @@ export function HostManagerEditor({
<FormItem className="flex-1">
<FormControl>
<Input
placeholder="Value"
placeholder={t("hosts.variableValue")}
{...field}
onBlur={(e) => {
field.onChange(
@@ -2903,7 +2897,7 @@ export function HostManagerEditor({
}}
>
<Plus className="h-4 w-4 mr-2" />
Add Variable
{t("hosts.addVariable")}
</Button>
</div>
</AccordionContent>
@@ -2916,7 +2910,7 @@ export function HostManagerEditor({
name="enableDocker"
render={({ field }) => (
<FormItem>
<FormLabel>Enable Docker</FormLabel>
<FormLabel>{t("hosts.enableDocker")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
@@ -2924,7 +2918,7 @@ export function HostManagerEditor({
/>
</FormControl>
<FormDescription>
Enable Docker integration for this host
{t("hosts.enableDockerDesc")}
</FormDescription>
</FormItem>
)}
@@ -3075,7 +3069,7 @@ export function HostManagerEditor({
</FormLabel>
<FormControl>
<Input
placeholder="22"
placeholder={t("placeholders.defaultPort")}
{...sourcePortField}
/>
</FormControl>
@@ -3094,7 +3088,7 @@ export function HostManagerEditor({
</FormLabel>
<FormControl>
<Input
placeholder="224"
placeholder={t("placeholders.defaultEndpointPort")}
{...endpointPortField}
/>
</FormControl>

View File

@@ -381,7 +381,9 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
URL.revokeObjectURL(url);
toast.success(
`Exported host configuration for ${host.name || host.username}@${host.ip}`,
t("hosts.exportedHostConfig", {
name: host.name || `${host.username}@${host.ip}`,
}),
);
} catch {
toast.error(t("hosts.failedToExportHost"));
@@ -1072,7 +1074,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
}}
title={
folder !== t("hosts.uncategorized")
? "Click to rename folder"
? t("hosts.clickToRenameFolder")
: ""
}
>
@@ -1087,7 +1089,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
startFolderEdit(folder);
}}
className="h-4 w-4 p-0 opacity-50 hover:opacity-100 transition-opacity"
title="Rename folder"
title={t("hosts.renameFolder")}
>
<Pencil className="h-3 w-3" />
</Button>
@@ -1234,7 +1236,9 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</TooltipTrigger>
<TooltipContent>
<p>
Remove from folder "{host.folder}"
{t("hosts.removeFromFolder", {
folder: host.folder,
})}
</p>
</TooltipContent>
</Tooltip>
@@ -1254,7 +1258,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Edit host</p>
<p>{t("hosts.editHostTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -1276,7 +1280,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Delete host</p>
<p>{t("hosts.deleteHostTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -1294,7 +1298,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Export host</p>
<p>{t("hosts.exportHostTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -1312,7 +1316,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Clone host</p>
<p>{t("hosts.cloneHostTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -1384,7 +1388,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
className="text-xs px-1 py-0"
>
<Container className="h-2 w-2 mr-0.5" />
Docker
{t("hosts.docker")}
</Badge>
)}
</div>
@@ -1414,7 +1418,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Terminal</p>
<p>{t("hosts.openTerminal")}</p>
</TooltipContent>
</Tooltip>
)}
@@ -1495,7 +1499,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Docker</p>
<p>{t("hosts.openDocker")}</p>
</TooltipContent>
</Tooltip>
)}
@@ -1530,10 +1534,10 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
<TooltipContent>
<div className="text-center">
<p className="font-medium">
Click to edit host
{t("hosts.clickToEditHost")}
</p>
<p className="text-xs text-muted-foreground">
Drag to move between folders
{t("hosts.dragToMoveBetweenFolders")}
</p>
</div>
</TooltipContent>

View File

@@ -684,9 +684,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
!sudoPromptShownRef.current
) {
sudoPromptShownRef.current = true;
confirmWithToast(
t("terminal.sudoPasswordPopupTitle", "Insert password?"),
async () => {
confirmWithToast(t("terminal.sudoPasswordPopupTitle"), async () => {
if (
webSocketRef.current &&
webSocketRef.current.readyState === WebSocket.OPEN
@@ -843,7 +841,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}
} else if (msg.type === "totp_required") {
setTotpRequired(true);
setTotpPrompt(msg.prompt || "Verification code:");
setTotpPrompt(msg.prompt || t("terminal.totpCodeLabel"));
setIsPasswordPrompt(false);
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
@@ -851,7 +849,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}
} else if (msg.type === "password_required") {
setTotpRequired(true);
setTotpPrompt(msg.prompt || "Password:");
setTotpPrompt(msg.prompt || t("common.password"));
setIsPasswordPrompt(true);
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);

View File

@@ -283,9 +283,9 @@ export function SSHToolsSidebar({
console.error("Failed to fetch command history", err);
const errorMessage =
err?.response?.status === 401
? "Authentication required. Please refresh the page."
? t("commandHistory.authRequiredRefresh")
: err?.response?.status === 403
? "Data access locked. Please re-authenticate."
? t("commandHistory.dataAccessLockedReauth")
: err?.message || "Failed to load command history";
setHistoryError(errorMessage);
@@ -814,9 +814,7 @@ export function SSHToolsSidebar({
if (sourceFolder !== targetFolder) {
toast.error(
t("snippets.reorderSameFolder", {
defaultValue: "Can only reorder snippets within the same folder",
}),
t("snippets.reorderSameFolder"),
);
setDraggedSnippet(null);
setDragOverFolder(null);
@@ -853,16 +851,12 @@ export function SSHToolsSidebar({
try {
await reorderSnippets(updates);
toast.success(
t("snippets.reorderSuccess", {
defaultValue: "Snippets reordered successfully",
}),
t("snippets.reorderSuccess"),
);
fetchSnippets();
} catch {
toast.error(
t("snippets.reorderFailed", {
defaultValue: "Failed to reorder snippets",
}),
t("snippets.reorderFailed"),
);
}
@@ -901,23 +895,13 @@ export function SSHToolsSidebar({
confirmWithToast(
t("snippets.deleteFolderConfirm", {
name: folderName,
defaultValue: `Delete folder "${folderName}"? All snippets will be moved to Uncategorized.`,
}),
async () => {
try {
await deleteSnippetFolder(folderName);
toast.success(
t("snippets.deleteFolderSuccess", {
defaultValue: "Folder deleted successfully",
}),
);
toast.success(t("snippets.deleteFolderSuccess"));
fetchSnippets();
} catch {
toast.error(
t("snippets.deleteFolderFailed", {
defaultValue: "Failed to delete folder",
}),
);
toast.error(t("snippets.deleteFolderFailed"));
}
},
"destructive",
@@ -944,22 +928,14 @@ export function SSHToolsSidebar({
color: folderFormData.color || undefined,
icon: folderFormData.icon || undefined,
});
toast.success(
t("snippets.updateFolderSuccess", {
defaultValue: "Folder updated successfully",
}),
);
toast.success(t("snippets.updateFolderSuccess"));
} else {
await createSnippetFolder({
name: folderFormData.name,
color: folderFormData.color || undefined,
icon: folderFormData.icon || undefined,
});
toast.success(
t("snippets.createFolderSuccess", {
defaultValue: "Folder created successfully",
}),
);
toast.success(t("snippets.createFolderSuccess"));
}
setShowFolderDialog(false);
@@ -967,12 +943,8 @@ export function SSHToolsSidebar({
} catch {
toast.error(
editingFolder
? t("snippets.updateFolderFailed", {
defaultValue: "Failed to update folder",
})
: t("snippets.createFolderFailed", {
defaultValue: "Failed to create folder",
}),
? t("snippets.updateFolderFailed")
: t("snippets.createFolderFailed"),
);
}
};
@@ -1042,9 +1014,7 @@ export function SSHToolsSidebar({
if (splitAssignments.size === 0) {
toast.error(
t("splitScreen.error.noAssignments", {
defaultValue: "Please drag tabs to cells before applying",
}),
t("splitScreen.error.noAssignments"),
);
return;
}
@@ -1054,7 +1024,6 @@ export function SSHToolsSidebar({
if (splitAssignments.size < requiredSlots) {
toast.error(
t("splitScreen.error.fillAllSlots", {
defaultValue: `Please fill all ${requiredSlots} layout spots before applying`,
count: requiredSlots,
}),
);
@@ -1083,9 +1052,7 @@ export function SSHToolsSidebar({
}
toast.success(
t("splitScreen.success", {
defaultValue: "Split screen applied",
}),
t("splitScreen.success"),
);
};
@@ -1099,9 +1066,7 @@ export function SSHToolsSidebar({
setPreviewKey((prev) => prev + 1);
toast.success(
t("splitScreen.cleared", {
defaultValue: "Split screen cleared",
}),
t("splitScreen.cleared"),
);
};
@@ -1121,15 +1086,11 @@ export function SSHToolsSidebar({
await deleteCommandFromHistory(activeTerminalHostId, command);
setCommandHistory((prev) => prev.filter((c) => c !== command));
toast.success(
t("commandHistory.deleteSuccess", {
defaultValue: "Command deleted from history",
}),
t("commandHistory.deleteSuccess"),
);
} catch {
toast.error(
t("commandHistory.deleteFailed", {
defaultValue: "Failed to delete command.",
}),
t("commandHistory.deleteFailed"),
);
}
}
@@ -1159,7 +1120,7 @@ export function SSHToolsSidebar({
variant="outline"
onClick={() => setSidebarWidth(400)}
className="w-[28px] h-[28px]"
title="Reset sidebar width"
title={t("common.resetSidebarWidth")}
>
<RotateCcw className="h-4 w-4" />
</Button>
@@ -1189,10 +1150,10 @@ export function SSHToolsSidebar({
{t("snippets.title")}
</TabsTrigger>
<TabsTrigger value="command-history">
{t("commandHistory.title", { defaultValue: "History" })}
{t("commandHistory.title")}
</TabsTrigger>
<TabsTrigger value="split-screen">
{t("splitScreen.title", { defaultValue: "Split Screen" })}
{t("splitScreen.title")}
</TabsTrigger>
</TabsList>
@@ -1301,20 +1262,14 @@ export function SSHToolsSidebar({
<>
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("snippets.selectTerminals", {
defaultValue: "Select Terminals (optional)",
})}
{t("snippets.selectTerminals")}
</label>
<p className="text-xs text-muted-foreground">
{selectedSnippetTabIds.length > 0
? t("snippets.executeOnSelected", {
defaultValue: `Execute on ${selectedSnippetTabIds.length} selected terminal(s)`,
count: selectedSnippetTabIds.length,
})
: t("snippets.executeOnCurrent", {
defaultValue:
"Execute on current terminal (click to select multiple)",
})}
: t("snippets.executeOnCurrent")}
</p>
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
{terminalTabs.map((tab) => (
@@ -1354,9 +1309,7 @@ export function SSHToolsSidebar({
variant="outline"
>
<FolderPlus className="w-4 h-4 mr-2" />
{t("snippets.newFolder", {
defaultValue: "New Folder",
})}
{t("snippets.newFolder")}
</Button>
</div>
</div>
@@ -1606,9 +1559,7 @@ export function SSHToolsSidebar({
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t("commandHistory.searchPlaceholder", {
defaultValue: "Search commands...",
})}
placeholder={t("commandHistory.searchPlaceholder")}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
@@ -1627,10 +1578,7 @@ export function SSHToolsSidebar({
)}
</div>
<p className="text-xs text-muted-foreground bg-muted/30 px-2 py-1.5 rounded">
{t("commandHistory.tabHint", {
defaultValue:
"Use Tab in Terminal to autocomplete from command history",
})}
{t("commandHistory.tabHint")}
</p>
</div>
@@ -1639,9 +1587,7 @@ export function SSHToolsSidebar({
<div className="text-center py-8">
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 mb-4">
<p className="text-destructive font-medium mb-2">
{t("commandHistory.error", {
defaultValue: "Error loading history",
})}
{t("commandHistory.error")}
</p>
<p className="text-sm text-muted-foreground">
{historyError}
@@ -1653,32 +1599,23 @@ export function SSHToolsSidebar({
}
variant="outline"
>
{t("common.retry", { defaultValue: "Retry" })}
{t("common.retry")}
</Button>
</div>
) : !activeTerminal ? (
<div className="text-center text-muted-foreground py-8">
<Terminal className="h-12 w-12 mb-4 opacity-20 mx-auto" />
<p className="mb-2 font-medium">
{t("commandHistory.noTerminal", {
defaultValue: "No active terminal",
})}
</p>
{t("commandHistory.noTerminal")} </p>
<p className="text-sm">
{t("commandHistory.noTerminalHint", {
defaultValue:
"Open a terminal to see its command history.",
})}
{t("commandHistory.noTerminalHint")}
</p>
</div>
) : isHistoryLoading && commandHistory.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<Loader2 className="h-12 w-12 mb-4 opacity-20 mx-auto animate-spin" />
<p className="mb-2 font-medium">
{t("commandHistory.loading", {
defaultValue: "Loading command history...",
})}
</p>
{t("commandHistory.loading")} </p>
</div>
) : filteredCommands.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
@@ -1686,13 +1623,10 @@ export function SSHToolsSidebar({
<>
<Search className="h-12 w-12 mb-2 opacity-20 mx-auto" />
<p className="mb-2 font-medium">
{t("commandHistory.noResults", {
defaultValue: "No commands found",
})}
{t("commandHistory.noResults")}
</p>
<p className="text-sm">
{t("commandHistory.noResultsHint", {
defaultValue: `No commands matching "${searchQuery}"`,
query: searchQuery,
})}
</p>
@@ -1700,15 +1634,10 @@ export function SSHToolsSidebar({
) : (
<>
<p className="mb-2 font-medium">
{t("commandHistory.empty", {
defaultValue: "No command history yet",
})}
{t("commandHistory.empty")}
</p>
<p className="text-sm">
{t("commandHistory.emptyHint", {
defaultValue:
"Execute commands in the active terminal to build its history.",
})}
{t("commandHistory.emptyHint")}
</p>
</>
)}
@@ -1739,9 +1668,7 @@ export function SSHToolsSidebar({
e.stopPropagation();
handleCommandDelete(command);
}}
title={t("commandHistory.deleteTooltip", {
defaultValue: "Delete command",
})}
title={t("commandHistory.deleteTooltip")}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
@@ -1769,23 +1696,14 @@ export function SSHToolsSidebar({
>
<TabsList className="w-full grid grid-cols-4">
<TabsTrigger value="none">
{t("splitScreen.none", { defaultValue: "None" })}
{t("splitScreen.none")}
</TabsTrigger>
<TabsTrigger value="2">
{t("splitScreen.twoSplit", {
defaultValue: "2-Split",
})}
</TabsTrigger>
{t("splitScreen.twoSplit")} </TabsTrigger>
<TabsTrigger value="3">
{t("splitScreen.threeSplit", {
defaultValue: "3-Split",
})}
</TabsTrigger>
{t("splitScreen.threeSplit")} </TabsTrigger>
<TabsTrigger value="4">
{t("splitScreen.fourSplit", {
defaultValue: "4-Split",
})}
</TabsTrigger>
{t("splitScreen.fourSplit")} </TabsTrigger>
</TabsList>
</Tabs>
@@ -1795,15 +1713,10 @@ export function SSHToolsSidebar({
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("splitScreen.availableTabs", {
defaultValue: "Available Tabs",
})}
{t("splitScreen.availableTabs")}
</label>
<p className="text-xs text-muted-foreground mb-2">
{t("splitScreen.dragTabsHint", {
defaultValue:
"Drag tabs into the grid below to position them",
})}
{t("splitScreen.dragTabsHint")}
</p>
<div className="space-y-1 max-h-[200px] overflow-y-auto">
{splittableTabs.map((tab) => {
@@ -1843,9 +1756,7 @@ export function SSHToolsSidebar({
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("splitScreen.layout", {
defaultValue: "Layout",
})}
{t("splitScreen.layout")}
</label>
<div
className={`grid gap-2 ${
@@ -1904,14 +1815,12 @@ export function SSHToolsSidebar({
}
className="h-6 text-xs hover:bg-red-500/20"
>
Remove
{t("common.remove")}
</Button>
</>
) : (
<span className="text-xs text-muted-foreground">
{t("splitScreen.dropHere", {
defaultValue: "Drop tab here",
})}
{t("splitScreen.dropHere")}
</span>
)}
</div>
@@ -1927,18 +1836,14 @@ export function SSHToolsSidebar({
className="flex-1"
disabled={splitAssignments.size === 0}
>
{t("splitScreen.apply", {
defaultValue: "Apply Split",
})}
{t("splitScreen.apply")}
</Button>
<Button
variant="outline"
onClick={handleClearSplit}
className="flex-1"
>
{t("splitScreen.clear", {
defaultValue: "Clear",
})}
{t("splitScreen.clear")}
</Button>
</div>
</>
@@ -1948,16 +1853,10 @@ export function SSHToolsSidebar({
<div className="text-center py-8">
<LayoutGrid className="h-12 w-12 mb-4 opacity-20 mx-auto" />
<p className="text-sm text-muted-foreground mb-2">
{t("splitScreen.selectMode", {
defaultValue:
"Select a split mode to get started",
})}
{t("splitScreen.selectMode")}
</p>
<p className="text-xs text-muted-foreground">
{t("splitScreen.helpText", {
defaultValue:
"Choose how many tabs you want to display at once",
})}
{t("splitScreen.helpText")}
</p>
</div>
)}
@@ -1987,7 +1886,7 @@ export function SSHToolsSidebar({
e.currentTarget.style.backgroundColor = "transparent";
}
}}
title="Drag to resize sidebar"
title={t("common.dragToResizeSidebar")}
/>
)}
</Sidebar>
@@ -2056,7 +1955,7 @@ export function SSHToolsSidebar({
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center gap-2">
<Folder className="h-4 w-4" />
{t("snippets.folder", { defaultValue: "Folder" })}
{t("snippets.folder")}
<span className="text-muted-foreground">
({t("common.optional")})
</span>
@@ -2072,16 +1971,12 @@ export function SSHToolsSidebar({
>
<SelectTrigger>
<SelectValue
placeholder={t("snippets.selectFolder", {
defaultValue: "Select a folder or leave empty",
})}
placeholder={t("snippets.selectFolder")}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="__no_folder__">
{t("snippets.noFolder", {
defaultValue: "No folder (Uncategorized)",
})}
{t("snippets.noFolder")}
</SelectItem>
{snippetFolders.map((folder) => {
const FolderIcon = getFolderIcon(folder.name);
@@ -2155,26 +2050,20 @@ export function SSHToolsSidebar({
<div className="mb-6">
<h2 className="text-xl font-semibold text-white">
{editingFolder
? t("snippets.editFolder", { defaultValue: "Edit Folder" })
: t("snippets.createFolder", {
defaultValue: "Create Folder",
})}
? t("snippets.editFolder")
: t("snippets.createFolder")
</h2>
<p className="text-sm text-muted-foreground mt-1">
{editingFolder
? t("snippets.editFolderDescription", {
defaultValue: "Customize your snippet folder",
})
: t("snippets.createFolderDescription", {
defaultValue: "Organize your snippets into folders",
})}
? t("snippets.editFolderDescription")
: t("snippets.createFolderDescription")}
</p>
</div>
<div className="space-y-5">
<div className="space-y-2">
<label className="text-sm font-medium text-white flex items-center gap-1">
{t("snippets.folderName", { defaultValue: "Folder Name" })}
{t("snippets.folderName")}
<span className="text-destructive">*</span>
</label>
<Input
@@ -2185,24 +2074,20 @@ export function SSHToolsSidebar({
name: e.target.value,
})
}
placeholder={t("snippets.folderNamePlaceholder", {
defaultValue: "e.g., System Commands, Docker Scripts",
})}
placeholder={t("sshTools.scripts.inputPlaceholder")}
className={`${folderFormErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`}
autoFocus
/>
{folderFormErrors.name && (
<p className="text-xs text-destructive mt-1">
{t("snippets.folderNameRequired", {
defaultValue: "Folder name is required",
})}
{t("snippets.folderNameRequired")}
</p>
)}
</div>
<div className="space-y-3">
<Label className="text-base font-semibold text-white">
{t("snippets.folderColor", { defaultValue: "Folder Color" })}
{t("snippets.folderColor")}
</Label>
<div className="grid grid-cols-4 gap-3">
{AVAILABLE_COLORS.map((color) => (
@@ -2229,7 +2114,7 @@ export function SSHToolsSidebar({
<div className="space-y-3">
<Label className="text-base font-semibold text-white">
{t("snippets.folderIcon", { defaultValue: "Folder Icon" })}
{t("snippets.folderIcon")}
</Label>
<div className="grid grid-cols-5 gap-3">
{AVAILABLE_ICONS.map(({ value, label, Icon }) => (
@@ -2254,7 +2139,7 @@ export function SSHToolsSidebar({
<div className="space-y-3">
<Label className="text-base font-semibold text-white">
{t("snippets.preview", { defaultValue: "Preview" })}
{t("snippets.preview")}
</Label>
<div className="flex items-center gap-3 p-4 rounded-md bg-dark-bg-darker border border-dark-border">
{(() => {
@@ -2271,7 +2156,7 @@ export function SSHToolsSidebar({
})()}
<span className="font-medium">
{folderFormData.name ||
t("snippets.folderName", { defaultValue: "Folder Name" })}
t("snippets.folderName")}
</span>
</div>
</div>
@@ -2289,12 +2174,8 @@ export function SSHToolsSidebar({
</Button>
<Button onClick={handleFolderSubmit} className="flex-1">
{editingFolder
? t("snippets.updateFolder", {
defaultValue: "Update Folder",
})
: t("snippets.createFolder", {
defaultValue: "Create Folder",
})}
? t("snippets.updateFolder")
: t("snippets.createFolder")
</Button>
</div>
</div>

View File

@@ -104,7 +104,7 @@ export function TunnelObject({
default:
return {
icon: <WifiOff className="h-4 w-4" />,
text: statusValue,
text: t("tunnels.unknown"),
color: "text-muted-foreground",
bgColor: "bg-muted/30",
borderColor: "border-border",
@@ -243,7 +243,7 @@ export function TunnelObject({
>
{t("tunnels.discord")}
</a>{" "}
or create a{" "}
{t("tunnels.orCreate")}{" "}
<a
href="https://github.com/Termix-SSH/Termix/issues/new"
target="_blank"
@@ -479,7 +479,7 @@ export function TunnelObject({
>
{t("tunnels.discord")}
</a>{" "}
or create a{" "}
{t("tunnels.orCreate")}{" "}
<a
href="https://github.com/Termix-SSH/Termix/issues/new"
target="_blank"