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" />
<title>Termix</title>
<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 {
scrollbar-width: thin;
scrollbar-color: #4a4a4a transparent;
+1 -1
View File
@@ -2,7 +2,7 @@
* Credential sidebar preferences model. Shared by the frontend sidebar and
* the backend preferences endpoint (no framework imports, mirrors
* ./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
* 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 [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 [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
// Split tabs are restored once their child tabs have stable live ids.
@@ -383,7 +383,7 @@ export function AppShell({
((options: ConfirmOptions) => Promise<boolean>) | 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.
const [rightRailView, setRightRailView] = useState<RailView | null>(() => {
const saved = localStorage.getItem("termix_rightRailView");
@@ -698,7 +698,7 @@ export function AppShell({
const paneElsRef = useRef<Map<string, HTMLDivElement>>(new Map());
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
// the normal-view container and the pane container via vanilla DOM so React's portal
// target never changes (changing the target causes a remount).
@@ -708,7 +708,7 @@ export function AppShell({
const mainAreaRef = useRef<HTMLDivElement>(null);
// 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
// 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 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
// visual order. Reordering tabs in the bar reorders `tabs`, and mapping
// 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
// down and rebuilds every passive effect underneath (including
// 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);
if (tabDigit !== null) {
const currentTabs = tabsRef.current.filter(
@@ -869,7 +869,7 @@ export function AppShell({
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.code === "BracketRight" || e.code === "BracketLeft") {
e.preventDefault();
@@ -1368,7 +1368,7 @@ export function AppShell({
// Restored tabs are in the tab bar, not in background records
}
} 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[]);
}
} catch {
@@ -2471,10 +2471,10 @@ export function AppShell({
}, [tabs, activeTab]);
// 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.
// 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.
useLayoutEffect(() => {
const normalView = normalViewRef.current;
@@ -3113,7 +3113,7 @@ export function AppShell({
</div>
</SurfaceScope>
{/* Bottom nav bar — mobile only */}
{/* Bottom nav bar, mobile only */}
<MobileBar
railView={railView}
sidebarOpen={sidebarOpen}
@@ -3126,7 +3126,7 @@ export function AppShell({
/>
</div>
{/* Right dock — desktop only, holds a second reference panel */}
{/* Right dock, desktop only, holds a second reference panel */}
{!isMobile && rightRailView && (
<DockPanel
side="right"
+3 -2
View File
@@ -1,3 +1,4 @@
import i18n from "i18next";
import {
handleApiError,
rbacApi,
@@ -16,7 +17,7 @@ async function getSharingTarget(hostId: number, syncId?: string | null) {
if (!api || !syncId) return { api: rbacApi, hostId };
const remoteHostId = await resolveRemoteHostId(syncId);
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 };
}
@@ -27,7 +28,7 @@ async function getSharedCopyTarget(hostId: number, syncId?: string | null) {
const api = await getConnectedRemoteApi();
const remoteHostId = api ? await resolveRemoteHostId(syncId) : 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 };
}
+1 -1
View File
@@ -98,7 +98,7 @@ export function ElectronLoginForm({
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.
if (type === "OIDC_SYSTEM_BROWSER_AUTH" && authUrl && callbackPort) {
const sendResultToIframe = (result: {
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* 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
* happens the caller gets `fallback` instead, so error paths never surface a
* raw `[object Object]` in logs or UI.
+21 -137
View File
@@ -139,12 +139,8 @@
"offline": "Offline",
"unknown": "Unknown",
"continue": "Continue",
"maintenance": "Maintenance",
"degraded": "Degraded",
"error": "Error",
"warning": "Warning",
"unsavedChanges": "Unsaved changes",
"dismiss": "Dismiss",
"more": "More",
"loading": "Loading...",
"host": "Host",
@@ -154,15 +150,7 @@
"connect": "Connect",
"copied": "Copied",
"connecting": "Connecting...",
"updateAvailable": "Update Available",
"appName": "Termix",
"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",
"username": "Username",
"login": "Login",
@@ -177,15 +165,10 @@
"rename": "Rename",
"edit": "Edit",
"confirm": "Confirm",
"no": "No",
"or": "OR",
"next": "Next",
"previous": "Previous",
"refresh": "Refresh",
"language": "Language",
"checking": "Checking...",
"checkingDatabase": "Checking database connection...",
"checkingAuthentication": "Checking authentication...",
"backendReconnected": "Server connection restored",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
@@ -194,12 +177,6 @@
"copyFailed": "Failed to copy to clipboard",
"maximize": "Maximize",
"restore": "Restore",
"of": "of",
"saved": "Saved",
"deleted": "Deleted",
"deleteFailed": "Failed to delete",
"saveFailed": "Failed to save",
"required": "Required",
"remove": "Remove",
"add": "Add",
"update": "Update",
@@ -221,8 +198,8 @@
"skip": "Skip setup",
"next": "Next",
"finish": "Done",
"pluginsTitle": "Choose your features",
"pluginsIntro": "Pick the features to keep. The recommended ones cover what most people need.",
"pluginsTitle": "Choose your plugins",
"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.",
"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",
@@ -259,19 +236,11 @@
"nav": {
"home": "Home",
"terminal": "Terminal",
"fileManager": "File Manager",
"serverStats": "Host Metrics",
"admin": "Admin",
"userProfile": "User Profile",
"confirmClose": "Close this active session?",
"close": "Close",
"cancel": "Cancel",
"sshManager": "SSH Manager",
"hostTabTitle": "{{username}}@{{ip}}:{{port}}",
"copyPassword": "Copy Password",
"copySudoPassword": "Copy Sudo Password",
"passwordCopied": "Password copied to clipboard",
"noPasswordAvailable": "No password available",
"failedToCopyPassword": "Failed to copy password",
"refreshTab": "Refresh connection",
"renameTab": "Rename tab",
@@ -282,13 +251,7 @@
"openInRightDock": "Open in right dock",
"closeRightDock": "Close 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",
"hostManager": "Host Manager",
"credentials": "Credentials",
"connections": "Connections",
"keepSideMenuOpen": "Keep Menu Open",
@@ -660,8 +623,8 @@
"showPinIconDesc": "Show a pin next to pinned hosts.",
"showSharedBadge": "Shared badge",
"showSharedBadgeDesc": "Show a badge on hosts shared with you.",
"showBadges": "Feature badges",
"showBadgesDesc": "Show badges that features add next to the host name.",
"showBadges": "Plugin badges",
"showBadgesDesc": "Show badges that plugins add next to the host name.",
"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.",
"hostClickNewTab": "Always open a new tab",
@@ -734,7 +697,7 @@
"groupTags": "Tags & pin",
"groupProxy": "Proxy",
"groupJumpHosts": "Jump hosts",
"groupFeatureFlags": "Feature flags",
"groupFeatureFlags": "Plugin settings",
"groupAdvanced": "Advanced config",
"preview": "Preview",
"moreHosts": "... {{count}} more hosts",
@@ -751,7 +714,7 @@
"failedToDeleteCredential2": "Failed to delete credential",
"noFolderOption": "(No folder)",
"nSelected": "{{count}} selected",
"featuresMenu": "Features",
"featuresMenu": "Plugins",
"moveMenu": "Move",
"connectSelected": "Connect",
"cancelSelection": "Cancel",
@@ -767,6 +730,7 @@
"noHostsYet": "No hosts yet",
"noHostsYetHint": "Add a server to open terminals, browse files and see metrics.",
"noHostsMatchSearch": "No hosts match your search",
"noHostSelected": "Pick a host to open this.",
"searchHosts": "Search hosts...",
"sortHosts": "Sort Hosts",
"sortDefault": "Default Order",
@@ -892,12 +856,12 @@
"authFieldsAfterSave": "Save the host first, then fill in these fields.",
"enablePluginFeature": "Enable {{name}}",
"disablePluginFeature": "Disable {{name}}",
"filterFeaturesGroup": "Features",
"filterFeaturesGroup": "Plugins",
"localOnly": "Keep on this device only",
"localOnlyDesc": "This host is not sent to the server and does not appear on your other devices.",
"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.",
"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",
"editorSaving": "Saving...",
"editorUpdate": "Update Host",
@@ -914,23 +878,15 @@
"quickActions": "Quick Actions",
"navigation": "Navigation",
"orShortcut": "or",
"hostManager": "Host Manager",
"hostManagerDesc": "Manage, add, or edit hosts",
"addNewHost": "Add New Host",
"addNewHostDesc": "Register a new host",
"adminSettings": "Admin Settings",
"adminSettingsDesc": "Configure system preferences and users",
"userProfile": "User Profile",
"userProfileDesc": "Manage your account and preferences",
"addCredential": "Add Credential",
"addCredentialDesc": "Store SSH keys or passwords",
"recentActivity": "Recent Activity",
"serversAndHosts": "Servers & Hosts",
"noHostsFound": "No hosts found matching \"{{search}}\"",
"links": "Links",
"navigate": "Navigate",
"select": "Select",
"toggleWith": "Toggle with",
"pluginActions": "More actions",
"noTargetTab": "No terminal tab is open",
"documentation": "Documentation"
@@ -965,7 +921,6 @@
"unsplit": "Unsplit",
"layouts": "Layouts",
"applyLayout": "Change layout",
"newSplit": "New split",
"maxPanes": "A split can hold up to {{count}} panes",
"dropSplit": "Drop to split",
"dropHere": "Drop to show here",
@@ -992,16 +947,10 @@
"connectedFor": "Connected for {{duration}}",
"connected": "Connected",
"disconnected": "Disconnected",
"closeTab": "Close tab",
"closeConnection": "Close connection",
"forgetTab": "Forget",
"removeBackground": "Remove",
"reconnect": "Reconnect",
"reopenTab": "Reopen",
"sectionOpen": "Open",
"sectionBackground": "Background",
"backgroundDesc": "Sessions persist for {{minutes}} minutes after disconnect and can be reconnected.",
"persisted": "Persisted in background",
"expiresIn": "Expires in {{duration}}",
"search": "Search connections...",
"noSearchResults": "No connections match your search",
@@ -1041,7 +990,7 @@
"empty": "No cards. Use Add below to place cards."
},
"auth": {
"tagline": "Self-hosted, plugin-based server management",
"tagline": "Self-hosted, plugin-based server management.",
"loginTitle": "Welcome back",
"registerTitle": "Create Account",
"forgotPassword": "Forgot Password?",
@@ -1063,34 +1012,19 @@
"sshKeyPasswordDescription": "If your SSH key is encrypted, enter the passphrase here.",
"passphraseRequired": "Passphrase Required",
"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",
"loginWithExternal": "Login with External 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}}",
"orContinueWith": "or continue with",
"resetPasswordButton": "Reset Password",
"sendResetCode": "Send Reset Code",
"resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.",
"resetCode": "Reset Code",
"verifyCodeButton": "Verify Code",
"enterResetCode": "Enter the 6-digit code from the docker container logs for user:",
"newPassword": "New Password",
"confirmNewPassword": "Confirm Password",
"enterNewPassword": "Enter your new password for user:",
"signUp": "Sign Up",
"desktopApp": "Desktop App",
"loggingInToDesktopApp": "Logging in to the desktop app",
"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",
"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.",
@@ -1122,6 +1056,8 @@
"databaseConnection": "Could not connect to the database",
"embeddedServerFailed": "Termix's local server could not start",
"unknownError": "Unknown error",
"syncedHostMissing": "The synced host does not exist on the linked server",
"linkedServerUnreachable": "The linked server is not reachable",
"loginFailed": "Login failed",
"failedPasswordReset": "Failed to initiate password reset",
"failedVerifyCode": "Failed to verify reset code",
@@ -1135,12 +1071,7 @@
"requiredField": "This field is required",
"minLength": "Minimum length is {{min}}",
"passwordMismatch": "Passwords do not match",
"passwordLoginDisabled": "Username/password login is currently disabled",
"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",
"authTokenMissing": "The server did not return a new authentication token. Please sign in again.",
"failedToLoadServer": "Failed to load server",
@@ -1160,7 +1091,6 @@
"registrationSuccess": "Registration successful"
},
"placeholders": {
"language": "Language",
"keyPassword": "key password",
"pastePrivateKey": "Paste your private key here...",
"enterPassword": "Enter your password",
@@ -1173,7 +1103,6 @@
},
"dashboard": {
"title": "Dashboard",
"loading": "Loading dashboard...",
"github": "GitHub",
"reportBug": "Report a bug",
"discord": "Discord",
@@ -1181,7 +1110,6 @@
"donate": "Donate",
"serverOverview": "Server Overview",
"version": "Version",
"upToDate": "Up to Date",
"updateAvailable": "Update Available",
"beta": "Beta",
"uptime": "Uptime",
@@ -1192,7 +1120,6 @@
"totalCredentials": "Total Credentials",
"recentActivity": "Recent Activity",
"reset": "Reset",
"loadingRecentActivity": "Loading recent activity...",
"noRecentActivity": "No recent activity",
"quickActions": "Quick Actions",
"addHost": "Add Host",
@@ -1200,20 +1127,8 @@
"adminSettings": "Admin Settings",
"userProfile": "User Profile",
"serverStats": "Server Stats",
"loadingServerStats": "Loading server stats...",
"noServerData": "No server data available",
"customizeLayout": "Customize Dashboard",
"dashboardSettings": "Dashboard Settings",
"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"
"justNow": "just now"
},
"donation": {
"title": "Enjoying Termix?",
@@ -1224,24 +1139,16 @@
},
"dashboardTab": {
"stable": "STABLE",
"hostsOnline": "Hosts Online",
"registerNewServer": "Register a new server",
"storeSshKeysOrPasswords": "Store SSH keys or passwords",
"manageUsersAndRoles": "Manage users and roles",
"manageYourAccount": "Manage your account",
"hostStatus": "Host Status",
"noHostsConfigured": "No hosts configured",
"online": "ONLINE",
"offline": "OFFLINE",
"checking": "CHECKING",
"onlineLower": "Online",
"add": "Add:",
"commandPalette": "Command Palette",
"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",
"viewServerDetails": "View server details",
"dragToResizeColumns": "Drag to resize columns",
"hostsAvailable": "Hosts Available",
"removeSection": "Remove section",
@@ -1393,7 +1300,6 @@
"noCustomRoles": "No custom roles defined",
"removeRoleFailed": "Failed to remove role",
"assignRoleFailed": "Failed to assign role",
"deleteRoleFailed": "Failed to delete role",
"userAdminAccess": "Administrator",
"userAdminAccessDesc": "Full access to all admin settings",
"userRoles": "Roles",
@@ -1416,7 +1322,6 @@
"createUserPassword": "Password",
"createUserPasswordHint": "Minimum 6 characters.",
"createUserEnterUsername": "Enter username",
"createUserEnterPassword": "Enter password",
"createUserSubmit": "Create User",
"editUserTitle": "Manage User: {{username}}",
"editUserDesc": "Edit roles, admin status, sessions, and account settings.",
@@ -1447,7 +1352,6 @@
"unlinkAccountInProgress": "Unlinking...",
"unlinkAccountSuccess": "External sign-in unlinked",
"unlinkAccountFailed": "Failed to unlink external sign-in",
"saving": "Saving...",
"updateRegistrationFailed": "Failed to update registration setting",
"updatePasswordLoginFailed": "Failed to update password login setting",
"updateSecondFactorAfterExternalLoginFailed": "Failed to update second factor setting",
@@ -1529,7 +1433,6 @@
"importFailed": "Import failed: {{error}}",
"importError": "Database import failed",
"sectionHostDefaults": "Host Defaults",
"minutes": "minutes",
"rolePermissions": {
"count": "{{count}} permissions",
"editAction": "Edit permissions",
@@ -1538,7 +1441,7 @@
"saveError": "Failed to save role permissions",
"save": "Save",
"saving": "Saving...",
"pluginDisabled": "Feature disabled",
"pluginDisabled": "Plugin disabled",
"groups": {
"hosts": "Hosts",
"credentials": "Credentials",
@@ -1581,7 +1484,6 @@
"hostLabel": "Host",
"hostPlaceholder": "192.168.1.1 or example.com",
"portLabel": "Port",
"portPlaceholder": "22",
"usernameLabel": "Username",
"usernamePlaceholder": "username",
"authLabel": "Auth",
@@ -1599,7 +1501,6 @@
"noTarget": "Nothing installed can connect to this host type"
},
"keybindings": {
"title": "Keyboard Shortcuts",
"description": "Bind keys to actions in the terminal and the app, or change the built-in shortcuts.",
"defaultsHeading": "Built-in Shortcuts",
"fixedHeading": "App Shortcuts",
@@ -1625,7 +1526,6 @@
"customize": "Customize",
"resetToDefault": "Reset to default",
"resetAllToDefaults": "Reset all to defaults",
"close": "Close",
"cancel": "Cancel",
"saveBinding": "Save Shortcut",
"saveError": "Failed to save keyboard shortcuts",
@@ -1696,7 +1596,6 @@
"cliLearnMore": "Learn More",
"deleteAccount": "Delete Account",
"deleteAccountDescription": "Permanently delete your account",
"changeServerDescription": "Switch to a different Termix backend server",
"deleteButton": "Delete",
"deleteAccountPermanent": "This action is permanent and cannot be undone.",
"deleteAccountWarning": "All sessions, hosts, credentials, and settings will be permanently deleted.",
@@ -1710,8 +1609,6 @@
"keyboardShortcuts": "Keyboard Shortcuts",
"keyboardShortcutsDescription": "Customize terminal copy, paste, and command shortcuts",
"manageShortcuts": "Manage",
"historyTracking": "History Tracking",
"historyTrackingDesc": "Track terminal commands",
"commandPalette": "Command Palette",
"commandPaletteDesc": "Enable keyboard shortcut",
"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.",
"showHostTags": "Show Host Tags",
"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",
"statusColorsDesc": "Use green/red for online/offline status instead of the accent color",
"pinAppRail": "Pin App Rail",
"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",
"expandAppRailOnHoverDesc": "Allow the left sidebar app rail to expand when the pointer moves over it",
"showPinAppRailButton": "Show Pin App Rail Button",
"showPinAppRailButtonDesc": "Show a button in the app rail to pin it open. Off by default.",
"settingsNavigation": "Navigation",
"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",
"disableUpdateChecks": "Disable Update Checks",
"disableUpdateChecksDesc": "Stop checking for updates",
@@ -1835,7 +1720,6 @@
}
}
},
"terminalToolbar": {},
"plugins": {
"capabilities": {
"credentials:read": {
@@ -1947,8 +1831,8 @@
"consequence": "Server settings, not your hosts or credentials."
},
"plugins:read": {
"title": "See installed features",
"consequence": "It can see which features are installed and running."
"title": "See installed plugins",
"consequence": "It can see which plugins are installed and running."
},
"ui:surface": {
"title": "Add its own screens",
@@ -1957,9 +1841,9 @@
},
"runtime": {
"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.",
"missingHint": "The feature it came from has been removed.",
"missingHint": "The plugin it came from has been removed.",
"failed": "{{name}} could not start",
"failedHint": "An admin can check the server logs for details.",
"loading": "Loading {{name}}",
@@ -2331,7 +2215,7 @@
"sshCredentials": "Credentials",
"sshFolders": "Folders",
"userPreferences": "Preferences",
"pluginUserSettings": "Feature settings",
"pluginUserSettings": "Plugin settings",
"sharedHosts": "Hosts shared with you",
"sharedCredentials": "Credentials shared with you",
"vaultProfiles": "Vault profiles",
@@ -2430,7 +2314,7 @@
"confirmNoChanges": "No existing host changes. New hosts will start with these defaults.",
"menuMyDefaults": "My host 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",
"resetMenu": "Reset",
"resetAll": "Everything to defaults",
@@ -2460,7 +2344,7 @@
"needsAttention_one": "One thing needs attention.",
"needsAttention_other": "{{count}} things need attention.",
"bandHost": "Host",
"bandFeatures": "Protocols and features",
"bandFeatures": "Protocols and plugins",
"bandCredential": "Credential",
"sshConnection": "SSH connection",
"errorAddressRequired": "An address is required.",
-4
View File
@@ -170,7 +170,6 @@ if (isElectron()) {
if (!localStorage.getItem(key)) {
electronSettingsCache.set(key, value);
localStorage.setItem(key, value);
console.log(`[Electron] Loaded setting ${key} from main process`);
} else {
// Even if we don't overwrite localStorage, update the cache
electronSettingsCache.set(key, localStorage.getItem(key)!);
@@ -204,8 +203,6 @@ export function setCookie(
console.error(`[Electron] Failed to persist setting ${name}:`, err);
});
}
console.log(`[Electron] Set setting: ${name}`);
} catch (error) {
console.error(`[Electron] Failed to set setting: ${name}`, error);
}
@@ -230,7 +227,6 @@ export function getCookie(name: string): string | undefined {
if (token) {
electronSettingsCache.set(name, token);
}
console.log(`[Electron] Get setting: ${name} = ${token}`);
return token;
} catch (error) {
console.error(`[Electron] Failed to get setting: ${name}`, error);
+11 -10
View File
@@ -410,6 +410,14 @@ function onFocus() {
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[]> {
const response = await fetch(getBackendUrl("/plugins/public"));
if (!response.ok) return [];
@@ -460,11 +468,7 @@ export async function startPreLoginPlugins(
return;
}
if (list.length === 0 || started) return;
setPluginLocaleResolver((namespace, _language, file) => {
const summary = getPluginRecord(namespace)?.summary;
if (!summary) return Promise.resolve(null);
return currentDeps().loadLocale(summary, file);
});
installLocaleResolver();
await enqueue(async () => {
// Once signed in the full sync owns the list, and reconciling this
// subset would unload everything else. Auth remounts during the
@@ -487,16 +491,12 @@ export function startPluginRuntime(
options: { guest?: boolean } = {},
): Promise<void> {
installPluginHostBridge();
installLocaleResolver();
if (options.guest) {
guestMode = true;
if (!deps) configurePluginLoader({ fetchPlugins: fetchGuestPlugins });
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") {
started = true;
@@ -527,6 +527,7 @@ export function isPluginFrontendActive(pluginId: string): boolean {
export async function resetPluginLoader(): Promise<void> {
await stopPluginRuntime();
deps = null;
guestMode = false;
queue = Promise.resolve();
failedVersions.clear();
}
+1 -1
View File
@@ -269,7 +269,7 @@ export function SyncPanel() {
{t("sync.account")}
</span>
<span className="font-semibold truncate">
{status.account?.username || "—"}
{status.account?.username || "-"}
</span>
</div>
<div className="flex flex-col">
+2 -2
View File
@@ -568,7 +568,7 @@ export function AdminRolesSection({
<span className="text-accent-brand">*</span>
</label>
<Input
placeholder="e.g., developer"
placeholder="developer"
value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)}
className="text-xs"
@@ -580,7 +580,7 @@ export function AdminRolesSection({
<span className="text-accent-brand">*</span>
</label>
<Input
placeholder="e.g., Developer"
placeholder="Developer"
value={newRoleDisplayName}
onChange={(e) => setNewRoleDisplayName(e.target.value)}
className="text-xs"
+1 -1
View File
@@ -146,7 +146,7 @@ export function HostEditorGeneralTab({
return (
<>
<HostOnly>
{/* Protocols — enable/disable each connection type */}
{/* Protocols, enable/disable each connection type */}
<SectionCard
title={t("hosts.protocols")}
icon={<Globe className="size-3.5" />}
+6 -6
View File
@@ -493,7 +493,7 @@ export function UserProfilePanel({
const { theme, setTheme } = useTheme();
const localSnapshot = useRef<Record<string, string | null>>({});
// Appearance state — initialized from localStorage
// Appearance state, initialized from localStorage
const [accentColor, setAccentColor] = useState(
() => localStorage.getItem("termix-accent") ?? "#f59145",
);
@@ -534,7 +534,7 @@ export function UserProfilePanel({
}
}, [linkedAccount, storageMode, onPrefsChange]);
// Settings toggles — all backed by localStorage
// Settings toggles, all backed by localStorage
const [commandPaletteEnabled, setCommandPaletteEnabled] = useState(() => {
const v = localStorage.getItem("commandPaletteShortcutEnabled");
return v !== null ? v === "true" : true;
@@ -1137,7 +1137,7 @@ export function UserProfilePanel({
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,
so this stays forced to local storage and hidden. */}
{(!isElectron() || isLinked) && shows("data") && (
@@ -1224,7 +1224,7 @@ export function UserProfilePanel({
{t("newUi.sidebar.userProfile.usernameLabel")}
</span>
<span className="text-sm font-semibold mt-0.5">
{accountUsername || "—"}
{accountUsername || "-"}
</span>
</div>
<div className="flex flex-col py-2">
@@ -1233,7 +1233,7 @@ export function UserProfilePanel({
</span>
<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">
{userRole || "—"}
{userRole || "-"}
</span>
{userRoles.map((r) => (
<span
@@ -1252,7 +1252,7 @@ export function UserProfilePanel({
{t("newUi.sidebar.userProfile.authMethodLabel")}
</span>
<span className="text-sm font-semibold mt-0.5">
{authMethod || "—"}
{authMethod || "-"}
</span>
</div>
<div className="flex flex-col py-2">
+1 -1
View File
@@ -1282,7 +1282,7 @@ export function HostItem({
</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 && (
<div className={trayVisibilityClass}>
{tokens.showResourceRow &&
+1 -1
View File
@@ -617,7 +617,7 @@ export function SidebarTree({
pin: host.pin ?? false,
notes: host.notes,
// 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.
authType: host.authType === "key" ? "password" : host.authType,
password: host.authType === "key" ? null : (host.password ?? null),
@@ -135,7 +135,7 @@ describe("onboarding translations", () => {
it("uses no em dashes", () => {
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,
} from "@/sidebar/rail-items";
import { getTabType, resetTabTypes } from "@/shell/tab-registry";
import i18n from "@/i18n/i18n";
function summary(
id: string,
@@ -263,6 +264,21 @@ describe("plugin loader", () => {
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 () => {
vi.spyOn(console, "error").mockImplementation(() => {});
configurePluginLoader({
@@ -6,7 +6,7 @@ import {
statusCheckEnabled,
} 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 {
return {
id: 1,
@@ -90,7 +90,7 @@ describe("buildStatusTooltip", () => {
it("does not render key paths when a translator is supplied", () => {
const host = makeHost();
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.
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>
);
}