Documents
Hotkey and Keyboard Shortcut Handling
Hotkey and Keyboard Shortcut Handling
Type
Document
Status
Published
Created
Aug 8, 2025
Updated
Apr 3, 2026
Updated by
Dosu Bot

Hotkey, Keyboard Shortcut, and Input Translation Handling#

Global Keyboard Shortcuts#

The extension supports global keyboard shortcuts to control translation features and navigate the Options UI.

Page Translation Shortcut#

The primary shortcut toggles translation for the entire page and is fully configurable in the Options UI under "Customize translation shortcut". By default, the shortcut is set by the extension (see Options UI for the current default), but users can select any modifier + key combination.

When the user presses the configured shortcut, the system checks if translation is currently active. If active, translation stops; if inactive, translation starts. This logic is managed by the PageTranslationManager and implemented using the @tanstack/hotkeys library (v0.7.1). The shortcut configuration is stored in translate.page.shortcut as a single portable hotkey string (e.g., "Mod+E") in the config object, and all references in the codebase use this location.

Example logic:

// The shortcut is stored as a single portable string in config.translate.page.shortcut
const registration = HotkeyManager.getInstance().register(
  config.translate.page.shortcut,
  () => {
    if (manager.isActive) {
      manager.stop()
    } else {
      manager.start()
    }
  },
  {
    ignoreInputs: true,
    preventDefault: true,
    stopPropagation: true,
  },
)

// Returns a cleanup function to unregister the hotkey
return () => {
  registration.unregister()
}

The portable "Mod" semantic maps to Command on macOS and Control on Windows/Linux, allowing cross-platform shortcuts. The configuration migration system automatically converts legacy shortcut formats (string arrays) to the new portable format, so no manual action is required when upgrading.

Settings Command Palette Shortcut#

The extension provides a global keyboard shortcut to open the Settings command palette for quick navigation within the Options UI. This shortcut is ⌘K on macOS and Ctrl+K on Windows and Linux.

Pressing the shortcut opens the command palette dialog, which allows users to search and navigate to any settings section. Pressing the shortcut again (or pressing Escape) closes the dialog. The command palette is available throughout the Options page and provides instant access to all settings cards and sections.

The shortcut is implemented as a global keydown event listener that checks for either e.metaKey (⌘ on macOS) or e.ctrlKey (Ctrl on other platforms) combined with the 'k' key:

Example logic:

function handleKeyDown(e: KeyboardEvent) {
  if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
    e.preventDefault()
    setOpen(prev => !prev)
  }
}

The command palette integrates with the extension's routing and scroll system, allowing users to deep-link to specific sections via URL query parameters and ensuring smooth scrolling to the selected section when it mounts.

Customizing the Translation Shortcut#

A UI component, ShortcutKeyRecorder, allows users to record and set their preferred shortcut key combination for toggling page translation. This setting is available in the Options > Translation section as "Customize translation shortcut". The chosen shortcut is displayed in the popup and used for toggling translation in the content script.

