mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-11 01:53:58 +00:00
translate strings in auth pages for ssh, vnc, and rdp
This commit is contained in:
@@ -3456,5 +3456,41 @@
|
||||
"sshErrorWebSocket": "WebSocket connection failed",
|
||||
"sshErrorAuthFailed": "Authentication failed",
|
||||
"sshErrorConnectionClosed": "Connection closed before authentication completed",
|
||||
"sitePangolinSshDescription": "Allow SSH access to resources on this site. This can be changed later."
|
||||
"sitePangolinSshDescription": "Allow SSH access to resources on this site. This can be changed later.",
|
||||
"browserGatewayNoResourceForDomain": "No resource found for this domain",
|
||||
"browserGatewayNoTarget": "No target",
|
||||
"browserGatewayConnect": "Connect",
|
||||
"browserGatewayCtrlAltDel": "Ctrl+Alt+Del",
|
||||
"sshErrorSignKeyFailed": "Failed to sign SSH key for PAM push authentication. Did you sign in as a user?",
|
||||
"sshTerminalError": "Error: {error}",
|
||||
"sshConnectionClosedCode": "Connection closed (code {code})",
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Enter your VNC password to connect",
|
||||
"vncPasswordOptional": "Password (optional)",
|
||||
"vncNoResourceTarget": "No resource target is available",
|
||||
"vncFailedToLoadNovnc": "Failed to load noVNC",
|
||||
"vncAuthFailedStatus": "Status {status}",
|
||||
"vncPasteClipboard": "Paste clipboard",
|
||||
"rdpTitle": "RDP",
|
||||
"rdpSignInTitle": "Sign in to Remote Desktop",
|
||||
"rdpSignInDescription": "Enter Windows credentials to connect",
|
||||
"rdpLoadingModule": "Loading module...",
|
||||
"rdpFailedToLoadModule": "Failed to load RDP module",
|
||||
"rdpNotReady": "Not ready",
|
||||
"rdpModuleInitializing": "RDP module is still initializing",
|
||||
"rdpDownloadingFiles": "Downloading {count} file(s) from remote…",
|
||||
"rdpDownloadFailed": "Download failed: {fileName}",
|
||||
"rdpUploaded": "Uploaded: {fileName}",
|
||||
"rdpNoConnectionTarget": "No connection target available",
|
||||
"rdpConnectionFailed": "Connection failed",
|
||||
"rdpFit": "Fit",
|
||||
"rdpFull": "Full",
|
||||
"rdpReal": "Real",
|
||||
"rdpMeta": "Meta",
|
||||
"rdpUploadFiles": "Upload files",
|
||||
"rdpFilesReadyToPaste": "Files ready to paste",
|
||||
"rdpFilesReadyToPasteDescription": "{count} file(s) copied to remote clipboard — press Ctrl+V on the remote desktop to paste.",
|
||||
"rdpUploadFailed": "Upload failed",
|
||||
"rdpUnicodeKeyboardMode": "Unicode keyboard mode"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@app/components/ui/card";
|
||||
import BrandedAuthSurface from "@app/components/BrandedAuthSurface";
|
||||
import PoweredByPangolin from "@app/components/PoweredByPangolin";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
declare module "react" {
|
||||
namespace JSX {
|
||||
@@ -68,6 +69,7 @@ export default function RdpClient({
|
||||
error: string | null;
|
||||
primaryColor?: string | null;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const STORAGE_KEY = "pangolin_rdp_credentials";
|
||||
|
||||
const [form, setForm] = useState<FormState>(() => {
|
||||
@@ -141,7 +143,7 @@ export default function RdpClient({
|
||||
console.error("Failed to load iron-remote-desktop modules", err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to load RDP module",
|
||||
title: t("rdpFailedToLoadModule"),
|
||||
description: `${err}`
|
||||
});
|
||||
});
|
||||
@@ -175,8 +177,8 @@ export default function RdpClient({
|
||||
setConnecting(false);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Not ready",
|
||||
description: "RDP module is still initializing"
|
||||
title: t("rdpNotReady"),
|
||||
description: t("rdpModuleInitializing")
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -196,7 +198,9 @@ export default function RdpClient({
|
||||
const downloadable = files.filter((f) => !f.isDirectory);
|
||||
if (downloadable.length === 0) return;
|
||||
toast({
|
||||
title: `Downloading ${downloadable.length} file(s) from remote…`
|
||||
title: t("rdpDownloadingFiles", {
|
||||
count: downloadable.length
|
||||
})
|
||||
});
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
@@ -214,7 +218,9 @@ export default function RdpClient({
|
||||
.catch((err) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: `Download failed: ${file.name}`,
|
||||
title: t("rdpDownloadFailed", {
|
||||
fileName: file.name
|
||||
}),
|
||||
description: `${err}`
|
||||
});
|
||||
});
|
||||
@@ -223,7 +229,7 @@ export default function RdpClient({
|
||||
|
||||
// Notify when individual uploads complete (remote pasted a file).
|
||||
fileTransfer.on("upload-complete", (file: File) => {
|
||||
toast({ title: `Uploaded: ${file.name}` });
|
||||
toast({ title: t("rdpUploaded", { fileName: file.name }) });
|
||||
});
|
||||
|
||||
// Register with the web component so CLIPRDR extensions are
|
||||
@@ -237,8 +243,8 @@ export default function RdpClient({
|
||||
setConnecting(false);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "No target",
|
||||
description: "No connection target available"
|
||||
title: t("browserGatewayNoTarget"),
|
||||
description: t("rdpNoConnectionTarget")
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -290,13 +296,13 @@ export default function RdpClient({
|
||||
if (isIronError(err)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Connection failed",
|
||||
title: t("rdpConnectionFailed"),
|
||||
description: err.backtrace()
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Connection failed",
|
||||
title: t("rdpConnectionFailed"),
|
||||
description: `${err}`
|
||||
});
|
||||
}
|
||||
@@ -322,7 +328,7 @@ export default function RdpClient({
|
||||
<PoweredByPangolin />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>RDP</CardTitle>
|
||||
<CardTitle>{t("rdpTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
@@ -339,14 +345,14 @@ export default function RdpClient({
|
||||
<PoweredByPangolin />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign in to Remote Desktop</CardTitle>
|
||||
<CardTitle>{t("rdpSignInTitle")}</CardTitle>
|
||||
<CardDescription>
|
||||
Enter Windows credentials to access xxxx
|
||||
{t("rdpSignInDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Field label="Domain" id="domain">
|
||||
<Field label={t("domain")} id="domain">
|
||||
<Input
|
||||
id="domain"
|
||||
value={form.domain}
|
||||
@@ -355,7 +361,7 @@ export default function RdpClient({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Username" id="username">
|
||||
<Field label={t("username")} id="username">
|
||||
<Input
|
||||
id="username"
|
||||
value={form.username}
|
||||
@@ -364,7 +370,7 @@ export default function RdpClient({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password" id="password">
|
||||
<Field label={t("password")} id="password">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
@@ -414,8 +420,8 @@ export default function RdpClient({
|
||||
className="w-full"
|
||||
>
|
||||
{moduleReady
|
||||
? "Connect"
|
||||
: "Loading module..."}
|
||||
? t("browserGatewayConnect")
|
||||
: t("rdpLoadingModule")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -433,35 +439,35 @@ export default function RdpClient({
|
||||
variant="secondary"
|
||||
onClick={() => ui()?.setScale(1)}
|
||||
>
|
||||
Fit
|
||||
{t("rdpFit")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => ui()?.setScale(2)}
|
||||
>
|
||||
Full
|
||||
{t("rdpFull")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => ui()?.setScale(3)}
|
||||
>
|
||||
Real
|
||||
{t("rdpReal")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => ui()?.ctrlAltDel()}
|
||||
>
|
||||
Ctrl+Alt+Del
|
||||
{t("browserGatewayCtrlAltDel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => ui()?.metaKey()}
|
||||
>
|
||||
Meta
|
||||
{t("rdpMeta")}
|
||||
</Button>
|
||||
{/* <Button
|
||||
size="sm"
|
||||
@@ -483,19 +489,22 @@ export default function RdpClient({
|
||||
try {
|
||||
ft.uploadFiles(files);
|
||||
toast({
|
||||
title: "Files ready to paste",
|
||||
description: `${files.length} file(s) copied to remote clipboard — press Ctrl+V on the remote desktop to paste.`
|
||||
title: t("rdpFilesReadyToPaste"),
|
||||
description: t(
|
||||
"rdpFilesReadyToPasteDescription",
|
||||
{ count: files.length }
|
||||
)
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Upload failed",
|
||||
title: t("rdpUploadFailed"),
|
||||
description: `${err}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload files
|
||||
{t("rdpUploadFiles")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -505,7 +514,7 @@ export default function RdpClient({
|
||||
setShowLogin(true);
|
||||
}}
|
||||
>
|
||||
Terminate
|
||||
{t("sshTerminate")}
|
||||
</Button>
|
||||
<label className="ml-2 flex items-center gap-2">
|
||||
<input
|
||||
@@ -516,7 +525,7 @@ export default function RdpClient({
|
||||
ui()?.setKeyboardUnicodeMode(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
Unicode keyboard mode
|
||||
{t("rdpUnicodeKeyboardMode")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getBrowserTargetForRequest } from "@app/lib/getBrowserTargetForRequest"
|
||||
import { loadOrgLoginPageBranding } from "@app/lib/loadOrgLoginPageBranding";
|
||||
import RdpClient from "./RdpClient";
|
||||
import AuthFooter from "@app/components/AuthFooter";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -11,8 +12,9 @@ export async function generateMetadata() {
|
||||
}
|
||||
|
||||
export default async function RdpPage() {
|
||||
const t = await getTranslations();
|
||||
const { target } = await getBrowserTargetForRequest();
|
||||
const error = target ? null : "No resource found for this domain";
|
||||
const error = target ? null : t("browserGatewayNoResourceForDomain");
|
||||
const { primaryColor } = target
|
||||
? await loadOrgLoginPageBranding(target.orgId)
|
||||
: { primaryColor: null };
|
||||
|
||||
@@ -274,7 +274,7 @@ export default function SshClient({
|
||||
);
|
||||
} else {
|
||||
xtermRef.current?.writeln(
|
||||
`\r\n\x1b[31mError: ${msg.error}\x1b[0m\r\n`
|
||||
`\r\n\x1b[31m${t("sshTerminalError", { error: msg.error ?? "" })}\x1b[0m\r\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -309,7 +309,7 @@ export default function SshClient({
|
||||
if (authConfirmed) {
|
||||
setConnected(false);
|
||||
xtermRef.current?.writeln(
|
||||
`\r\n\x1b[33mConnection closed (code ${evt.code})\x1b[0m\r\n`
|
||||
`\r\n\x1b[33m${t("sshConnectionClosedCode", { code: evt.code })}\x1b[0m\r\n`
|
||||
);
|
||||
}
|
||||
// If auth was never confirmed the login form is already visible;
|
||||
@@ -510,7 +510,9 @@ export default function SshClient({
|
||||
privateKey: e.target.value
|
||||
})
|
||||
}
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
placeholder={t(
|
||||
"sshPrivateKeyPlaceholder"
|
||||
)}
|
||||
rows={5}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import SshClient from "./SshClient";
|
||||
import crypto from "crypto";
|
||||
import AuthFooter from "@app/components/AuthFooter";
|
||||
import type { SignSshKeyResponse } from "@server/routers/ssh/types";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
const pollInitialDelayMs = 250;
|
||||
const pollStartIntervalMs = 250;
|
||||
@@ -107,6 +108,7 @@ export async function generateMetadata() {
|
||||
}
|
||||
|
||||
export default async function SshPage() {
|
||||
const t = await getTranslations();
|
||||
const headersList = await headers();
|
||||
const cookieHeader = headersList.get("cookie") || "";
|
||||
|
||||
@@ -119,7 +121,7 @@ export default async function SshPage() {
|
||||
target = browserTarget;
|
||||
|
||||
if (!target) {
|
||||
error = "No resource found for this domain";
|
||||
error = t("browserGatewayNoResourceForDomain");
|
||||
} else if (target.pamMode === "push") {
|
||||
try {
|
||||
const { privateKeyPem, publicKeyOpenSSH } =
|
||||
@@ -150,8 +152,7 @@ export default async function SshPage() {
|
||||
await waitForRoundTripCompletion(messageIds, cookieHeader);
|
||||
} catch (err) {
|
||||
console.error("Error signing SSH key:", err);
|
||||
error =
|
||||
"Failed to sign SSH key for PAM push authentication. Did you sign in as a user?";
|
||||
error = t("sshErrorSignKeyFailed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "@app/components/ui/card";
|
||||
import BrandedAuthSurface from "@app/components/BrandedAuthSurface";
|
||||
import PoweredByPangolin from "@app/components/PoweredByPangolin";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type FormState = {
|
||||
password: string;
|
||||
@@ -29,6 +30,7 @@ export default function VncClient({
|
||||
error: string | null;
|
||||
primaryColor?: string | null;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const STORAGE_KEY = "pangolin_vnc_credentials";
|
||||
|
||||
const [form, setForm] = useState<FormState>(() => {
|
||||
@@ -67,8 +69,8 @@ export default function VncClient({
|
||||
if (!target) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "No target",
|
||||
description: "No resource target is available"
|
||||
title: t("browserGatewayNoTarget"),
|
||||
description: t("vncNoResourceTarget")
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -92,7 +94,7 @@ export default function VncClient({
|
||||
} catch (err) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to load noVNC",
|
||||
title: t("vncFailedToLoadNovnc"),
|
||||
description: `${err}`
|
||||
});
|
||||
return;
|
||||
@@ -144,8 +146,12 @@ export default function VncClient({
|
||||
(e: { detail: { status: number; reason?: string } }) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Authentication failed",
|
||||
description: e.detail.reason ?? `Status ${e.detail.status}`
|
||||
title: t("sshErrorAuthFailed"),
|
||||
description:
|
||||
e.detail.reason ??
|
||||
t("vncAuthFailedStatus", {
|
||||
status: e.detail.status
|
||||
})
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -159,7 +165,7 @@ export default function VncClient({
|
||||
<PoweredByPangolin />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>VNC</CardTitle>
|
||||
<CardTitle>{t("vncTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
@@ -176,15 +182,15 @@ export default function VncClient({
|
||||
<PoweredByPangolin />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>VNC</CardTitle>
|
||||
<CardTitle>{t("vncTitle")}</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your credentials to access xxxx
|
||||
{t("vncSignInDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
label="Password (optional)"
|
||||
label={t("vncPasswordOptional")}
|
||||
id="password"
|
||||
>
|
||||
<Input
|
||||
@@ -198,7 +204,7 @@ export default function VncClient({
|
||||
</Field>
|
||||
|
||||
<Button onClick={connect} className="w-full">
|
||||
Connect
|
||||
{t("browserGatewayConnect")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -220,7 +226,7 @@ export default function VncClient({
|
||||
}
|
||||
}}
|
||||
>
|
||||
Ctrl+Alt+Del
|
||||
{t("browserGatewayCtrlAltDel")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -234,14 +240,14 @@ export default function VncClient({
|
||||
.catch(() => {});
|
||||
}}
|
||||
>
|
||||
Paste clipboard
|
||||
{t("vncPasteClipboard")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={disconnect}
|
||||
>
|
||||
Terminate
|
||||
{t("sshTerminate")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getBrowserTargetForRequest } from "@app/lib/getBrowserTargetForRequest"
|
||||
import { loadOrgLoginPageBranding } from "@app/lib/loadOrgLoginPageBranding";
|
||||
import VncClient from "./VncClient";
|
||||
import AuthFooter from "@app/components/AuthFooter";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -11,8 +12,9 @@ export async function generateMetadata() {
|
||||
}
|
||||
|
||||
export default async function VncPage() {
|
||||
const t = await getTranslations();
|
||||
const { target } = await getBrowserTargetForRequest();
|
||||
const error = target ? null : "No resource found for this domain";
|
||||
const error = target ? null : t("browserGatewayNoResourceForDomain");
|
||||
const { primaryColor } = target
|
||||
? await loadOrgLoginPageBranding(target.orgId)
|
||||
: { primaryColor: null };
|
||||
|
||||
Reference in New Issue
Block a user