+ |
+ {t(`actions.${descriptor.id}`)}
+ |
+
+
+ |
+
+ )
+}
diff --git a/src/frontend/src/features/shortcuts/formatLabels.ts b/src/frontend/src/features/shortcuts/formatLabels.ts
new file mode 100644
index 00000000..598fb38b
--- /dev/null
+++ b/src/frontend/src/features/shortcuts/formatLabels.ts
@@ -0,0 +1,61 @@
+import { Shortcut } from './types'
+import { isMacintosh } from '@/utils/livekit'
+
+// Visible label for a shortcut (uses ⌘/Ctrl prefix when needed).
+export const formatShortcutLabel = (shortcut?: Shortcut) => {
+ if (!shortcut) return '—'
+ const key = shortcut.key?.toUpperCase()
+ if (!key) return '—'
+ const parts: string[] = []
+ if (shortcut.ctrlKey) parts.push(isMacintosh() ? '⌘' : 'Ctrl')
+ if (shortcut.altKey) parts.push(isMacintosh() ? '⌥' : 'Alt')
+ if (shortcut.shiftKey) parts.push('Shift')
+ parts.push(key)
+ return parts.join('+')
+}
+
+// SR-friendly label for a shortcut (reads “Control plus D”).
+export const formatShortcutLabelForSR = (
+ shortcut: Shortcut | undefined,
+ {
+ controlLabel,
+ commandLabel,
+ plusLabel,
+ noShortcutLabel,
+ }: {
+ controlLabel: string
+ commandLabel: string
+ plusLabel: string
+ noShortcutLabel: string
+ }
+) => {
+ if (!shortcut) return noShortcutLabel
+ const key = shortcut.key?.toUpperCase()
+ if (!key) return noShortcutLabel
+ const ctrlWord = isMacintosh() ? commandLabel : controlLabel
+ const parts: string[] = []
+ if (shortcut.ctrlKey) parts.push(ctrlWord)
+ if (shortcut.altKey) parts.push('Alt')
+ if (shortcut.shiftKey) parts.push('Shift')
+ parts.push(key)
+ return parts.join(` ${plusLabel} `)
+}
+
+// Extract displayable key name from KeyboardEvent.code (ex: KeyV -> V).
+export const getKeyLabelFromCode = (code?: string) => {
+ if (!code) return ''
+ if (code.startsWith('Key') && code.length === 4) return code.slice(3)
+ if (code.startsWith('Digit') && code.length === 6) return code.slice(5)
+ if (code === 'Space') return '␣'
+ if (code.startsWith('Arrow')) return code.slice(5) // Up, Down, Left, Right
+ return code
+}
+
+// Long-press label (visual or SR), e.g. “Hold V”.
+export const formatLongPressLabel = (
+ codeLabel: string,
+ holdTemplate: string
+) => {
+ if (!codeLabel) return holdTemplate.replace('{{key}}', '?')
+ return holdTemplate.replace('{{key}}', codeLabel)
+}
diff --git a/src/frontend/src/features/shortcuts/hooks/useShortcutFormatting.ts b/src/frontend/src/features/shortcuts/hooks/useShortcutFormatting.ts
new file mode 100644
index 00000000..44e75d9b
--- /dev/null
+++ b/src/frontend/src/features/shortcuts/hooks/useShortcutFormatting.ts
@@ -0,0 +1,51 @@
+import { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Shortcut } from '../types'
+import {
+ formatShortcutLabel,
+ formatShortcutLabelForSR,
+ formatLongPressLabel,
+ getKeyLabelFromCode,
+} from '../formatLabels'
+
+export const useShortcutFormatting = () => {
+ const { t } = useTranslation('rooms')
+
+ const formatVisual = useCallback(
+ (shortcut?: Shortcut, code?: string, kind?: string) => {
+ if (code && kind === 'longPress') {
+ const label = getKeyLabelFromCode(code)
+ return formatLongPressLabel(
+ label,
+ t('shortcutsPanel.visual.hold', { key: '{{key}}' })
+ )
+ }
+ return formatShortcutLabel(shortcut)
+ },
+ [t]
+ )
+
+ const formatForSR = useCallback(
+ (shortcut?: Shortcut, code?: string, kind?: string) => {
+ if (code && kind === 'longPress') {
+ const label = getKeyLabelFromCode(code)
+ return formatLongPressLabel(
+ label,
+ t('shortcutsPanel.sr.hold', { key: '{{key}}' })
+ )
+ }
+ return formatShortcutLabelForSR(shortcut, {
+ controlLabel: t('shortcutsPanel.sr.control'),
+ commandLabel: t('shortcutsPanel.sr.command'),
+ plusLabel: t('shortcutsPanel.sr.plus'),
+ noShortcutLabel: t('shortcutsPanel.sr.noShortcut'),
+ })
+ },
+ [t]
+ )
+
+ return {
+ formatVisual,
+ formatForSR,
+ }
+}
diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json
index d994855a..2f170f7c 100644
--- a/src/frontend/src/locales/de/rooms.json
+++ b/src/frontend/src/locales/de/rooms.json
@@ -595,6 +595,38 @@
"muteParticipant": "{{name}} stummschalten",
"fullScreen": "Vollbild"
},
+ "shortcutsPanel": {
+ "title": "Tastenkombinationen",
+ "categories": {
+ "navigation": "Navigation",
+ "media": "Medien",
+ "interaction": "Interaktion"
+ },
+ "actions": {
+ "open-shortcuts": "Tastenkürzel-Hilfe öffnen",
+ "focus-toolbar": "Fokus auf die untere Symbolleiste",
+ "toggle-microphone": "Mikrofon umschalten",
+ "toggle-camera": "Kamera umschalten",
+ "push-to-talk": "Push-to-talk (gedrückt halten zum Einschalten)",
+ "reaction": "Reaktionspanel",
+ "fullscreen": "Vollbild umschalten",
+ "recording": "Aufnahmepanel umschalten",
+ "raise-hand": "Hand heben oder senken",
+ "toggle-chat": "Chat anzeigen/ausblenden",
+ "toggle-participants": "Teilnehmer anzeigen/ausblenden",
+ "open-shortcuts-settings": "Tastenkürzel-Einstellungen öffnen"
+ },
+ "sr": {
+ "control": "Steuerung",
+ "command": "Befehl",
+ "plus": "plus",
+ "hold": "Halte {{key}} gedrückt",
+ "noShortcut": "Kein Tastenkürzel"
+ },
+ "visual": {
+ "hold": "Halte {{key}} gedrückt"
+ }
+ },
"fullScreenWarning": {
"message": "Um eine Endlosschleife zu vermeiden, teile nicht deinen gesamten Bildschirm. Teile stattdessen einen Tab oder ein anderes Fenster.",
"stop": "Präsentation beenden",
diff --git a/src/frontend/src/locales/de/settings.json b/src/frontend/src/locales/de/settings.json
index 1664b612..8602f427 100644
--- a/src/frontend/src/locales/de/settings.json
+++ b/src/frontend/src/locales/de/settings.json
@@ -100,6 +100,11 @@
}
}
},
+ "shortcuts": {
+ "listLabel": "Tastenkürzel",
+ "columnAction": "Aktion",
+ "columnShortcut": "Tastenkürzel"
+ },
"dialog": {
"heading": "Einstellungen"
},
@@ -120,6 +125,7 @@
"general": "Allgemein",
"notifications": "Benachrichtigungen",
"accessibility": "Barrierefreiheit",
- "transcription": "Transkription"
+ "transcription": "Transkription",
+ "shortcuts": "Tastenkürzel"
}
}
diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json
index 71800d70..0baa228e 100644
--- a/src/frontend/src/locales/en/rooms.json
+++ b/src/frontend/src/locales/en/rooms.json
@@ -595,6 +595,38 @@
"muteParticipant": "Mute {{name}}",
"fullScreen": "Full screen"
},
+ "shortcutsPanel": {
+ "title": "Keyboard shortcuts",
+ "categories": {
+ "navigation": "Navigation",
+ "media": "Media",
+ "interaction": "Interaction"
+ },
+ "actions": {
+ "open-shortcuts": "Open shortcuts help",
+ "focus-toolbar": "Focus bottom toolbar",
+ "toggle-microphone": "Toggle microphone",
+ "toggle-camera": "Toggle camera",
+ "push-to-talk": "Push-to-talk (hold to unmute)",
+ "reaction": "Emoji reaction panel",
+ "fullscreen": "Toggle fullscreen",
+ "recording": "Toggle recording panel",
+ "raise-hand": "Raise or lower hand",
+ "toggle-chat": "Toggle chat",
+ "toggle-participants": "Toggle participants",
+ "open-shortcuts-settings": "Open shortcuts settings"
+ },
+ "sr": {
+ "control": "Control",
+ "command": "Command",
+ "plus": "plus",
+ "hold": "Hold {{key}}",
+ "noShortcut": "No shortcut"
+ },
+ "visual": {
+ "hold": "Hold {{key}}"
+ }
+ },
"fullScreenWarning": {
"message": "To avoid infinite loop display, do not share your entire screen. Instead, share a tab or another window.",
"stop": "Stop presenting",
diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json
index 0c1865dc..6d0420bf 100644
--- a/src/frontend/src/locales/en/settings.json
+++ b/src/frontend/src/locales/en/settings.json
@@ -100,6 +100,11 @@
}
}
},
+ "shortcuts": {
+ "listLabel": "Keyboard shortcuts",
+ "columnAction": "Action",
+ "columnShortcut": "Shortcut"
+ },
"dialog": {
"heading": "Settings"
},
@@ -120,6 +125,7 @@
"general": "General",
"notifications": "Notifications",
"accessibility": "Accessibility",
- "transcription": "Transcription"
+ "transcription": "Transcription",
+ "shortcuts": "Shortcuts"
}
}
diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json
index 8144d4d3..b8d56006 100644
--- a/src/frontend/src/locales/fr/rooms.json
+++ b/src/frontend/src/locales/fr/rooms.json
@@ -595,6 +595,38 @@
"muteParticipant": "Couper le micro de {{name}}",
"fullScreen": "Plein écran"
},
+ "shortcutsPanel": {
+ "title": "Raccourcis clavier",
+ "categories": {
+ "navigation": "Navigation",
+ "media": "Média",
+ "interaction": "Interaction"
+ },
+ "actions": {
+ "open-shortcuts": "Ouvrir l’aide des raccourcis",
+ "focus-toolbar": "Mettre le focus sur la barre d’outils du bas",
+ "toggle-microphone": "Activer ou désactiver le micro",
+ "toggle-camera": "Activer ou désactiver la caméra",
+ "push-to-talk": "Appuyer pour parler (maintenir pour réactiver)",
+ "reaction": "Panneau des réactions",
+ "fullscreen": "Basculer en plein écran",
+ "recording": "Basculer le panneau d’enregistrement",
+ "raise-hand": "Lever ou baisser la main",
+ "toggle-chat": "Afficher/Masquer le chat",
+ "toggle-participants": "Afficher/Masquer les participants",
+ "open-shortcuts-settings": "Ouvrir les réglages des raccourcis"
+ },
+ "sr": {
+ "control": "Contrôle",
+ "command": "Commande",
+ "plus": "plus",
+ "hold": "Maintenir {{key}}",
+ "noShortcut": "Aucun raccourci"
+ },
+ "visual": {
+ "hold": "Maintenir {{key}}"
+ }
+ },
"fullScreenWarning": {
"message": "Pour éviter l'affichage en boucle infinie, ne partagez pas l'intégralité de votre écran. Partagez plutôt un onglet ou une autre fenêtre.",
"stop": "Arrêter la présentation",
diff --git a/src/frontend/src/locales/fr/settings.json b/src/frontend/src/locales/fr/settings.json
index c5d96efa..dfefb828 100644
--- a/src/frontend/src/locales/fr/settings.json
+++ b/src/frontend/src/locales/fr/settings.json
@@ -100,6 +100,11 @@
}
}
},
+ "shortcuts": {
+ "listLabel": "Liste des raccourcis clavier",
+ "columnAction": "Action",
+ "columnShortcut": "Raccourci"
+ },
"dialog": {
"heading": "Paramètres"
},
@@ -120,6 +125,7 @@
"general": "Général",
"notifications": "Notifications",
"accessibility": "Accessibilité",
- "transcription": "Transcription"
+ "transcription": "Transcription",
+ "shortcuts": "Raccourcis"
}
}
diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json
index a76dc10c..938af7bb 100644
--- a/src/frontend/src/locales/nl/rooms.json
+++ b/src/frontend/src/locales/nl/rooms.json
@@ -595,6 +595,38 @@
"muteParticipant": "Demp {{name}}",
"fullScreen": "Volledig scherm"
},
+ "shortcutsPanel": {
+ "title": "Sneltoetsen",
+ "categories": {
+ "navigation": "Navigatie",
+ "media": "Media",
+ "interaction": "Interactie"
+ },
+ "actions": {
+ "open-shortcuts": "Sneltoetsenhulp openen",
+ "focus-toolbar": "Focus op de onderste werkbalk",
+ "toggle-microphone": "Microfoon aan/uit",
+ "toggle-camera": "Camera aan/uit",
+ "push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)",
+ "reaction": "Reactiepaneel",
+ "fullscreen": "Volledig scherm wisselen",
+ "recording": "Opnamepaneel wisselen",
+ "raise-hand": "Hand opsteken of laten zakken",
+ "toggle-chat": "Chat tonen/verbergen",
+ "toggle-participants": "Deelnemers tonen/verbergen",
+ "open-shortcuts-settings": "Sneltoets-instellingen openen"
+ },
+ "sr": {
+ "control": "Control",
+ "command": "Command",
+ "plus": "plus",
+ "hold": "Houd {{key}} ingedrukt",
+ "noShortcut": "Geen sneltoets"
+ },
+ "visual": {
+ "hold": "Houd {{key}} ingedrukt"
+ }
+ },
"fullScreenWarning": {
"message": "Om niet oneindige uw scherm in zichzelf te delen, kunt u beter niet het hele scherm delen. Deel in plaats daarvan een tab of een ander venster.",
"stop": "Stop met presenteren",
diff --git a/src/frontend/src/locales/nl/settings.json b/src/frontend/src/locales/nl/settings.json
index 31edf1c5..160c03fc 100644
--- a/src/frontend/src/locales/nl/settings.json
+++ b/src/frontend/src/locales/nl/settings.json
@@ -100,6 +100,11 @@
}
}
},
+ "shortcuts": {
+ "listLabel": "Sneltoetsen",
+ "columnAction": "Actie",
+ "columnShortcut": "Sneltoets"
+ },
"dialog": {
"heading": "Instellingen"
},
@@ -114,6 +119,7 @@
"video": "Video",
"general": "Algemeen",
"notifications": "Meldingen",
- "transcription": "Transcriptie"
+ "transcription": "Transcriptie",
+ "shortcuts": "Sneltoetsen"
}
}