Merge pull request #407 from Open-Less/pr/settings-vocab-collapsible
refactor(ui): Collapsible groups in Settings & Vocab
This commit is contained in:
8 files changed
+238
-93
No files matched your search
@@ -171,6 +171,7 @@ export const en: typeof zhCN = {
|
||||
kicker: 'VOCABULARY',
|
||||
title: 'Vocabulary',
|
||||
desc: 'Tell the model about words to expect — new terms, names, jargon. They are passed both as ASR hot words and as model context.',
|
||||
sectionTitle: 'Entries',
|
||||
placeholder: 'Type a word, press Enter or click Add…',
|
||||
tip: 'Mixed Chinese/English supported · numeric prefixes are matched literally · hits counted automatically',
|
||||
loadFailed: 'Load failed: {{err}}',
|
||||
@@ -299,34 +300,35 @@ export const en: typeof zhCN = {
|
||||
},
|
||||
recording: {
|
||||
title: 'Recording',
|
||||
desc: 'Define the global recording hotkey and how it triggers.',
|
||||
desc: 'Global recording hotkey and trigger mode.',
|
||||
hotkeyLabel: 'Recording hotkey',
|
||||
hotkeyDescAcc: 'Pressing it captures voice globally. Requires Accessibility permission.',
|
||||
hotkeyDescNoAcc: 'Pressing it captures voice globally. No Accessibility permission required.',
|
||||
hotkeyDescAcc: 'Press to capture voice globally (requires Accessibility permission).',
|
||||
hotkeyDescNoAcc: 'Press to capture voice globally.',
|
||||
modeLabel: 'Trigger mode',
|
||||
modeDesc: 'Toggle = press once to start, again to stop. Push-to-talk = hold to record, release to stop.',
|
||||
modeDesc: 'Toggle = tap once to start, again to stop. Push-to-talk = hold to record.',
|
||||
modeToggle: 'Toggle',
|
||||
modeHold: 'Push-to-talk',
|
||||
migrationNoticeTitle: 'Default recording mode is now Toggle',
|
||||
migrationNoticeDesc: 'If you changed the hotkey trigger mode before, please confirm it once. This update changes both the default value and the preference-reading path; if you prefer push-to-talk, switch it back manually.',
|
||||
migrationNoticeDesc: 'This update changes the default; if you prefer push-to-talk, switch it back here.',
|
||||
microphoneLabel: 'Preferred microphone',
|
||||
microphoneDesc: 'Choose the preferred input device. If it is temporarily unavailable, OpenLess uses the system default microphone and switches back when it reconnects.',
|
||||
microphoneDesc: 'Choose the preferred input device; falls back to system default when unavailable.',
|
||||
microphoneDefault: 'System default microphone',
|
||||
microphoneDefaultDesc: 'Use the system default input device',
|
||||
microphoneSystemDefault: 'system default',
|
||||
microphoneUnavailable: 'unavailable',
|
||||
microphoneLoadError: 'Failed to load microphones: {{message}}',
|
||||
microphoneDialogTitle: 'Microphone',
|
||||
microphoneDialogDesc: 'Choose a microphone that can pick up your voice. If the meter does not move, try another microphone.',
|
||||
microphoneDialogDesc: 'Choose a microphone that can pick up your voice.',
|
||||
microphoneMonitorError: 'Failed to monitor input level: {{message}}',
|
||||
capsuleLabel: 'Recording capsule',
|
||||
capsuleDesc: 'Show a translucent capsule at the bottom of the screen while recording / transcribing.',
|
||||
capsuleDesc: 'Show a translucent capsule at the bottom of the screen while recording.',
|
||||
muteDuringRecordingLabel: 'Mute while recording',
|
||||
muteDuringRecordingDesc: 'Temporarily mute system output during voice input, then restore the previous mute state when recording stops, is cancelled, or fails.',
|
||||
muteDuringRecordingDesc: 'Temporarily mute system output during voice input to avoid speaker echo.',
|
||||
insertGroupTitle: 'Insertion & clipboard',
|
||||
restoreClipboardLabel: 'Restore clipboard after insert',
|
||||
restoreClipboardDesc: 'Windows / Linux only: restore your original clipboard after a successful paste (default on). Turn off to keep the dictation text in the clipboard so you can manually Ctrl+V if the simulated paste did not actually land. See issue #111.',
|
||||
restoreClipboardDesc: 'Restore your original clipboard after a successful paste (Windows / Linux only).',
|
||||
pasteShortcutLabel: 'Simulated paste shortcut',
|
||||
pasteShortcutDesc: 'Windows / Linux only: which key combo to simulate when inserting text. Terminals like kitty, alacritty, wezterm, gnome-terminal, and foot only accept Ctrl+Shift+V; xterm / urxvt accept Shift+Insert. Takes effect on your next dictation. See issue #360.',
|
||||
pasteShortcutDesc: 'Which paste combo to simulate when inserting; some terminals need Ctrl+Shift+V (Windows / Linux only).',
|
||||
pasteShortcutCtrlV: 'Ctrl+V (default / most apps)',
|
||||
pasteShortcutCtrlShiftV: 'Ctrl+Shift+V (kitty / alacritty / wezterm / most terminals)',
|
||||
pasteShortcutShiftInsert: 'Shift+Insert (xterm / urxvt)',
|
||||
@@ -338,15 +340,17 @@ export const en: typeof zhCN = {
|
||||
comboClear: 'Clear',
|
||||
comboConflict: 'This shortcut combination is not available',
|
||||
allowNonTsfFallbackLabel: 'Allow non-TSF fallback',
|
||||
allowNonTsfFallbackDesc: 'Windows only: if direct TSF insertion fails, allow Unicode SendInput, shortcut paste, or WM_PASTE. Turn off to verify that insertion is really coming from TSF.',
|
||||
allowNonTsfFallbackDesc: 'Windows: when TSF insertion fails, allow Unicode SendInput / shortcut paste.',
|
||||
historyGroupTitle: 'History & context',
|
||||
historyRetentionLabel: 'History retention (days)',
|
||||
historyRetentionDesc: 'History entries older than this many days are pruned when a new entry is written. 0 = no time-based pruning (still capped at 200). Default 7 days.',
|
||||
historyRetentionDesc: 'Entries older than this are pruned on new writes; 0 = no time-based pruning.',
|
||||
polishContextWindowLabel: 'Polish context window (minutes)',
|
||||
polishContextWindowDesc: 'Feed the LLM successfully-polished transcripts from the last N minutes as multi-turn context, so pronouns and unfinished sentences resolve against prior dictation. 0 = disabled (single-turn polish, legacy behavior). Default 5 minutes; values above 60 rarely help.',
|
||||
polishContextWindowDesc: 'Use the last N minutes of polished transcripts as multi-turn context; 0 = disabled.',
|
||||
startupGroupTitle: 'Startup',
|
||||
startMinimizedLabel: 'Start minimized (no main window)',
|
||||
startMinimizedDesc: 'When on, the main window never appears on launch (including login autostart) — OpenLess runs in the menu bar / tray only. Click the tray icon to open the main window when needed. Useful with Windows autostart.',
|
||||
startMinimizedDesc: 'No main window on any launch path — menu bar / tray only.',
|
||||
startupAtBoot: 'Launch at login',
|
||||
startupAtBootDesc: 'Start OpenLess automatically when you sign in. macOS uses a LaunchAgent, Linux writes ~/.config/autostart, Windows writes HKCU\\Run (no admin required). See issue #194.',
|
||||
startupAtBootDesc: 'Start OpenLess automatically when you sign in.',
|
||||
startupAtBootError: 'Failed to toggle launch at login: {{message}}',
|
||||
},
|
||||
providers: {
|
||||
|
||||
@@ -173,6 +173,7 @@ export const ja: typeof zhCN = {
|
||||
kicker: 'VOCABULARY',
|
||||
title: '語彙',
|
||||
desc: '認識前に出現する可能性がある単語をモデルに伝えます——新語や専門用語など。ASR ホットワードと後処理モデルのコンテキスト両方に渡されます。',
|
||||
sectionTitle: '項目',
|
||||
placeholder: '単語を入力し、Enter または追加をクリック…',
|
||||
tip: '日本語と英数の混在対応 · 数字始まりは字面通り認識 · ヒット回数を自動カウント',
|
||||
loadFailed: '読み込み失敗:{{err}}',
|
||||
@@ -324,11 +325,12 @@ export const ja: typeof zhCN = {
|
||||
capsuleLabel: '録音カプセル',
|
||||
capsuleDesc: '録音 / 転写中、画面下部に半透明のカプセルを表示。',
|
||||
muteDuringRecordingLabel: '録音中はミュート',
|
||||
muteDuringRecordingDesc: '音声入力開始時にシステム出力を一時的にミュートし、停止/キャンセル/エラー後に元のミュート状態を復元。スピーカーの音がマイクに入らないようにします。',
|
||||
muteDuringRecordingDesc: '録音中にシステム出力を一時的にミュートし、スピーカーのエコーを防ぎます。',
|
||||
insertGroupTitle: '挿入とクリップボード',
|
||||
restoreClipboardLabel: '入力後にクリップボードを復元',
|
||||
restoreClipboardDesc: 'Windows / Linux のみ:ペースト成功後に元のクリップボード内容を復元(既定 ON)。OFF にするとディクテーションテキストがクリップボードに残り、ペーストが効かなかった場合に手動で Ctrl+V できます。詳細は issue #111。',
|
||||
restoreClipboardDesc: 'ペースト成功後に元のクリップボード内容を復元(Windows / Linux のみ)。',
|
||||
pasteShortcutLabel: '貼り付けショートカット',
|
||||
pasteShortcutDesc: 'Windows / Linux のみ:テキスト挿入時に模擬するショートカット。kitty / alacritty / wezterm / gnome-terminal などのターミナルは Ctrl+Shift+V のみを受け付けます。xterm / urxvt は Shift+Insert。次回のディクテーションから有効。詳細は issue #360。',
|
||||
pasteShortcutDesc: '挿入時に模擬するペーストショートカット。一部のターミナルでは Ctrl+Shift+V が必要(Windows / Linux のみ)。',
|
||||
pasteShortcutCtrlV: 'Ctrl+V(既定 / ほとんどのアプリ)',
|
||||
pasteShortcutCtrlShiftV: 'Ctrl+Shift+V(kitty / alacritty / wezterm / ほとんどのターミナル)',
|
||||
pasteShortcutShiftInsert: 'Shift+Insert(xterm / urxvt)',
|
||||
@@ -340,15 +342,17 @@ export const ja: typeof zhCN = {
|
||||
comboClear: 'クリア',
|
||||
comboConflict: 'このショートカットの組み合わせは使用できません',
|
||||
allowNonTsfFallbackLabel: '非 TSF フォールバックを許可',
|
||||
allowNonTsfFallbackDesc: 'Windows のみ:TSF による直接入力が失敗した場合、Unicode SendInput / ショートカットペースト / WM_PASTE への切り替えを許可。OFF にすると TSF 入力が実際に使われているか検証できます。',
|
||||
allowNonTsfFallbackDesc: 'Windows:TSF 入力が失敗した時に Unicode SendInput / ショートカットペーストへの切替を許可。',
|
||||
historyGroupTitle: '履歴とコンテキスト',
|
||||
historyRetentionLabel: '履歴保持期間(日)',
|
||||
historyRetentionDesc: 'この日数を超えた履歴は新しいエントリ書き込み時に削除されます。0 = 時間による削除なし(200 件上限は維持)。デフォルト 7 日。',
|
||||
historyRetentionDesc: '保持日数を超えた履歴は新規書き込み時に削除されます。0 = 時間で削除しない。',
|
||||
polishContextWindowLabel: '会話コンテキスト窓(分)',
|
||||
polishContextWindowDesc: '直近 N 分間の整文済み転写をマルチターン文脈として LLM に渡し、代名詞や未完了の文を前のディクテーションと突き合わせて解釈できるようにします。0 = 無効(シングルターン整文)。デフォルト 5 分。60 分超は実質意味なし。',
|
||||
polishContextWindowDesc: '直近 N 分間の整文済み転写をマルチターン文脈として渡します。0 = 無効。',
|
||||
startupGroupTitle: '起動',
|
||||
startMinimizedLabel: '起動時にメインウィンドウを表示しない',
|
||||
startMinimizedDesc: 'ON にすると、ログイン時の自動起動を含むすべての起動でメインウィンドウは表示されません。メニューバー / トレイのみで動作し、必要時にトレイアイコンをクリックしてメインウィンドウを開きます。Windows の自動起動と組み合わせて便利。',
|
||||
startMinimizedDesc: 'どの起動経路でもメインウィンドウを開かず、メニューバー / トレイのみで動作。',
|
||||
startupAtBoot: '起動時に自動起動',
|
||||
startupAtBootDesc: 'ログイン後に OpenLess を自動起動。macOS は LaunchAgent、Linux は ~/.config/autostart、Windows は HKCU\\Run(管理者不要)。詳細は issue #194。',
|
||||
startupAtBootDesc: 'ログイン時に OpenLess を自動起動。',
|
||||
startupAtBootError: '自動起動の切り替えに失敗:{{message}}',
|
||||
},
|
||||
providers: {
|
||||
|
||||
@@ -173,6 +173,7 @@ export const ko: typeof zhCN = {
|
||||
kicker: 'VOCABULARY',
|
||||
title: '어휘',
|
||||
desc: '인식 전 등장할 수 있는 단어를 모델에 알려 주세요 — 새 단어, 신조어, 전문 용어. ASR 핫워드와 후처리 모델 컨텍스트 양쪽에 모두 전달됩니다.',
|
||||
sectionTitle: '항목',
|
||||
placeholder: '단어를 입력하고 Enter 또는 추가 클릭…',
|
||||
tip: '한영 혼용 지원 · 숫자로 시작하면 그대로 인식 · 적중 횟수 자동 카운트',
|
||||
loadFailed: '로드 실패: {{err}}',
|
||||
@@ -324,11 +325,12 @@ export const ko: typeof zhCN = {
|
||||
capsuleLabel: '녹음 캡슐',
|
||||
capsuleDesc: '녹음 / 전사 중 화면 하단에 반투명 캡슐을 표시합니다.',
|
||||
muteDuringRecordingLabel: '녹음 중 음소거',
|
||||
muteDuringRecordingDesc: '음성 입력 시작 시 시스템 출력을 일시적으로 음소거하고, 정지/취소/오류 후 원래 음소거 상태를 복원합니다. 스피커 소리가 마이크에 들어가지 않도록 합니다.',
|
||||
muteDuringRecordingDesc: '녹음 중 시스템 출력을 일시적으로 음소거하여 스피커 에코를 방지합니다.',
|
||||
insertGroupTitle: '삽입 및 클립보드',
|
||||
restoreClipboardLabel: '입력 후 클립보드 복원',
|
||||
restoreClipboardDesc: 'Windows / Linux 만: 붙여넣기 성공 후 원래 클립보드 내용을 복원합니다(기본 ON). OFF 시 받아쓰기 텍스트가 클립보드에 남아 붙여넣기가 실패한 경우 수동으로 Ctrl+V 할 수 있습니다. issue #111 참조.',
|
||||
restoreClipboardDesc: '붙여넣기 성공 후 원래 클립보드 내용을 복원합니다 (Windows / Linux 만).',
|
||||
pasteShortcutLabel: '붙여넣기 단축키',
|
||||
pasteShortcutDesc: 'Windows / Linux 만: 텍스트 삽입 시 시뮬레이션할 단축키. kitty / alacritty / wezterm / gnome-terminal 같은 터미널은 Ctrl+Shift+V 만 받습니다. xterm / urxvt 는 Shift+Insert. 다음 받아쓰기부터 적용. issue #360 참조.',
|
||||
pasteShortcutDesc: '삽입 시 시뮬레이션할 붙여넣기 단축키. 일부 터미널은 Ctrl+Shift+V 가 필요 (Windows / Linux 만).',
|
||||
pasteShortcutCtrlV: 'Ctrl+V (기본 / 대부분 앱)',
|
||||
pasteShortcutCtrlShiftV: 'Ctrl+Shift+V (kitty / alacritty / wezterm / 대부분 터미널)',
|
||||
pasteShortcutShiftInsert: 'Shift+Insert (xterm / urxvt)',
|
||||
@@ -340,15 +342,17 @@ export const ko: typeof zhCN = {
|
||||
comboClear: '지우기',
|
||||
comboConflict: '이 단축키 조합은 사용할 수 없습니다',
|
||||
allowNonTsfFallbackLabel: '비 TSF 폴백 허용',
|
||||
allowNonTsfFallbackDesc: 'Windows 만: TSF 직접 입력이 실패할 경우 Unicode SendInput / 단축키 붙여넣기 / WM_PASTE 로 전환을 허용합니다. OFF 시 실제로 TSF 입력이 사용되는지 검증할 수 있습니다.',
|
||||
allowNonTsfFallbackDesc: 'Windows: TSF 입력 실패 시 Unicode SendInput / 단축키 붙여넣기로 전환 허용.',
|
||||
historyGroupTitle: '기록 및 컨텍스트',
|
||||
historyRetentionLabel: '기록 보관 기간(일)',
|
||||
historyRetentionDesc: '이 기간을 초과한 기록은 새 항목을 쓸 때 정리됩니다. 0 = 시간 기반 정리 비활성화(200건 상한은 유지). 기본 7일.',
|
||||
historyRetentionDesc: '보관 기간을 초과한 기록은 새 항목 작성 시 정리됩니다. 0 = 시간 기반 정리 비활성화.',
|
||||
polishContextWindowLabel: '대화 컨텍스트 윈도(분)',
|
||||
polishContextWindowDesc: '최근 N분간 성공적으로 정리된 전사를 멀티턴 컨텍스트로 LLM 에 전달하여 대명사와 미완성 문장을 이전 받아쓰기와 대조해 해석할 수 있도록 합니다. 0 = 비활성화(단일턴 정리). 기본 5분; 60분 초과는 의미가 거의 없습니다.',
|
||||
polishContextWindowDesc: '최근 N 분간 정리된 전사를 멀티턴 컨텍스트로 전달합니다. 0 = 비활성화.',
|
||||
startupGroupTitle: '시작',
|
||||
startMinimizedLabel: '시작 시 메인 창 숨기기',
|
||||
startMinimizedDesc: '켜면 로그인 자동 시작을 포함한 모든 시작 경로에서 메인 창이 나타나지 않습니다. 메뉴 막대 / 트레이에서만 실행되며, 필요할 때 트레이 아이콘을 클릭해 메인 창을 엽니다. Windows 자동 시작과 함께 사용하면 편리합니다.',
|
||||
startMinimizedDesc: '모든 시작 경로에서 메인 창을 열지 않고 메뉴 막대 / 트레이에서만 실행합니다.',
|
||||
startupAtBoot: '부팅 시 자동 시작',
|
||||
startupAtBootDesc: '로그인 후 OpenLess 자동 시작. macOS 는 LaunchAgent, Linux 는 ~/.config/autostart, Windows 는 HKCU\\Run(관리자 불필요). issue #194 참조.',
|
||||
startupAtBootDesc: '로그인 시 OpenLess 자동 시작.',
|
||||
startupAtBootError: '자동 시작 전환 실패: {{message}}',
|
||||
},
|
||||
providers: {
|
||||
|
||||
@@ -169,6 +169,7 @@ export const zhCN = {
|
||||
kicker: 'VOCABULARY',
|
||||
title: '词汇表',
|
||||
desc: '告诉模型识别前可能出现的词——生词、新词或专业词汇。同时进入 ASR 热词与后期模型上下文。',
|
||||
sectionTitle: '词条',
|
||||
placeholder: '输入词语,按 Enter 或点添加…',
|
||||
tip: '支持中英混合 · 数字开头按字面识别 · 命中次数自动计数',
|
||||
loadFailed: '加载失败:{{err}}',
|
||||
@@ -297,54 +298,57 @@ export const zhCN = {
|
||||
},
|
||||
recording: {
|
||||
title: '录音',
|
||||
desc: '定义全局录音的快捷键与触发方式。',
|
||||
desc: '全局录音的快捷键与触发方式。',
|
||||
hotkeyLabel: '录音快捷键',
|
||||
hotkeyDescAcc: '按下即开始捕获语音,全局生效。需要授予辅助功能权限。',
|
||||
hotkeyDescNoAcc: '按下即开始捕获语音,全局生效。无需额外辅助功能授权。',
|
||||
hotkeyDescAcc: '按下开始捕获语音,全局生效(需辅助功能权限)。',
|
||||
hotkeyDescNoAcc: '按下开始捕获语音,全局生效。',
|
||||
modeLabel: '录音方式',
|
||||
modeDesc: '切换式 = 按一次开始、再按一次结束;按住说话 = 按住开始、松开结束。',
|
||||
modeDesc: '切换式按一次开始、再按一次结束;按住说话按下保持、松开结束。',
|
||||
modeToggle: '切换式',
|
||||
modeHold: '按住说话',
|
||||
migrationNoticeTitle: '默认已改为切换式说话',
|
||||
migrationNoticeDesc: '如果你之前改过快捷键触发方式,请在这里手动确认一次。本次更新调整了快捷键方式的默认值与读取逻辑;如果你更习惯按住说话,可以重新切回“按住说话”。',
|
||||
migrationNoticeDesc: '本次更新调整了默认值,如果习惯按住说话,请在此处切回。',
|
||||
microphoneLabel: '首选麦克风',
|
||||
microphoneDesc: '选择优先使用的输入设备。设备暂时不可用时会使用系统默认麦克风,重新连接后自动切回首选设备。',
|
||||
microphoneDesc: '选择优先输入设备。设备断开时自动切到系统默认。',
|
||||
microphoneDefault: '系统默认麦克风',
|
||||
microphoneDefaultDesc: '使用系统默认输入设备',
|
||||
microphoneSystemDefault: '系统默认',
|
||||
microphoneUnavailable: '不可用',
|
||||
microphoneLoadError: '麦克风列表读取失败:{{message}}',
|
||||
microphoneDialogTitle: '麦克风',
|
||||
microphoneDialogDesc: '选择能捕捉到您声音的麦克风。如果指示条没有移动,请尝试其他麦克风。',
|
||||
microphoneDialogDesc: '选择能捕捉到您声音的麦克风。',
|
||||
microphoneMonitorError: '输入电平监听失败:{{message}}',
|
||||
capsuleLabel: '录音胶囊',
|
||||
capsuleDesc: '录音 / 转写时在屏幕底部显示半透明胶囊。',
|
||||
capsuleDesc: '录音 / 转写时显示屏幕底部胶囊。',
|
||||
muteDuringRecordingLabel: '录音时静音',
|
||||
muteDuringRecordingDesc: '开始语音输入时临时静音系统输出,停止、取消或出错后恢复原来的静音状态,避免扬声器声音被麦克风收进去。',
|
||||
muteDuringRecordingDesc: '录音期间临时静音系统输出,避免扬声器回音。',
|
||||
insertGroupTitle: '插入与剪贴板',
|
||||
restoreClipboardLabel: '插入后恢复剪贴板',
|
||||
restoreClipboardDesc: '仅 Windows / Linux:粘贴成功后恢复你原来的剪贴板内容(默认开)。关掉就把听写文本留在剪贴板,模拟粘贴没真正落地时可以手动 Ctrl+V 找回。详见 issue #111。',
|
||||
restoreClipboardDesc: '粘贴成功后恢复你原来的剪贴板内容(仅 Windows / Linux)。',
|
||||
pasteShortcutLabel: '模拟粘贴快捷键',
|
||||
pasteShortcutDesc: '仅 Windows / Linux:插入文本时模拟按下的粘贴键。kitty / alacritty / wezterm / gnome-terminal 等终端只接受 Ctrl+Shift+V;xterm / urxvt 用 Shift+Insert。改完下一次听写即生效。详见 issue #360。',
|
||||
pasteShortcutDesc: '插入时模拟按下的粘贴键,部分终端类应用需要 Ctrl+Shift+V(仅 Windows / Linux)。',
|
||||
pasteShortcutCtrlV: 'Ctrl+V(默认 / 多数应用)',
|
||||
pasteShortcutCtrlShiftV: 'Ctrl+Shift+V(kitty / alacritty / wezterm / 多数终端)',
|
||||
pasteShortcutShiftInsert: 'Shift+Insert(xterm / urxvt)',
|
||||
comboRecordLabel: '录制快捷键',
|
||||
comboRecordDesc: '点击后按下你想要的快捷键组合(如 ⌘⇧D),支持 Toggle 和 Hold 模式。',
|
||||
comboRecordDesc: '点击后按下想要的快捷键组合(如 ⌘⇧D)。',
|
||||
comboRecordBtn: '录制快捷键',
|
||||
comboRecordHint: '请按下快捷键组合…',
|
||||
comboRecorded: '已录制',
|
||||
comboClear: '清除',
|
||||
comboConflict: '该快捷键组合不可用',
|
||||
allowNonTsfFallbackLabel: '允许非 TSF 兜底',
|
||||
allowNonTsfFallbackDesc: '仅 Windows:TSF 直接上屏失败后,允许改用 Unicode SendInput、快捷键粘贴或 WM_PASTE。关闭后可验证是否真实使用 TSF 输入。',
|
||||
allowNonTsfFallbackDesc: 'Windows:TSF 失败时改用 Unicode SendInput / 快捷键粘贴。',
|
||||
historyGroupTitle: '历史与上下文',
|
||||
historyRetentionLabel: '历史保留天数',
|
||||
historyRetentionDesc: '超过这个天数的历史会在写入新条目时被清理。0 = 不按时间清理(仍受 200 条上限)。默认 7 天。',
|
||||
historyRetentionDesc: '超过保留天数的历史在写入新条目时被清理;0 = 不按时间清理。',
|
||||
polishContextWindowLabel: '对话上下文窗口(分钟)',
|
||||
polishContextWindowDesc: '把最近 N 分钟内已成功润色的转写作为多轮上下文喂给 LLM,让代词与未完整句子能被正确解析。0 = 关闭,单轮独立润色。默认 5 分钟;超过 60 分钟意义不大。',
|
||||
polishContextWindowDesc: '把最近 N 分钟内已润色的转写作为多轮上下文,0 = 关闭。',
|
||||
startupGroupTitle: '启动',
|
||||
startMinimizedLabel: '启动时静默运行',
|
||||
startMinimizedDesc: '打开后所有启动路径都不弹主窗口(包括开机自启),App 仅在菜单栏 / 托盘运行。需要主窗口时点托盘图标打开。Windows 开机自启场景常用。',
|
||||
startMinimizedDesc: '所有启动路径都不弹主窗口,仅菜单栏 / 托盘运行。',
|
||||
startupAtBoot: '开机自启',
|
||||
startupAtBootDesc: '登录后自动启动 OpenLess。macOS 写 LaunchAgent,Linux 写 ~/.config/autostart,Windows 写 HKCU\\Run(不需要管理员)。详见 issue #194。',
|
||||
startupAtBootDesc: '登录系统时自动启动 OpenLess。',
|
||||
startupAtBootError: '开机自启切换失败:{{message}}',
|
||||
},
|
||||
providers: {
|
||||
|
||||
@@ -171,6 +171,7 @@ export const zhTW: typeof zhCN = {
|
||||
kicker: 'VOCABULARY',
|
||||
title: '詞彙表',
|
||||
desc: '告訴模型識別前可能出現的詞——生詞、新詞或專業詞彙。同時進入 ASR 熱詞與後期模型上下文。',
|
||||
sectionTitle: '詞條',
|
||||
placeholder: '輸入詞語,按 Enter 或點添加…',
|
||||
tip: '支持中英混合 · 數字開頭按字面識別 · 命中次數自動計數',
|
||||
loadFailed: '加載失敗:{{err}}',
|
||||
@@ -329,24 +330,27 @@ export const zhTW: typeof zhCN = {
|
||||
capsuleLabel: '錄音膠囊',
|
||||
capsuleDesc: '錄音 / 轉寫時在屏幕底部顯示半透明膠囊。',
|
||||
muteDuringRecordingLabel: '錄音時靜音',
|
||||
muteDuringRecordingDesc: '開始語音輸入時臨時靜音系統輸出,停止、取消或出錯後恢復原來的靜音狀態,避免揚聲器聲音被麥克風收進去。',
|
||||
muteDuringRecordingDesc: '錄音期間臨時靜音系統輸出,避免揚聲器回音。',
|
||||
insertGroupTitle: '插入與剪貼板',
|
||||
restoreClipboardLabel: '插入後恢復剪貼板',
|
||||
restoreClipboardDesc: '僅 Windows / Linux:粘貼成功後恢復你原來的剪貼板內容(默認開)。關掉就把聽寫文本留在剪貼板,模擬粘貼沒真正落地時可以手動 Ctrl+V 找回。詳見 issue #111。',
|
||||
restoreClipboardDesc: '粘貼成功後恢復你原來的剪貼板內容(僅 Windows / Linux)。',
|
||||
pasteShortcutLabel: '模擬粘貼快捷鍵',
|
||||
pasteShortcutDesc: '僅 Windows / Linux:插入文本時模擬按下的粘貼鍵。kitty / alacritty / wezterm / gnome-terminal 等終端只接受 Ctrl+Shift+V;xterm / urxvt 用 Shift+Insert。改完下一次聽寫即生效。詳見 issue #360。',
|
||||
pasteShortcutDesc: '插入時模擬按下的粘貼鍵,部分終端類應用需要 Ctrl+Shift+V(僅 Windows / Linux)。',
|
||||
pasteShortcutCtrlV: 'Ctrl+V(默認 / 多數應用)',
|
||||
pasteShortcutCtrlShiftV: 'Ctrl+Shift+V(kitty / alacritty / wezterm / 多數終端)',
|
||||
pasteShortcutShiftInsert: 'Shift+Insert(xterm / urxvt)',
|
||||
allowNonTsfFallbackLabel: '允許非 TSF 兜底',
|
||||
allowNonTsfFallbackDesc: '僅 Windows:TSF 直接上屏失敗後,允許改用 Unicode SendInput、快捷鍵粘貼或 WM_PASTE。關閉後可驗證是否真實使用 TSF 輸入。',
|
||||
allowNonTsfFallbackDesc: 'Windows:TSF 失敗時改用 Unicode SendInput / 快捷鍵粘貼。',
|
||||
historyGroupTitle: '歷史與上下文',
|
||||
historyRetentionLabel: '歷史保留天數',
|
||||
historyRetentionDesc: '超過這個天數的歷史會在寫入新條目時被清理。0 = 不按時間清理(仍受 200 條上限)。默認 7 天。',
|
||||
historyRetentionDesc: '超過保留天數的歷史在寫入新條目時被清理;0 = 不按時間清理。',
|
||||
polishContextWindowLabel: '對話上下文窗口(分鐘)',
|
||||
polishContextWindowDesc: '把最近 N 分鐘內已成功潤色的轉寫作為多輪上下文喂給 LLM,讓代詞與未完整句子能被正確解析。0 = 關閉,單輪獨立潤色。默認 5 分鐘;超過 60 分鐘意義不大。',
|
||||
polishContextWindowDesc: '把最近 N 分鐘內已潤色的轉寫作為多輪上下文,0 = 關閉。',
|
||||
startupGroupTitle: '啟動',
|
||||
startMinimizedLabel: '啓動時靜默運行',
|
||||
startMinimizedDesc: '打開後所有啓動路徑都不彈主窗口(包括開機自啓),App 僅在選單欄 / 托盤運行。需要主窗口時點托盤圖示打開。Windows 開機自啓場景常用。',
|
||||
startMinimizedDesc: '所有啓動路徑都不彈主窗口,僅選單欄 / 托盤運行。',
|
||||
startupAtBoot: '開機自啓',
|
||||
startupAtBootDesc: '登錄後自動啓動 OpenLess。macOS 寫 LaunchAgent,Linux 寫 ~/.config/autostart,Windows 寫 HKCU\\Run(不需要管理員)。詳見 issue #194。',
|
||||
startupAtBootDesc: '登錄系統時自動啓動 OpenLess。',
|
||||
startupAtBootError: '開機自啓切換失敗:{{message}}',
|
||||
},
|
||||
providers: {
|
||||
|
||||
@@ -63,7 +63,7 @@ import i18n, {
|
||||
setLocalePreference,
|
||||
type SupportedLocale,
|
||||
} from '../i18n';
|
||||
import { Btn, Card, PageHeader, Pill } from './_atoms';
|
||||
import { Btn, Card, Collapsible, PageHeader, Pill } from './_atoms';
|
||||
import {
|
||||
deleteLocalAsrModel,
|
||||
getLocalAsrSettings,
|
||||
@@ -191,7 +191,10 @@ export function Settings({ embedded = false, initialSection = 'recording' }: Set
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
...(embedded ? { minHeight: 0, overflow: 'auto', paddingRight: 4 } : {}),
|
||||
// paddingBottom: 滚到底时让最后一张 Card / Collapsible 的 border + box-shadow
|
||||
// 不被滚动容器底边吃掉。16 跟 var(--ol-shadow-sm) 的扩散距离 + Card chrome 留白
|
||||
// 匹配,视觉上跟顶部 toolbar 的呼吸感对齐。
|
||||
...(embedded ? { minHeight: 0, overflow: 'auto', paddingRight: 4, paddingBottom: 16 } : {}),
|
||||
}}
|
||||
>
|
||||
{section === 'recording' && <RecordingSection />}
|
||||
@@ -350,6 +353,7 @@ function RecordingSection() {
|
||||
: t('settings.recording.microphoneDefault');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{t('settings.recording.title')}</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginBottom: 6 }}>{t('settings.recording.desc')}</div>
|
||||
@@ -472,6 +476,10 @@ function RecordingSection() {
|
||||
>
|
||||
<Toggle on={prefs.muteDuringRecording} onToggle={onMuteDuringRecordingChange} />
|
||||
</SettingRow>
|
||||
</Card>
|
||||
|
||||
{/* ─── 插入与剪贴板(折叠) ──────────────────────────────────── */}
|
||||
<Collapsible title={t('settings.recording.insertGroupTitle')}>
|
||||
<SettingRow
|
||||
label={t('settings.recording.restoreClipboardLabel')}
|
||||
desc={t('settings.recording.restoreClipboardDesc')}
|
||||
@@ -505,6 +513,10 @@ function RecordingSection() {
|
||||
/>
|
||||
</SettingRow>
|
||||
)}
|
||||
</Collapsible>
|
||||
|
||||
{/* ─── 历史与上下文(折叠) ────────────────────────────────── */}
|
||||
<Collapsible title={t('settings.recording.historyGroupTitle')}>
|
||||
<SettingRow
|
||||
label={t('settings.recording.historyRetentionLabel')}
|
||||
desc={t('settings.recording.historyRetentionDesc')}
|
||||
@@ -531,6 +543,10 @@ function RecordingSection() {
|
||||
style={{ ...inputStyle, width: 80, textAlign: 'right' }}
|
||||
/>
|
||||
</SettingRow>
|
||||
</Collapsible>
|
||||
|
||||
{/* ─── 启动(折叠) ──────────────────────────────────────────── */}
|
||||
<Collapsible title={t('settings.recording.startupGroupTitle')}>
|
||||
<AutostartRow />
|
||||
<SettingRow
|
||||
label={t('settings.recording.startMinimizedLabel')}
|
||||
@@ -543,7 +559,8 @@ function RecordingSection() {
|
||||
{capability.statusHint}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '../lib/ipc';
|
||||
import type { CorrectionRule, DictionaryEntry, VocabPreset } from '../lib/types';
|
||||
import { DEFAULT_VOCAB_PRESETS, loadVocabPresets, persistVocabPresets } from '../lib/vocabPresets';
|
||||
import { Btn, Card, PageHeader } from './_atoms';
|
||||
import { Btn, Card, Collapsible, PageHeader } from './_atoms';
|
||||
|
||||
const NEW_PRESET_DRAFT_ID = '__new__';
|
||||
const NUM_TOKEN = '{num}';
|
||||
@@ -267,9 +267,12 @@ export function Vocab() {
|
||||
}
|
||||
/>
|
||||
<Card padding={0}>
|
||||
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)' }}>
|
||||
<Collapsible
|
||||
embedded
|
||||
title={t('vocab.presets.title')}
|
||||
desc={t('vocab.presets.tip')}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<strong style={{ fontSize: 12 }}>{t('vocab.presets.title')}</strong>
|
||||
{presets.map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
@@ -288,7 +291,6 @@ export function Vocab() {
|
||||
<Btn size="sm" variant="ghost" onClick={createPreset}>{t('vocab.presets.create')}</Btn>
|
||||
<Btn size="sm" variant="primary" onClick={applySelectedPresets}>{t('vocab.presets.apply')}</Btn>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', marginTop: 10 }}>{t('vocab.presets.tip')}</div>
|
||||
{editingPresetId && (
|
||||
<div style={{ marginTop: 10, display: 'grid', gap: 8 }}>
|
||||
<input value={presetNameDraft} onChange={e => setPresetNameDraft(e.target.value)} placeholder={t('vocab.presets.namePlaceholder')} />
|
||||
@@ -300,7 +302,7 @@ export function Vocab() {
|
||||
</div>
|
||||
)}
|
||||
{!editingPresetId && presets.length > 0 && (
|
||||
<div style={{ marginTop: 8, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ marginTop: 10, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{presets.map(p => (
|
||||
<Btn key={`${p.id}-edit`} size="sm" variant="ghost" onClick={() => startEditPreset(p)}>
|
||||
{t('vocab.presets.edit', { name: p.name })}
|
||||
@@ -308,13 +310,13 @@ export function Vocab() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)' }}>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
embedded
|
||||
title={t('vocab.corrections.title')}
|
||||
desc={t('vocab.corrections.tip')}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div>
|
||||
<strong style={{ fontSize: 12 }}>{t('vocab.corrections.title')}</strong>
|
||||
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', marginTop: 6 }}>{t('vocab.corrections.tip')}</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto minmax(0, 1fr) auto', gap: 8, alignItems: 'center' }}>
|
||||
<input
|
||||
value={rulePatternDraft}
|
||||
@@ -345,8 +347,13 @@ export function Vocab() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)' }}>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
embedded
|
||||
defaultOpen
|
||||
title={t('vocab.sectionTitle')}
|
||||
desc={t('vocab.tip')}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -363,26 +370,23 @@ export function Vocab() {
|
||||
/>
|
||||
<Btn variant="primary" icon="plus" onClick={onAdd}>{t('common.add')}</Btn>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', marginTop: 10 }}>
|
||||
{t('vocab.tip')}
|
||||
<div style={{ marginTop: 12, display: 'flex', flexWrap: 'wrap', gap: 8, minHeight: 80 }}>
|
||||
{loading && <div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>{t('common.loading')}</div>}
|
||||
{!loading && error && (
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-err)', lineHeight: 1.6 }}>
|
||||
{t('vocab.loadFailed', { err: error })}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && entries.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>
|
||||
{t('vocab.empty')}
|
||||
</div>
|
||||
)}
|
||||
{!error && entries.map(e => (
|
||||
<VocabChip key={e.id} entry={e} onRemove={() => onRemove(e.id)} onToggle={() => onToggle(e)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 18, display: 'flex', flexWrap: 'wrap', gap: 8, minHeight: 80 }}>
|
||||
{loading && <div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>{t('common.loading')}</div>}
|
||||
{!loading && error && (
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-err)', lineHeight: 1.6 }}>
|
||||
{t('vocab.loadFailed', { err: error })}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && entries.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>
|
||||
{t('vocab.empty')}
|
||||
</div>
|
||||
)}
|
||||
{!error && entries.map(e => (
|
||||
<VocabChip key={e.id} entry={e} onRemove={() => onRemove(e.id)} onToggle={() => onToggle(e)} />
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
<style>{`
|
||||
@keyframes ol-chip-in {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Ported verbatim from design_handoff_openless/pages.jsx (PageHeader, Card,
|
||||
// Pill, Btn). Inline styles preserved 1:1.
|
||||
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import { Icon } from '../components/Icon';
|
||||
|
||||
interface PageHeaderProps {
|
||||
@@ -143,3 +143,107 @@ export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onC
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface CollapsibleProps {
|
||||
/// 标题行内容(短文案,居左)。
|
||||
title: ReactNode;
|
||||
/// 可选的副标题 / 描述(小字,居标题下方)。
|
||||
desc?: ReactNode;
|
||||
/// 默认是否展开。默认 false(折叠,符合"默认只显示标题 + 右箭头"语义)。
|
||||
defaultOpen?: boolean;
|
||||
/// 嵌在 Card padding=0 容器里时设为 true:移除上下 margin,仅靠 Card 的 borderBottom 分割。
|
||||
embedded?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/// 折叠栏:默认收起,标题行右侧显示一个 `›` 箭头,点击切换展开/收起。展开时箭头
|
||||
/// 旋转 90°。内容区域用 `grid-template-rows: 0fr ↔ 1fr` 过渡——浏览器把 `1fr`
|
||||
/// 解析为内容实际高度,过渡到真实高度,避免 max-height 固定大值时短内容也走完
|
||||
/// 整段动画 / 短内容关闭时延迟生效的"卡卡"感。要求 Chromium 117+(Tauri 自带),
|
||||
/// 现代版本完全支持。
|
||||
///
|
||||
/// `embedded=true`:嵌在 `<Card padding={0}>` 里、与其他 Collapsible 共享一张 Card 时使用,
|
||||
/// 底部加一道 0.5px 分隔。
|
||||
/// `embedded=false`:独立 block,自带 Card 同款外观(border / radius / shadow)。
|
||||
export function Collapsible({ title, desc, defaultOpen = false, embedded = false, children }: CollapsibleProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderBottom: embedded ? '0.5px solid var(--ol-line)' : undefined,
|
||||
border: embedded ? undefined : '0.5px solid var(--ol-line)',
|
||||
borderRadius: embedded ? 0 : 'var(--ol-r-lg)',
|
||||
background: embedded ? 'transparent' : 'var(--ol-surface)',
|
||||
boxShadow: embedded ? 'none' : 'var(--ol-shadow-sm)',
|
||||
overflow: 'hidden',
|
||||
// 父级 flex column 带 minHeight:0 + overflow:auto 时,所有 flex 子项默认
|
||||
// shrink:1,会把 header 按钮也压成一条线。锁住不压缩,溢出走父容器滚动。
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(o => !o)}
|
||||
aria-expanded={open}
|
||||
// 让屏幕阅读器朗读折叠状态;键盘 focus 保留浏览器默认 outline 而不是
|
||||
// outline: 'none',避免 Tab 切换时丢失视觉焦点指示(pr-agent #407 反馈)。
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 18px',
|
||||
background: 'transparent',
|
||||
border: 0,
|
||||
textAlign: 'left',
|
||||
fontFamily: 'inherit',
|
||||
color: 'inherit',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>{title}</div>
|
||||
{desc && (
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginTop: 3, lineHeight: 1.5 }}>{desc}</div>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: 'var(--ol-ink-4)',
|
||||
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.18s var(--ol-motion-quick)',
|
||||
}}
|
||||
>
|
||||
<Icon name="chevRight" size={14} />
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
// grid-template-rows: 0fr → 1fr 让浏览器把 1fr 解析为内容实际高度。
|
||||
gridTemplateRows: open ? '1fr' : '0fr',
|
||||
transition: 'grid-template-rows 0.22s var(--ol-motion-soft)',
|
||||
}}
|
||||
// inert 把内部交互元素从 tab 顺序 + a11y 树移除,避免折叠后键盘用户
|
||||
// 仍能 tab 到不可见的输入框 / 按钮 / Toggle(pr-agent #407 反馈)。
|
||||
// 受支持范围:Chromium 102+(Tauri WebView 远高于此)/ Safari 15.4+。
|
||||
// React 18 类型没收 `inert`,用 spread 传 string-boolean 绕过编译器。
|
||||
{...(!open ? { inert: '' } : {})}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
{/* minHeight: 0 必填:默认 grid item 不允许收缩到小于内容固有高度,
|
||||
没这条 trick 不生效,行高动画也跟着失败。 */}
|
||||
<div style={{ overflow: 'hidden', minHeight: 0 }}>
|
||||
<div style={{ padding: '0 18px 18px' }}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user