)}
- {error && {error}
}
+ {error && {nativeError ? t(`macDictationKey.${nativeError}`) : error}
}
);
}
@@ -426,6 +453,8 @@ function modifierPrimaryFromCode(code: string, key: string): string {
}
function primaryFromKeyboardEvent(e: KeyboardEvent): string {
+ const functionKey = functionKeyPrimaryFromEvent(e);
+ if (functionKey) return functionKey;
const printable = primaryFromPrintableCode(e.code);
if (printable) return printable;
if (e.key.length === 1) return e.key;
diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts
index b41cfd459..5ec3e3f17 100644
--- a/openless-all/app/src/i18n/en.ts
+++ b/openless-all/app/src/i18n/en.ts
@@ -4,6 +4,14 @@ import type { zhCN } from './zh-CN';
// Type-level guarantee that en mirrors the zh-CN shape.
export const en: typeof zhCN = {
+ macDictationKey: {
+ Changed: 'The shortcut changed while saving. Please try again.',
+ label: "Mac Dictation key",
+ description: "Replaces the current dictation shortcut with the microphone key. Quitting OpenLess releases it to macOS.",
+ Permission: "Allow OpenLess in macOS Privacy & Security → Accessibility, then retry.",
+ Busy: "Finish the current dictation before changing its shortcut.",
+ Unavailable: "Could not activate this shortcut. The saved binding is unchanged; retry or choose another key.",
+ },
app: {
name: 'OpenLess',
tagline: 'Speak naturally, write perfectly',
diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts
index 88a69f24f..73039e3dc 100644
--- a/openless-all/app/src/i18n/zh-CN.ts
+++ b/openless-all/app/src/i18n/zh-CN.ts
@@ -2,6 +2,14 @@
// 添加新 key 时,必须同步更新 en.ts,否则首次切换到 English 会回落到中文残留。
export const zhCN = {
+ macDictationKey: {
+ Changed: '保存期间快捷键已改变,请重试。',
+ label: "Mac 听写键",
+ description: "用麦克风图标键替换当前听写快捷键。退出 OpenLess 后,此键交回 macOS。",
+ Permission: "请在 macOS「隐私与安全性 → 辅助功能」中允许 OpenLess 后重试。",
+ Busy: "请先结束当前听写,再更改快捷键。",
+ Unavailable: "无法启用此快捷键,已保存的绑定未改变。请重试或选择其他键。",
+ },
app: {
name: 'OpenLess',
tagline: '自然说话,完美书写',
diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts
index 905fd5d24..39f8e9a10 100644
--- a/openless-all/app/src/i18n/zh-TW.ts
+++ b/openless-all/app/src/i18n/zh-TW.ts
@@ -4,6 +4,14 @@ import type { zhCN } from './zh-CN';
// 新增 key 時,必須同步更新 en.ts,避免切換到 English 後出現中文殘留。
export const zhTW: typeof zhCN = {
+ macDictationKey: {
+ Changed: '儲存期間快捷鍵已變更,請重試。',
+ label: "Mac 聽寫鍵",
+ description: "用麥克風圖示鍵替換目前的聽寫快捷鍵。結束 OpenLess 後,此鍵交回 macOS。",
+ Permission: "請在 macOS「隱私權與安全性 → 輔助使用」中允許 OpenLess 後重試。",
+ Busy: "請先結束目前的聽寫,再變更快捷鍵。",
+ Unavailable: "無法啟用此快捷鍵,已儲存的綁定未變更。請重試或選擇其他鍵。",
+ },
app: {
name: 'OpenLess',
tagline: '自然說話,完美書寫',
diff --git a/openless-all/app/src/lib/hotkey.ts b/openless-all/app/src/lib/hotkey.ts
index 1196e0188..bf09a0b52 100644
--- a/openless-all/app/src/lib/hotkey.ts
+++ b/openless-all/app/src/lib/hotkey.ts
@@ -368,6 +368,7 @@ function formatPrimary(primary: string): string {
const isMac = currentPlatform().isMac;
if (isMac) {
switch (trimmed.toLowerCase()) {
+ case 'macdictationkey': return i18n.t('macDictationKey.label');
case 'space': return '\u2423';
case 'enter':
case 'return': return '\u21A9';
diff --git a/openless-all/app/src/lib/hotkeyRecorder.test.ts b/openless-all/app/src/lib/hotkeyRecorder.test.ts
index a1a5841af..d9d8b333c 100644
--- a/openless-all/app/src/lib/hotkeyRecorder.test.ts
+++ b/openless-all/app/src/lib/hotkeyRecorder.test.ts
@@ -1,5 +1,6 @@
import {
createHotkeyRecorderState,
+ functionKeyPrimaryFromEvent,
orderHotkeyCodes,
updateHotkeyRecorderState,
} from './hotkeyRecorder';
@@ -83,3 +84,11 @@ function apply(
}
assertDeepEqual(orderHotkeyCodes(['Mouse4', 'ControlLeft']), ['ControlLeft', 'Mouse4'], 'orders mouse after modifiers');
+
+for (let i = 1; i <= 20; i++) {
+ assertEqual(functionKeyPrimaryFromEvent({ code: `F${i}`, key: `F${i}` }), `F${i}`, 'function key');
+}
+assertEqual(functionKeyPrimaryFromEvent({ code: 'F20', key: '\uF717' }), 'F20', 'WebKit private-use key');
+assertEqual(functionKeyPrimaryFromEvent({ code: 'F20', key: 'Unidentified' }), 'F20', 'physical F20');
+assertEqual(functionKeyPrimaryFromEvent({ code: '', key: 'F20' }), 'F20', 'named F20 fallback');
+assertEqual(functionKeyPrimaryFromEvent({ code: 'KeyA', key: 'a' }), null, 'printable key preserved');
diff --git a/openless-all/app/src/lib/hotkeyRecorder.ts b/openless-all/app/src/lib/hotkeyRecorder.ts
index 8ab692f5d..8473eedcd 100644
--- a/openless-all/app/src/lib/hotkeyRecorder.ts
+++ b/openless-all/app/src/lib/hotkeyRecorder.ts
@@ -68,3 +68,11 @@ const HOTKEY_CODE_ORDER = [
'NumpadSubtract', 'NumpadMultiply', 'NumpadDivide', 'NumpadDecimal', 'NumpadEnter',
'Mouse4', 'Mouse5',
];
+
+/** Prefer physical function-key codes; WebKit can expose a private-use key value. */
+export function functionKeyPrimaryFromEvent(event: { code: string; key: string }): string | null {
+ const supported = /^F([1-9]|1[0-9]|20)$/;
+ if (supported.test(event.code)) return event.code;
+ if (supported.test(event.key)) return event.key;
+ return null;
+}
diff --git a/openless-all/app/src/pages/settings/RecordingInputSection.tsx b/openless-all/app/src/pages/settings/RecordingInputSection.tsx
index 8fc63cc78..12639d282 100644
--- a/openless-all/app/src/pages/settings/RecordingInputSection.tsx
+++ b/openless-all/app/src/pages/settings/RecordingInputSection.tsx
@@ -265,17 +265,18 @@ export function RecordingInputSection() {