The ShortcutKeyRecorder component:

  • Accepts and emits a single portable hotkey string (e.g., "Mod+E") instead of a string array
  • Uses keyboard event capture via global keydown listeners instead of the hotkeys-js scope system
  • Supports Escape to cancel recording and Backspace/Delete to clear the current shortcut
  • Stores shortcuts in portable format using utility functions from src/utils/page-translation-shortcut.ts
  • On macOS, preserves physical keys for Option shortcuts (e.g., Option+3 records as Alt+3, not Alt+#)

Node-Level (Fine-Grained) Translation Hotkey#

In addition to the global shortcut, the extension supports node-level translation toggling using a configurable hotkey. The default is Control, but users can set their preferred node translation shortcut in the Options > Translation section as "Customize node translation shortcut". This uses the same ShortcutKeyRecorder UI component and supports any single key or modifier + key combination.

Enabling and Disabling Node Translation#

The node translation hotkey feature can be toggled on or off via a Switch control in the Options > Translation section. This setting is stored in translateConfig.node.enabled. When disabled, the entire node-level translation feature is turned off, and the hotkey selector becomes disabled and non-interactive.

When the toggle is disabled (translateConfig.node.enabled = false), the hotkey selector's opacity is reduced to 50% and pointer events are disabled, providing a clear visual indication that the feature is inactive. The hotkey configuration is preserved when toggling off, so re-enabling the feature will restore the previously selected hotkey. This gives users complete control to disable hover translation without losing their hotkey preference.

Node-level translation is managed by global keydown and keyup listeners. Translation is only triggered if the configured hotkey is pressed and held without any other key being pressed before or during the hotkey session. If any other key or modifier is pressed before or during the hotkey press, translation will not trigger. After holding the hotkey alone for 1000ms, translation toggles for the node under the current mouse position. Key events are ignored if the target element is editable (such as an input or textarea) to avoid interfering with typing.

Session purity logic:

  • If any non-hotkey key is pressed, or if the hotkey is pressed in combination with any other key or modifier, the session is marked as impure and translation is canceled.
  • When the hotkey is pressed, translation only triggers if the session is pure (no other keys or modifiers involved).
  • The purity flag is reset when the hotkey is released.

Example logic:

let isHotkeyPressed = false;
let isHotkeySessionPure = true;
let timerId: number | null = null;

document.addEventListener('keydown', async (e) => {
  const hotkey = await getNodeHotkey();
  if (e.key === hotkey && !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey) {
    if (!isHotkeyPressed) {
      isHotkeyPressed = true;
      timerId = setTimeout(async () => {
        if (isHotkeySessionPure && isHotkeyPressed) {
          // trigger translation
        }
        timerId = null;
      }, 1000);
      if (!isHotkeySessionPure && timerId) {
        clearTimeout(timerId);
        timerId = null;
      }
    }
  } else {
    isHotkeySessionPure = false;
    if (isHotkeyPressed && timerId) {
      clearTimeout(timerId);
      timerId = null;
    }
  }
});

document.addEventListener('keyup', async (e) => {
  const hotkey = await getNodeHotkey();
  if (e.key === hotkey && !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey) {
    if (isHotkeySessionPure) {
      // trigger translation
    }
    isHotkeyPressed = false;
    isHotkeySessionPure = true;
  }
});

This ensures translation is only triggered by a clean hotkey press, preventing accidental actions when users press key combinations or modifiers. The node translation shortcut is fully user-configurable in the Options UI.

Triple-Space Input Translation#

Overview#

The extension introduces a triple-space input translation feature, allowing users to quickly translate text in any input or contentEditable field by pressing the spacebar three times rapidly. This works in most standard input fields and contentEditable elements, except for password fields (which are ignored for safety).

How It Works#

  • Trigger: Press the spacebar three times quickly (within a configurable time threshold) in any input or contentEditable field.
  • Translation Direction Modes:
    • Normal: Native → Foreign (for others to read your message)
    • Reverse: Foreign → Native (for yourself to understand foreign text)
    • Cycle: Alternate between directions each time
  • Configurable Time Threshold: The maximum interval between consecutive space presses can be set between 100–1000ms (default: 300ms).
  • Undo Support: After translation, pressing Ctrl+Z restores the original text.
  • Safe Application: If you keep typing while translation is in progress, the translation will not overwrite your changes.
  • Inline Loading Spinner: A small spinner appears near the input while translation is in progress.
  • Full i18n Support: All UI and messages are localized in all supported languages.

Configuration#

A new settings page, Input Translation, is available under Overlay Tools in the Options UI. The following options are provided:

  • Enable Triple-Space Translation: Toggle the feature on or off.
  • Translation Direction: Choose between Normal, Reverse, or Cycle modes.
  • Target Language: Select whether to use Read Frog's source language or a custom target language for translation.
  • Trigger Threshold: Set the maximum time interval (in milliseconds) between space presses to trigger translation.

All settings are stored in the inputTranslation field of the config object. The config schema version is now v040, and migration scripts ensure automatic upgrades.

Technical Details#

  • The feature is implemented via a global keyboard event listener that tracks spacebar presses in input and contentEditable fields.
  • Uses document.execCommand('insertText') to ensure undo stack compatibility, so users can revert translations with Ctrl+Z.
  • The translation logic is handled by the useInputTranslation hook, which manages keyboard events, translation direction, and safe application of results.
  • The translation function supports bidirectional translation and per-direction caching.
  • Password fields are explicitly ignored for security.

Usage Example#

  1. Focus any input or contentEditable field (except password fields).
  2. Type your message.
  3. Quickly press the spacebar three times (within the configured threshold).
  4. The text will be translated according to your selected direction and target language.
  5. If you want to undo the translation, press Ctrl+Z.

Hotkey Detection and OS-Specific Formatting#

The system detects the user's operating system and formats hotkey strings accordingly. On macOS, modifier keys are displayed as symbols (e.g., ⌥ for Alt, ⌘ for Command, ⇧ for Shift), while on Windows and Linux, textual names are used. The formatted hotkey string is used throughout the UI, including the popup and shortcut selector.

For page translation shortcuts specifically, the formatting and validation logic is handled by the utility module src/utils/page-translation-shortcut.ts, which provides:

  • formatPageTranslationShortcut(): Format portable shortcuts (e.g., "Mod+E") for display based on the user's platform
  • isValidConfiguredPageTranslationShortcut(): Validate that a shortcut string includes at least one modifier and one non-modifier key
  • normalizePageTranslationShortcut(): Normalize shortcuts to portable format with "Mod" semantics
  • keyboardEventToPageTranslationShortcut(): Convert keyboard events to portable shortcut strings, preserving physical keys on macOS Option combos

Example:

// Format a portable shortcut for display
const displayString = formatPageTranslationShortcut("Mod+E")
// On macOS: "⌘E"
// On Windows/Linux: "Ctrl+E"

// Convert a keyboard event to a portable shortcut
const shortcut = keyboardEventToPageTranslationShortcut(event)
// Returns "Mod+E", "Alt+3", etc.

Shortcut Binding Implementation#

The binding logic for the page translation shortcut is implemented in src/entrypoints/host.content/translation-control/bind-translation-shortcut.ts. This module:

  • Returns a cleanup function (instead of void) that unregisters the hotkey when called
  • Uses HotkeyManager.getInstance().register() from @tanstack/hotkeys instead of the hotkeys() function
  • Validates shortcuts using isValidConfiguredPageTranslationShortcut() before registration
  • Registers with options: ignoreInputs: true, preventDefault: true, stopPropagation: true

Example implementation:

import { HotkeyManager } from "@tanstack/hotkeys"
import { isValidConfiguredPageTranslationShortcut } from "@/utils/page-translation-shortcut"

export async function bindTranslationShortcutKey(pageTranslationManager: PageTranslationManager) {
  const config = await getLocalConfig()
  if (!config || !isValidConfiguredPageTranslationShortcut(config.translate.page.shortcut)) {
    return () => {}
  }

  const registration = HotkeyManager.getInstance().register(
    config.translate.page.shortcut,
    () => {
      if (pageTranslationManager.isActive) {
        pageTranslationManager.stop()
      } else {
        pageTranslationManager.start()
      }
    },
    {
      ignoreInputs: true,
      preventDefault: true,
      stopPropagation: true,
    },
  )

  return () => {
    registration.unregister()
  }
}

Translation Toggling Logic#

When a translation hotkey is triggered, the system determines whether to show or hide translated content based on the node under the cursor and the currently selected translation mode. The translation mode is selected by the user via the Translation Mode Selector in the popup UI. If the selected mode is "translation only" and the current provider does not support it (e.g., Google), the extension will automatically switch to a supported provider, such as Microsoft or the first enabled LLM provider.

The toggling function is named removeOrShowNodeTranslation(point: Point, translationMode: TranslationMode), and it applies the correct translation or restoration logic for the node under the cursor based on the selected mode. MathML elements are excluded from translation, and Reddit-specific improvements prevent double translation and improve performance.

Example logic:

export async function removeOrShowNodeTranslation(point: Point, translationMode: TranslationMode) {
  const node = findNearestAncestorBlockNodeAt(point)
  if (!node || !isHTMLElement(node)) return

  // Prevent translation of MathML elements
  if (node.closest('math, [data-mathml]')) return

  if (!globalConfig) throw new Error('Global config is not initialized')
  if (!validateTranslationConfig({
    providersConfig: globalConfig.providersConfig,
    translate: globalConfig.translate,
    language: globalConfig.language,
  })) return

  // Provider fallback logic for translationOnly mode
  let providerId = globalConfig.translate.providerId
  if (translationMode === 'translationOnly') {
    const currentProvider = getProviderConfigById(globalConfig.providersConfig, providerId)
    if (currentProvider.provider === 'google') {
      const enabledProviders = filterEnabledProvidersConfig(globalConfig.providersConfig)
      const microsoftProvider = enabledProviders.find(p => p.provider === 'microsoft')
      providerId = microsoftProvider ? microsoftProvider.id : getLLMTranslateProvidersConfig(enabledProviders)[0]?.id
    }
  }

  const id = crypto.randomUUID()
  walkAndLabelElement(node, id)
  await translateWalkedElement(node, id, translationMode, true)
}

Translation Mode Selector in Popup#

A Translation Mode Selector in the extension's popup UI allows users to choose the translation mode (e.g., "bilingual" or "translation only"). The selector displays localized labels and tooltips for each mode. If a mode is selected that is not supported by the current provider, the extension will automatically switch to a supported provider.

Cache Management#

Users can clear all translation cache from the browser via the Options > Translation section using the "Clear Cache" button. This action cannot be undone and will remove all stored translation results.

Configuration Migration#

The extension includes a v065-to-v066 migration script that automatically converts legacy page translation shortcuts from string arrays (e.g., ["alt", "e"]) to portable hotkey strings (e.g., "Alt+E"). The migration:

  • Converts modifier keys (ctrl, command, alt, shift) to portable format with "Mod" semantics
  • Normalizes key names (e.g., "esc" → "Escape", single characters → uppercase)
  • Validates the resulting shortcut and falls back to "Alt+E" if invalid
  • Runs automatically on config upgrade, requiring no manual action

Migration behavior:

  • Legacy ["alt", "e"]"Alt+E"
  • Legacy ["ctrl", "t"] or ["command", "t"]"Mod+T"
  • Invalid or empty shortcuts → "Alt+E" (default)

Additional Features#

  • Selection Toolbar: A new configuration setting, selectionToolbar, is enabled by default and provides additional controls related to text selection and translation.
  • Vocabulary Table: The database schema includes a vocabulary table and a language enum, supporting multilingual vocabulary tracking for language learning and translation scenarios.

For further details on these features and configuration, refer to the Options UI and migration scripts in the repository.


Summary of New Input Translation Feature:

  • Trigger translation in any input or contentEditable field by pressing the spacebar three times quickly.
  • Configurable direction (normal, reverse, cycle), target language, and time threshold.
  • Undo support and safe application.
  • Fully localized and integrated with the extension's settings and migration system.
  • Password fields are ignored for safety.

See the Input Translation settings page in the Options UI for configuration.