From 6b04bcb383a370398a58d457d9ee813b701f9bfe Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 4 Jun 2026 17:35:43 -0700 Subject: [PATCH] translate strings in auth pages for ssh, vnc, and rdp --- messages/en-US.json | 38 +++++++++++++++++++++- src/app/rdp/RdpClient.tsx | 67 ++++++++++++++++++++++----------------- src/app/rdp/page.tsx | 4 ++- src/app/ssh/SshClient.tsx | 8 +++-- src/app/ssh/page.tsx | 7 ++-- src/app/vnc/VncClient.tsx | 32 +++++++++++-------- src/app/vnc/page.tsx | 4 ++- 7 files changed, 109 insertions(+), 51 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 0727a9a2d..4d4a41e43 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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" } diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 721fd037b..def63fff0 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -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(() => { @@ -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({ - RDP + {t("rdpTitle")}

{error}

@@ -339,14 +345,14 @@ export default function RdpClient({ - Sign in to Remote Desktop + {t("rdpSignInTitle")} - Enter Windows credentials to access xxxx + {t("rdpSignInDescription")}
- + - + - + {moduleReady - ? "Connect" - : "Loading module..."} + ? t("browserGatewayConnect") + : t("rdpLoadingModule")}
@@ -433,35 +439,35 @@ export default function RdpClient({ variant="secondary" onClick={() => ui()?.setScale(1)} > - Fit + {t("rdpFit")} {/* diff --git a/src/app/rdp/page.tsx b/src/app/rdp/page.tsx index 408a95dea..b7190b428 100644 --- a/src/app/rdp/page.tsx +++ b/src/app/rdp/page.tsx @@ -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 }; diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index 945963ec0..e4ca9c806 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -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" /> diff --git a/src/app/ssh/page.tsx b/src/app/ssh/page.tsx index 5e2e057b0..8ab62d110 100644 --- a/src/app/ssh/page.tsx +++ b/src/app/ssh/page.tsx @@ -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"); } } diff --git a/src/app/vnc/VncClient.tsx b/src/app/vnc/VncClient.tsx index 7a93537fd..cec474df3 100644 --- a/src/app/vnc/VncClient.tsx +++ b/src/app/vnc/VncClient.tsx @@ -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(() => { @@ -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({ - VNC + {t("vncTitle")}

{error}

@@ -176,15 +182,15 @@ export default function VncClient({ - VNC + {t("vncTitle")} - Enter your credentials to access xxxx + {t("vncSignInDescription")}
@@ -220,7 +226,7 @@ export default function VncClient({ } }} > - Ctrl+Alt+Del + {t("browserGatewayCtrlAltDel")} diff --git a/src/app/vnc/page.tsx b/src/app/vnc/page.tsx index b7ec90c59..01c8c15cf 100644 --- a/src/app/vnc/page.tsx +++ b/src/app/vnc/page.tsx @@ -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 };