("installed");
const installedMeta = useMarketplaceStore((s) => s.installedMeta);
+ const catalog = useMarketplaceStore((s) => s.catalog);
+ const catalogLoading = useMarketplaceStore((s) => s.catalogLoading);
+ const fetchCatalog = useMarketplaceStore((s) => s.fetchCatalog);
const isAndroid = useIsAndroid();
const totalCount = visiblePlugins(BUNDLED_PLUGINS, isAndroid).length + installedMeta.length;
- const tabLabel = (tabKey: Tab) =>
- tabKey === "installed"
- ? t("settings.plugins.tabs.installed", { count: totalCount })
- : t("settings.plugins.tabs.browse");
+ // Fetch the catalog once on mount so update detection works before visiting Browse.
+ useEffect(() => {
+ if (catalog.length === 0 && !catalogLoading) void fetchCatalog();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const updateCount = installedMeta.filter((m) => availableUpdate(m, catalog)).length;
+
+ const tabLabel = (tabKey: Tab) => {
+ if (tabKey === "browse") return t("settings.plugins.tabs.browse");
+ const base = t("settings.plugins.tabs.installed", { count: totalCount });
+ return updateCount > 0
+ ? `${base} ${t("settings.plugins.tabs.updateCount", { count: updateCount })}`
+ : base;
+ };
return (
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json
index b9dba0f8..e7fe5437 100644
--- a/src/i18n/locales/en/settings.json
+++ b/src/i18n/locales/en/settings.json
@@ -22,7 +22,9 @@
},
"appearance": {
"interface": "Interface",
- "language": { "title": "Language" },
+ "language": {
+ "title": "Language"
+ },
"colorTheme": "Color Theme",
"import": "Import",
"importTitle": "Import theme(s)",
@@ -61,18 +63,119 @@
}
},
"nav": {
- "appearance": { "label": "Appearance", "keywords": ["theme", "color", "font", "ui"] },
- "account": { "label": "Account", "keywords": ["profile", "login", "auth"] },
- "sync": { "label": "Sync", "keywords": ["backup", "cloud", "gist", "sync", "pro"] },
- "vaults": { "label": "Vaults", "keywords": ["secret", "password", "storage", "keyring"] },
- "plugins": { "label": "Plugins", "keywords": ["extensions", "addons", "marketplace"] },
- "terminal": { "label": "Terminal", "keywords": ["scrollback", "paste", "bracketed", "copy", "minimap", "buffer"] },
- "sftp": { "label": "SFTP", "keywords": ["file", "transfer", "ftp", "files"] },
- "portForwarding": { "label": "Port Forwarding", "keywords": ["tunnel", "forward", "ports", "ssh", "auto"] },
- "hosts": { "label": "Hosts", "keywords": ["ping", "reachability", "connectivity", "status"] },
- "shortcuts": { "label": "Shortcuts", "keywords": ["keybind", "hotkey", "shortcut", "rebind", "keyboard"] },
- "diagnostics": { "label": "Diagnostics", "keywords": ["diagnostics", "bug", "report", "log", "debug", "issue"] },
- "about": { "label": "About", "keywords": ["version", "update", "release", "changelog"] }
+ "appearance": {
+ "label": "Appearance",
+ "keywords": [
+ "theme",
+ "color",
+ "font",
+ "ui"
+ ]
+ },
+ "account": {
+ "label": "Account",
+ "keywords": [
+ "profile",
+ "login",
+ "auth"
+ ]
+ },
+ "sync": {
+ "label": "Sync",
+ "keywords": [
+ "backup",
+ "cloud",
+ "gist",
+ "sync",
+ "pro"
+ ]
+ },
+ "vaults": {
+ "label": "Vaults",
+ "keywords": [
+ "secret",
+ "password",
+ "storage",
+ "keyring"
+ ]
+ },
+ "plugins": {
+ "label": "Plugins",
+ "keywords": [
+ "extensions",
+ "addons",
+ "marketplace"
+ ]
+ },
+ "terminal": {
+ "label": "Terminal",
+ "keywords": [
+ "scrollback",
+ "paste",
+ "bracketed",
+ "copy",
+ "minimap",
+ "buffer"
+ ]
+ },
+ "sftp": {
+ "label": "SFTP",
+ "keywords": [
+ "file",
+ "transfer",
+ "ftp",
+ "files"
+ ]
+ },
+ "portForwarding": {
+ "label": "Port Forwarding",
+ "keywords": [
+ "tunnel",
+ "forward",
+ "ports",
+ "ssh",
+ "auto"
+ ]
+ },
+ "hosts": {
+ "label": "Hosts",
+ "keywords": [
+ "ping",
+ "reachability",
+ "connectivity",
+ "status"
+ ]
+ },
+ "shortcuts": {
+ "label": "Shortcuts",
+ "keywords": [
+ "keybind",
+ "hotkey",
+ "shortcut",
+ "rebind",
+ "keyboard"
+ ]
+ },
+ "diagnostics": {
+ "label": "Diagnostics",
+ "keywords": [
+ "diagnostics",
+ "bug",
+ "report",
+ "log",
+ "debug",
+ "issue"
+ ]
+ },
+ "about": {
+ "label": "About",
+ "keywords": [
+ "version",
+ "update",
+ "release",
+ "changelog"
+ ]
+ }
},
"chrome": {
"title": "Settings",
@@ -316,11 +419,26 @@
"prefsFooter": "Disabled types won't trigger automatic syncs when changed. Individual objects can also be excluded via their edit panel.",
"quickToggleLabel": "Sync {{label}}",
"objectType": {
- "connection": { "label": "Hosts", "sub": "SSH connections" },
- "identity": { "label": "Identities", "sub": "Usernames and credentials" },
- "key": { "label": "SSH Keys", "sub": "Key pairs stored in keychain" },
- "folder": { "label": "Folders", "sub": "Folder structure for organizing objects" },
- "port-forwarding-rule": { "label": "Port Forwarding", "sub": "Saved tunnel rules" }
+ "connection": {
+ "label": "Hosts",
+ "sub": "SSH connections"
+ },
+ "identity": {
+ "label": "Identities",
+ "sub": "Usernames and credentials"
+ },
+ "key": {
+ "label": "SSH Keys",
+ "sub": "Key pairs stored in keychain"
+ },
+ "folder": {
+ "label": "Folders",
+ "sub": "Folder structure for organizing objects"
+ },
+ "port-forwarding-rule": {
+ "label": "Port Forwarding",
+ "sub": "Saved tunnel rules"
+ }
}
},
"vaults": {
@@ -432,23 +550,74 @@
"terminalSessions": "Terminal Sessions"
},
"perm": {
- "VIEW_SECRETS": { "label": "View secrets", "desc": "See passwords and private keys in plain text" },
- "COPY_SECRETS": { "label": "Copy secrets", "desc": "Copy passwords and keys to clipboard" },
- "CONNECT": { "label": "Connect", "desc": "Launch SSH connections" },
- "EDIT_CONNECTIONS": { "label": "Edit connections", "desc": "Create, modify, and delete connections" },
- "EDIT_IDENTITIES": { "label": "Edit identities", "desc": "Create, modify, and delete SSH identities" },
- "EDIT_KEYS": { "label": "Edit keys", "desc": "Create, modify, and delete SSH keys" },
- "EDIT_SNIPPETS": { "label": "Edit snippets", "desc": "Create, modify, and delete command snippets" },
- "EDIT_FOLDERS": { "label": "Edit folders", "desc": "Manage folder structure" },
- "VIEW_AUDIT_LOG": { "label": "View audit log", "desc": "Read the activity audit log" },
- "INVITE_MEMBERS": { "label": "Invite members", "desc": "Invite new members to the vault" },
- "MANAGE_MEMBERS": { "label": "Manage members", "desc": "Assign roles and remove members" },
- "CREATE_CUSTOM_ROLES": { "label": "Manage roles (legacy)", "desc": "Retired permission — kept for compatibility" },
- "MANAGE_ROLES": { "label": "Manage roles", "desc": "Create, edit, and delete roles" },
- "MANAGE_VAULT": { "label": "Manage vault", "desc": "Rename vault and manage vault settings" },
- "START_TERMINAL_SESSION": { "label": "Start sessions", "desc": "Start multiplayer terminal sessions" },
- "JOIN_TERMINAL_SESSION": { "label": "Join sessions", "desc": "Join existing terminal sessions" },
- "VIEW_TERMINAL_SESSIONS": { "label": "View sessions", "desc": "See active terminal sessions" }
+ "VIEW_SECRETS": {
+ "label": "View secrets",
+ "desc": "See passwords and private keys in plain text"
+ },
+ "COPY_SECRETS": {
+ "label": "Copy secrets",
+ "desc": "Copy passwords and keys to clipboard"
+ },
+ "CONNECT": {
+ "label": "Connect",
+ "desc": "Launch SSH connections"
+ },
+ "EDIT_CONNECTIONS": {
+ "label": "Edit connections",
+ "desc": "Create, modify, and delete connections"
+ },
+ "EDIT_IDENTITIES": {
+ "label": "Edit identities",
+ "desc": "Create, modify, and delete SSH identities"
+ },
+ "EDIT_KEYS": {
+ "label": "Edit keys",
+ "desc": "Create, modify, and delete SSH keys"
+ },
+ "EDIT_SNIPPETS": {
+ "label": "Edit snippets",
+ "desc": "Create, modify, and delete command snippets"
+ },
+ "EDIT_FOLDERS": {
+ "label": "Edit folders",
+ "desc": "Manage folder structure"
+ },
+ "VIEW_AUDIT_LOG": {
+ "label": "View audit log",
+ "desc": "Read the activity audit log"
+ },
+ "INVITE_MEMBERS": {
+ "label": "Invite members",
+ "desc": "Invite new members to the vault"
+ },
+ "MANAGE_MEMBERS": {
+ "label": "Manage members",
+ "desc": "Assign roles and remove members"
+ },
+ "CREATE_CUSTOM_ROLES": {
+ "label": "Manage roles (legacy)",
+ "desc": "Retired permission — kept for compatibility"
+ },
+ "MANAGE_ROLES": {
+ "label": "Manage roles",
+ "desc": "Create, edit, and delete roles"
+ },
+ "MANAGE_VAULT": {
+ "label": "Manage vault",
+ "desc": "Rename vault and manage vault settings"
+ },
+ "START_TERMINAL_SESSION": {
+ "label": "Start sessions",
+ "desc": "Start multiplayer terminal sessions"
+ },
+ "JOIN_TERMINAL_SESSION": {
+ "label": "Join sessions",
+ "desc": "Join existing terminal sessions"
+ },
+ "VIEW_TERMINAL_SESSIONS": {
+ "label": "View sessions",
+ "desc": "See active terminal sessions"
+ }
},
"builtinBadge": "Built-in",
"dragToReorder": "Drag to reorder",
@@ -483,7 +652,9 @@
"categoryLabel": "Plugin",
"tabs": {
"installed": "Installed ({{count}})",
- "browse": "Browse"
+ "browse": "Browse",
+ "updateCount_one": "• {{count}} update",
+ "updateCount_other": "• {{count}} updates"
},
"installed": {
"filterPlaceholder": "Filter plugins…",
@@ -500,7 +671,10 @@
"sourceLocal": "Local",
"sourceUrl": "URL",
"sourceInstalled": "Installed",
- "unverified": "Unverified"
+ "unverified": "Unverified",
+ "update": "Update",
+ "updating": "Updating…",
+ "updateTitle": "Update to v{{version}}"
},
"browse": {
"searchPlaceholder": "Search plugins…",
@@ -517,11 +691,22 @@
"removing": "Removing…",
"uninstall": "Uninstall",
"installing": "Installing…",
- "install": "Install"
+ "install": "Install",
+ "updateBadge": "update available"
},
"install": {
"integrityFailed": "Plugin bundle failed its integrity check — install blocked.",
"failed": "Failed to install plugin."
+ },
+ "permissionModal": {
+ "installTitle": "Install {{name}}?",
+ "installBody": "This plugin runs with the app's full privileges. It requests these permissions:",
+ "updateTitle": "Update {{name}}?",
+ "updateBody": "This update requests new permissions. Review them before continuing:",
+ "newPermissions": "New permissions",
+ "noPermissions": "This plugin requests no special permissions.",
+ "installConfirm": "Install",
+ "updateConfirm": "Update"
}
},
"sftp": {
@@ -590,10 +775,22 @@
"desc": "How quickly a session is declared lost when the server stops responding. {{detail}}. Can be overridden per host."
},
"keepalivePresets": {
- "fast": { "label": "Fast", "detail": "Detects a dropped connection in ~4s" },
- "balanced": { "label": "Balanced", "detail": "~9s; tolerates a brief network blip" },
- "tolerant": { "label": "Tolerant", "detail": "~20s; for flaky networks" },
- "off": { "label": "Off", "detail": "No keepalive probes" }
+ "fast": {
+ "label": "Fast",
+ "detail": "Detects a dropped connection in ~4s"
+ },
+ "balanced": {
+ "label": "Balanced",
+ "detail": "~9s; tolerates a brief network blip"
+ },
+ "tolerant": {
+ "label": "Tolerant",
+ "detail": "~20s; for flaky networks"
+ },
+ "off": {
+ "label": "Off",
+ "detail": "No keepalive probes"
+ }
},
"persistentSessions": {
"title": "Persistent sessions",
@@ -611,26 +808,58 @@
}
},
"toggleDefs": {
- "scrollMinimap": { "label": "Scroll Minimap" },
- "selectToCopy": { "label": "Select to Copy" },
- "ignoreBracketedPaste": { "label": "Plain Paste (No Bracketed Paste)" },
- "autoForward": { "label": "Automatic Port Forwarding" },
- "forwardingNotifications": { "label": "Port Forwarding Notifications" },
- "sftpTar": { "label": "SFTP Tar Acceleration" },
- "sftpAutoRefresh": { "label": "SFTP Auto-Refresh" },
- "reachability": { "label": "Reachability Check" },
- "teamPresence": { "label": "Team Presence" },
- "shellIntegration": { "label": "Shell Integration" },
- "persistentSessions": { "label": "Persistent Sessions" },
- "restoreWorkspace": { "label": "Restore Workspace on Launch" },
- "crossDeviceSessions": { "label": "Cross-Device Sessions" },
- "changelogPopup": { "label": "Show What's New After Updates" },
+ "scrollMinimap": {
+ "label": "Scroll Minimap"
+ },
+ "selectToCopy": {
+ "label": "Select to Copy"
+ },
+ "ignoreBracketedPaste": {
+ "label": "Plain Paste (No Bracketed Paste)"
+ },
+ "autoForward": {
+ "label": "Automatic Port Forwarding"
+ },
+ "forwardingNotifications": {
+ "label": "Port Forwarding Notifications"
+ },
+ "sftpTar": {
+ "label": "SFTP Tar Acceleration"
+ },
+ "sftpAutoRefresh": {
+ "label": "SFTP Auto-Refresh"
+ },
+ "reachability": {
+ "label": "Reachability Check"
+ },
+ "teamPresence": {
+ "label": "Team Presence"
+ },
+ "shellIntegration": {
+ "label": "Shell Integration"
+ },
+ "persistentSessions": {
+ "label": "Persistent Sessions"
+ },
+ "restoreWorkspace": {
+ "label": "Restore Workspace on Launch"
+ },
+ "crossDeviceSessions": {
+ "label": "Cross-Device Sessions"
+ },
+ "changelogPopup": {
+ "label": "Show What's New After Updates"
+ },
"category": {
"appearance": "Appearance",
"portForwarding": "Port Forwarding",
"sftp": "SFTP",
"hosts": "Hosts",
- "updates": "Updates"
+ "updates": "Updates",
+ "plugins": "Plugins"
+ },
+ "pluginInstallReview": {
+ "label": "Review permissions before installing plugins"
}
},
"shortcuts": {
@@ -656,22 +885,70 @@
"other": "Other"
},
"items": {
- "omni": { "label": "Omni Search", "desc": "Access everything from one place" },
- "shortcuts": { "label": "Keyboard Shortcuts", "desc": "Open shortcut settings" },
- "themes": { "label": "Settings", "desc": "Open settings" },
- "newTab": { "label": "New Tab", "desc": "Go to hosts view" },
- "closeTab": { "label": "Close Tab", "desc": "Close active session" },
- "nextTab": { "label": "Next Tab", "desc": "Switch to next tab" },
- "prevTab": { "label": "Previous Tab", "desc": "Switch to prev tab" },
- "sidebar": { "label": "Toggle Sidebar", "desc": "Show/hide sidebar" },
- "delete": { "label": "Delete Selected", "desc": "Delete selected items" },
- "undo": { "label": "Undo", "desc": "Undo last action" },
- "redo": { "label": "Redo", "desc": "Redo last undone action" },
- "filter": { "label": "Focus Filter", "desc": "Focus the search filter" },
- "terminalSearch": { "label": "Find in Terminal", "desc": "Search the terminal scrollback" },
- "history": { "label": "History Panel", "desc": "Open command history in right panel" },
- "snippets": { "label": "Snippets Panel", "desc": "Open snippets in right panel" },
- "panelThemes": { "label": "Themes Panel", "desc": "Open themes in right panel" }
+ "omni": {
+ "label": "Omni Search",
+ "desc": "Access everything from one place"
+ },
+ "shortcuts": {
+ "label": "Keyboard Shortcuts",
+ "desc": "Open shortcut settings"
+ },
+ "themes": {
+ "label": "Settings",
+ "desc": "Open settings"
+ },
+ "newTab": {
+ "label": "New Tab",
+ "desc": "Go to hosts view"
+ },
+ "closeTab": {
+ "label": "Close Tab",
+ "desc": "Close active session"
+ },
+ "nextTab": {
+ "label": "Next Tab",
+ "desc": "Switch to next tab"
+ },
+ "prevTab": {
+ "label": "Previous Tab",
+ "desc": "Switch to prev tab"
+ },
+ "sidebar": {
+ "label": "Toggle Sidebar",
+ "desc": "Show/hide sidebar"
+ },
+ "delete": {
+ "label": "Delete Selected",
+ "desc": "Delete selected items"
+ },
+ "undo": {
+ "label": "Undo",
+ "desc": "Undo last action"
+ },
+ "redo": {
+ "label": "Redo",
+ "desc": "Redo last undone action"
+ },
+ "filter": {
+ "label": "Focus Filter",
+ "desc": "Focus the search filter"
+ },
+ "terminalSearch": {
+ "label": "Find in Terminal",
+ "desc": "Search the terminal scrollback"
+ },
+ "history": {
+ "label": "History Panel",
+ "desc": "Open command history in right panel"
+ },
+ "snippets": {
+ "label": "Snippets Panel",
+ "desc": "Open snippets in right panel"
+ },
+ "panelThemes": {
+ "label": "Themes Panel",
+ "desc": "Open themes in right panel"
+ }
}
}
}
diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json
index 63ea8f0f..c532c62b 100644
--- a/src/i18n/locales/fr/settings.json
+++ b/src/i18n/locales/fr/settings.json
@@ -22,7 +22,9 @@
},
"appearance": {
"interface": "Interface",
- "language": { "title": "Langue" },
+ "language": {
+ "title": "Langue"
+ },
"colorTheme": "Thème de couleurs",
"import": "Importer",
"importTitle": "Importer un ou plusieurs thèmes",
@@ -61,18 +63,119 @@
}
},
"nav": {
- "appearance": { "label": "Apparence", "keywords": ["thème", "couleur", "police", "interface"] },
- "account": { "label": "Compte", "keywords": ["profil", "connexion", "authentification"] },
- "sync": { "label": "Synchronisation", "keywords": ["sauvegarde", "nuage", "gist", "synchronisation", "pro"] },
- "vaults": { "label": "Coffres-forts", "keywords": ["secret", "mot de passe", "stockage", "trousseau"] },
- "plugins": { "label": "Extensions", "keywords": ["extensions", "modules", "boutique"] },
- "terminal": { "label": "Terminal", "keywords": ["historique", "collage", "crochets", "copier", "mini-carte", "tampon"] },
- "sftp": { "label": "SFTP", "keywords": ["fichier", "transfert", "ftp", "fichiers"] },
- "portForwarding": { "label": "Transfert de ports", "keywords": ["tunnel", "redirection", "ports", "ssh", "auto"] },
- "hosts": { "label": "Hôtes", "keywords": ["ping", "accessibilité", "connectivité", "statut"] },
- "shortcuts": { "label": "Raccourcis", "keywords": ["raccourci", "touche", "clavier", "rebind", "keyboard"] },
- "diagnostics": { "label": "Diagnostics", "keywords": ["diagnostics", "bug", "rapport", "journal", "débogage", "problème"] },
- "about": { "label": "À propos", "keywords": ["version", "mise à jour", "publication", "changelog"] }
+ "appearance": {
+ "label": "Apparence",
+ "keywords": [
+ "thème",
+ "couleur",
+ "police",
+ "interface"
+ ]
+ },
+ "account": {
+ "label": "Compte",
+ "keywords": [
+ "profil",
+ "connexion",
+ "authentification"
+ ]
+ },
+ "sync": {
+ "label": "Synchronisation",
+ "keywords": [
+ "sauvegarde",
+ "nuage",
+ "gist",
+ "synchronisation",
+ "pro"
+ ]
+ },
+ "vaults": {
+ "label": "Coffres-forts",
+ "keywords": [
+ "secret",
+ "mot de passe",
+ "stockage",
+ "trousseau"
+ ]
+ },
+ "plugins": {
+ "label": "Extensions",
+ "keywords": [
+ "extensions",
+ "modules",
+ "boutique"
+ ]
+ },
+ "terminal": {
+ "label": "Terminal",
+ "keywords": [
+ "historique",
+ "collage",
+ "crochets",
+ "copier",
+ "mini-carte",
+ "tampon"
+ ]
+ },
+ "sftp": {
+ "label": "SFTP",
+ "keywords": [
+ "fichier",
+ "transfert",
+ "ftp",
+ "fichiers"
+ ]
+ },
+ "portForwarding": {
+ "label": "Transfert de ports",
+ "keywords": [
+ "tunnel",
+ "redirection",
+ "ports",
+ "ssh",
+ "auto"
+ ]
+ },
+ "hosts": {
+ "label": "Hôtes",
+ "keywords": [
+ "ping",
+ "accessibilité",
+ "connectivité",
+ "statut"
+ ]
+ },
+ "shortcuts": {
+ "label": "Raccourcis",
+ "keywords": [
+ "raccourci",
+ "touche",
+ "clavier",
+ "rebind",
+ "keyboard"
+ ]
+ },
+ "diagnostics": {
+ "label": "Diagnostics",
+ "keywords": [
+ "diagnostics",
+ "bug",
+ "rapport",
+ "journal",
+ "débogage",
+ "problème"
+ ]
+ },
+ "about": {
+ "label": "À propos",
+ "keywords": [
+ "version",
+ "mise à jour",
+ "publication",
+ "changelog"
+ ]
+ }
},
"chrome": {
"title": "Paramètres",
@@ -316,11 +419,26 @@
"prefsFooter": "Les types désactivés ne déclenchent pas de synchronisation automatique lors de modifications. Les objets individuels peuvent également être exclus via leur panneau d'édition.",
"quickToggleLabel": "Synchroniser {{label}}",
"objectType": {
- "connection": { "label": "Hôtes", "sub": "Connexions SSH" },
- "identity": { "label": "Identités", "sub": "Noms d'utilisateur et informations d'identification" },
- "key": { "label": "Clés SSH", "sub": "Paires de clés stockées dans le trousseau" },
- "folder": { "label": "Dossiers", "sub": "Structure de dossiers pour organiser les objets" },
- "port-forwarding-rule": { "label": "Transfert de ports", "sub": "Règles de tunnel enregistrées" }
+ "connection": {
+ "label": "Hôtes",
+ "sub": "Connexions SSH"
+ },
+ "identity": {
+ "label": "Identités",
+ "sub": "Noms d'utilisateur et informations d'identification"
+ },
+ "key": {
+ "label": "Clés SSH",
+ "sub": "Paires de clés stockées dans le trousseau"
+ },
+ "folder": {
+ "label": "Dossiers",
+ "sub": "Structure de dossiers pour organiser les objets"
+ },
+ "port-forwarding-rule": {
+ "label": "Transfert de ports",
+ "sub": "Règles de tunnel enregistrées"
+ }
}
},
"vaults": {
@@ -432,23 +550,74 @@
"terminalSessions": "Sessions de terminal"
},
"perm": {
- "VIEW_SECRETS": { "label": "Voir les secrets", "desc": "Voir les mots de passe et clés privées en clair" },
- "COPY_SECRETS": { "label": "Copier les secrets", "desc": "Copier les mots de passe et clés dans le presse-papiers" },
- "CONNECT": { "label": "Se connecter", "desc": "Lancer des connexions SSH" },
- "EDIT_CONNECTIONS": { "label": "Modifier les connexions", "desc": "Créer, modifier et supprimer des connexions" },
- "EDIT_IDENTITIES": { "label": "Modifier les identités", "desc": "Créer, modifier et supprimer des identités SSH" },
- "EDIT_KEYS": { "label": "Modifier les clés", "desc": "Créer, modifier et supprimer des clés SSH" },
- "EDIT_SNIPPETS": { "label": "Modifier les snippets", "desc": "Créer, modifier et supprimer des extraits de commandes" },
- "EDIT_FOLDERS": { "label": "Modifier les dossiers", "desc": "Gérer la structure des dossiers" },
- "VIEW_AUDIT_LOG": { "label": "Voir le journal d'audit", "desc": "Lire le journal d'activité" },
- "INVITE_MEMBERS": { "label": "Inviter des membres", "desc": "Inviter de nouveaux membres dans le coffre-fort" },
- "MANAGE_MEMBERS": { "label": "Gérer les membres", "desc": "Attribuer des rôles et retirer des membres" },
- "CREATE_CUSTOM_ROLES": { "label": "Gérer les rôles (hérité)", "desc": "Permission retirée — conservée pour compatibilité" },
- "MANAGE_ROLES": { "label": "Gérer les rôles", "desc": "Créer, modifier et supprimer des rôles" },
- "MANAGE_VAULT": { "label": "Gérer le coffre-fort", "desc": "Renommer le coffre-fort et gérer ses paramètres" },
- "START_TERMINAL_SESSION": { "label": "Démarrer des sessions", "desc": "Démarrer des sessions de terminal multijoueur" },
- "JOIN_TERMINAL_SESSION": { "label": "Rejoindre des sessions", "desc": "Rejoindre des sessions de terminal existantes" },
- "VIEW_TERMINAL_SESSIONS": { "label": "Voir les sessions", "desc": "Voir les sessions de terminal actives" }
+ "VIEW_SECRETS": {
+ "label": "Voir les secrets",
+ "desc": "Voir les mots de passe et clés privées en clair"
+ },
+ "COPY_SECRETS": {
+ "label": "Copier les secrets",
+ "desc": "Copier les mots de passe et clés dans le presse-papiers"
+ },
+ "CONNECT": {
+ "label": "Se connecter",
+ "desc": "Lancer des connexions SSH"
+ },
+ "EDIT_CONNECTIONS": {
+ "label": "Modifier les connexions",
+ "desc": "Créer, modifier et supprimer des connexions"
+ },
+ "EDIT_IDENTITIES": {
+ "label": "Modifier les identités",
+ "desc": "Créer, modifier et supprimer des identités SSH"
+ },
+ "EDIT_KEYS": {
+ "label": "Modifier les clés",
+ "desc": "Créer, modifier et supprimer des clés SSH"
+ },
+ "EDIT_SNIPPETS": {
+ "label": "Modifier les snippets",
+ "desc": "Créer, modifier et supprimer des extraits de commandes"
+ },
+ "EDIT_FOLDERS": {
+ "label": "Modifier les dossiers",
+ "desc": "Gérer la structure des dossiers"
+ },
+ "VIEW_AUDIT_LOG": {
+ "label": "Voir le journal d'audit",
+ "desc": "Lire le journal d'activité"
+ },
+ "INVITE_MEMBERS": {
+ "label": "Inviter des membres",
+ "desc": "Inviter de nouveaux membres dans le coffre-fort"
+ },
+ "MANAGE_MEMBERS": {
+ "label": "Gérer les membres",
+ "desc": "Attribuer des rôles et retirer des membres"
+ },
+ "CREATE_CUSTOM_ROLES": {
+ "label": "Gérer les rôles (hérité)",
+ "desc": "Permission retirée — conservée pour compatibilité"
+ },
+ "MANAGE_ROLES": {
+ "label": "Gérer les rôles",
+ "desc": "Créer, modifier et supprimer des rôles"
+ },
+ "MANAGE_VAULT": {
+ "label": "Gérer le coffre-fort",
+ "desc": "Renommer le coffre-fort et gérer ses paramètres"
+ },
+ "START_TERMINAL_SESSION": {
+ "label": "Démarrer des sessions",
+ "desc": "Démarrer des sessions de terminal multijoueur"
+ },
+ "JOIN_TERMINAL_SESSION": {
+ "label": "Rejoindre des sessions",
+ "desc": "Rejoindre des sessions de terminal existantes"
+ },
+ "VIEW_TERMINAL_SESSIONS": {
+ "label": "Voir les sessions",
+ "desc": "Voir les sessions de terminal actives"
+ }
},
"builtinBadge": "Intégré",
"dragToReorder": "Glisser pour réordonner",
@@ -483,7 +652,9 @@
"categoryLabel": "Extension",
"tabs": {
"installed": "Installées ({{count}})",
- "browse": "Parcourir"
+ "browse": "Parcourir",
+ "updateCount_one": "• {{count}} mise à jour",
+ "updateCount_other": "• {{count}} mises à jour"
},
"installed": {
"filterPlaceholder": "Filtrer les extensions…",
@@ -500,7 +671,10 @@
"sourceLocal": "Locale",
"sourceUrl": "URL",
"sourceInstalled": "Installée",
- "unverified": "Non vérifié"
+ "unverified": "Non vérifié",
+ "update": "Mettre à jour",
+ "updating": "Mise à jour…",
+ "updateTitle": "Mettre à jour vers la v{{version}}"
},
"browse": {
"searchPlaceholder": "Rechercher des extensions…",
@@ -517,11 +691,22 @@
"removing": "Suppression…",
"uninstall": "Désinstaller",
"installing": "Installation…",
- "install": "Installer"
+ "install": "Installer",
+ "updateBadge": "mise à jour disponible"
},
"install": {
"integrityFailed": "L'intégrité du module a échoué — installation bloquée.",
"failed": "Échec de l'installation du module."
+ },
+ "permissionModal": {
+ "installTitle": "Installer {{name}} ?",
+ "installBody": "Ce plugin s'exécute avec tous les privilèges de l'application. Il demande ces autorisations :",
+ "updateTitle": "Mettre à jour {{name}} ?",
+ "updateBody": "Cette mise à jour demande de nouvelles autorisations. Vérifiez-les avant de continuer :",
+ "newPermissions": "Nouvelles autorisations",
+ "noPermissions": "Ce plugin ne demande aucune autorisation particulière.",
+ "installConfirm": "Installer",
+ "updateConfirm": "Mettre à jour"
}
},
"sftp": {
@@ -590,10 +775,22 @@
"desc": "Délai avant qu'une session soit considérée comme perdue lorsque le serveur cesse de répondre. {{detail}}. Peut être remplacé par hôte."
},
"keepalivePresets": {
- "fast": { "label": "Rapide", "detail": "Détecte une connexion perdue en ~4s" },
- "balanced": { "label": "Équilibré", "detail": "~9s ; tolère une brève coupure réseau" },
- "tolerant": { "label": "Tolérant", "detail": "~20s ; pour les réseaux instables" },
- "off": { "label": "Désactivé", "detail": "Aucune sonde de keepalive" }
+ "fast": {
+ "label": "Rapide",
+ "detail": "Détecte une connexion perdue en ~4s"
+ },
+ "balanced": {
+ "label": "Équilibré",
+ "detail": "~9s ; tolère une brève coupure réseau"
+ },
+ "tolerant": {
+ "label": "Tolérant",
+ "detail": "~20s ; pour les réseaux instables"
+ },
+ "off": {
+ "label": "Désactivé",
+ "detail": "Aucune sonde de keepalive"
+ }
},
"persistentSessions": {
"title": "Sessions persistantes",
@@ -611,26 +808,58 @@
}
},
"toggleDefs": {
- "scrollMinimap": { "label": "Minimap de défilement" },
- "selectToCopy": { "label": "Sélectionner pour copier" },
- "ignoreBracketedPaste": { "label": "Collage simple (sans crochets)" },
- "autoForward": { "label": "Transfert de ports automatique" },
- "forwardingNotifications": { "label": "Notifications de transfert de ports" },
- "sftpTar": { "label": "Accélération Tar SFTP" },
- "sftpAutoRefresh": { "label": "Actualisation automatique SFTP" },
- "reachability": { "label": "Vérification d'accessibilité" },
- "teamPresence": { "label": "Présence d'équipe" },
- "shellIntegration": { "label": "Intégration du shell" },
- "persistentSessions": { "label": "Sessions persistantes" },
- "restoreWorkspace": { "label": "Restaurer l'espace de travail au lancement" },
- "crossDeviceSessions": { "label": "Sessions multi-appareils" },
- "changelogPopup": { "label": "Afficher les nouveautés après les mises à jour" },
+ "scrollMinimap": {
+ "label": "Minimap de défilement"
+ },
+ "selectToCopy": {
+ "label": "Sélectionner pour copier"
+ },
+ "ignoreBracketedPaste": {
+ "label": "Collage simple (sans crochets)"
+ },
+ "autoForward": {
+ "label": "Transfert de ports automatique"
+ },
+ "forwardingNotifications": {
+ "label": "Notifications de transfert de ports"
+ },
+ "sftpTar": {
+ "label": "Accélération Tar SFTP"
+ },
+ "sftpAutoRefresh": {
+ "label": "Actualisation automatique SFTP"
+ },
+ "reachability": {
+ "label": "Vérification d'accessibilité"
+ },
+ "teamPresence": {
+ "label": "Présence d'équipe"
+ },
+ "shellIntegration": {
+ "label": "Intégration du shell"
+ },
+ "persistentSessions": {
+ "label": "Sessions persistantes"
+ },
+ "restoreWorkspace": {
+ "label": "Restaurer l'espace de travail au lancement"
+ },
+ "crossDeviceSessions": {
+ "label": "Sessions multi-appareils"
+ },
+ "changelogPopup": {
+ "label": "Afficher les nouveautés après les mises à jour"
+ },
"category": {
"appearance": "Apparence",
"portForwarding": "Transfert de ports",
"sftp": "SFTP",
"hosts": "Hôtes",
- "updates": "Mises à jour"
+ "updates": "Mises à jour",
+ "plugins": "Plugins"
+ },
+ "pluginInstallReview": {
+ "label": "Vérifier les autorisations avant d'installer des plugins"
}
},
"shortcuts": {
@@ -656,22 +885,70 @@
"other": "Autre"
},
"items": {
- "omni": { "label": "Recherche universelle", "desc": "Accéder à tout depuis un seul endroit" },
- "shortcuts": { "label": "Raccourcis clavier", "desc": "Ouvrir les paramètres des raccourcis" },
- "themes": { "label": "Paramètres", "desc": "Ouvrir les paramètres" },
- "newTab": { "label": "Nouvel onglet", "desc": "Aller à la vue des hôtes" },
- "closeTab": { "label": "Fermer l'onglet", "desc": "Fermer la session active" },
- "nextTab": { "label": "Onglet suivant", "desc": "Passer à l'onglet suivant" },
- "prevTab": { "label": "Onglet précédent", "desc": "Passer à l'onglet précédent" },
- "sidebar": { "label": "Basculer la barre latérale", "desc": "Afficher/masquer la barre latérale" },
- "delete": { "label": "Supprimer la sélection", "desc": "Supprimer les éléments sélectionnés" },
- "undo": { "label": "Annuler", "desc": "Annuler la dernière action" },
- "redo": { "label": "Rétablir", "desc": "Rétablir la dernière action annulée" },
- "filter": { "label": "Focus sur le filtre", "desc": "Mettre le focus sur le filtre de recherche" },
- "terminalSearch": { "label": "Rechercher dans le terminal", "desc": "Rechercher dans l'historique du terminal" },
- "history": { "label": "Panneau d'historique", "desc": "Ouvrir l'historique des commandes dans le panneau latéral" },
- "snippets": { "label": "Panneau des extraits", "desc": "Ouvrir les extraits dans le panneau latéral" },
- "panelThemes": { "label": "Panneau des thèmes", "desc": "Ouvrir les thèmes dans le panneau latéral" }
+ "omni": {
+ "label": "Recherche universelle",
+ "desc": "Accéder à tout depuis un seul endroit"
+ },
+ "shortcuts": {
+ "label": "Raccourcis clavier",
+ "desc": "Ouvrir les paramètres des raccourcis"
+ },
+ "themes": {
+ "label": "Paramètres",
+ "desc": "Ouvrir les paramètres"
+ },
+ "newTab": {
+ "label": "Nouvel onglet",
+ "desc": "Aller à la vue des hôtes"
+ },
+ "closeTab": {
+ "label": "Fermer l'onglet",
+ "desc": "Fermer la session active"
+ },
+ "nextTab": {
+ "label": "Onglet suivant",
+ "desc": "Passer à l'onglet suivant"
+ },
+ "prevTab": {
+ "label": "Onglet précédent",
+ "desc": "Passer à l'onglet précédent"
+ },
+ "sidebar": {
+ "label": "Basculer la barre latérale",
+ "desc": "Afficher/masquer la barre latérale"
+ },
+ "delete": {
+ "label": "Supprimer la sélection",
+ "desc": "Supprimer les éléments sélectionnés"
+ },
+ "undo": {
+ "label": "Annuler",
+ "desc": "Annuler la dernière action"
+ },
+ "redo": {
+ "label": "Rétablir",
+ "desc": "Rétablir la dernière action annulée"
+ },
+ "filter": {
+ "label": "Focus sur le filtre",
+ "desc": "Mettre le focus sur le filtre de recherche"
+ },
+ "terminalSearch": {
+ "label": "Rechercher dans le terminal",
+ "desc": "Rechercher dans l'historique du terminal"
+ },
+ "history": {
+ "label": "Panneau d'historique",
+ "desc": "Ouvrir l'historique des commandes dans le panneau latéral"
+ },
+ "snippets": {
+ "label": "Panneau des extraits",
+ "desc": "Ouvrir les extraits dans le panneau latéral"
+ },
+ "panelThemes": {
+ "label": "Panneau des thèmes",
+ "desc": "Ouvrir les thèmes dans le panneau latéral"
+ }
}
}
}
diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json
index d3eba3b2..5b388509 100644
--- a/src/i18n/locales/ru/settings.json
+++ b/src/i18n/locales/ru/settings.json
@@ -22,7 +22,9 @@
},
"appearance": {
"interface": "Интерфейс",
- "language": { "title": "Язык" },
+ "language": {
+ "title": "Язык"
+ },
"colorTheme": "Цветовая тема",
"import": "Импорт",
"importTitle": "Импортировать тему(-ы)",
@@ -61,18 +63,119 @@
}
},
"nav": {
- "appearance": { "label": "Оформление", "keywords": ["тема", "цвет", "шрифт", "интерфейс"] },
- "account": { "label": "Аккаунт", "keywords": ["профиль", "вход", "аутентификация"] },
- "sync": { "label": "Синхронизация", "keywords": ["резервная копия", "облако", "gist", "синхронизация", "pro"] },
- "vaults": { "label": "Хранилища", "keywords": ["секрет", "пароль", "хранилище", "связка ключей"] },
- "plugins": { "label": "Плагины", "keywords": ["расширения", "дополнения", "маркетплейс"] },
- "terminal": { "label": "Терминал", "keywords": ["буфер", "вставка", "скобочная", "копирование", "мини-карта", "прокрутка"] },
- "sftp": { "label": "SFTP", "keywords": ["файл", "передача", "ftp", "файлы"] },
- "portForwarding": { "label": "Проброс портов", "keywords": ["туннель", "проброс", "порты", "ssh", "авто"] },
- "hosts": { "label": "Хосты", "keywords": ["ping", "доступность", "подключение", "статус"] },
- "shortcuts": { "label": "Сочетания клавиш", "keywords": ["сочетание клавиш", "горячая клавиша", "шорткат", "переназначить", "клавиатура"] },
- "diagnostics": { "label": "Диагностика", "keywords": ["диагностика", "баг", "отчёт", "журнал", "отладка", "проблема"] },
- "about": { "label": "О программе", "keywords": ["версия", "обновление", "релиз", "список изменений"] }
+ "appearance": {
+ "label": "Оформление",
+ "keywords": [
+ "тема",
+ "цвет",
+ "шрифт",
+ "интерфейс"
+ ]
+ },
+ "account": {
+ "label": "Аккаунт",
+ "keywords": [
+ "профиль",
+ "вход",
+ "аутентификация"
+ ]
+ },
+ "sync": {
+ "label": "Синхронизация",
+ "keywords": [
+ "резервная копия",
+ "облако",
+ "gist",
+ "синхронизация",
+ "pro"
+ ]
+ },
+ "vaults": {
+ "label": "Хранилища",
+ "keywords": [
+ "секрет",
+ "пароль",
+ "хранилище",
+ "связка ключей"
+ ]
+ },
+ "plugins": {
+ "label": "Плагины",
+ "keywords": [
+ "расширения",
+ "дополнения",
+ "маркетплейс"
+ ]
+ },
+ "terminal": {
+ "label": "Терминал",
+ "keywords": [
+ "буфер",
+ "вставка",
+ "скобочная",
+ "копирование",
+ "мини-карта",
+ "прокрутка"
+ ]
+ },
+ "sftp": {
+ "label": "SFTP",
+ "keywords": [
+ "файл",
+ "передача",
+ "ftp",
+ "файлы"
+ ]
+ },
+ "portForwarding": {
+ "label": "Проброс портов",
+ "keywords": [
+ "туннель",
+ "проброс",
+ "порты",
+ "ssh",
+ "авто"
+ ]
+ },
+ "hosts": {
+ "label": "Хосты",
+ "keywords": [
+ "ping",
+ "доступность",
+ "подключение",
+ "статус"
+ ]
+ },
+ "shortcuts": {
+ "label": "Сочетания клавиш",
+ "keywords": [
+ "сочетание клавиш",
+ "горячая клавиша",
+ "шорткат",
+ "переназначить",
+ "клавиатура"
+ ]
+ },
+ "diagnostics": {
+ "label": "Диагностика",
+ "keywords": [
+ "диагностика",
+ "баг",
+ "отчёт",
+ "журнал",
+ "отладка",
+ "проблема"
+ ]
+ },
+ "about": {
+ "label": "О программе",
+ "keywords": [
+ "версия",
+ "обновление",
+ "релиз",
+ "список изменений"
+ ]
+ }
},
"chrome": {
"title": "Настройки",
@@ -320,11 +423,26 @@
"prefsFooter": "Отключённые типы не будут запускать автоматическую синхронизацию при изменении. Отдельные объекты также можно исключить в панели редактирования.",
"quickToggleLabel": "Синхронизировать {{label}}",
"objectType": {
- "connection": { "label": "Хосты", "sub": "SSH-подключения" },
- "identity": { "label": "Личности", "sub": "Имена пользователей и учётные данные" },
- "key": { "label": "SSH-ключи", "sub": "Пары ключей, хранящиеся в связке ключей" },
- "folder": { "label": "Папки", "sub": "Структура папок для организации объектов" },
- "port-forwarding-rule": { "label": "Проброс портов", "sub": "Сохранённые правила туннелей" }
+ "connection": {
+ "label": "Хосты",
+ "sub": "SSH-подключения"
+ },
+ "identity": {
+ "label": "Личности",
+ "sub": "Имена пользователей и учётные данные"
+ },
+ "key": {
+ "label": "SSH-ключи",
+ "sub": "Пары ключей, хранящиеся в связке ключей"
+ },
+ "folder": {
+ "label": "Папки",
+ "sub": "Структура папок для организации объектов"
+ },
+ "port-forwarding-rule": {
+ "label": "Проброс портов",
+ "sub": "Сохранённые правила туннелей"
+ }
}
},
"vaults": {
@@ -444,23 +562,74 @@
"terminalSessions": "Сессии терминала"
},
"perm": {
- "VIEW_SECRETS": { "label": "Просмотр секретов", "desc": "Просматривать пароли и приватные ключи в открытом виде" },
- "COPY_SECRETS": { "label": "Копирование секретов", "desc": "Копировать пароли и ключи в буфер обмена" },
- "CONNECT": { "label": "Подключение", "desc": "Запускать SSH-подключения" },
- "EDIT_CONNECTIONS": { "label": "Изменение подключений", "desc": "Создавать, изменять и удалять подключения" },
- "EDIT_IDENTITIES": { "label": "Изменение личностей", "desc": "Создавать, изменять и удалять SSH-личности" },
- "EDIT_KEYS": { "label": "Изменение ключей", "desc": "Создавать, изменять и удалять SSH-ключи" },
- "EDIT_SNIPPETS": { "label": "Изменение сниппетов", "desc": "Создавать, изменять и удалять сниппеты команд" },
- "EDIT_FOLDERS": { "label": "Изменение папок", "desc": "Управлять структурой папок" },
- "VIEW_AUDIT_LOG": { "label": "Просмотр журнала аудита", "desc": "Читать журнал аудита действий" },
- "INVITE_MEMBERS": { "label": "Приглашение участников", "desc": "Приглашать новых участников в хранилище" },
- "MANAGE_MEMBERS": { "label": "Управление участниками", "desc": "Назначать роли и удалять участников" },
- "CREATE_CUSTOM_ROLES": { "label": "Управление ролями (устар.)", "desc": "Устаревшее право — сохранено для совместимости" },
- "MANAGE_ROLES": { "label": "Управление ролями", "desc": "Создавать, изменять и удалять роли" },
- "MANAGE_VAULT": { "label": "Управление хранилищем", "desc": "Переименовывать хранилище и управлять его настройками" },
- "START_TERMINAL_SESSION": { "label": "Запуск сессий", "desc": "Запускать совместные сессии терминала" },
- "JOIN_TERMINAL_SESSION": { "label": "Присоединение к сессиям", "desc": "Присоединяться к существующим сессиям терминала" },
- "VIEW_TERMINAL_SESSIONS": { "label": "Просмотр сессий", "desc": "Видеть активные сессии терминала" }
+ "VIEW_SECRETS": {
+ "label": "Просмотр секретов",
+ "desc": "Просматривать пароли и приватные ключи в открытом виде"
+ },
+ "COPY_SECRETS": {
+ "label": "Копирование секретов",
+ "desc": "Копировать пароли и ключи в буфер обмена"
+ },
+ "CONNECT": {
+ "label": "Подключение",
+ "desc": "Запускать SSH-подключения"
+ },
+ "EDIT_CONNECTIONS": {
+ "label": "Изменение подключений",
+ "desc": "Создавать, изменять и удалять подключения"
+ },
+ "EDIT_IDENTITIES": {
+ "label": "Изменение личностей",
+ "desc": "Создавать, изменять и удалять SSH-личности"
+ },
+ "EDIT_KEYS": {
+ "label": "Изменение ключей",
+ "desc": "Создавать, изменять и удалять SSH-ключи"
+ },
+ "EDIT_SNIPPETS": {
+ "label": "Изменение сниппетов",
+ "desc": "Создавать, изменять и удалять сниппеты команд"
+ },
+ "EDIT_FOLDERS": {
+ "label": "Изменение папок",
+ "desc": "Управлять структурой папок"
+ },
+ "VIEW_AUDIT_LOG": {
+ "label": "Просмотр журнала аудита",
+ "desc": "Читать журнал аудита действий"
+ },
+ "INVITE_MEMBERS": {
+ "label": "Приглашение участников",
+ "desc": "Приглашать новых участников в хранилище"
+ },
+ "MANAGE_MEMBERS": {
+ "label": "Управление участниками",
+ "desc": "Назначать роли и удалять участников"
+ },
+ "CREATE_CUSTOM_ROLES": {
+ "label": "Управление ролями (устар.)",
+ "desc": "Устаревшее право — сохранено для совместимости"
+ },
+ "MANAGE_ROLES": {
+ "label": "Управление ролями",
+ "desc": "Создавать, изменять и удалять роли"
+ },
+ "MANAGE_VAULT": {
+ "label": "Управление хранилищем",
+ "desc": "Переименовывать хранилище и управлять его настройками"
+ },
+ "START_TERMINAL_SESSION": {
+ "label": "Запуск сессий",
+ "desc": "Запускать совместные сессии терминала"
+ },
+ "JOIN_TERMINAL_SESSION": {
+ "label": "Присоединение к сессиям",
+ "desc": "Присоединяться к существующим сессиям терминала"
+ },
+ "VIEW_TERMINAL_SESSIONS": {
+ "label": "Просмотр сессий",
+ "desc": "Видеть активные сессии терминала"
+ }
},
"builtinBadge": "Встроенная",
"dragToReorder": "Перетащите для изменения порядка",
@@ -495,7 +664,11 @@
"categoryLabel": "Плагин",
"tabs": {
"installed": "Установлено ({{count}})",
- "browse": "Обзор"
+ "browse": "Обзор",
+ "updateCount_one": "• {{count}} обновление",
+ "updateCount_few": "• {{count}} обновления",
+ "updateCount_many": "• {{count}} обновлений",
+ "updateCount_other": "• {{count}} обновлений"
},
"installed": {
"filterPlaceholder": "Фильтр плагинов…",
@@ -511,7 +684,11 @@
"noneInstalled": "Нет установленных плагинов.",
"sourceLocal": "Локальный",
"sourceUrl": "URL",
- "sourceInstalled": "Установлен"
+ "sourceInstalled": "Установлен",
+ "update": "Обновить",
+ "updating": "Обновление…",
+ "updateTitle": "Обновить до v{{version}}",
+ "unverified": "Непроверено"
},
"browse": {
"searchPlaceholder": "Поиск плагинов…",
@@ -528,7 +705,22 @@
"removing": "Удаление…",
"uninstall": "Удалить",
"installing": "Установка…",
- "install": "Установить"
+ "install": "Установить",
+ "updateBadge": "доступно обновление"
+ },
+ "permissionModal": {
+ "installTitle": "Установить {{name}}?",
+ "installBody": "Этот плагин выполняется с полными правами приложения. Он запрашивает следующие разрешения:",
+ "updateTitle": "Обновить {{name}}?",
+ "updateBody": "Это обновление запрашивает новые разрешения. Проверьте их перед продолжением:",
+ "newPermissions": "Новые разрешения",
+ "noPermissions": "Этот плагин не запрашивает особых разрешений.",
+ "installConfirm": "Установить",
+ "updateConfirm": "Обновить"
+ },
+ "install": {
+ "integrityFailed": "Пакет плагина не прошёл проверку целостности — установка заблокирована.",
+ "failed": "Не удалось установить плагин."
}
},
"sftp": {
@@ -597,10 +789,22 @@
"desc": "Как быстро сессия считается потерянной, если сервер перестаёт отвечать. {{detail}}. Можно переопределить для отдельного хоста."
},
"keepalivePresets": {
- "fast": { "label": "Быстрый", "detail": "Обнаруживает разрыв соединения примерно за 4 с" },
- "balanced": { "label": "Сбалансированный", "detail": "~9 с; выдерживает кратковременный сбой сети" },
- "tolerant": { "label": "Устойчивый", "detail": "~20 с; для нестабильных сетей" },
- "off": { "label": "Выкл", "detail": "Без проверок keepalive" }
+ "fast": {
+ "label": "Быстрый",
+ "detail": "Обнаруживает разрыв соединения примерно за 4 с"
+ },
+ "balanced": {
+ "label": "Сбалансированный",
+ "detail": "~9 с; выдерживает кратковременный сбой сети"
+ },
+ "tolerant": {
+ "label": "Устойчивый",
+ "detail": "~20 с; для нестабильных сетей"
+ },
+ "off": {
+ "label": "Выкл",
+ "detail": "Без проверок keepalive"
+ }
},
"persistentSessions": {
"title": "Постоянные сессии",
@@ -618,26 +822,58 @@
}
},
"toggleDefs": {
- "scrollMinimap": { "label": "Мини-карта прокрутки" },
- "selectToCopy": { "label": "Копирование по выделению" },
- "ignoreBracketedPaste": { "label": "Простая вставка (без «скобок»)" },
- "autoForward": { "label": "Автоматический проброс портов" },
- "forwardingNotifications": { "label": "Уведомления о пробросе портов" },
- "sftpTar": { "label": "Ускорение SFTP через tar" },
- "sftpAutoRefresh": { "label": "Автообновление SFTP" },
- "reachability": { "label": "Проверка доступности" },
- "teamPresence": { "label": "Присутствие команды" },
- "shellIntegration": { "label": "Интеграция с оболочкой" },
- "persistentSessions": { "label": "Постоянные сессии" },
- "restoreWorkspace": { "label": "Восстанавливать рабочее пространство при запуске" },
- "crossDeviceSessions": { "label": "Сессии между устройствами" },
- "changelogPopup": { "label": "Показывать список изменений после обновления" },
+ "scrollMinimap": {
+ "label": "Мини-карта прокрутки"
+ },
+ "selectToCopy": {
+ "label": "Копирование по выделению"
+ },
+ "ignoreBracketedPaste": {
+ "label": "Простая вставка (без «скобок»)"
+ },
+ "autoForward": {
+ "label": "Автоматический проброс портов"
+ },
+ "forwardingNotifications": {
+ "label": "Уведомления о пробросе портов"
+ },
+ "sftpTar": {
+ "label": "Ускорение SFTP через tar"
+ },
+ "sftpAutoRefresh": {
+ "label": "Автообновление SFTP"
+ },
+ "reachability": {
+ "label": "Проверка доступности"
+ },
+ "teamPresence": {
+ "label": "Присутствие команды"
+ },
+ "shellIntegration": {
+ "label": "Интеграция с оболочкой"
+ },
+ "persistentSessions": {
+ "label": "Постоянные сессии"
+ },
+ "restoreWorkspace": {
+ "label": "Восстанавливать рабочее пространство при запуске"
+ },
+ "crossDeviceSessions": {
+ "label": "Сессии между устройствами"
+ },
+ "changelogPopup": {
+ "label": "Показывать список изменений после обновления"
+ },
"category": {
"appearance": "Оформление",
"portForwarding": "Проброс портов",
"sftp": "SFTP",
"hosts": "Хосты",
- "updates": "Обновления"
+ "updates": "Обновления",
+ "plugins": "Плагины"
+ },
+ "pluginInstallReview": {
+ "label": "Проверять разрешения перед установкой плагинов"
}
},
"shortcuts": {
@@ -665,22 +901,70 @@
"other": "Другое"
},
"items": {
- "omni": { "label": "Универсальный поиск", "desc": "Доступ ко всему из одного места" },
- "shortcuts": { "label": "Сочетания клавиш", "desc": "Открыть настройки сочетаний клавиш" },
- "themes": { "label": "Настройки", "desc": "Открыть настройки" },
- "newTab": { "label": "Новая вкладка", "desc": "Перейти к списку хостов" },
- "closeTab": { "label": "Закрыть вкладку", "desc": "Закрыть активную сессию" },
- "nextTab": { "label": "Следующая вкладка", "desc": "Переключиться на следующую вкладку" },
- "prevTab": { "label": "Предыдущая вкладка", "desc": "Переключиться на предыдущую вкладку" },
- "sidebar": { "label": "Показать/скрыть боковую панель", "desc": "Показать или скрыть боковую панель" },
- "delete": { "label": "Удалить выбранное", "desc": "Удалить выбранные элементы" },
- "undo": { "label": "Отменить", "desc": "Отменить последнее действие" },
- "redo": { "label": "Повторить", "desc": "Повторить последнее отменённое действие" },
- "filter": { "label": "Фокус на фильтре", "desc": "Установить фокус на поле фильтра" },
- "terminalSearch": { "label": "Поиск в терминале", "desc": "Искать в истории терминала" },
- "history": { "label": "Панель истории", "desc": "Открыть историю команд в правой панели" },
- "snippets": { "label": "Панель сниппетов", "desc": "Открыть сниппеты в правой панели" },
- "panelThemes": { "label": "Панель тем", "desc": "Открыть темы в правой панели" }
+ "omni": {
+ "label": "Универсальный поиск",
+ "desc": "Доступ ко всему из одного места"
+ },
+ "shortcuts": {
+ "label": "Сочетания клавиш",
+ "desc": "Открыть настройки сочетаний клавиш"
+ },
+ "themes": {
+ "label": "Настройки",
+ "desc": "Открыть настройки"
+ },
+ "newTab": {
+ "label": "Новая вкладка",
+ "desc": "Перейти к списку хостов"
+ },
+ "closeTab": {
+ "label": "Закрыть вкладку",
+ "desc": "Закрыть активную сессию"
+ },
+ "nextTab": {
+ "label": "Следующая вкладка",
+ "desc": "Переключиться на следующую вкладку"
+ },
+ "prevTab": {
+ "label": "Предыдущая вкладка",
+ "desc": "Переключиться на предыдущую вкладку"
+ },
+ "sidebar": {
+ "label": "Показать/скрыть боковую панель",
+ "desc": "Показать или скрыть боковую панель"
+ },
+ "delete": {
+ "label": "Удалить выбранное",
+ "desc": "Удалить выбранные элементы"
+ },
+ "undo": {
+ "label": "Отменить",
+ "desc": "Отменить последнее действие"
+ },
+ "redo": {
+ "label": "Повторить",
+ "desc": "Повторить последнее отменённое действие"
+ },
+ "filter": {
+ "label": "Фокус на фильтре",
+ "desc": "Установить фокус на поле фильтра"
+ },
+ "terminalSearch": {
+ "label": "Поиск в терминале",
+ "desc": "Искать в истории терминала"
+ },
+ "history": {
+ "label": "Панель истории",
+ "desc": "Открыть историю команд в правой панели"
+ },
+ "snippets": {
+ "label": "Панель сниппетов",
+ "desc": "Открыть сниппеты в правой панели"
+ },
+ "panelThemes": {
+ "label": "Панель тем",
+ "desc": "Открыть темы в правой панели"
+ }
}
}
}
diff --git a/src/plugins/updates.test.ts b/src/plugins/updates.test.ts
new file mode 100644
index 00000000..7d69211d
--- /dev/null
+++ b/src/plugins/updates.test.ts
@@ -0,0 +1,127 @@
+import { test, expect } from "vitest";
+import { compareSemver, availableUpdate, addedPermissions } from "./updates";
+import type { InstalledPluginMeta, MarketplacePlugin } from "@/stores/marketplaceStore";
+
+// ─── compareSemver ─────────────────────────────────────────────────────────
+
+test("compareSemver orders major/minor/patch", () => {
+ expect(compareSemver("1.1.0", "1.0.0")).toBe(1);
+ expect(compareSemver("1.0.0", "1.1.0")).toBe(-1);
+ expect(compareSemver("2.0.0", "1.9.9")).toBe(1);
+ expect(compareSemver("1.0.1", "1.0.0")).toBe(1);
+});
+
+test("compareSemver treats equal versions as 0", () => {
+ expect(compareSemver("1.2.3", "1.2.3")).toBe(0);
+});
+
+test("compareSemver handles unequal segment counts", () => {
+ expect(compareSemver("1.2", "1.2.0")).toBe(0);
+ expect(compareSemver("1.2.1", "1.2")).toBe(1);
+ expect(compareSemver("1", "1.0.1")).toBe(-1);
+});
+
+test("compareSemver ignores pre-release/build suffixes (loose)", () => {
+ expect(compareSemver("1.2.3-beta", "1.2.3")).toBe(0);
+ expect(compareSemver("1.3.0-rc.1", "1.2.0")).toBe(1);
+});
+
+// ─── availableUpdate ───────────────────────────────────────────────────────
+
+const plugin = (over: Partial): MarketplacePlugin => ({
+ id: "p",
+ name: "P",
+ author: "a",
+ description: "",
+ repo: "https://example.com/p",
+ version: "1.0.0",
+ tags: [],
+ theme: false,
+ sourceId: "voltius",
+ ...over,
+});
+
+const meta = (over: Partial): InstalledPluginMeta => ({
+ id: "p",
+ version: "1.0.0",
+ sourceId: "voltius",
+ hash: null,
+ ...over,
+});
+
+test("availableUpdate: newer catalog version is an update", () => {
+ const got = availableUpdate(meta({ version: "1.0.0" }), [plugin({ version: "1.1.0" })]);
+ expect(got?.version).toBe("1.1.0");
+});
+
+test("availableUpdate: same version, no hash signal -> no update", () => {
+ expect(availableUpdate(meta({ version: "1.0.0" }), [plugin({ version: "1.0.0" })])).toBeNull();
+});
+
+test("availableUpdate: same version but differing hashes (both present) -> update", () => {
+ const got = availableUpdate(
+ meta({ version: "1.0.0", hash: "aaa" }),
+ [plugin({ version: "1.0.0", hash: "bbb" })],
+ );
+ expect(got?.hash).toBe("bbb");
+});
+
+test("availableUpdate: same version, same hash -> no update", () => {
+ expect(
+ availableUpdate(meta({ version: "1.0.0", hash: "aaa" }), [plugin({ version: "1.0.0", hash: "aaa" })]),
+ ).toBeNull();
+});
+
+test("availableUpdate: null installed hash ignores hash signal", () => {
+ expect(
+ availableUpdate(meta({ version: "1.0.0", hash: null }), [plugin({ version: "1.0.0", hash: "bbb" })]),
+ ).toBeNull();
+});
+
+test("availableUpdate: older catalog version is not an update", () => {
+ expect(availableUpdate(meta({ version: "2.0.0" }), [plugin({ version: "1.0.0" })])).toBeNull();
+});
+
+test("availableUpdate: no matching catalog entry -> null", () => {
+ expect(availableUpdate(meta({ id: "p" }), [plugin({ id: "other", version: "9.0.0" })])).toBeNull();
+});
+
+test("availableUpdate: matches the entry with the same sourceId, ignoring others", () => {
+ const got = availableUpdate(meta({ sourceId: "voltius", version: "1.0.0" }), [
+ plugin({ sourceId: "other", version: "5.0.0" }),
+ plugin({ sourceId: "voltius", version: "1.1.0" }),
+ ]);
+ expect(got?.sourceId).toBe("voltius");
+ expect(got?.version).toBe("1.1.0");
+});
+
+test("availableUpdate: a local plugin is never updated from an id-colliding catalog entry", () => {
+ const got = availableUpdate(
+ meta({ id: "p", sourceId: "local", version: "1.0.0", hash: null }),
+ [plugin({ id: "p", sourceId: "voltius", version: "9.0.0" })],
+ );
+ expect(got).toBeNull();
+});
+
+test("availableUpdate: no entry from the installed source -> null (no cross-source fallback)", () => {
+ const got = availableUpdate(
+ meta({ sourceId: "voltius", version: "1.0.0" }),
+ [plugin({ sourceId: "other", version: "9.0.0" })],
+ );
+ expect(got).toBeNull();
+});
+
+// ─── addedPermissions ──────────────────────────────────────────────────────
+
+test("addedPermissions returns only newly-declared permissions", () => {
+ expect(addedPermissions(["themes"], ["themes", "notifications"])).toEqual(["notifications"]);
+});
+
+test("addedPermissions is empty when nothing new", () => {
+ expect(addedPermissions(["themes", "fs"], ["fs", "themes"])).toEqual([]);
+ expect(addedPermissions(["a", "b"], ["a"])).toEqual([]);
+});
+
+test("addedPermissions treats all as new against an empty current set", () => {
+ expect(addedPermissions([], ["a", "b"])).toEqual(["a", "b"]);
+});
diff --git a/src/plugins/updates.ts b/src/plugins/updates.ts
new file mode 100644
index 00000000..b7dd37b1
--- /dev/null
+++ b/src/plugins/updates.ts
@@ -0,0 +1,50 @@
+import type { InstalledPluginMeta, MarketplacePlugin } from "@/stores/marketplaceStore";
+
+/**
+ * Loose numeric-dotted semver compare (no dependency). Pre-release/build suffixes are ignored,
+ * missing segments count as 0. Returns -1 | 0 | 1.
+ */
+export function compareSemver(a: string, b: string): number {
+ const parse = (v: string) =>
+ v
+ .split(/[-+]/, 1)[0]
+ .split(".")
+ .map((n) => parseInt(n, 10) || 0);
+ const pa = parse(a);
+ const pb = parse(b);
+ const len = Math.max(pa.length, pb.length);
+ for (let i = 0; i < len; i++) {
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
+ if (d !== 0) return d > 0 ? 1 : -1;
+ }
+ return 0;
+}
+
+/**
+ * The catalog entry that represents an update for an installed plugin, or null.
+ *
+ * An update exists when the catalog `version` is newer, OR the version is unchanged but both the
+ * installed and catalog hashes are present and differ (catches versionless re-stamps of an in-repo
+ * bundle served from a mutable ref). When the installed hash is unknown (unverified/local), only
+ * the version signal is used.
+ */
+export function availableUpdate(
+ meta: InstalledPluginMeta,
+ catalog: MarketplacePlugin[],
+): MarketplacePlugin | null {
+ // Require an exact source match: never treat an arbitrary catalog entry that merely shares an id
+ // as an update for a plugin installed from a different (or "local"/"url") source — updating from
+ // it would overwrite the installed bundle from an unrelated repo.
+ const entry = catalog.find((p) => p.id === meta.id && p.sourceId === meta.sourceId);
+ if (!entry) return null;
+
+ if (compareSemver(entry.version, meta.version) > 0) return entry;
+ if (meta.hash && entry.hash && entry.hash.toLowerCase() !== meta.hash.toLowerCase()) return entry;
+ return null;
+}
+
+/** Permissions declared in `next` that are not in `current`. */
+export function addedPermissions(current: string[], next: string[]): string[] {
+ const have = new Set(current);
+ return next.filter((p) => !have.has(p));
+}
diff --git a/src/stores/marketplaceStore.ts b/src/stores/marketplaceStore.ts
index b0d6426b..2f67e401 100644
--- a/src/stores/marketplaceStore.ts
+++ b/src/stores/marketplaceStore.ts
@@ -90,7 +90,8 @@ interface MarketplaceState {
// Install / uninstall
installing: Set;
- installPlugin: (plugin: MarketplacePlugin) => Promise;
+ fetchManifest: (plugin: MarketplacePlugin) => Promise<{ manifest: PluginManifest; manifestText: string }>;
+ installPlugin: (plugin: MarketplacePlugin, reviewedManifestText?: string) => Promise;
uninstallPlugin: (id: string) => Promise;
reloadPlugin: (id: string) => Promise;
@@ -160,7 +161,17 @@ export const useMarketplaceStore = create((set, get) => ({
// ── Install ───────────────────────────────────────────────────────────
installing: new Set(),
- async installPlugin(plugin: MarketplacePlugin) {
+ // Fetch just the manifest (no side effects) to preview declared permissions before
+ // installing/updating. The executed index.js is still hash-verified in installPlugin.
+ async fetchManifest(plugin: MarketplacePlugin) {
+ const base = plugin.repo.startsWith("http")
+ ? plugin.repo
+ : `https://github.com/${plugin.repo}/releases/latest/download`;
+ const manifestText = await invoke("plugin_fetch_url", { url: `${base}/manifest.json` });
+ return { manifest: JSON.parse(manifestText) as PluginManifest, manifestText };
+ },
+
+ async installPlugin(plugin: MarketplacePlugin, reviewedManifestText?: string) {
const { installing, installedMeta } = get();
if (installing.has(plugin.id)) return;
@@ -170,10 +181,17 @@ export const useMarketplaceStore = create((set, get) => ({
? plugin.repo
: `https://github.com/${plugin.repo}/releases/latest/download`;
- const [manifestText, jsText] = await Promise.all([
- invoke("plugin_fetch_url", { url: `${base}/manifest.json` }),
+ // When the caller previewed the manifest for consent, reuse that exact text so the executed
+ // permission set is precisely the one shown — closing the fetch→consent→load TOCTOU
+ // (manifest.json is not hash-pinned; index.js still is). Only fetch the manifest fresh when
+ // no reviewed copy was supplied (e.g. the review-disclosure setting is off).
+ const [fetchedManifestText, jsText] = await Promise.all([
+ reviewedManifestText !== undefined
+ ? Promise.resolve(reviewedManifestText)
+ : invoke("plugin_fetch_url", { url: `${base}/manifest.json` }),
invoke("plugin_fetch_url", { url: `${base}/index.js` }),
]);
+ const manifestText = fetchedManifestText;
const manifest = JSON.parse(manifestText) as PluginManifest;
diff --git a/src/stores/toggleSettingsStore.ts b/src/stores/toggleSettingsStore.ts
index 154f6598..69debd0a 100644
--- a/src/stores/toggleSettingsStore.ts
+++ b/src/stores/toggleSettingsStore.ts
@@ -122,6 +122,13 @@ export const TOGGLE_DEFS = {
keywords: ["changelog", "popup", "release", "notes", "whats new", "update", "version"],
default: true,
},
+ "plugin-install-review": {
+ labelKey: "settings.toggleDefs.pluginInstallReview.label",
+ icon: "lucide:shield-check",
+ descriptionKey: "settings.toggleDefs.category.plugins",
+ keywords: ["plugin", "permission", "install", "review", "consent", "disclosure", "security"],
+ default: true,
+ },
} as const satisfies Record;
export type ToggleId = keyof typeof TOGGLE_DEFS;