Remap any shortcut in Settings → Keyboard; the macOS menu bar follows

A Keyboard section in the Settings window edits the shared keymap: rows
by group with a search field, a chip per key (x removes), + to record,
reset per row and Reset all. Recording takes the keydown before every
other key in the window (Cmd+W records instead of closing), refuses
reserved combos without preventDefault (Cmd+Q still quits), requires a
chord or F-key for editor commands, and offers Replace when another
command in an overlapping scope owns the key. Rows flag conflicts and
plain character keys that would stop starting a type-to-filter query.
Fixed keys are listed read-only, in the platform's spelling.

Remaps are ordinary settings (only rows that differ from the defaults),
so every window, the menu hints and the tooltips follow live — and the
macOS menu bar: its accelerators are built from the keymap (only the
combos Electrobun's parser expresses faithfully) and rebuilt on a remap.
A main-window menu action fired from the editor or Settings window
brings the main window forward first.

Also: the "Enter / double-click on a file" setting under File list (do
nothing / open local files in their default app), arrow keys in the
Settings sidebar with focus on open and a hand-walked Tab, translations
for all seven languages (the spec now checks every language labels every
command), and a README shortcut table generated from the keymap.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-10-05 14:57:27 +02:00
co-authored by Claude
parent d361297690
commit 8f8f230711
18 changed files with 1256 additions and 50 deletions
+15 -7
View File
@@ -108,7 +108,7 @@ const manager = new ManagerWindow({
connectionsChanged: () => rpc.send.connectionsChanged({}),
menuAction: (info) => rpc.send.menuAction(info),
},
rebuildMenu: () => void applyApplicationMenu(store),
rebuildMenu: () => void applyApplicationMenu(store, () => currentSettings.keybindings),
onSettingsSaved: (s) => settingsSaved(s, "manager"),
});
const settingsWin = new SettingsWindow({
@@ -134,12 +134,14 @@ function settingsSaved(
): void {
engine.setConcurrency(effectiveConcurrency(s));
const localeChanged = s.locale !== currentSettings.locale;
const keysChanged = JSON.stringify(s.keybindings) !== JSON.stringify(currentSettings.keybindings);
currentSettings = s; // future windows' preloads bake the fresh theme
if (localeChanged) {
setBackendLocale(s.locale);
void applyApplicationMenu(store); // relabel the native menu in the new language
settingsWin.retitle();
}
// relabel the native menu in the new language / give it the remapped keys
if (localeChanged || keysChanged) void applyApplicationMenu(store, () => currentSettings.keybindings);
if (sender !== "main") rpc.send.settingsChanged({ settings: s });
if (sender !== "manager") manager.broadcastSettings(s);
if (sender !== "settings") settingsWin.broadcastSettings(s);
@@ -258,16 +260,19 @@ events.sessionClosed = (info) => {
rpc.send.sessionClosed(info);
editors.notifySessionClosed(info);
};
events.connectionsChanged = () => void applyApplicationMenu(store);
events.connectionsChanged = () => void applyApplicationMenu(store, () => currentSettings.keybindings);
engine.setConcurrency(effectiveConcurrency(currentSettings));
void applyApplicationMenu(store);
void applyApplicationMenu(store, () => currentSettings.keybindings);
ApplicationMenu.on("application-menu-clicked", (event) => {
const action = (event as { data?: { action?: string } }).data?.action;
if (!action) return;
// ⌘W while the Settings window or an editor window is focused closes THAT
// window (an editor with its dirty prompt), not a main-window tab. Other
// menu actions deliberately still target the main window.
// Their pages claim close-window (⌘W) themselves — macOS hands key
// equivalents to the page first — so a close-tab that still arrives while
// the Settings window or an editor is focused is a ⌘W the page let through
// (or the menu item clicked): close THAT window (an editor with its dirty
// prompt), never a main-window tab behind it. Other menu actions
// deliberately still target the main window.
if (action === "close-tab") {
if (settingsWin.isFocused()) {
settingsWin.close();
@@ -299,6 +304,9 @@ ApplicationMenu.on("application-menu-clicked", (event) => {
}
// the manager is modal to the main window — don't drive main behind it
if (manager.isOpen()) return;
// everything below acts on the main window: from a focused editor or the
// Settings window (⌘T, ⌘N…), bring it forward so the effect is visible
if (settingsWin.isFocused() || editors.focusedKey()) win.activate();
rpc.send.menuAction({ action });
});
+33 -11
View File
@@ -1,5 +1,6 @@
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/main";
import { BRANDING } from "../shared/branding";
import { effectiveKeymap, type ShortcutId, toAccelerator } from "../shared/keymap";
import type { SavedConnection } from "../shared/types";
import { t } from "./i18n";
import type { ConnectionStore } from "./store/connections";
@@ -7,13 +8,30 @@ import type { ConnectionStore } from "./store/connections";
/**
* macOS application menu (electrobun menus are macOS-only). Standard roles plus
* app actions; custom actions are forwarded to the webview as menuAction
* messages. Rebuilt whenever saved connections change so the Connect submenu
* stays current.
* messages. Rebuilt whenever saved connections, the language or the keyboard
* remaps change.
*
* Accelerators come from the shared keymap, so the menu bar shows — and
* fires — the user's keys. The page sees ⌘-keys first and claims the ones it
* handles; the menu only runs what falls through (see shortcuts.service.ts).
* ⌘Q stays fixed: quitting must always work.
*/
export async function applyApplicationMenu(store: ConnectionStore): Promise<void> {
export async function applyApplicationMenu(
store: ConnectionStore,
keybindings: () => Record<string, string[]>,
): Promise<void> {
if (process.platform !== "darwin") return;
const { stacks, connections } = await store.load();
// read after the await: a remap saved meanwhile must not be lost
const keymap = effectiveKeymap(keybindings(), true);
const accelerator = (id: ShortcutId): string | undefined => {
for (const combo of keymap.find((b) => b.id === id)?.combos ?? []) {
const native = toAccelerator(combo);
if (native) return native;
}
return undefined;
};
const item = (c: SavedConnection): ApplicationMenuItemConfig => ({
label: c.name,
action: `connect:${c.id}`,
@@ -28,7 +46,11 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
...looseItems,
...stackMenus,
...(looseItems.length || stackMenus.length ? [{ type: "separator" } as const] : []),
{ label: t("menu.connectionManager"), action: "manage-connections" },
{
label: t("menu.connectionManager"),
action: "manage-connections",
accelerator: accelerator("connection-manager"),
},
];
ApplicationMenu.setApplicationMenu([
@@ -36,7 +58,7 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
submenu: [
{ label: t("menu.about", { app: BRANDING.appName }), action: "about" },
{ type: "separator" },
{ label: t("menu.settings"), action: "settings", accelerator: "," },
{ label: t("menu.settings"), action: "settings", accelerator: accelerator("open-settings") },
{ type: "separator" },
{ label: t("menu.checkForUpdates"), action: "check-for-updates" },
{ type: "separator" },
@@ -53,13 +75,13 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
{
label: t("menu.file"),
submenu: [
{ label: t("menu.newTab"), action: "new-tab", accelerator: "t" },
{ label: t("menu.closeTab"), action: "close-tab", accelerator: "w" },
{ label: t("menu.newTab"), action: "new-tab", accelerator: accelerator("new-tab") },
{ label: t("menu.closeTab"), action: "close-tab", accelerator: accelerator("close-tab") },
{ type: "separator" },
{ label: t("menu.newFolder"), action: "new-folder" },
{ label: t("menu.newFile"), action: "new-file" },
{ label: t("menu.newFolder"), action: "new-folder", accelerator: accelerator("new-folder") },
{ label: t("menu.newFile"), action: "new-file", accelerator: accelerator("new-file") },
{ type: "separator" },
{ label: t("menu.editFile"), action: "edit-file", accelerator: "e" },
{ label: t("menu.editFile"), action: "edit-file", accelerator: accelerator("edit") },
{ type: "separator" },
{ label: t("menu.connect"), submenu: connectSubmenu },
],
@@ -81,7 +103,7 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
{
label: t("menu.view"),
submenu: [
{ label: t("menu.refresh"), action: "refresh-active", accelerator: "r" },
{ label: t("menu.refresh"), action: "refresh-active", accelerator: accelerator("refresh") },
{ type: "separator" },
{ role: "toggleFullScreen" },
],
+104
View File
@@ -36,6 +36,7 @@ export const de = {
transfers: "Übertragungen",
editor: "Editor",
fileList: "Dateiliste",
keyboard: "Tastatur",
about: "Über",
},
columns: "Sichtbare Spalten",
@@ -51,6 +52,7 @@ export const de = {
systemDefault: "Systemstandard",
resetDefault: "Auf Standard zurücksetzen",
finishedTransfers: { label: "Abgeschlossene Übertragungen", autoClear: "Auto-löschen", keep: "Behalten" },
openFiles: { label: "Eingabe / Doppelklick auf eine Datei", nothing: "Nichts tun", open: "Öffnen", hint: "Lokale Dateien öffnen sich in ihrer Standard-App; Ordner öffnen sich immer." },
},
kinds: {
folder: "Ordner",
@@ -293,6 +295,108 @@ export const de = {
trashTooltip: "Finders Wiederherstellen ist für von hier gelöschte Elemente nicht verfügbar",
},
keys: {
"cursor-up": "Nach oben",
"cursor-down": "Nach unten",
"page-up": "Seite nach oben",
"page-down": "Seite nach unten",
"cursor-first": "Erster Eintrag",
"cursor-last": "Letzter Eintrag",
"toggle-select": "Auswählen / abwählen",
"select-all": "Alles auswählen",
open: "Öffnen",
parent: "Übergeordneter Ordner",
"switch-panel": "Panel wechseln",
"row-menu": "Menü des Eintrags zeigen",
send: "An anderes Panel senden",
edit: "Bearbeiten",
rename: "Umbenennen",
"new-folder": "Neuer Ordner",
"new-file": "Neue Datei",
chmod: "Berechtigungen",
delete: "Löschen",
trash: "In den Papierkorb",
"copy-path": "Pfad kopieren",
reveal: "Im Dateimanager zeigen",
"open-default": "Mit Standard-App öffnen",
refresh: "Aktualisieren",
"focus-path": "Zum Pfad",
"source-picker": "Panelinhalt wählen",
"sort-name": "Nach Name sortieren",
"sort-size": "Nach Größe sortieren",
"sort-mtime": "Nach Änderungsdatum sortieren",
"sort-kind": "Nach Art sortieren",
"sort-rights": "Nach Rechten sortieren",
"sort-owner": "Nach Eigentümer sortieren",
"sort-group": "Nach Gruppe sortieren",
"new-tab": "Neuer Tab",
"close-tab": "Tab schließen",
"close-other-tabs": "Andere Tabs schließen",
"close-tabs-right": "Tabs rechts schließen",
"next-tab": "Nächster Tab",
"prev-tab": "Vorheriger Tab",
"go-to-tab": "Zu Tab {n}",
"go-to-last-tab": "Zum letzten Tab",
"move-tab-left": "Tab nach links",
"move-tab-right": "Tab nach rechts",
"disconnect-tab": "Tab trennen",
"open-settings": "Einstellungen",
"connection-manager": "Verbindungsverwaltung",
"toggle-transfers": "Übertragungen ein-/ausblenden",
"install-update": "Update installieren",
"scale-up": "Oberfläche größer",
"scale-down": "Oberfläche kleiner",
"scale-reset": "Oberfläche auf 100 %",
"close-window": "Fenster schließen",
save: "Speichern",
find: "Suchen",
"find-next": "Weitersuchen",
"find-prev": "Rückwärts suchen",
"toggle-wrap": "Zeilen umbrechen",
"conn-new": "Neue Verbindung",
"conn-new-stack": "Neue Gruppe",
"conn-save": "Verbindung speichern",
"conn-connect": "Verbinden / Gruppe öffnen",
"conn-delete": "Verbindung oder Gruppe löschen",
"conn-rename-stack": "Gruppe umbenennen",
"conn-import": "Aus WinSCP importieren",
groups: {
panels: "Dateiliste",
files: "Dateien",
sort: "Sortierung",
tabs: "Tabs",
window: "Fenster",
editor: "Editor",
connections: "Verbindungsverwaltung",
},
ui: {
filter: "Tastenkürzel suchen",
intro: "Auf + klicken und die Tasten drücken, um ein Kürzel hinzuzufügen. Das erste zeigen Menüs und Tooltips an.",
add: "Kürzel hinzufügen",
remove: "{key} entfernen",
reset: "Auf Standard zurücksetzen",
resetAll: "Alle zurücksetzen",
resetAllConfirm: "Alle Kürzel zurücksetzen",
press: "Tasten drücken… ({esc} bricht ab)",
unbound: "Kein Kürzel",
reserved: "{key} ist vom System reserviert",
needsModifier: "Im Editor würde {key} nur tippen – eine Sondertaste hinzufügen oder eine F-Taste nehmen",
usedBy: "{key} wird schon von „{action}“ benutzt",
replace: "Ersetzen",
conflict: "Gleiche Taste wie {action}",
plainLetter: "{key} in der Dateiliste tippen filtert sie nicht mehr",
noResults: "Kein Kürzel passt",
fixed: "Feste Tasten",
},
fixed: {
escape: "{esc} schließt Menüs, Dialoge und die Suchleiste",
escList: "{esc} in der Dateiliste leert den Filter, bricht dann eine Verbindung ab und hebt dann die Auswahl auf",
dialogs: "{enter} und {esc} bestätigen oder verwerfen einen Dialog; {tab} und {shiftTab} wechseln zwischen seinen Elementen",
menus: "↑ ↓ {home} {end} und {enter} funktionieren in jedem Menü",
filter: "Tippen in der Dateiliste filtert sie; {backspace} löscht das letzte Zeichen des Filters",
shift: "Umschalt mit einer Bewegungstaste wählt die überfahrenen Zeilen aus",
find: "{enter} und {shiftEnter} springen durch die Treffer der Suche",
sidebar: "↑ ↓ wechseln hier zwischen den Bereichen",
},
names: {
ctrl: "Strg",
alt: "Alt",
+33
View File
@@ -40,6 +40,7 @@ export const en = {
transfers: "Transfers",
editor: "Editor",
fileList: "File list",
keyboard: "Keyboard",
about: "About",
},
columns: "Visible columns",
@@ -55,6 +56,7 @@ export const en = {
systemDefault: "System default",
resetDefault: "Reset to system default",
finishedTransfers: { label: "Finished transfers", autoClear: "Auto-clear", keep: "Keep" },
openFiles: { label: "Enter / double-click on a file", nothing: "Do nothing", open: "Open", hint: "Local files open in their default app; folders always open." },
},
kinds: {
folder: "Folder",
@@ -372,6 +374,37 @@ export const en = {
editor: "Editor",
connections: "Connection manager",
},
// Settings → Keyboard
ui: {
filter: "Search shortcuts",
intro: "Press + and type the keys to add a shortcut. The first one is the hint menus and tooltips show.",
add: "Add a shortcut",
remove: "Remove {key}",
reset: "Reset to default",
resetAll: "Reset all",
resetAllConfirm: "Reset every shortcut",
press: "Press keys… ({esc} cancels)",
unbound: "No shortcut",
reserved: "{key} is reserved by the system",
needsModifier: "In the editor {key} would only type — add a modifier or use an F-key",
usedBy: "{key} is already used by “{action}”",
replace: "Replace",
conflict: "Same key as {action}",
plainLetter: "Typing {key} in the file list no longer filters it",
noResults: "No shortcut matches",
fixed: "Fixed keys",
},
// fixed keys, listed read-only there (key params come from the platform)
fixed: {
escape: "{esc} closes menus, dialogs and the find bar",
escList: "{esc} in the file list clears the filter, then cancels a connect, then clears the selection",
dialogs: "{enter} and {esc} confirm or cancel a dialog; {tab} and {shiftTab} move between its controls",
menus: "↑ ↓ {home} {end} and {enter} work in every menu",
filter: "Typing in the file list filters it; {backspace} trims the filter",
shift: "Shift with a movement key selects the rows the cursor passes",
find: "{enter} and {shiftEnter} step through the matches in the find box",
sidebar: "↑ ↓ move between these sections",
},
// key names for "Ctrl+Shift+Del"-style hints (not used on macOS, which shows ⌘⇧⌫)
names: {
ctrl: "Ctrl",
+104
View File
@@ -36,6 +36,7 @@ export const es = {
transfers: "Transferencias",
editor: "Editor",
fileList: "Lista de archivos",
keyboard: "Teclado",
about: "Acerca de",
},
columns: "Columnas visibles",
@@ -51,6 +52,7 @@ export const es = {
systemDefault: "Predeterminado del sistema",
resetDefault: "Restablecer al predeterminado",
finishedTransfers: { label: "Transferencias completadas", autoClear: "Auto-limpiar", keep: "Mantener" },
openFiles: { label: "Intro / doble clic en un archivo", nothing: "No hacer nada", open: "Abrir", hint: "Los archivos locales se abren con su app predeterminada; las carpetas siempre se abren." },
},
kinds: {
folder: "Carpeta",
@@ -293,6 +295,108 @@ export const es = {
trashTooltip: "La opción Restaurar de Finder no está disponible para elementos enviados a la papelera desde aquí",
},
keys: {
"cursor-up": "Subir",
"cursor-down": "Bajar",
"page-up": "Página arriba",
"page-down": "Página abajo",
"cursor-first": "Primer elemento",
"cursor-last": "Último elemento",
"toggle-select": "Seleccionar / deseleccionar",
"select-all": "Seleccionar todo",
open: "Abrir",
parent: "Carpeta superior",
"switch-panel": "Cambiar de panel",
"row-menu": "Mostrar el menú del elemento",
send: "Enviar al otro panel",
edit: "Editar",
rename: "Renombrar",
"new-folder": "Nueva carpeta",
"new-file": "Nuevo archivo",
chmod: "Permisos",
delete: "Eliminar",
trash: "Mover a la papelera",
"copy-path": "Copiar ruta",
reveal: "Mostrar en el gestor de archivos",
"open-default": "Abrir con la app predeterminada",
refresh: "Actualizar",
"focus-path": "Ir a la ruta",
"source-picker": "Elegir qué muestra el panel",
"sort-name": "Ordenar por nombre",
"sort-size": "Ordenar por tamaño",
"sort-mtime": "Ordenar por fecha de modificación",
"sort-kind": "Ordenar por tipo",
"sort-rights": "Ordenar por permisos",
"sort-owner": "Ordenar por propietario",
"sort-group": "Ordenar por grupo",
"new-tab": "Nueva pestaña",
"close-tab": "Cerrar pestaña",
"close-other-tabs": "Cerrar las demás pestañas",
"close-tabs-right": "Cerrar las pestañas de la derecha",
"next-tab": "Pestaña siguiente",
"prev-tab": "Pestaña anterior",
"go-to-tab": "Ir a la pestaña {n}",
"go-to-last-tab": "Ir a la última pestaña",
"move-tab-left": "Mover la pestaña a la izquierda",
"move-tab-right": "Mover la pestaña a la derecha",
"disconnect-tab": "Desconectar la pestaña",
"open-settings": "Ajustes",
"connection-manager": "Gestor de conexiones",
"toggle-transfers": "Mostrar / ocultar transferencias",
"install-update": "Instalar la actualización",
"scale-up": "Interfaz más grande",
"scale-down": "Interfaz más pequeña",
"scale-reset": "Interfaz al 100 %",
"close-window": "Cerrar la ventana",
save: "Guardar",
find: "Buscar",
"find-next": "Buscar siguiente",
"find-prev": "Buscar anterior",
"toggle-wrap": "Ajustar líneas",
"conn-new": "Nueva conexión",
"conn-new-stack": "Nuevo grupo",
"conn-save": "Guardar la conexión",
"conn-connect": "Conectar / abrir grupo",
"conn-delete": "Eliminar conexión o grupo",
"conn-rename-stack": "Renombrar grupo",
"conn-import": "Importar de WinSCP",
groups: {
panels: "Lista de archivos",
files: "Archivos",
sort: "Orden",
tabs: "Pestañas",
window: "Ventana",
editor: "Editor",
connections: "Gestor de conexiones",
},
ui: {
filter: "Buscar atajos",
intro: "Pulsa + y teclea las teclas para añadir un atajo. El primero es el que muestran los menús y las pistas.",
add: "Añadir un atajo",
remove: "Quitar {key}",
reset: "Restablecer el predeterminado",
resetAll: "Restablecer todo",
resetAllConfirm: "Restablecer todos los atajos",
press: "Pulsa las teclas… ({esc} cancela)",
unbound: "Sin atajo",
reserved: "{key} está reservado por el sistema",
needsModifier: "En el editor {key} solo escribiría: añade un modificador o usa una tecla F",
usedBy: "{key} ya lo usa «{action}»",
replace: "Reemplazar",
conflict: "Misma tecla que {action}",
plainLetter: "Teclear {key} en la lista de archivos ya no la filtra",
noResults: "Ningún atajo coincide",
fixed: "Teclas fijas",
},
fixed: {
escape: "{esc} cierra menús, diálogos y la barra de búsqueda",
escList: "{esc} en la lista de archivos borra el filtro, luego cancela una conexión y luego quita la selección",
dialogs: "{enter} y {esc} confirman o cancelan un diálogo; {tab} y {shiftTab} recorren sus controles",
menus: "↑ ↓ {home} {end} y {enter} funcionan en todos los menús",
filter: "Teclear en la lista de archivos la filtra; {backspace} borra el último carácter del filtro",
shift: "Mayús con una tecla de movimiento selecciona las filas por las que pasa el cursor",
find: "{enter} y {shiftEnter} recorren las coincidencias de la búsqueda",
sidebar: "↑ ↓ cambian de sección aquí",
},
names: {
ctrl: "Ctrl",
alt: "Alt",
+104
View File
@@ -38,6 +38,7 @@ export const eu = {
transfers: "Transferentziak",
editor: "Editorea",
fileList: "Fitxategi-zerrenda",
keyboard: "Teklatua",
about: "Honi buruz",
},
columns: "Zutabe ikusgaiak",
@@ -53,6 +54,7 @@ export const eu = {
systemDefault: "Sistemaren lehenetsia",
resetDefault: "Berrezarri lehenetsira",
finishedTransfers: { label: "Amaitutako transferentziak", autoClear: "Garbitu", keep: "Mantendu" },
openFiles: { label: "Sartu / klik bikoitza fitxategi batean", nothing: "Ezer ez", open: "Ireki", hint: "Fitxategi lokalak beren aplikazio lehenetsiarekin irekitzen dira; karpetak beti irekitzen dira." },
},
kinds: {
folder: "Karpeta",
@@ -295,6 +297,108 @@ export const eu = {
trashTooltip: "Finder-en «Jarri berriro» ez dago erabilgarri hemendik zakarrontzira botatako elementuentzat",
},
keys: {
"cursor-up": "Gora",
"cursor-down": "Behera",
"page-up": "Orrialde bat gora",
"page-down": "Orrialde bat behera",
"cursor-first": "Lehen elementua",
"cursor-last": "Azken elementua",
"toggle-select": "Hautatu / desautatu",
"select-all": "Hautatu dena",
open: "Ireki",
parent: "Karpeta gurasoa",
"switch-panel": "Aldatu panela",
"row-menu": "Erakutsi elementuaren menua",
send: "Bidali beste panelera",
edit: "Editatu",
rename: "Berrizendatu",
"new-folder": "Karpeta berria",
"new-file": "Fitxategi berria",
chmod: "Baimenak",
delete: "Ezabatu",
trash: "Eraman zakarrontzira",
"copy-path": "Kopiatu bidea",
reveal: "Erakutsi fitxategi-kudeatzailean",
"open-default": "Ireki aplikazio lehenetsiarekin",
refresh: "Freskatu",
"focus-path": "Joan bidera",
"source-picker": "Aukeratu panelak zer erakusten duen",
"sort-name": "Ordenatu izenaren arabera",
"sort-size": "Ordenatu tamainaren arabera",
"sort-mtime": "Ordenatu aldaketa-dataren arabera",
"sort-kind": "Ordenatu motaren arabera",
"sort-rights": "Ordenatu baimenen arabera",
"sort-owner": "Ordenatu jabearen arabera",
"sort-group": "Ordenatu taldearen arabera",
"new-tab": "Fitxa berria",
"close-tab": "Itxi fitxa",
"close-other-tabs": "Itxi beste fitxak",
"close-tabs-right": "Itxi eskuineko fitxak",
"next-tab": "Hurrengo fitxa",
"prev-tab": "Aurreko fitxa",
"go-to-tab": "Joan {n}. fitxara",
"go-to-last-tab": "Joan azken fitxara",
"move-tab-left": "Eraman fitxa ezkerrera",
"move-tab-right": "Eraman fitxa eskuinera",
"disconnect-tab": "Deskonektatu fitxa",
"open-settings": "Ezarpenak",
"connection-manager": "Konexio-kudeatzailea",
"toggle-transfers": "Erakutsi / ezkutatu transferentziak",
"install-update": "Instalatu eguneraketa",
"scale-up": "Interfaze handiagoa",
"scale-down": "Interfaze txikiagoa",
"scale-reset": "Interfazea % 100ean",
"close-window": "Itxi leihoa",
save: "Gorde",
find: "Bilatu",
"find-next": "Bilatu hurrengoa",
"find-prev": "Bilatu aurrekoa",
"toggle-wrap": "Doitu lerroak",
"conn-new": "Konexio berria",
"conn-new-stack": "Talde berria",
"conn-save": "Gorde konexioa",
"conn-connect": "Konektatu / ireki taldea",
"conn-delete": "Ezabatu konexioa edo taldea",
"conn-rename-stack": "Berrizendatu taldea",
"conn-import": "Inportatu WinSCPtik",
groups: {
panels: "Fitxategi-zerrenda",
files: "Fitxategiak",
sort: "Ordena",
tabs: "Fitxak",
window: "Leihoa",
editor: "Editorea",
connections: "Konexio-kudeatzailea",
},
ui: {
filter: "Bilatu lasterbideak",
intro: "Sakatu + eta idatzi teklak lasterbide bat gehitzeko. Lehenengoa da menuek eta argibideek erakusten dutena.",
add: "Gehitu lasterbide bat",
remove: "Kendu {key}",
reset: "Berrezarri lehenetsia",
resetAll: "Berrezarri dena",
resetAllConfirm: "Berrezarri lasterbide guztiak",
press: "Sakatu teklak… ({esc} utzi)",
unbound: "Lasterbiderik ez",
reserved: "{key} sistemak erreserbatuta dauka",
needsModifier: "Editorean {key}(e)k idatzi besterik ez luke egingo: gehitu aldatzaile bat edo erabili F tekla bat",
usedBy: "{key} «{action}»(e)k erabiltzen du dagoeneko",
replace: "Ordeztu",
conflict: "{action}(r)en tekla bera",
plainLetter: "Fitxategi-zerrendan {key} idazteak ez du jada iragazten",
noResults: "Ez dago bat datorren lasterbiderik",
fixed: "Tekla finkoak",
},
fixed: {
escape: "{esc}(e)k menuak, elkarrizketa-koadroak eta bilaketa-barra ixten ditu",
escList: "Fitxategi-zerrendan {esc}(e)k iragazkia garbitzen du, gero konexioa bertan behera uzten du eta gero hautapena kentzen du",
dialogs: "{enter} eta {esc} elkarrizketa-koadroa berresten edo uzten dute; {tab} eta {shiftTab} haren kontroletan mugitzen dira",
menus: "↑ ↓ {home} {end} eta {enter} menu guztietan dabiltza",
filter: "Fitxategi-zerrendan idazteak iragazi egiten du; {backspace}(e)k iragazkiaren azken karakterea ezabatzen du",
shift: "Maius mugimendu-tekla batekin kurtsoreak igarotako errenkadak hautatzen ditu",
find: "{enter} eta {shiftEnter} bilaketako bat-etortzeetan mugitzen dira",
sidebar: "↑ ↓ atal batetik bestera mugitzen dira hemen",
},
names: {
ctrl: "Ctrl",
alt: "Alt",
+104
View File
@@ -36,6 +36,7 @@ export const fr = {
transfers: "Transferts",
editor: "Éditeur",
fileList: "Liste des fichiers",
keyboard: "Clavier",
about: "À propos",
},
columns: "Colonnes visibles",
@@ -51,6 +52,7 @@ export const fr = {
systemDefault: "Valeur par défaut du système",
resetDefault: "Réinitialiser par défaut",
finishedTransfers: { label: "Transferts terminés", autoClear: "Effacer auto.", keep: "Garder" },
openFiles: { label: "Entrée / double-clic sur un fichier", nothing: "Ne rien faire", open: "Ouvrir", hint: "Les fichiers locaux s'ouvrent avec leur application par défaut ; les dossiers s'ouvrent toujours." },
},
kinds: {
folder: "Dossier",
@@ -293,6 +295,108 @@ export const fr = {
trashTooltip: "La restauration du Finder n'est pas disponible pour les éléments mis à la corbeille d'ici",
},
keys: {
"cursor-up": "Monter",
"cursor-down": "Descendre",
"page-up": "Page précédente",
"page-down": "Page suivante",
"cursor-first": "Premier élément",
"cursor-last": "Dernier élément",
"toggle-select": "Sélectionner / désélectionner",
"select-all": "Tout sélectionner",
open: "Ouvrir",
parent: "Dossier parent",
"switch-panel": "Changer de panneau",
"row-menu": "Afficher le menu de l'élément",
send: "Envoyer vers l'autre panneau",
edit: "Éditer",
rename: "Renommer",
"new-folder": "Nouveau dossier",
"new-file": "Nouveau fichier",
chmod: "Permissions",
delete: "Supprimer",
trash: "Mettre à la corbeille",
"copy-path": "Copier le chemin",
reveal: "Afficher dans le gestionnaire de fichiers",
"open-default": "Ouvrir avec l'application par défaut",
refresh: "Actualiser",
"focus-path": "Aller au chemin",
"source-picker": "Choisir ce qu'affiche le panneau",
"sort-name": "Trier par nom",
"sort-size": "Trier par taille",
"sort-mtime": "Trier par date de modification",
"sort-kind": "Trier par type",
"sort-rights": "Trier par droits",
"sort-owner": "Trier par propriétaire",
"sort-group": "Trier par groupe",
"new-tab": "Nouvel onglet",
"close-tab": "Fermer l'onglet",
"close-other-tabs": "Fermer les autres onglets",
"close-tabs-right": "Fermer les onglets à droite",
"next-tab": "Onglet suivant",
"prev-tab": "Onglet précédent",
"go-to-tab": "Aller à l'onglet {n}",
"go-to-last-tab": "Aller au dernier onglet",
"move-tab-left": "Déplacer l'onglet à gauche",
"move-tab-right": "Déplacer l'onglet à droite",
"disconnect-tab": "Déconnecter l'onglet",
"open-settings": "Réglages",
"connection-manager": "Gestionnaire de connexions",
"toggle-transfers": "Afficher / masquer les transferts",
"install-update": "Installer la mise à jour",
"scale-up": "Interface plus grande",
"scale-down": "Interface plus petite",
"scale-reset": "Interface à 100 %",
"close-window": "Fermer la fenêtre",
save: "Enregistrer",
find: "Rechercher",
"find-next": "Résultat suivant",
"find-prev": "Résultat précédent",
"toggle-wrap": "Retour à la ligne",
"conn-new": "Nouvelle connexion",
"conn-new-stack": "Nouveau groupe",
"conn-save": "Enregistrer la connexion",
"conn-connect": "Se connecter / ouvrir le groupe",
"conn-delete": "Supprimer la connexion ou le groupe",
"conn-rename-stack": "Renommer le groupe",
"conn-import": "Importer depuis WinSCP",
groups: {
panels: "Liste des fichiers",
files: "Fichiers",
sort: "Tri",
tabs: "Onglets",
window: "Fenêtre",
editor: "Éditeur",
connections: "Gestionnaire de connexions",
},
ui: {
filter: "Rechercher un raccourci",
intro: "Cliquez sur + et tapez les touches pour ajouter un raccourci. Le premier est celui qu'affichent les menus et les infobulles.",
add: "Ajouter un raccourci",
remove: "Retirer {key}",
reset: "Rétablir la valeur par défaut",
resetAll: "Tout rétablir",
resetAllConfirm: "Rétablir tous les raccourcis",
press: "Tapez les touches… ({esc} annule)",
unbound: "Aucun raccourci",
reserved: "{key} est réservé par le système",
needsModifier: "Dans l'éditeur, {key} ne ferait que taper : ajoutez un modificateur ou utilisez une touche F",
usedBy: "{key} est déjà utilisé par « {action} »",
replace: "Remplacer",
conflict: "Même touche que {action}",
plainLetter: "Taper {key} dans la liste des fichiers ne la filtre plus",
noResults: "Aucun raccourci ne correspond",
fixed: "Touches fixes",
},
fixed: {
escape: "{esc} ferme les menus, les dialogues et la barre de recherche",
escList: "{esc} dans la liste des fichiers efface le filtre, puis annule une connexion, puis vide la sélection",
dialogs: "{enter} et {esc} valident ou annulent un dialogue ; {tab} et {shiftTab} passent d'un contrôle à l'autre",
menus: "↑ ↓ {home} {end} et {enter} fonctionnent dans tous les menus",
filter: "Taper dans la liste des fichiers la filtre ; {backspace} efface le dernier caractère du filtre",
shift: "Maj avec une touche de déplacement sélectionne les lignes parcourues",
find: "{enter} et {shiftEnter} parcourent les résultats de la recherche",
sidebar: "↑ ↓ changent de section ici",
},
names: {
ctrl: "Ctrl",
alt: "Alt",
+104
View File
@@ -37,6 +37,7 @@ export const ja = {
transfers: "転送",
editor: "エディタ",
fileList: "ファイル一覧",
keyboard: "キーボード",
about: "情報",
},
columns: "表示する列",
@@ -52,6 +53,7 @@ export const ja = {
systemDefault: "システムの既定",
resetDefault: "既定に戻す",
finishedTransfers: { label: "完了した転送", autoClear: "自動クリア", keep: "保持" },
openFiles: { label: "Enter / ファイルをダブルクリック", nothing: "何もしない", open: "開く", hint: "ローカルファイルは既定のアプリで開きます。フォルダは常に開きます。" },
},
kinds: {
folder: "フォルダ",
@@ -293,6 +295,108 @@ export const ja = {
trashTooltip: "ここからゴミ箱に入れた項目は Finder の「戻す」を利用できません",
},
keys: {
"cursor-up": "上へ",
"cursor-down": "下へ",
"page-up": "前のページ",
"page-down": "次のページ",
"cursor-first": "先頭の項目",
"cursor-last": "最後の項目",
"toggle-select": "選択 / 選択解除",
"select-all": "すべて選択",
open: "開く",
parent: "親フォルダ",
"switch-panel": "パネルを切り替え",
"row-menu": "項目のメニューを表示",
send: "もう一方のパネルへ送信",
edit: "編集",
rename: "名前を変更",
"new-folder": "新規フォルダ",
"new-file": "新規ファイル",
chmod: "アクセス権",
delete: "削除",
trash: "ゴミ箱に移動",
"copy-path": "パスをコピー",
reveal: "ファイルマネージャーで表示",
"open-default": "既定のアプリで開く",
refresh: "更新",
"focus-path": "パスへ移動",
"source-picker": "パネルの表示内容を選択",
"sort-name": "名前で並べ替え",
"sort-size": "サイズで並べ替え",
"sort-mtime": "更新日時で並べ替え",
"sort-kind": "種類で並べ替え",
"sort-rights": "権限で並べ替え",
"sort-owner": "所有者で並べ替え",
"sort-group": "グループで並べ替え",
"new-tab": "新しいタブ",
"close-tab": "タブを閉じる",
"close-other-tabs": "他のタブを閉じる",
"close-tabs-right": "右側のタブを閉じる",
"next-tab": "次のタブ",
"prev-tab": "前のタブ",
"go-to-tab": "タブ {n} へ移動",
"go-to-last-tab": "最後のタブへ移動",
"move-tab-left": "タブを左へ移動",
"move-tab-right": "タブを右へ移動",
"disconnect-tab": "タブを切断",
"open-settings": "設定",
"connection-manager": "接続マネージャー",
"toggle-transfers": "転送の表示 / 非表示",
"install-update": "アップデートをインストール",
"scale-up": "インターフェースを拡大",
"scale-down": "インターフェースを縮小",
"scale-reset": "インターフェースを 100% に",
"close-window": "ウインドウを閉じる",
save: "保存",
find: "検索",
"find-next": "次を検索",
"find-prev": "前を検索",
"toggle-wrap": "行を折り返す",
"conn-new": "新規接続",
"conn-new-stack": "新規グループ",
"conn-save": "接続を保存",
"conn-connect": "接続 / グループを開く",
"conn-delete": "接続またはグループを削除",
"conn-rename-stack": "グループ名を変更",
"conn-import": "WinSCP からインポート",
groups: {
panels: "ファイル一覧",
files: "ファイル",
sort: "並べ替え",
tabs: "タブ",
window: "ウインドウ",
editor: "エディタ",
connections: "接続マネージャー",
},
ui: {
filter: "ショートカットを検索",
intro: "+ を押してキーを入力するとショートカットを追加できます。最初のものがメニューとツールチップに表示されます。",
add: "ショートカットを追加",
remove: "{key} を削除",
reset: "既定に戻す",
resetAll: "すべて戻す",
resetAllConfirm: "すべてのショートカットを戻す",
press: "キーを押してください…({esc} でキャンセル)",
unbound: "ショートカットなし",
reserved: "{key} はシステムで予約されています",
needsModifier: "エディタでは {key} は入力されるだけです。修飾キーを加えるか F キーを使ってください",
usedBy: "{key} はすでに「{action}」で使われています",
replace: "置き換える",
conflict: "{action} と同じキー",
plainLetter: "ファイル一覧で {key} を入力しても絞り込まれなくなります",
noResults: "一致するショートカットはありません",
fixed: "固定キー",
},
fixed: {
escape: "{esc} でメニュー、ダイアログ、検索バーを閉じます",
escList: "ファイル一覧の {esc} は、絞り込みの解除、接続のキャンセル、選択の解除の順に働きます",
dialogs: "{enter} と {esc} でダイアログを確定 / キャンセルし、{tab} と {shiftTab} でコントロール間を移動します",
menus: "↑ ↓ {home} {end} と {enter} はすべてのメニューで使えます",
filter: "ファイル一覧で入力すると絞り込まれます。{backspace} で最後の文字を消します",
shift: "Shift と移動キーで、カーソルが通過した行を選択します",
find: "{enter} と {shiftEnter} で検索結果を移動します",
sidebar: "↑ ↓ でここのセクションを切り替えます",
},
names: {
ctrl: "Ctrl",
alt: "Alt",
+104
View File
@@ -37,6 +37,7 @@ export const zh = {
transfers: "传输",
editor: "编辑器",
fileList: "文件列表",
keyboard: "键盘",
about: "关于",
},
columns: "显示的列",
@@ -52,6 +53,7 @@ export const zh = {
systemDefault: "系统默认",
resetDefault: "恢复为系统默认",
finishedTransfers: { label: "已完成传输", autoClear: "自动清除", keep: "保留" },
openFiles: { label: "回车 / 双击文件", nothing: "不执行操作", open: "打开", hint: "本地文件用默认应用打开;文件夹始终打开。" },
},
kinds: {
folder: "文件夹",
@@ -293,6 +295,108 @@ export const zh = {
trashTooltip: "从此处移入废纸篓的项目无法使用访达的放回原处功能",
},
keys: {
"cursor-up": "上移",
"cursor-down": "下移",
"page-up": "上一页",
"page-down": "下一页",
"cursor-first": "第一项",
"cursor-last": "最后一项",
"toggle-select": "选择 / 取消选择",
"select-all": "全选",
open: "打开",
parent: "上级目录",
"switch-panel": "切换面板",
"row-menu": "显示该项的菜单",
send: "发送到另一个面板",
edit: "编辑",
rename: "重命名",
"new-folder": "新建文件夹",
"new-file": "新建文件",
chmod: "权限",
delete: "删除",
trash: "移到废纸篓",
"copy-path": "复制路径",
reveal: "在文件管理器中显示",
"open-default": "用默认应用打开",
refresh: "刷新",
"focus-path": "转到路径",
"source-picker": "选择面板显示的内容",
"sort-name": "按名称排序",
"sort-size": "按大小排序",
"sort-mtime": "按修改时间排序",
"sort-kind": "按种类排序",
"sort-rights": "按权限排序",
"sort-owner": "按所有者排序",
"sort-group": "按组排序",
"new-tab": "新建标签页",
"close-tab": "关闭标签页",
"close-other-tabs": "关闭其他标签页",
"close-tabs-right": "关闭右侧标签页",
"next-tab": "下一个标签页",
"prev-tab": "上一个标签页",
"go-to-tab": "转到标签页 {n}",
"go-to-last-tab": "转到最后一个标签页",
"move-tab-left": "标签页左移",
"move-tab-right": "标签页右移",
"disconnect-tab": "断开标签页",
"open-settings": "设置",
"connection-manager": "连接管理器",
"toggle-transfers": "显示 / 隐藏传输",
"install-update": "安装更新",
"scale-up": "放大界面",
"scale-down": "缩小界面",
"scale-reset": "界面 100%",
"close-window": "关闭窗口",
save: "保存",
find: "查找",
"find-next": "查找下一个",
"find-prev": "查找上一个",
"toggle-wrap": "自动换行",
"conn-new": "新建连接",
"conn-new-stack": "新建分组",
"conn-save": "保存连接",
"conn-connect": "连接 / 打开分组",
"conn-delete": "删除连接或分组",
"conn-rename-stack": "重命名分组",
"conn-import": "从 WinSCP 导入",
groups: {
panels: "文件列表",
files: "文件",
sort: "排序",
tabs: "标签页",
window: "窗口",
editor: "编辑器",
connections: "连接管理器",
},
ui: {
filter: "搜索快捷键",
intro: "点击 + 并按下按键即可添加快捷键。第一个会显示在菜单和提示中。",
add: "添加快捷键",
remove: "移除 {key}",
reset: "恢复默认",
resetAll: "全部恢复",
resetAllConfirm: "恢复所有快捷键",
press: "请按键…({esc} 取消)",
unbound: "无快捷键",
reserved: "{key} 已被系统保留",
needsModifier: "在编辑器中 {key} 只会输入字符:请加上修饰键或使用 F 键",
usedBy: "{key} 已被“{action}”使用",
replace: "替换",
conflict: "与 {action} 的按键相同",
plainLetter: "在文件列表中输入 {key} 将不再进行筛选",
noResults: "没有匹配的快捷键",
fixed: "固定按键",
},
fixed: {
escape: "{esc} 关闭菜单、对话框和查找栏",
escList: "在文件列表中按 {esc} 依次清除筛选、取消连接、清除选择",
dialogs: "{enter} 和 {esc} 确认或取消对话框;{tab} 和 {shiftTab} 在其控件间移动",
menus: "↑ ↓ {home} {end} 和 {enter} 适用于所有菜单",
filter: "在文件列表中输入即可筛选;{backspace} 删除筛选的最后一个字符",
shift: "Shift 加移动键会选中光标经过的行",
find: "{enter} 和 {shiftEnter} 在查找结果间跳转",
sidebar: "↑ ↓ 在这些分区间切换",
},
names: {
ctrl: "Ctrl",
alt: "Alt",
+10 -4
View File
@@ -1,4 +1,5 @@
import { expect, test } from "bun:test";
import { DICTS } from "./i18n";
import { en } from "./i18n/en";
import {
BINDINGS,
@@ -111,10 +112,15 @@ for (const isMac of [true, false]) {
});
}
test("every command and group has an English label", () => {
const keys = en.keys as Record<string, unknown>;
for (const b of BINDINGS) expect(typeof keys["n" in b ? "go-to-tab" : b.id]).toBe("string");
for (const g of GROUPS) expect(typeof en.keys.groups[g]).toBe("string");
test("every command and group is labelled — in every shipped language", () => {
for (const [locale, dict] of Object.entries(DICTS)) {
const keys = dict.keys as Record<string, unknown> & { groups: Record<string, unknown> };
for (const b of BINDINGS) {
expect(`${locale}:${b.id}:${typeof keys["n" in b ? "go-to-tab" : b.id]}`).toBe(`${locale}:${b.id}:string`);
}
for (const g of GROUPS) expect(`${locale}:${g}:${typeof keys.groups[g]}`).toBe(`${locale}:${g}:string`);
}
expect(typeof en.keys.ui.press).toBe("string");
});
test("matching is exact about modifiers", () => {
+6
View File
@@ -403,6 +403,12 @@ export function firesInTextField(c: Combo, isMac: boolean): boolean {
return c.mod || c.alt;
}
/** Whether a plain press of the combo types a character — bound in the file
* list, that key then no longer starts a type-to-filter query. */
export function typesCharacter(c: Combo): boolean {
return !c.mod && !c.ctrl && !c.alt && (LETTER.test(c.key) || DIGIT.test(c.key) || Object.hasOwn(CHAR_OF, c.key));
}
/** Commands a held key may repeat; any other held shortcut fires once
* (holding ⌘W must not close every tab). */
const REPEATABLE = new Set<ShortcutId>([