fix: load plugin strings on guest pages, plugin wording and drop unused keys

This commit is contained in:
LukeGus committed 2026-10-09 07:37:51 -05:00
1 parent f803bfc140
commit 406766eddd
19 files changed
+81 -292

No files matched your search

-29
View File
@@ -20,35 +20,6 @@
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<title>Termix</title> <title>Termix</title>
<style> <style>
.hide-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.skinny-scrollbar {
scrollbar-width: thin;
scrollbar-color: #4a4a4a #1e1e21;
}
.skinny-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.skinny-scrollbar::-webkit-scrollbar-track {
background: #1e1e21;
}
.skinny-scrollbar::-webkit-scrollbar-thumb {
background-color: #4a4a4a;
border-radius: 3px;
border: 1px solid #1e1e21;
}
.toolbar-scrollbar { .toolbar-scrollbar {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #4a4a4a transparent; scrollbar-color: #4a4a4a transparent;
+1 -1
View File
@@ -2,7 +2,7 @@
* Credential sidebar preferences model. Shared by the frontend sidebar and * Credential sidebar preferences model. Shared by the frontend sidebar and
* the backend preferences endpoint (no framework imports, mirrors * the backend preferences endpoint (no framework imports, mirrors
* ./host-sidebar-preferences.ts's dependency-free convention). Independent * ./host-sidebar-preferences.ts's dependency-free convention). Independent
* from HostSidebarPreferences by design — a separate parallel system, not a * from HostSidebarPreferences by design, a separate parallel system, not a
* shared blob, matching how credentialSortKey/credentialFilterState were * shared blob, matching how credentialSortKey/credentialFilterState were
* already independently namespaced from hostSortKey/etc. before this port. * already independently namespaced from hostSortKey/etc. before this port.
* *
+12 -12
View File
@@ -289,7 +289,7 @@ export function AppShell({
}); });
const [userPrefsLoaded, setUserPrefsLoaded] = useState(false); const [userPrefsLoaded, setUserPrefsLoaded] = useState(false);
const [hostsLoaded, setHostsLoaded] = useState(false); const [hostsLoaded, setHostsLoaded] = useState(false);
// Flips to true once the initial DB read (restore or skip) is done — sync must not fire before this // Flips to true once the initial DB read (restore or skip) is done. Sync must not fire before this
const [tabsReady, setTabsReady] = useState(false); const [tabsReady, setTabsReady] = useState(false);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
// Split tabs are restored once their child tabs have stable live ids. // Split tabs are restored once their child tabs have stable live ids.
@@ -383,7 +383,7 @@ export function AppShell({
((options: ConfirmOptions) => Promise<boolean>) | null ((options: ConfirmOptions) => Promise<boolean>) | null
>(null); >(null);
// Right dock — a second panel column so reference panels like history can // Right dock: a second panel column so reference panels like history can
// stay visible while the left sidebar is used for something else. // stay visible while the left sidebar is used for something else.
const [rightRailView, setRightRailView] = useState<RailView | null>(() => { const [rightRailView, setRightRailView] = useState<RailView | null>(() => {
const saved = localStorage.getItem("termix_rightRailView"); const saved = localStorage.getItem("termix_rightRailView");
@@ -698,7 +698,7 @@ export function AppShell({
const paneElsRef = useRef<Map<string, HTMLDivElement>>(new Map()); const paneElsRef = useRef<Map<string, HTMLDivElement>>(new Map());
const [, setPaneElsVersion] = useState(0); const [, setPaneElsVersion] = useState(0);
// Stable per-tab DOM nodes — created once per tab, never destroyed while the tab lives. // Stable per-tab DOM nodes, created once per tab, never destroyed while the tab lives.
// We always portal each tab's content into its own node, then move that node between // We always portal each tab's content into its own node, then move that node between
// the normal-view container and the pane container via vanilla DOM so React's portal // the normal-view container and the pane container via vanilla DOM so React's portal
// target never changes (changing the target causes a remount). // target never changes (changing the target causes a remount).
@@ -708,7 +708,7 @@ export function AppShell({
const mainAreaRef = useRef<HTMLDivElement>(null); const mainAreaRef = useRef<HTMLDivElement>(null);
// Tab id the enter animation has already played for, so a re-render while // Tab id the enter animation has already played for, so a re-render while
// the tab stays active (there can be several right after a switch) doesn't // the tab stays active (there can be several right after a switch) doesn't
// replay it — only a genuine switch to a different tab should. // replay it, only a genuine switch to a different tab should.
const lastAnimatedTabIdRef = useRef<string | null>(null); const lastAnimatedTabIdRef = useRef<string | null>(null);
const getTabNode = useCallback((tabId: string, isTerminal: boolean) => { const getTabNode = useCallback((tabId: string, isTerminal: boolean) => {
@@ -726,7 +726,7 @@ export function AppShell({
// Portal render order for tab content, kept independent of the tab bar's // Portal render order for tab content, kept independent of the tab bar's
// visual order. Reordering tabs in the bar reorders `tabs`, and mapping // visual order. Reordering tabs in the bar reorders `tabs`, and mapping
// that array directly to portals reshuffles the Suspense-wrapped portal // that array directly to portals reshuffles the Suspense-wrapped portal
// children's sibling order in the fiber tree — React then runs its // children's sibling order in the fiber tree, React then runs its
// Offscreen disconnect/reconnect pass on the ones that moved, which tears // Offscreen disconnect/reconnect pass on the ones that moved, which tears
// down and rebuilds every passive effect underneath (including // down and rebuilds every passive effect underneath (including
// react-xtermjs's terminal-creation effect), dropping the live terminal // react-xtermjs's terminal-creation effect), dropping the live terminal
@@ -855,7 +855,7 @@ export function AppShell({
} }
} }
// Cmd+1..9 on macOS, Alt+1..9 elsewhere — jump directly to the tab at that position // Cmd+1..9 on macOS, Alt+1..9 elsewhere: jump directly to the tab at that position
const tabDigit = getTabJumpDigit(e); const tabDigit = getTabJumpDigit(e);
if (tabDigit !== null) { if (tabDigit !== null) {
const currentTabs = tabsRef.current.filter( const currentTabs = tabsRef.current.filter(
@@ -869,7 +869,7 @@ export function AppShell({
return; return;
} }
// Ctrl+Shift+] / Ctrl+Shift+[ — cycle through open tabs (] = next, [ = previous) // Ctrl+Shift+] / Ctrl+Shift+[: cycle through open tabs (] = next, [ = previous)
if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) { if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) {
if (e.code === "BracketRight" || e.code === "BracketLeft") { if (e.code === "BracketRight" || e.code === "BracketLeft") {
e.preventDefault(); e.preventDefault();
@@ -1368,7 +1368,7 @@ export function AppShell({
// Restored tabs are in the tab bar, not in background records // Restored tabs are in the tab bar, not in background records
} }
} else { } else {
// Not restoring to tab bar — keep as background records for ConnectionsPanel // Not restoring to tab bar, keep as background records for ConnectionsPanel
setBackgroundTabRecords(savedTabs as OpenTabRecord[]); setBackgroundTabRecords(savedTabs as OpenTabRecord[]);
} }
} catch { } catch {
@@ -2471,10 +2471,10 @@ export function AppShell({
}, [tabs, activeTab]); }, [tabs, activeTab]);
// Move each tab's stable DOM node to the right container (pane or normal-view). // Move each tab's stable DOM node to the right container (pane or normal-view).
// This is vanilla DOM so React's portal target never changes — changing the portal // This is vanilla DOM so React's portal target never changes. Changing the portal
// target causes a remount which is exactly what we're trying to avoid. // target causes a remount which is exactly what we're trying to avoid.
// useLayoutEffect (not useEffect) so visibility/display are corrected before // useLayoutEffect (not useEffect) so visibility/display are corrected before
// the browser paints — otherwise the previous tab's node can flash on screen // the browser paints, otherwise the previous tab's node can flash on screen
// for a frame while still visible. // for a frame while still visible.
useLayoutEffect(() => { useLayoutEffect(() => {
const normalView = normalViewRef.current; const normalView = normalViewRef.current;
@@ -3113,7 +3113,7 @@ export function AppShell({
</div> </div>
</SurfaceScope> </SurfaceScope>
{/* Bottom nav bar — mobile only */} {/* Bottom nav bar, mobile only */}
<MobileBar <MobileBar
railView={railView} railView={railView}
sidebarOpen={sidebarOpen} sidebarOpen={sidebarOpen}
@@ -3126,7 +3126,7 @@ export function AppShell({
/> />
</div> </div>
{/* Right dock — desktop only, holds a second reference panel */} {/* Right dock, desktop only, holds a second reference panel */}
{!isMobile && rightRailView && ( {!isMobile && rightRailView && (
<DockPanel <DockPanel
side="right" side="right"
+3 -2
View File
@@ -1,3 +1,4 @@
import i18n from "i18next";
import { import {
handleApiError, handleApiError,
rbacApi, rbacApi,
@@ -16,7 +17,7 @@ async function getSharingTarget(hostId: number, syncId?: string | null) {
if (!api || !syncId) return { api: rbacApi, hostId }; if (!api || !syncId) return { api: rbacApi, hostId };
const remoteHostId = await resolveRemoteHostId(syncId); const remoteHostId = await resolveRemoteHostId(syncId);
if (remoteHostId === null) { if (remoteHostId === null) {
throw new Error("The synced host does not exist on the remote server"); throw new Error(i18n.t("errors.syncedHostMissing"));
} }
return { api, hostId: remoteHostId }; return { api, hostId: remoteHostId };
} }
@@ -27,7 +28,7 @@ async function getSharedCopyTarget(hostId: number, syncId?: string | null) {
const api = await getConnectedRemoteApi(); const api = await getConnectedRemoteApi();
const remoteHostId = api ? await resolveRemoteHostId(syncId) : null; const remoteHostId = api ? await resolveRemoteHostId(syncId) : null;
if (!api || remoteHostId === null) { if (!api || remoteHostId === null) {
throw new Error("The linked server is not reachable"); throw new Error(i18n.t("errors.linkedServerUnreachable"));
} }
return { api, hostId: remoteHostId }; return { api, hostId: remoteHostId };
} }
+1 -1
View File
@@ -98,7 +98,7 @@ export function ElectronLoginForm({
return; return;
} }
// OIDC login requested from inside the iframe — open the system browser // OIDC login requested from inside the iframe, open the system browser
// so captcha stages (e.g. Cloudflare Turnstile) render correctly. // so captcha stages (e.g. Cloudflare Turnstile) render correctly.
if (type === "OIDC_SYSTEM_BROWSER_AUTH" && authUrl && callbackPort) { if (type === "OIDC_SYSTEM_BROWSER_AUTH" && authUrl && callbackPort) {
const sendResultToIframe = (result: { const sendResultToIframe = (result: {
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* Extract a stable, human-readable message from an unknown thrown value. * Extract a stable, human-readable message from an unknown thrown value.
* *
* A thrown value is not guaranteed to be an `Error` — libraries may throw * A thrown value is not guaranteed to be an `Error`, libraries may throw
* strings or plain objects, which carry no useful `.message`. When that * strings or plain objects, which carry no useful `.message`. When that
* happens the caller gets `fallback` instead, so error paths never surface a * happens the caller gets `fallback` instead, so error paths never surface a
* raw `[object Object]` in logs or UI. * raw `[object Object]` in logs or UI.
+21 -137
View File
@@ -139,12 +139,8 @@
"offline": "Offline", "offline": "Offline",
"unknown": "Unknown", "unknown": "Unknown",
"continue": "Continue", "continue": "Continue",
"maintenance": "Maintenance",
"degraded": "Degraded",
"error": "Error", "error": "Error",
"warning": "Warning",
"unsavedChanges": "Unsaved changes", "unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"more": "More", "more": "More",
"loading": "Loading...", "loading": "Loading...",
"host": "Host", "host": "Host",
@@ -154,15 +150,7 @@
"connect": "Connect", "connect": "Connect",
"copied": "Copied", "copied": "Copied",
"connecting": "Connecting...", "connecting": "Connecting...",
"updateAvailable": "Update Available",
"appName": "Termix",
"openInNewTab": "Open in New Tab", "openInNewTab": "Open in New Tab",
"noReleases": "No Releases",
"updatesAndReleases": "Updates & Releases",
"newVersionAvailable": "A new version ({{version}}) is available.",
"failedToFetchUpdateInfo": "Failed to fetch update information",
"preRelease": "Pre-release",
"noReleasesFound": "No releases found.",
"cancel": "Cancel", "cancel": "Cancel",
"username": "Username", "username": "Username",
"login": "Login", "login": "Login",
@@ -177,15 +165,10 @@
"rename": "Rename", "rename": "Rename",
"edit": "Edit", "edit": "Edit",
"confirm": "Confirm", "confirm": "Confirm",
"no": "No",
"or": "OR",
"next": "Next", "next": "Next",
"previous": "Previous", "previous": "Previous",
"refresh": "Refresh", "refresh": "Refresh",
"language": "Language", "language": "Language",
"checking": "Checking...",
"checkingDatabase": "Checking database connection...",
"checkingAuthentication": "Checking authentication...",
"backendReconnected": "Server connection restored", "backendReconnected": "Server connection restored",
"connectionDegraded": "Server connection lost, recovering…", "connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload", "reload": "Reload",
@@ -194,12 +177,6 @@
"copyFailed": "Failed to copy to clipboard", "copyFailed": "Failed to copy to clipboard",
"maximize": "Maximize", "maximize": "Maximize",
"restore": "Restore", "restore": "Restore",
"of": "of",
"saved": "Saved",
"deleted": "Deleted",
"deleteFailed": "Failed to delete",
"saveFailed": "Failed to save",
"required": "Required",
"remove": "Remove", "remove": "Remove",
"add": "Add", "add": "Add",
"update": "Update", "update": "Update",
@@ -221,8 +198,8 @@
"skip": "Skip setup", "skip": "Skip setup",
"next": "Next", "next": "Next",
"finish": "Done", "finish": "Done",
"pluginsTitle": "Choose your features", "pluginsTitle": "Choose your plugins",
"pluginsIntro": "Pick the features to keep. The recommended ones cover what most people need.", "pluginsIntro": "Pick the plugins to keep. The recommended ones cover what most people need.",
"pluginsUpgradeNote": "Everything you use now is kept as it is. Turn off or remove what you do not need.", "pluginsUpgradeNote": "Everything you use now is kept as it is. Turn off or remove what you do not need.",
"pluginsRemoveNote": "Plugins set to Remove are deleted from this server, with their data, when you continue. You can install any of them again at any time from the Plugins tab (this needs an internet connection). Pick Off to keep a plugin installed but turned off.", "pluginsRemoveNote": "Plugins set to Remove are deleted from this server, with their data, when you continue. You can install any of them again at any time from the Plugins tab (this needs an internet connection). Pick Off to keep a plugin installed but turned off.",
"pluginsUseRecommended": "Recommended", "pluginsUseRecommended": "Recommended",
@@ -259,19 +236,11 @@
"nav": { "nav": {
"home": "Home", "home": "Home",
"terminal": "Terminal", "terminal": "Terminal",
"fileManager": "File Manager",
"serverStats": "Host Metrics",
"admin": "Admin",
"userProfile": "User Profile",
"confirmClose": "Close this active session?",
"close": "Close", "close": "Close",
"cancel": "Cancel",
"sshManager": "SSH Manager",
"hostTabTitle": "{{username}}@{{ip}}:{{port}}", "hostTabTitle": "{{username}}@{{ip}}:{{port}}",
"copyPassword": "Copy Password", "copyPassword": "Copy Password",
"copySudoPassword": "Copy Sudo Password", "copySudoPassword": "Copy Sudo Password",
"passwordCopied": "Password copied to clipboard", "passwordCopied": "Password copied to clipboard",
"noPasswordAvailable": "No password available",
"failedToCopyPassword": "Failed to copy password", "failedToCopyPassword": "Failed to copy password",
"refreshTab": "Refresh connection", "refreshTab": "Refresh connection",
"renameTab": "Rename tab", "renameTab": "Rename tab",
@@ -282,13 +251,7 @@
"openInRightDock": "Open in right dock", "openInRightDock": "Open in right dock",
"closeRightDock": "Close right dock", "closeRightDock": "Close right dock",
"toggleRightDock": "Toggle right dock", "toggleRightDock": "Toggle right dock",
"multiPanelHintTitle": "Work with more than one panel",
"multiPanelHintBody": "Keep this panel open while the sidebar is used for something else. Two ways to move it:",
"multiPanelHintTabHow": "Ctrl+click its rail icon, or use the button in this header.",
"multiPanelHintDockHow": "Right-click its rail icon, or use the panel icon in the tab bar.",
"sidebarSettings": "Sidebar Settings...",
"hosts": "Hosts", "hosts": "Hosts",
"hostManager": "Host Manager",
"credentials": "Credentials", "credentials": "Credentials",
"connections": "Connections", "connections": "Connections",
"keepSideMenuOpen": "Keep Menu Open", "keepSideMenuOpen": "Keep Menu Open",
@@ -660,8 +623,8 @@
"showPinIconDesc": "Show a pin next to pinned hosts.", "showPinIconDesc": "Show a pin next to pinned hosts.",
"showSharedBadge": "Shared badge", "showSharedBadge": "Shared badge",
"showSharedBadgeDesc": "Show a badge on hosts shared with you.", "showSharedBadgeDesc": "Show a badge on hosts shared with you.",
"showBadges": "Feature badges", "showBadges": "Plugin badges",
"showBadgesDesc": "Show badges that features add next to the host name.", "showBadgesDesc": "Show badges that plugins add next to the host name.",
"hostClickBehavior": "Clicking a host", "hostClickBehavior": "Clicking a host",
"hostClickBehaviorDesc": "What happens when you click a host that already has an open tab. Ctrl/Cmd click, middle click, or \"Open in new tab\" always start a new one.", "hostClickBehaviorDesc": "What happens when you click a host that already has an open tab. Ctrl/Cmd click, middle click, or \"Open in new tab\" always start a new one.",
"hostClickNewTab": "Always open a new tab", "hostClickNewTab": "Always open a new tab",
@@ -734,7 +697,7 @@
"groupTags": "Tags & pin", "groupTags": "Tags & pin",
"groupProxy": "Proxy", "groupProxy": "Proxy",
"groupJumpHosts": "Jump hosts", "groupJumpHosts": "Jump hosts",
"groupFeatureFlags": "Feature flags", "groupFeatureFlags": "Plugin settings",
"groupAdvanced": "Advanced config", "groupAdvanced": "Advanced config",
"preview": "Preview", "preview": "Preview",
"moreHosts": "... {{count}} more hosts", "moreHosts": "... {{count}} more hosts",
@@ -751,7 +714,7 @@
"failedToDeleteCredential2": "Failed to delete credential", "failedToDeleteCredential2": "Failed to delete credential",
"noFolderOption": "(No folder)", "noFolderOption": "(No folder)",
"nSelected": "{{count}} selected", "nSelected": "{{count}} selected",
"featuresMenu": "Features", "featuresMenu": "Plugins",
"moveMenu": "Move", "moveMenu": "Move",
"connectSelected": "Connect", "connectSelected": "Connect",
"cancelSelection": "Cancel", "cancelSelection": "Cancel",
@@ -767,6 +730,7 @@
"noHostsYet": "No hosts yet", "noHostsYet": "No hosts yet",
"noHostsYetHint": "Add a server to open terminals, browse files and see metrics.", "noHostsYetHint": "Add a server to open terminals, browse files and see metrics.",
"noHostsMatchSearch": "No hosts match your search", "noHostsMatchSearch": "No hosts match your search",
"noHostSelected": "Pick a host to open this.",
"searchHosts": "Search hosts...", "searchHosts": "Search hosts...",
"sortHosts": "Sort Hosts", "sortHosts": "Sort Hosts",
"sortDefault": "Default Order", "sortDefault": "Default Order",
@@ -892,12 +856,12 @@
"authFieldsAfterSave": "Save the host first, then fill in these fields.", "authFieldsAfterSave": "Save the host first, then fill in these fields.",
"enablePluginFeature": "Enable {{name}}", "enablePluginFeature": "Enable {{name}}",
"disablePluginFeature": "Disable {{name}}", "disablePluginFeature": "Disable {{name}}",
"filterFeaturesGroup": "Features", "filterFeaturesGroup": "Plugins",
"localOnly": "Keep on this device only", "localOnly": "Keep on this device only",
"localOnlyDesc": "This host is not sent to the server and does not appear on your other devices.", "localOnlyDesc": "This host is not sent to the server and does not appear on your other devices.",
"folderLocalOnly": "Keep on this device only", "folderLocalOnly": "Keep on this device only",
"folderLocalOnlyDesc": "This folder and every host in it stay on this device and are not sent to the server.", "folderLocalOnlyDesc": "This folder and every host in it stay on this device and are not sent to the server.",
"sudoPasswordDesc": "Used when the terminal fills a sudo prompt, and by features that run commands with sudo.", "sudoPasswordDesc": "Used when the terminal fills a sudo prompt, and by plugins that run commands with sudo.",
"editorCancel": "Cancel", "editorCancel": "Cancel",
"editorSaving": "Saving...", "editorSaving": "Saving...",
"editorUpdate": "Update Host", "editorUpdate": "Update Host",
@@ -914,23 +878,15 @@
"quickActions": "Quick Actions", "quickActions": "Quick Actions",
"navigation": "Navigation", "navigation": "Navigation",
"orShortcut": "or", "orShortcut": "or",
"hostManager": "Host Manager",
"hostManagerDesc": "Manage, add, or edit hosts",
"addNewHost": "Add New Host", "addNewHost": "Add New Host",
"addNewHostDesc": "Register a new host",
"adminSettings": "Admin Settings", "adminSettings": "Admin Settings",
"adminSettingsDesc": "Configure system preferences and users",
"userProfile": "User Profile",
"userProfileDesc": "Manage your account and preferences",
"addCredential": "Add Credential", "addCredential": "Add Credential",
"addCredentialDesc": "Store SSH keys or passwords",
"recentActivity": "Recent Activity", "recentActivity": "Recent Activity",
"serversAndHosts": "Servers & Hosts", "serversAndHosts": "Servers & Hosts",
"noHostsFound": "No hosts found matching \"{{search}}\"", "noHostsFound": "No hosts found matching \"{{search}}\"",
"links": "Links", "links": "Links",
"navigate": "Navigate", "navigate": "Navigate",
"select": "Select", "select": "Select",
"toggleWith": "Toggle with",
"pluginActions": "More actions", "pluginActions": "More actions",
"noTargetTab": "No terminal tab is open", "noTargetTab": "No terminal tab is open",
"documentation": "Documentation" "documentation": "Documentation"
@@ -965,7 +921,6 @@
"unsplit": "Unsplit", "unsplit": "Unsplit",
"layouts": "Layouts", "layouts": "Layouts",
"applyLayout": "Change layout", "applyLayout": "Change layout",
"newSplit": "New split",
"maxPanes": "A split can hold up to {{count}} panes", "maxPanes": "A split can hold up to {{count}} panes",
"dropSplit": "Drop to split", "dropSplit": "Drop to split",
"dropHere": "Drop to show here", "dropHere": "Drop to show here",
@@ -992,16 +947,10 @@
"connectedFor": "Connected for {{duration}}", "connectedFor": "Connected for {{duration}}",
"connected": "Connected", "connected": "Connected",
"disconnected": "Disconnected", "disconnected": "Disconnected",
"closeTab": "Close tab",
"closeConnection": "Close connection",
"forgetTab": "Forget",
"removeBackground": "Remove",
"reconnect": "Reconnect", "reconnect": "Reconnect",
"reopenTab": "Reopen",
"sectionOpen": "Open", "sectionOpen": "Open",
"sectionBackground": "Background", "sectionBackground": "Background",
"backgroundDesc": "Sessions persist for {{minutes}} minutes after disconnect and can be reconnected.", "backgroundDesc": "Sessions persist for {{minutes}} minutes after disconnect and can be reconnected.",
"persisted": "Persisted in background",
"expiresIn": "Expires in {{duration}}", "expiresIn": "Expires in {{duration}}",
"search": "Search connections...", "search": "Search connections...",
"noSearchResults": "No connections match your search", "noSearchResults": "No connections match your search",
@@ -1041,7 +990,7 @@
"empty": "No cards. Use Add below to place cards." "empty": "No cards. Use Add below to place cards."
}, },
"auth": { "auth": {
"tagline": "Self-hosted, plugin-based server management", "tagline": "Self-hosted, plugin-based server management.",
"loginTitle": "Welcome back", "loginTitle": "Welcome back",
"registerTitle": "Create Account", "registerTitle": "Create Account",
"forgotPassword": "Forgot Password?", "forgotPassword": "Forgot Password?",
@@ -1063,34 +1012,19 @@
"sshKeyPasswordDescription": "If your SSH key is encrypted, enter the passphrase here.", "sshKeyPasswordDescription": "If your SSH key is encrypted, enter the passphrase here.",
"passphraseRequired": "Passphrase Required", "passphraseRequired": "Passphrase Required",
"passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.", "passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.",
"back": "Back",
"firstUser": "First User",
"firstUserMessage": "You are the first user and will be made an admin. You can view admin settings in the sidebar user dropdown. If you think this is a mistake, check the docker logs, or create a GitHub issue.",
"external": "External", "external": "External",
"loginWithExternal": "Login with External Provider", "loginWithExternal": "Login with External Provider",
"loginWithExternalDesc": "Login using your configured external identity provider", "loginWithExternalDesc": "Login using your configured external identity provider",
"externalNotSupportedInElectron": "External sign-in is not supported in the desktop app yet. Please use the web version.",
"loginWithProvider": "Login with {{name}}", "loginWithProvider": "Login with {{name}}",
"orContinueWith": "or continue with",
"resetPasswordButton": "Reset Password", "resetPasswordButton": "Reset Password",
"sendResetCode": "Send Reset Code", "sendResetCode": "Send Reset Code",
"resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.", "resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.",
"resetCode": "Reset Code", "resetCode": "Reset Code",
"verifyCodeButton": "Verify Code", "verifyCodeButton": "Verify Code",
"enterResetCode": "Enter the 6-digit code from the docker container logs for user:",
"newPassword": "New Password", "newPassword": "New Password",
"confirmNewPassword": "Confirm Password", "confirmNewPassword": "Confirm Password",
"enterNewPassword": "Enter your new password for user:",
"signUp": "Sign Up", "signUp": "Sign Up",
"desktopApp": "Desktop App",
"loggingInToDesktopApp": "Logging in to the desktop app",
"loadingServer": "Loading server...", "loadingServer": "Loading server...",
"dataLossWarning": "Resetting your password this way will delete all your saved SSH hosts, credentials, and other encrypted data. This action cannot be undone. Only use this if you have forgotten your password and are not logged in.",
"authenticationDisabled": "Authentication Disabled",
"authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator.",
"passwordLoginDisabledDesc": "Password login is disabled. Use a passkey or an external authentication provider.",
"attemptsRemaining": "{{count}} attempts remaining",
"confirmResetDataWipe": "This account has not logged in since the encryption upgrade, so its stored data cannot be recovered without the old password. Resetting will permanently delete its hosts, credentials and snippets. Continue?",
"password": "Password", "password": "Password",
"secondFactorUnavailable": "Your account uses a second factor that is not available right now. Contact an admin to reset it.", "secondFactorUnavailable": "Your account uses a second factor that is not available right now. Contact an admin to reset it.",
"secondFactorNoUI": "This second factor cannot be shown on this screen. Contact an admin.", "secondFactorNoUI": "This second factor cannot be shown on this screen. Contact an admin.",
@@ -1122,6 +1056,8 @@
"databaseConnection": "Could not connect to the database", "databaseConnection": "Could not connect to the database",
"embeddedServerFailed": "Termix's local server could not start", "embeddedServerFailed": "Termix's local server could not start",
"unknownError": "Unknown error", "unknownError": "Unknown error",
"syncedHostMissing": "The synced host does not exist on the linked server",
"linkedServerUnreachable": "The linked server is not reachable",
"loginFailed": "Login failed", "loginFailed": "Login failed",
"failedPasswordReset": "Failed to initiate password reset", "failedPasswordReset": "Failed to initiate password reset",
"failedVerifyCode": "Failed to verify reset code", "failedVerifyCode": "Failed to verify reset code",
@@ -1135,12 +1071,7 @@
"requiredField": "This field is required", "requiredField": "This field is required",
"minLength": "Minimum length is {{min}}", "minLength": "Minimum length is {{min}}",
"passwordMismatch": "Passwords do not match", "passwordMismatch": "Passwords do not match",
"passwordLoginDisabled": "Username/password login is currently disabled",
"sessionExpired": "Session expired - please log in again", "sessionExpired": "Session expired - please log in again",
"totpRateLimited": "Rate limited: Too many TOTP verification attempts. Please try again later.",
"totpRateLimitedWithTime": "Rate limited: Too many TOTP verification attempts. Please wait {{time}} seconds before trying again.",
"resetCodeRateLimited": "Rate limited: Too many verification attempts. Please try again later.",
"resetCodeRateLimitedWithTime": "Rate limited: Too many verification attempts. Please wait {{time}} seconds before trying again.",
"authTokenSaveFailed": "Failed to save authentication token", "authTokenSaveFailed": "Failed to save authentication token",
"authTokenMissing": "The server did not return a new authentication token. Please sign in again.", "authTokenMissing": "The server did not return a new authentication token. Please sign in again.",
"failedToLoadServer": "Failed to load server", "failedToLoadServer": "Failed to load server",
@@ -1160,7 +1091,6 @@
"registrationSuccess": "Registration successful" "registrationSuccess": "Registration successful"
}, },
"placeholders": { "placeholders": {
"language": "Language",
"keyPassword": "key password", "keyPassword": "key password",
"pastePrivateKey": "Paste your private key here...", "pastePrivateKey": "Paste your private key here...",
"enterPassword": "Enter your password", "enterPassword": "Enter your password",
@@ -1173,7 +1103,6 @@
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
"loading": "Loading dashboard...",
"github": "GitHub", "github": "GitHub",
"reportBug": "Report a bug", "reportBug": "Report a bug",
"discord": "Discord", "discord": "Discord",
@@ -1181,7 +1110,6 @@
"donate": "Donate", "donate": "Donate",
"serverOverview": "Server Overview", "serverOverview": "Server Overview",
"version": "Version", "version": "Version",
"upToDate": "Up to Date",
"updateAvailable": "Update Available", "updateAvailable": "Update Available",
"beta": "Beta", "beta": "Beta",
"uptime": "Uptime", "uptime": "Uptime",
@@ -1192,7 +1120,6 @@
"totalCredentials": "Total Credentials", "totalCredentials": "Total Credentials",
"recentActivity": "Recent Activity", "recentActivity": "Recent Activity",
"reset": "Reset", "reset": "Reset",
"loadingRecentActivity": "Loading recent activity...",
"noRecentActivity": "No recent activity", "noRecentActivity": "No recent activity",
"quickActions": "Quick Actions", "quickActions": "Quick Actions",
"addHost": "Add Host", "addHost": "Add Host",
@@ -1200,20 +1127,8 @@
"adminSettings": "Admin Settings", "adminSettings": "Admin Settings",
"userProfile": "User Profile", "userProfile": "User Profile",
"serverStats": "Server Stats", "serverStats": "Server Stats",
"loadingServerStats": "Loading server stats...",
"noServerData": "No server data available",
"customizeLayout": "Customize Dashboard", "customizeLayout": "Customize Dashboard",
"dashboardSettings": "Dashboard Settings", "justNow": "just now"
"enableDisableCards": "Enable/Disable Cards",
"resetLayout": "Reset to Default",
"serverOverviewCard": "Server Overview",
"recentActivityCard": "Recent Activity",
"quickActionsCard": "Quick Actions",
"serverStatsCard": "Server Stats",
"panelMain": "Main",
"panelSide": "Side",
"justNow": "just now",
"activityFileManager": "File manager"
}, },
"donation": { "donation": {
"title": "Enjoying Termix?", "title": "Enjoying Termix?",
@@ -1224,24 +1139,16 @@
}, },
"dashboardTab": { "dashboardTab": {
"stable": "STABLE", "stable": "STABLE",
"hostsOnline": "Hosts Online",
"registerNewServer": "Register a new server", "registerNewServer": "Register a new server",
"storeSshKeysOrPasswords": "Store SSH keys or passwords", "storeSshKeysOrPasswords": "Store SSH keys or passwords",
"manageUsersAndRoles": "Manage users and roles", "manageUsersAndRoles": "Manage users and roles",
"manageYourAccount": "Manage your account", "manageYourAccount": "Manage your account",
"hostStatus": "Host Status", "hostStatus": "Host Status",
"noHostsConfigured": "No hosts configured", "noHostsConfigured": "No hosts configured",
"online": "ONLINE",
"offline": "OFFLINE",
"checking": "CHECKING", "checking": "CHECKING",
"onlineLower": "Online",
"add": "Add:",
"commandPalette": "Command Palette", "commandPalette": "Command Palette",
"done": "Done", "done": "Done",
"editModeInstructions": "Drag cards to reorder. Drag the column divider to resize columns. Drag the bottom edge of a card to change its height. Use the trash button to remove a card.",
"empty": "Empty",
"clear": "Clear", "clear": "Clear",
"viewServerDetails": "View server details",
"dragToResizeColumns": "Drag to resize columns", "dragToResizeColumns": "Drag to resize columns",
"hostsAvailable": "Hosts Available", "hostsAvailable": "Hosts Available",
"removeSection": "Remove section", "removeSection": "Remove section",
@@ -1393,7 +1300,6 @@
"noCustomRoles": "No custom roles defined", "noCustomRoles": "No custom roles defined",
"removeRoleFailed": "Failed to remove role", "removeRoleFailed": "Failed to remove role",
"assignRoleFailed": "Failed to assign role", "assignRoleFailed": "Failed to assign role",
"deleteRoleFailed": "Failed to delete role",
"userAdminAccess": "Administrator", "userAdminAccess": "Administrator",
"userAdminAccessDesc": "Full access to all admin settings", "userAdminAccessDesc": "Full access to all admin settings",
"userRoles": "Roles", "userRoles": "Roles",
@@ -1416,7 +1322,6 @@
"createUserPassword": "Password", "createUserPassword": "Password",
"createUserPasswordHint": "Minimum 6 characters.", "createUserPasswordHint": "Minimum 6 characters.",
"createUserEnterUsername": "Enter username", "createUserEnterUsername": "Enter username",
"createUserEnterPassword": "Enter password",
"createUserSubmit": "Create User", "createUserSubmit": "Create User",
"editUserTitle": "Manage User: {{username}}", "editUserTitle": "Manage User: {{username}}",
"editUserDesc": "Edit roles, admin status, sessions, and account settings.", "editUserDesc": "Edit roles, admin status, sessions, and account settings.",
@@ -1447,7 +1352,6 @@
"unlinkAccountInProgress": "Unlinking...", "unlinkAccountInProgress": "Unlinking...",
"unlinkAccountSuccess": "External sign-in unlinked", "unlinkAccountSuccess": "External sign-in unlinked",
"unlinkAccountFailed": "Failed to unlink external sign-in", "unlinkAccountFailed": "Failed to unlink external sign-in",
"saving": "Saving...",
"updateRegistrationFailed": "Failed to update registration setting", "updateRegistrationFailed": "Failed to update registration setting",
"updatePasswordLoginFailed": "Failed to update password login setting", "updatePasswordLoginFailed": "Failed to update password login setting",
"updateSecondFactorAfterExternalLoginFailed": "Failed to update second factor setting", "updateSecondFactorAfterExternalLoginFailed": "Failed to update second factor setting",
@@ -1529,7 +1433,6 @@
"importFailed": "Import failed: {{error}}", "importFailed": "Import failed: {{error}}",
"importError": "Database import failed", "importError": "Database import failed",
"sectionHostDefaults": "Host Defaults", "sectionHostDefaults": "Host Defaults",
"minutes": "minutes",
"rolePermissions": { "rolePermissions": {
"count": "{{count}} permissions", "count": "{{count}} permissions",
"editAction": "Edit permissions", "editAction": "Edit permissions",
@@ -1538,7 +1441,7 @@
"saveError": "Failed to save role permissions", "saveError": "Failed to save role permissions",
"save": "Save", "save": "Save",
"saving": "Saving...", "saving": "Saving...",
"pluginDisabled": "Feature disabled", "pluginDisabled": "Plugin disabled",
"groups": { "groups": {
"hosts": "Hosts", "hosts": "Hosts",
"credentials": "Credentials", "credentials": "Credentials",
@@ -1581,7 +1484,6 @@
"hostLabel": "Host", "hostLabel": "Host",
"hostPlaceholder": "192.168.1.1 or example.com", "hostPlaceholder": "192.168.1.1 or example.com",
"portLabel": "Port", "portLabel": "Port",
"portPlaceholder": "22",
"usernameLabel": "Username", "usernameLabel": "Username",
"usernamePlaceholder": "username", "usernamePlaceholder": "username",
"authLabel": "Auth", "authLabel": "Auth",
@@ -1599,7 +1501,6 @@
"noTarget": "Nothing installed can connect to this host type" "noTarget": "Nothing installed can connect to this host type"
}, },
"keybindings": { "keybindings": {
"title": "Keyboard Shortcuts",
"description": "Bind keys to actions in the terminal and the app, or change the built-in shortcuts.", "description": "Bind keys to actions in the terminal and the app, or change the built-in shortcuts.",
"defaultsHeading": "Built-in Shortcuts", "defaultsHeading": "Built-in Shortcuts",
"fixedHeading": "App Shortcuts", "fixedHeading": "App Shortcuts",
@@ -1625,7 +1526,6 @@
"customize": "Customize", "customize": "Customize",
"resetToDefault": "Reset to default", "resetToDefault": "Reset to default",
"resetAllToDefaults": "Reset all to defaults", "resetAllToDefaults": "Reset all to defaults",
"close": "Close",
"cancel": "Cancel", "cancel": "Cancel",
"saveBinding": "Save Shortcut", "saveBinding": "Save Shortcut",
"saveError": "Failed to save keyboard shortcuts", "saveError": "Failed to save keyboard shortcuts",
@@ -1696,7 +1596,6 @@
"cliLearnMore": "Learn More", "cliLearnMore": "Learn More",
"deleteAccount": "Delete Account", "deleteAccount": "Delete Account",
"deleteAccountDescription": "Permanently delete your account", "deleteAccountDescription": "Permanently delete your account",
"changeServerDescription": "Switch to a different Termix backend server",
"deleteButton": "Delete", "deleteButton": "Delete",
"deleteAccountPermanent": "This action is permanent and cannot be undone.", "deleteAccountPermanent": "This action is permanent and cannot be undone.",
"deleteAccountWarning": "All sessions, hosts, credentials, and settings will be permanently deleted.", "deleteAccountWarning": "All sessions, hosts, credentials, and settings will be permanently deleted.",
@@ -1710,8 +1609,6 @@
"keyboardShortcuts": "Keyboard Shortcuts", "keyboardShortcuts": "Keyboard Shortcuts",
"keyboardShortcutsDescription": "Customize terminal copy, paste, and command shortcuts", "keyboardShortcutsDescription": "Customize terminal copy, paste, and command shortcuts",
"manageShortcuts": "Manage", "manageShortcuts": "Manage",
"historyTracking": "History Tracking",
"historyTrackingDesc": "Track terminal commands",
"commandPalette": "Command Palette", "commandPalette": "Command Palette",
"commandPaletteDesc": "Enable keyboard shortcut", "commandPaletteDesc": "Enable keyboard shortcut",
"reopenTabsOnLogin": "Reopen Tabs on Login", "reopenTabsOnLogin": "Reopen Tabs on Login",
@@ -1724,28 +1621,16 @@
"sidebarSettingsMoved": "Host sidebar display settings have moved to the Customize sidebar panel, opened from the Hosts toolbar.", "sidebarSettingsMoved": "Host sidebar display settings have moved to the Customize sidebar panel, opened from the Hosts toolbar.",
"showHostTags": "Show Host Tags", "showHostTags": "Show Host Tags",
"showHostTagsDesc": "Display tags in host list", "showHostTagsDesc": "Display tags in host list",
"hostTrayOnClick": "Click to Expand Host Actions",
"hostTrayOnClickDesc": "Always show connection buttons; click to expand management options instead of hover",
"compactHostView": "Compact Host View",
"compactHostViewDesc": "Collapse each host to a single line showing only its name and address",
"statusColors": "Real Status Colors", "statusColors": "Real Status Colors",
"statusColorsDesc": "Use green/red for online/offline status instead of the accent color", "statusColorsDesc": "Use green/red for online/offline status instead of the accent color",
"pinAppRail": "Pin App Rail", "pinAppRail": "Pin App Rail",
"pinAppRailDesc": "Keep the left sidebar app rail always expanded instead of expanding on hover", "pinAppRailDesc": "Keep the left sidebar app rail always expanded instead of expanding on hover",
"openFullscreenSettings": "Open settings full screen",
"exitFullscreenSettings": "Exit full-screen settings",
"expandAppRailOnHover": "Expand App Rail on Hover", "expandAppRailOnHover": "Expand App Rail on Hover",
"expandAppRailOnHoverDesc": "Allow the left sidebar app rail to expand when the pointer moves over it", "expandAppRailOnHoverDesc": "Allow the left sidebar app rail to expand when the pointer moves over it",
"showPinAppRailButton": "Show Pin App Rail Button", "showPinAppRailButton": "Show Pin App Rail Button",
"showPinAppRailButtonDesc": "Show a button in the app rail to pin it open. Off by default.", "showPinAppRailButtonDesc": "Show a button in the app rail to pin it open. Off by default.",
"settingsNavigation": "Navigation", "settingsNavigation": "Navigation",
"navigationTabsDesc": "Choose which tabs appear in the app rail", "navigationTabsDesc": "Choose which tabs appear in the app rail",
"showSnippetCommands": "Show Commands",
"showSnippetCommandsDesc": "Show each snippet's command under its name. Turn off for a compact list.",
"foldersCollapsed": "Folders Collapsed",
"foldersCollapsedDesc": "Collapse folders by default",
"confirmExecution": "Confirm Execution",
"confirmExecutionDesc": "Confirm before running snippets",
"settingsUpdates": "Updates", "settingsUpdates": "Updates",
"disableUpdateChecks": "Disable Update Checks", "disableUpdateChecks": "Disable Update Checks",
"disableUpdateChecksDesc": "Stop checking for updates", "disableUpdateChecksDesc": "Stop checking for updates",
@@ -1835,7 +1720,6 @@
} }
} }
}, },
"terminalToolbar": {},
"plugins": { "plugins": {
"capabilities": { "capabilities": {
"credentials:read": { "credentials:read": {
@@ -1947,8 +1831,8 @@
"consequence": "Server settings, not your hosts or credentials." "consequence": "Server settings, not your hosts or credentials."
}, },
"plugins:read": { "plugins:read": {
"title": "See installed features", "title": "See installed plugins",
"consequence": "It can see which features are installed and running." "consequence": "It can see which plugins are installed and running."
}, },
"ui:surface": { "ui:surface": {
"title": "Add its own screens", "title": "Add its own screens",
@@ -1957,9 +1841,9 @@
}, },
"runtime": { "runtime": {
"needsPlugin": "{{name}} is not available", "needsPlugin": "{{name}} is not available",
"needsUnknownPlugin": "This feature is not available", "needsUnknownPlugin": "This plugin is not available",
"disabledHint": "It is turned off on this server.", "disabledHint": "It is turned off on this server.",
"missingHint": "The feature it came from has been removed.", "missingHint": "The plugin it came from has been removed.",
"failed": "{{name}} could not start", "failed": "{{name}} could not start",
"failedHint": "An admin can check the server logs for details.", "failedHint": "An admin can check the server logs for details.",
"loading": "Loading {{name}}", "loading": "Loading {{name}}",
@@ -2331,7 +2215,7 @@
"sshCredentials": "Credentials", "sshCredentials": "Credentials",
"sshFolders": "Folders", "sshFolders": "Folders",
"userPreferences": "Preferences", "userPreferences": "Preferences",
"pluginUserSettings": "Feature settings", "pluginUserSettings": "Plugin settings",
"sharedHosts": "Hosts shared with you", "sharedHosts": "Hosts shared with you",
"sharedCredentials": "Credentials shared with you", "sharedCredentials": "Credentials shared with you",
"vaultProfiles": "Vault profiles", "vaultProfiles": "Vault profiles",
@@ -2430,7 +2314,7 @@
"confirmNoChanges": "No existing host changes. New hosts will start with these defaults.", "confirmNoChanges": "No existing host changes. New hosts will start with these defaults.",
"menuMyDefaults": "My host defaults", "menuMyDefaults": "My host defaults",
"folderDefaultsAction": "Folder defaults", "folderDefaultsAction": "Folder defaults",
"adminSectionDescription": "Set the defaults every host on this server follows, for every setting a host has, including each feature's. Users and folders can set their own on top, and any host can still use its own value.", "adminSectionDescription": "Set the defaults every host on this server follows, for every setting a host has, including each plugin's. Users and folders can set their own on top, and any host can still use its own value.",
"editServerDefaults": "Edit server defaults", "editServerDefaults": "Edit server defaults",
"resetMenu": "Reset", "resetMenu": "Reset",
"resetAll": "Everything to defaults", "resetAll": "Everything to defaults",
@@ -2460,7 +2344,7 @@
"needsAttention_one": "One thing needs attention.", "needsAttention_one": "One thing needs attention.",
"needsAttention_other": "{{count}} things need attention.", "needsAttention_other": "{{count}} things need attention.",
"bandHost": "Host", "bandHost": "Host",
"bandFeatures": "Protocols and features", "bandFeatures": "Protocols and plugins",
"bandCredential": "Credential", "bandCredential": "Credential",
"sshConnection": "SSH connection", "sshConnection": "SSH connection",
"errorAddressRequired": "An address is required.", "errorAddressRequired": "An address is required.",
-4
View File
@@ -170,7 +170,6 @@ if (isElectron()) {
if (!localStorage.getItem(key)) { if (!localStorage.getItem(key)) {
electronSettingsCache.set(key, value); electronSettingsCache.set(key, value);
localStorage.setItem(key, value); localStorage.setItem(key, value);
console.log(`[Electron] Loaded setting ${key} from main process`);
} else { } else {
// Even if we don't overwrite localStorage, update the cache // Even if we don't overwrite localStorage, update the cache
electronSettingsCache.set(key, localStorage.getItem(key)!); electronSettingsCache.set(key, localStorage.getItem(key)!);
@@ -204,8 +203,6 @@ export function setCookie(
console.error(`[Electron] Failed to persist setting ${name}:`, err); console.error(`[Electron] Failed to persist setting ${name}:`, err);
}); });
} }
console.log(`[Electron] Set setting: ${name}`);
} catch (error) { } catch (error) {
console.error(`[Electron] Failed to set setting: ${name}`, error); console.error(`[Electron] Failed to set setting: ${name}`, error);
} }
@@ -230,7 +227,6 @@ export function getCookie(name: string): string | undefined {
if (token) { if (token) {
electronSettingsCache.set(name, token); electronSettingsCache.set(name, token);
} }
console.log(`[Electron] Get setting: ${name} = ${token}`);
return token; return token;
} catch (error) { } catch (error) {
console.error(`[Electron] Failed to get setting: ${name}`, error); console.error(`[Electron] Failed to get setting: ${name}`, error);
+11 -10
View File
@@ -410,6 +410,14 @@ function onFocus() {
void syncPlugins(); void syncPlugins();
} }
function installLocaleResolver(): void {
setPluginLocaleResolver((namespace, _language, file) => {
const summary = getPluginRecord(namespace)?.summary;
if (!summary) return Promise.resolve(null);
return currentDeps().loadLocale(summary, file);
});
}
async function fetchGuestPlugins(): Promise<PluginSummary[]> { async function fetchGuestPlugins(): Promise<PluginSummary[]> {
const response = await fetch(getBackendUrl("/plugins/public")); const response = await fetch(getBackendUrl("/plugins/public"));
if (!response.ok) return []; if (!response.ok) return [];
@@ -460,11 +468,7 @@ export async function startPreLoginPlugins(
return; return;
} }
if (list.length === 0 || started) return; if (list.length === 0 || started) return;
setPluginLocaleResolver((namespace, _language, file) => { installLocaleResolver();
const summary = getPluginRecord(namespace)?.summary;
if (!summary) return Promise.resolve(null);
return currentDeps().loadLocale(summary, file);
});
await enqueue(async () => { await enqueue(async () => {
// Once signed in the full sync owns the list, and reconciling this // Once signed in the full sync owns the list, and reconciling this
// subset would unload everything else. Auth remounts during the // subset would unload everything else. Auth remounts during the
@@ -487,16 +491,12 @@ export function startPluginRuntime(
options: { guest?: boolean } = {}, options: { guest?: boolean } = {},
): Promise<void> { ): Promise<void> {
installPluginHostBridge(); installPluginHostBridge();
installLocaleResolver();
if (options.guest) { if (options.guest) {
guestMode = true; guestMode = true;
if (!deps) configurePluginLoader({ fetchPlugins: fetchGuestPlugins }); if (!deps) configurePluginLoader({ fetchPlugins: fetchGuestPlugins });
return syncPlugins(); return syncPlugins();
} }
setPluginLocaleResolver((namespace, _language, file) => {
const summary = getPluginRecord(namespace)?.summary;
if (!summary) return Promise.resolve(null);
return currentDeps().loadLocale(summary, file);
});
if (!started && typeof window !== "undefined") { if (!started && typeof window !== "undefined") {
started = true; started = true;
@@ -527,6 +527,7 @@ export function isPluginFrontendActive(pluginId: string): boolean {
export async function resetPluginLoader(): Promise<void> { export async function resetPluginLoader(): Promise<void> {
await stopPluginRuntime(); await stopPluginRuntime();
deps = null; deps = null;
guestMode = false;
queue = Promise.resolve(); queue = Promise.resolve();
failedVersions.clear(); failedVersions.clear();
} }
+1 -1
View File
@@ -269,7 +269,7 @@ export function SyncPanel() {
{t("sync.account")} {t("sync.account")}
</span> </span>
<span className="font-semibold truncate"> <span className="font-semibold truncate">
{status.account?.username || "—"} {status.account?.username || "-"}
</span> </span>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
+2 -2
View File
@@ -568,7 +568,7 @@ export function AdminRolesSection({
<span className="text-accent-brand">*</span> <span className="text-accent-brand">*</span>
</label> </label>
<Input <Input
placeholder="e.g., developer" placeholder="developer"
value={newRoleName} value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)} onChange={(e) => setNewRoleName(e.target.value)}
className="text-xs" className="text-xs"
@@ -580,7 +580,7 @@ export function AdminRolesSection({
<span className="text-accent-brand">*</span> <span className="text-accent-brand">*</span>
</label> </label>
<Input <Input
placeholder="e.g., Developer" placeholder="Developer"
value={newRoleDisplayName} value={newRoleDisplayName}
onChange={(e) => setNewRoleDisplayName(e.target.value)} onChange={(e) => setNewRoleDisplayName(e.target.value)}
className="text-xs" className="text-xs"
+1 -1
View File
@@ -146,7 +146,7 @@ export function HostEditorGeneralTab({
return ( return (
<> <>
<HostOnly> <HostOnly>
{/* Protocols — enable/disable each connection type */} {/* Protocols, enable/disable each connection type */}
<SectionCard <SectionCard
title={t("hosts.protocols")} title={t("hosts.protocols")}
icon={<Globe className="size-3.5" />} icon={<Globe className="size-3.5" />}
+6 -6
View File
@@ -493,7 +493,7 @@ export function UserProfilePanel({
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const localSnapshot = useRef<Record<string, string | null>>({}); const localSnapshot = useRef<Record<string, string | null>>({});
// Appearance state — initialized from localStorage // Appearance state, initialized from localStorage
const [accentColor, setAccentColor] = useState( const [accentColor, setAccentColor] = useState(
() => localStorage.getItem("termix-accent") ?? "#f59145", () => localStorage.getItem("termix-accent") ?? "#f59145",
); );
@@ -534,7 +534,7 @@ export function UserProfilePanel({
} }
}, [linkedAccount, storageMode, onPrefsChange]); }, [linkedAccount, storageMode, onPrefsChange]);
// Settings toggles — all backed by localStorage // Settings toggles, all backed by localStorage
const [commandPaletteEnabled, setCommandPaletteEnabled] = useState(() => { const [commandPaletteEnabled, setCommandPaletteEnabled] = useState(() => {
const v = localStorage.getItem("commandPaletteShortcutEnabled"); const v = localStorage.getItem("commandPaletteShortcutEnabled");
return v !== null ? v === "true" : true; return v !== null ? v === "true" : true;
@@ -1137,7 +1137,7 @@ export function UserProfilePanel({
userId={userId} userId={userId}
/> />
{/* Storage mode toggle — only meaningful once a remote server is {/* Storage mode toggle, only meaningful once a remote server is
connected; with no sync there's nowhere for "cloud" to sync to, connected; with no sync there's nowhere for "cloud" to sync to,
so this stays forced to local storage and hidden. */} so this stays forced to local storage and hidden. */}
{(!isElectron() || isLinked) && shows("data") && ( {(!isElectron() || isLinked) && shows("data") && (
@@ -1224,7 +1224,7 @@ export function UserProfilePanel({
{t("newUi.sidebar.userProfile.usernameLabel")} {t("newUi.sidebar.userProfile.usernameLabel")}
</span> </span>
<span className="text-sm font-semibold mt-0.5"> <span className="text-sm font-semibold mt-0.5">
{accountUsername || "—"} {accountUsername || "-"}
</span> </span>
</div> </div>
<div className="flex flex-col py-2"> <div className="flex flex-col py-2">
@@ -1233,7 +1233,7 @@ export function UserProfilePanel({
</span> </span>
<div className="flex flex-wrap gap-1 mt-0.5"> <div className="flex flex-wrap gap-1 mt-0.5">
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-semibold border border-accent-brand/40 bg-accent-brand/10 text-accent-brand w-fit"> <span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-semibold border border-accent-brand/40 bg-accent-brand/10 text-accent-brand w-fit">
{userRole || "—"} {userRole || "-"}
</span> </span>
{userRoles.map((r) => ( {userRoles.map((r) => (
<span <span
@@ -1252,7 +1252,7 @@ export function UserProfilePanel({
{t("newUi.sidebar.userProfile.authMethodLabel")} {t("newUi.sidebar.userProfile.authMethodLabel")}
</span> </span>
<span className="text-sm font-semibold mt-0.5"> <span className="text-sm font-semibold mt-0.5">
{authMethod || "—"} {authMethod || "-"}
</span> </span>
</div> </div>
<div className="flex flex-col py-2"> <div className="flex flex-col py-2">
+1 -1
View File
@@ -1282,7 +1282,7 @@ export function HostItem({
</div> </div>
)} )}
{/* Action tray — slides open on hover (default) or via chevron in click-tray mode */} {/* Action tray, slides open on hover (default) or via chevron in click-tray mode */}
{!isCompact && ( {!isCompact && (
<div className={trayVisibilityClass}> <div className={trayVisibilityClass}>
{tokens.showResourceRow && {tokens.showResourceRow &&
+1 -1
View File
@@ -617,7 +617,7 @@ export function SidebarTree({
pin: host.pin ?? false, pin: host.pin ?? false,
notes: host.notes, notes: host.notes,
// Key material is never sent to the frontend, so a cloned key-auth // Key material is never sent to the frontend, so a cloned key-auth
// host would have authType "key" with no key — unusable. Reset to // host would have authType "key" with no key, which is unusable. Reset to
// password so the clone is in a connectable (editable) state. // password so the clone is in a connectable (editable) state.
authType: host.authType === "key" ? "password" : host.authType, authType: host.authType === "key" ? "password" : host.authType,
password: host.authType === "key" ? null : (host.password ?? null), password: host.authType === "key" ? null : (host.password ?? null),
@@ -135,7 +135,7 @@ describe("onboarding translations", () => {
it("uses no em dashes", () => { it("uses no em dashes", () => {
const text = JSON.stringify((en as Record<string, unknown>).onboarding); const text = JSON.stringify((en as Record<string, unknown>).onboarding);
expect(text).not.toContain("—"); expect(text).not.toContain("\u2014");
}); });
}); });
+16
View File
@@ -24,6 +24,7 @@ import {
visibleRailItems, visibleRailItems,
} from "@/sidebar/rail-items"; } from "@/sidebar/rail-items";
import { getTabType, resetTabTypes } from "@/shell/tab-registry"; import { getTabType, resetTabTypes } from "@/shell/tab-registry";
import i18n from "@/i18n/i18n";
function summary( function summary(
id: string, id: string,
@@ -263,6 +264,21 @@ describe("plugin loader", () => {
expect(isPluginFrontendActive("alpha")).toBe(true); expect(isPluginFrontendActive("alpha")).toBe(true);
}); });
it("loads plugin strings on guest pages", async () => {
modules.guest = railPlugin("guest");
plugins = [summary("guest", { locales: ["en"] })];
configurePluginLoader({
fetchPlugins: async () => plugins,
importFrontend: async (entry) => modules[entry.id],
loadLocale: async () => ({ hello: "Hello guest" }),
injectCss: () => null,
});
await startPluginRuntime({ guest: true });
await i18n.loadNamespaces("guest");
expect(i18n.t("hello", { ns: "guest" })).toBe("Hello guest");
});
it("settles even when the plugin list cannot be fetched", async () => { it("settles even when the plugin list cannot be fetched", async () => {
vi.spyOn(console, "error").mockImplementation(() => {}); vi.spyOn(console, "error").mockImplementation(() => {});
configurePluginLoader({ configurePluginLoader({
@@ -6,7 +6,7 @@ import {
statusCheckEnabled, statusCheckEnabled,
} from "@/sidebar/tree/HostItem/HostItem"; } from "@/sidebar/tree/HostItem/HostItem";
// Minimal host factory – only the fields buildStatusTooltip reads. // Minimal host factory, only the fields buildStatusTooltip reads.
function makeHost(overrides: Partial<Host> = {}): Host { function makeHost(overrides: Partial<Host> = {}): Host {
return { return {
id: 1, id: 1,
@@ -90,7 +90,7 @@ describe("buildStatusTooltip", () => {
it("does not render key paths when a translator is supplied", () => { it("does not render key paths when a translator is supplied", () => {
const host = makeHost(); const host = makeHost();
const tooltip = buildStatusTooltip(host, "online", t); const tooltip = buildStatusTooltip(host, "online", t);
// The tooltip must never show the raw key path – that means the // The tooltip must never show the raw key path, that means the
// translation resource is missing, which is the bug #1265 fixed. // translation resource is missing, which is the bug #1265 fixed.
expect(tooltip).not.toMatch(/hosts\.status\./); expect(tooltip).not.toMatch(/hosts\.status\./);
}); });
-80
View File
@@ -1,80 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Select2 } from "@/components/select2";
import { Globe } from "lucide-react";
import { saveUserPreferences } from "@/main-axios";
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
const languages = [
{ code: "en", name: "English", nativeName: "English" },
{ code: "af", name: "Afrikaans", nativeName: "Afrikaans" },
{ code: "ar", name: "Arabic", nativeName: "العربية" },
{ code: "bn", name: "Bengali", nativeName: "বাংলা" },
{ code: "bg", name: "Bulgarian", nativeName: "Български" },
{ code: "ca", name: "Catalan", nativeName: "Català" },
{ code: "zh-CN", name: "Chinese Simplified", nativeName: "简体中文" },
{ code: "zh-TW", name: "Chinese Traditional", nativeName: "繁体中文" },
{ code: "cs", name: "Czech", nativeName: "Čeština" },
{ code: "da", name: "Danish", nativeName: "Dansk" },
{ code: "nl", name: "Dutch", nativeName: "Nederlands" },
{ code: "fi", name: "Finnish", nativeName: "Suomi" },
{ code: "fr", name: "French", nativeName: "Français" },
{ code: "de", name: "German", nativeName: "Deutsch" },
{ code: "el", name: "Greek", nativeName: "Ελληνικά" },
{ code: "he", name: "Hebrew", nativeName: "עברית" },
{ code: "hi", name: "Hindi", nativeName: "हिन्दी" },
{ code: "hu", name: "Hungarian", nativeName: "Magyar" },
{ code: "id", name: "Indonesian", nativeName: "Bahasa Indonesia" },
{ code: "it", name: "Italian", nativeName: "Italiano" },
{ code: "ja", name: "Japanese", nativeName: "日本語" },
{ code: "ko", name: "Korean", nativeName: "한국어" },
{ code: "no", name: "Norwegian", nativeName: "Norsk" },
{ code: "pl", name: "Polish", nativeName: "Polski" },
{
code: "pt-PT",
name: "Portuguese",
nativeName: "Português",
},
{
code: "pt-BR",
name: "Portuguese (Brazil)",
nativeName: "Português (Brasil)",
},
{ code: "ro", name: "Romanian", nativeName: "Română" },
{ code: "ru", name: "Russian", nativeName: "Русский" },
{ code: "sr", name: "Serbian", nativeName: "Српски" },
{ code: "es-ES", name: "Spanish", nativeName: "Español" },
{ code: "sv-SE", name: "Swedish", nativeName: "Svenska" },
{ code: "th", name: "Thai", nativeName: "ไทย" },
{ code: "tr", name: "Turkish", nativeName: "Türkçe" },
{ code: "uk", name: "Ukrainian", nativeName: "Українська" },
{ code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt" },
];
export function LanguageSwitcher() {
const { i18n, t } = useTranslation();
const handleLanguageChange = (value: string) => {
void changeAppLanguage(value)
.then((language) => saveUserPreferences({ language }))
.catch(() => {});
};
return (
<div className="flex items-center gap-2 relative z-[99999]">
<Globe className="h-4 w-4 text-muted-foreground" />
<Select2
value={normalizeLanguageCode(i18n.resolvedLanguage || i18n.language)}
onChange={(event) => handleLanguageChange(event.target.value)}
placeholder={t("placeholders.language")}
className="w-[120px]"
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</Select2>
</div>
);
}