translate strings in auth pages for ssh, vnc, and rdp

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