feat: More prominent keyboard shortcuts in hints (#10057)

* Initial

* Memoize

* Styling

* Use double angle brackets for keyboard shortcuts

* Use rem in gap

* Use an existing function for substituting tags in a string

* Revert styling

* Avoid unique key warnings

* Styling

* getTransChildren -> nodesFromTextWithTags

* Use height and padding instead of padding only

* Initial new idea

* WIP shortcut substitutions

* Use simple regex for parsing shortcuts

* Use single shortcut for combos

* Use kbd instead of span

* shortcutFromKeyString -> getTaggedShortcutKey

* Bug fix

* FlowChart -> Flowchart

* memo is useless here

* Trigger CI

* Translate in getShortcutKey

* More normalized shortcuts

* improve shortcut normalization and replacement & support multi-key tagged shortcuts

* fix regex

* tweak css

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Omar Brikaa
2025-10-20 16:09:20 +02:00
committed by GitHub
co-authored by dwelle
parent 725412ebd3
commit 8013eb5e16
19 changed files with 188 additions and 81 deletions
-14
View File
@@ -20,7 +20,6 @@ import {
ENV, ENV,
FONT_FAMILY, FONT_FAMILY,
getFontFamilyFallbacks, getFontFamilyFallbacks,
isDarwin,
isAndroid, isAndroid,
isIOS, isIOS,
WINDOWS_EMOJI_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT,
@@ -426,19 +425,6 @@ export const allowFullScreen = () =>
export const exitFullScreen = () => document.exitFullscreen(); export const exitFullScreen = () => document.exitFullscreen();
export const getShortcutKey = (shortcut: string): string => {
shortcut = shortcut
.replace(/\bAlt\b/i, "Alt")
.replace(/\bShift\b/i, "Shift")
.replace(/\b(Enter|Return)\b/i, "Enter");
if (isDarwin) {
return shortcut
.replace(/\bCtrlOrCmd\b/gi, "Cmd")
.replace(/\bAlt\b/i, "Option");
}
return shortcut.replace(/\bCtrlOrCmd\b/gi, "Ctrl");
};
export const viewportCoordsToSceneCoords = ( export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number }, { clientX, clientY }: { clientX: number; clientY: number },
{ {
+3 -1
View File
@@ -4,7 +4,7 @@ import { isFrameLikeElement } from "@excalidraw/element";
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element"; import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common"; import { KEYS, arrayToMap } from "@excalidraw/common";
import { alignElements } from "@excalidraw/element"; import { alignElements } from "@excalidraw/element";
@@ -30,6 +30,8 @@ import { t } from "../i18n";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
import type { AppClassProperties, AppState, UIAppState } from "../types"; import type { AppClassProperties, AppState, UIAppState } from "../types";
+1 -1
View File
@@ -7,7 +7,6 @@ import {
MIN_ZOOM, MIN_ZOOM,
THEME, THEME,
ZOOM_STEP, ZOOM_STEP,
getShortcutKey,
updateActiveTool, updateActiveTool,
CODES, CODES,
KEYS, KEYS,
@@ -46,6 +45,7 @@ import { t } from "../i18n";
import { getNormalizedZoom } from "../scene"; import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
@@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
import { isFrameLikeElement } from "@excalidraw/element"; import { isFrameLikeElement } from "@excalidraw/element";
import { CODES, KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common"; import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element"; import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
@@ -26,6 +26,8 @@ import { t } from "../i18n";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
import type { AppClassProperties, AppState } from "../types"; import type { AppClassProperties, AppState } from "../types";
@@ -3,7 +3,6 @@ import {
KEYS, KEYS,
MOBILE_ACTION_BUTTON_BG, MOBILE_ACTION_BUTTON_BG,
arrayToMap, arrayToMap,
getShortcutKey,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { getNonDeletedElements } from "@excalidraw/element"; import { getNonDeletedElements } from "@excalidraw/element";
@@ -26,6 +25,7 @@ import { DuplicateIcon } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
+3 -1
View File
@@ -14,7 +14,7 @@ import {
replaceAllElementsInFrame, replaceAllElementsInFrame,
} from "@excalidraw/element"; } from "@excalidraw/element";
import { KEYS, randomId, arrayToMap, getShortcutKey } from "@excalidraw/common"; import { KEYS, randomId, arrayToMap } from "@excalidraw/common";
import { import {
getSelectedGroupIds, getSelectedGroupIds,
@@ -43,6 +43,8 @@ import { t } from "../i18n";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
import type { AppClassProperties, AppState } from "../types"; import type { AppClassProperties, AppState } from "../types";
+2 -2
View File
@@ -1,6 +1,6 @@
import { isEmbeddableElement } from "@excalidraw/element"; import { isEmbeddableElement } from "@excalidraw/element";
import { KEYS, getShortcutKey } from "@excalidraw/common"; import { KEYS } from "@excalidraw/common";
import { CaptureUpdateAction } from "@excalidraw/element"; import { CaptureUpdateAction } from "@excalidraw/element";
@@ -8,8 +8,8 @@ import { ToolButton } from "../components/ToolButton";
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink"; import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
import { LinkIcon } from "../components/icons"; import { LinkIcon } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
@@ -17,7 +17,6 @@ import {
randomInteger, randomInteger,
arrayToMap, arrayToMap,
getFontFamilyString, getFontFamilyString,
getShortcutKey,
getLineHeight, getLineHeight,
isTransparent, isTransparent,
reduceToCommonValue, reduceToCommonValue,
@@ -142,6 +141,8 @@ import {
restoreCaretPosition, restoreCaretPosition,
} from "../hooks/useTextEditorFocus"; } from "../hooks/useTextEditorFocus";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
import type { AppClassProperties, AppState, Primitive } from "../types"; import type { AppClassProperties, AppState, Primitive } from "../types";
+2 -1
View File
@@ -1,4 +1,4 @@
import { KEYS, CODES, getShortcutKey, isDarwin } from "@excalidraw/common"; import { KEYS, CODES, isDarwin } from "@excalidraw/common";
import { import {
moveOneLeft, moveOneLeft,
@@ -16,6 +16,7 @@ import {
SendToBackIcon, SendToBackIcon,
} from "../components/icons"; } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../shortcut";
import { register } from "./register"; import { register } from "./register";
+2 -1
View File
@@ -1,8 +1,9 @@
import { isDarwin, getShortcutKey } from "@excalidraw/common"; import { isDarwin } from "@excalidraw/common";
import type { SubtypeOf } from "@excalidraw/common/utility-types"; import type { SubtypeOf } from "@excalidraw/common/utility-types";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../shortcut";
import type { ActionName } from "./types"; import type { ActionName } from "./types";
+2 -1
View File
@@ -80,7 +80,6 @@ import {
wrapEvent, wrapEvent,
updateObject, updateObject,
updateActiveTool, updateActiveTool,
getShortcutKey,
isTransparent, isTransparent,
easeToValuesRAF, easeToValuesRAF,
muteFSAbortError, muteFSAbortError,
@@ -406,6 +405,8 @@ import { LassoTrail } from "../lasso";
import { EraserTrail } from "../eraser"; import { EraserTrail } from "../eraser";
import { getShortcutKey } from "../shortcut";
import ConvertElementTypePopup, { import ConvertElementTypePopup, {
getConversionTypeFromElements, getConversionTypeFromElements,
convertElementTypePopupAtom, convertElementTypePopupAtom,
@@ -1,8 +1,9 @@
import clsx from "clsx"; import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { KEYS, getShortcutKey } from "@excalidraw/common"; import { KEYS } from "@excalidraw/common";
import { getShortcutKey } from "../..//shortcut";
import { useAtom } from "../../editor-jotai"; import { useAtom } from "../../editor-jotai";
import { t } from "../../i18n"; import { t } from "../../i18n";
import { useDevice } from "../App"; import { useDevice } from "../App";
@@ -7,12 +7,13 @@ import {
EVENT, EVENT,
KEYS, KEYS,
capitalizeString, capitalizeString,
getShortcutKey,
isWritableElement, isWritableElement,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch"; import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
import { getShortcutKey } from "@excalidraw/excalidraw/shortcut";
import type { MarkRequired } from "@excalidraw/common/utility-types"; import type { MarkRequired } from "@excalidraw/common/utility-types";
import { import {
@@ -2,11 +2,12 @@ import React from "react";
import { isDarwin, isFirefox, isWindows } from "@excalidraw/common"; import { isDarwin, isFirefox, isWindows } from "@excalidraw/common";
import { KEYS, getShortcutKey } from "@excalidraw/common"; import { KEYS } from "@excalidraw/common";
import { getShortcutFromShortcutName } from "../actions/shortcuts"; import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { probablySupportsClipboardBlob } from "../clipboard"; import { probablySupportsClipboardBlob } from "../clipboard";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../shortcut";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { ExternalLinkIcon, GithubIcon, youtubeIcon } from "./icons"; import { ExternalLinkIcon, GithubIcon, youtubeIcon } from "./icons";
@@ -28,11 +28,24 @@ $wide-viewport-width: 1000px;
> span { > span {
padding: 0.25rem; padding: 0.25rem;
} }
kbd {
display: inline-block;
margin: 0 1px;
font-family: monospace;
border: 1px solid var(--color-gray-40);
border-radius: 4px;
padding: 1px 3px;
font-size: 10px;
}
} }
&.theme--dark { &.theme--dark {
.HintViewer { .HintViewer {
color: var(--color-gray-60); color: var(--color-gray-60);
kbd {
border-color: var(--color-gray-60);
}
} }
} }
} }
+96 -31
View File
@@ -9,11 +9,10 @@ import {
isTextElement, isTextElement,
} from "@excalidraw/element"; } from "@excalidraw/element";
import { getShortcutKey } from "@excalidraw/common";
import { isNodeInFlowchart } from "@excalidraw/element"; import { isNodeInFlowchart } from "@excalidraw/element";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../shortcut";
import { isEraserActive } from "../appState"; import { isEraserActive } from "../appState";
import { isGridModeEnabled } from "../snapping"; import { isGridModeEnabled } from "../snapping";
@@ -28,6 +27,11 @@ interface HintViewerProps {
app: AppClassProperties; app: AppClassProperties;
} }
const getTaggedShortcutKey = (key: string | string[]) =>
Array.isArray(key)
? `<kbd>${key.map(getShortcutKey).join(" + ")}</kbd>`
: `<kbd>${getShortcutKey(key)}</kbd>`;
const getHints = ({ const getHints = ({
appState, appState,
isMobile, isMobile,
@@ -42,7 +46,9 @@ const getHints = ({
appState.openSidebar.tab === CANVAS_SEARCH_TAB && appState.openSidebar.tab === CANVAS_SEARCH_TAB &&
appState.searchMatches?.matches.length appState.searchMatches?.matches.length
) { ) {
return t("hints.dismissSearch"); return t("hints.dismissSearch", {
shortcut: getTaggedShortcutKey("Escape"),
});
} }
if (appState.openSidebar && !device.editor.canFitSidebar) { if (appState.openSidebar && !device.editor.canFitSidebar) {
@@ -50,14 +56,21 @@ const getHints = ({
} }
if (isEraserActive(appState)) { if (isEraserActive(appState)) {
return t("hints.eraserRevert"); return t("hints.eraserRevert", {
shortcut: getTaggedShortcutKey("Alt"),
});
} }
if (activeTool.type === "arrow" || activeTool.type === "line") { if (activeTool.type === "arrow" || activeTool.type === "line") {
if (multiMode) { if (multiMode) {
return t("hints.linearElementMulti"); return t("hints.linearElementMulti", {
shortcut_1: getTaggedShortcutKey("Escape"),
shortcut_2: getTaggedShortcutKey("Enter"),
});
} }
if (activeTool.type === "arrow") { if (activeTool.type === "arrow") {
return t("hints.arrowTool", { arrowShortcut: getShortcutKey("A") }); return t("hints.arrowTool", {
shortcut: getTaggedShortcutKey("A"),
});
} }
return t("hints.linearElement"); return t("hints.linearElement");
} }
@@ -83,31 +96,51 @@ const getHints = ({
) { ) {
const targetElement = selectedElements[0]; const targetElement = selectedElements[0];
if (isLinearElement(targetElement) && targetElement.points.length === 2) { if (isLinearElement(targetElement) && targetElement.points.length === 2) {
return t("hints.lockAngle"); return t("hints.lockAngle", {
shortcut: getTaggedShortcutKey("Shift"),
});
} }
return isImageElement(targetElement) return isImageElement(targetElement)
? t("hints.resizeImage") ? t("hints.resizeImage", {
: t("hints.resize"); shortcut_1: getTaggedShortcutKey("Shift"),
shortcut_2: getTaggedShortcutKey("Alt"),
})
: t("hints.resize", {
shortcut_1: getTaggedShortcutKey("Shift"),
shortcut_2: getTaggedShortcutKey("Alt"),
});
} }
if (isRotating && lastPointerDownWith === "mouse") { if (isRotating && lastPointerDownWith === "mouse") {
return t("hints.rotate"); return t("hints.rotate", {
shortcut: getTaggedShortcutKey("Shift"),
});
} }
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) { if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected"); return t("hints.text_selected", {
shortcut: getTaggedShortcutKey("Enter"),
});
} }
if (appState.editingTextElement) { if (appState.editingTextElement) {
return t("hints.text_editing"); return t("hints.text_editing", {
shortcut_1: getTaggedShortcutKey("Escape"),
shortcut_2: getTaggedShortcutKey(["CtrlOrCmd", "Enter"]),
});
} }
if (appState.croppingElementId) { if (appState.croppingElementId) {
return t("hints.leaveCropEditor"); return t("hints.leaveCropEditor", {
shortcut_1: getTaggedShortcutKey("Enter"),
shortcut_2: getTaggedShortcutKey("Escape"),
});
} }
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) { if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
return t("hints.enterCropEditor"); return t("hints.enterCropEditor", {
shortcut: getTaggedShortcutKey("Enter"),
});
} }
if (activeTool.type === "selection") { if (activeTool.type === "selection") {
@@ -117,33 +150,60 @@ const getHints = ({
!appState.editingTextElement && !appState.editingTextElement &&
!appState.selectedLinearElement?.isEditing !appState.selectedLinearElement?.isEditing
) { ) {
return [t("hints.deepBoxSelect")]; return [
t("hints.deepBoxSelect", {
shortcut: getTaggedShortcutKey("CtrlOrCmd"),
}),
];
} }
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) { if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
return t("hints.disableSnapping"); return t("hints.disableSnapping", {
shortcut: getTaggedShortcutKey("CtrlOrCmd"),
});
} }
if (!selectedElements.length && !isMobile) { if (!selectedElements.length && !isMobile) {
return [t("hints.canvasPanning")]; return [
t("hints.canvasPanning", {
shortcut: getTaggedShortcutKey("Space"),
}),
];
} }
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
if (isLinearElement(selectedElements[0])) { if (isLinearElement(selectedElements[0])) {
if (appState.selectedLinearElement?.isEditing) { if (appState.selectedLinearElement?.isEditing) {
return appState.selectedLinearElement.selectedPointsIndices return appState.selectedLinearElement.selectedPointsIndices
? t("hints.lineEditor_pointSelected") ? t("hints.lineEditor_pointSelected", {
: t("hints.lineEditor_nothingSelected"); shortcut_1: getTaggedShortcutKey("Delete"),
shortcut_2: getTaggedShortcutKey(["CtrlOrCmd", "D"]),
})
: t("hints.lineEditor_nothingSelected", {
shortcut_1: getTaggedShortcutKey("Shift"),
shortcut_2: getTaggedShortcutKey("Alt"),
});
} }
return isLineElement(selectedElements[0]) return isLineElement(selectedElements[0])
? t("hints.lineEditor_line_info") ? t("hints.lineEditor_line_info", {
: t("hints.lineEditor_info"); shortcut: getTaggedShortcutKey("Enter"),
})
: t("hints.lineEditor_info", {
shortcut_1: getTaggedShortcutKey("CtrlOrCmd"),
shortcut_2: getTaggedShortcutKey(["CtrlOrCmd", "Enter"]),
});
} }
if ( if (
!appState.newElement && !appState.newElement &&
!appState.selectedElementsAreBeingDragged && !appState.selectedElementsAreBeingDragged &&
isTextBindableContainer(selectedElements[0]) isTextBindableContainer(selectedElements[0])
) { ) {
const bindTextToElement = t("hints.bindTextToElement", {
shortcut: getTaggedShortcutKey("Enter"),
});
const createFlowchart = t("hints.createFlowchart", {
shortcut: getTaggedShortcutKey("CtrlOrCmd"),
});
if (isFlowchartNodeElement(selectedElements[0])) { if (isFlowchartNodeElement(selectedElements[0])) {
if ( if (
isNodeInFlowchart( isNodeInFlowchart(
@@ -151,13 +211,13 @@ const getHints = ({
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
) )
) { ) {
return [t("hints.bindTextToElement"), t("hints.createFlowchart")]; return [bindTextToElement, createFlowchart];
} }
return [t("hints.bindTextToElement"), t("hints.createFlowchart")]; return [bindTextToElement, createFlowchart];
} }
return t("hints.bindTextToElement"); return bindTextToElement;
} }
} }
} }
@@ -183,16 +243,21 @@ export const HintViewer = ({
} }
const hint = Array.isArray(hints) const hint = Array.isArray(hints)
? hints ? hints.map((hint) => hint.replace(/\. ?$/, "")).join(". ")
.map((hint) => { : hints;
return getShortcutKey(hint).replace(/\. ?$/, "");
}) const hintJSX = hint.split(/(<kbd>[^<]+<\/kbd>)/g).map((part, index) => {
.join(". ") if (index % 2 === 1) {
: getShortcutKey(hints); const shortcutMatch =
part[0] === "<" && part.match(/^<kbd>([^<]+)<\/kbd>$/);
return <kbd key={index}>{shortcutMatch ? shortcutMatch[1] : part}</kbd>;
}
return part;
});
return ( return (
<div className="HintViewer"> <div className="HintViewer">
<span>{hint}</span> <span>{hintJSX}</span>
</div> </div>
); );
}; };
@@ -1,4 +1,4 @@
import { getShortcutKey } from "@excalidraw/common"; import { getShortcutKey } from "@excalidraw/excalidraw/shortcut";
export const TTDDialogSubmitShortcut = () => { export const TTDDialogSubmitShortcut = () => {
return ( return (
+32 -21
View File
@@ -337,33 +337,33 @@
"shapes": "Shapes" "shapes": "Shapes"
}, },
"hints": { "hints": {
"dismissSearch": "Escape to dismiss search", "dismissSearch": "{{shortcut}} to dismiss search",
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool", "canvasPanning": "To move canvas, hold mouse wheel or {{shortcut}} while dragging, or use the hand tool",
"linearElement": "Click to start multiple points, drag for single line", "linearElement": "Click to start multiple points, drag for single line",
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.", "arrowTool": "Click to start multiple points, drag for single line. Press {{shortcut}} again to change arrow type.",
"freeDraw": "Click and drag, release when you're finished", "freeDraw": "Click and drag, release when you're finished",
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool", "text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
"embeddable": "Click-drag to create a website embed", "embeddable": "Click-drag to create a website embed",
"text_selected": "Double-click or press ENTER to edit text", "text_selected": "Double-click or press {{shortcut}} to edit text",
"text_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing", "text_editing": "Press {{shortcut_1}} or {{shortcut_2}} to finish editing",
"linearElementMulti": "Click on last point or press Escape or Enter to finish", "linearElementMulti": "Click on last point or press {{shortcut_1}} or {{shortcut_2}} to finish",
"lockAngle": "You can constrain angle by holding SHIFT", "lockAngle": "You can constrain angle by holding {{shortcut}}",
"resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center", "resize": "You can constrain proportions by holding {{shortcut_1}} while resizing,\nhold {{shortcut_2}} to resize from the center",
"resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center", "resizeImage": "You can resize freely by holding {{shortcut_1}},\nhold {{shortcut_2}} to resize from the center",
"rotate": "You can constrain angles by holding SHIFT while rotating", "rotate": "You can constrain angles by holding {{shortcut}} while rotating",
"lineEditor_info": "Hold CtrlOrCmd and Double-click or press CtrlOrCmd + Enter to edit points", "lineEditor_info": "Hold {{shortcut_1}} and Double-click or press {{shortcut_2}} to edit points",
"lineEditor_line_info": "Double-click or press Enter to edit points", "lineEditor_line_info": "Double-click or press {{shortcut}} to edit points",
"lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move", "lineEditor_pointSelected": "Press {{shortcut_1}} to remove point(s),\n{{shortcut_2}} to duplicate, or drag to move",
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points", "lineEditor_nothingSelected": "Select a point to edit (hold {{shortcut_1}} to select multiple),\nor hold {{shortcut_2}} and click to add new points",
"publishLibrary": "Publish your own library", "publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text", "bindTextToElement": "Press {{shortcut}} to add text",
"createFlowchart": "Hold CtrlOrCmd and Arrow key to create a flowchart", "createFlowchart": "Hold {{shortcut}} and Arrow key to create a flowchart",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", "deepBoxSelect": "Hold {{shortcut}} to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion", "eraserRevert": "Hold {{shortcut}} to revert the elements marked for deletion",
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.", "firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
"disableSnapping": "Hold CtrlOrCmd to disable snapping", "disableSnapping": "Hold {{shortcut}} to disable snapping",
"enterCropEditor": "Double click the image or press ENTER to crop the image", "enterCropEditor": "Double click the image or press {{shortcut}} to crop the image",
"leaveCropEditor": "Click outside the image or press ENTER or ESCAPE to finish cropping" "leaveCropEditor": "Click outside the image or press {{shortcut_1}} or {{shortcut_2}} to finish cropping"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",
@@ -648,5 +648,16 @@
}, },
"itemNotAvailable": "Command is not available...", "itemNotAvailable": "Command is not available...",
"shortcutHint": "For Command palette, use {{shortcut}}" "shortcutHint": "For Command palette, use {{shortcut}}"
},
"keys": {
"ctrl": "Ctrl",
"option": "Option",
"cmd": "Cmd",
"alt": "Alt",
"escape": "Esc",
"enter": "Enter",
"shift": "Shift",
"spacebar": "Space",
"delete": "Delete"
} }
} }
+19
View File
@@ -0,0 +1,19 @@
import { isDarwin } from "@excalidraw/common";
import { t } from "./i18n";
export const getShortcutKey = (shortcut: string): string =>
shortcut
.replace(
/\b(Opt(?:ion)?|Alt)\b/i,
isDarwin ? t("keys.option") : t("keys.alt"),
)
.replace(/\bShift\b/i, t("keys.shift"))
.replace(/\b(Enter|Return)\b/i, t("keys.enter"))
.replace(
/\b(Ctrl|Cmd|Command|CtrlOrCmd)\b/gi,
isDarwin ? t("keys.cmd") : t("keys.ctrl"),
)
.replace(/\b(Esc(?:ape)?)\b/i, t("keys.escape"))
.replace(/\b(Space(?:bar)?)\b/i, t("keys.spacebar"))
.replace(/\b(Del(?:ete)?)\b/i, t("keys.delete"));