diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 52c156ad..30843cd9 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -208,6 +208,12 @@ export const en: typeof zhCN = { desc: 'Pick a language and pressing Shift any time during recording will translate the transcript into it before insertion. Pick "Disabled" to make Shift a no-op (regular polish runs instead).', disabled: 'Disabled (Shift does nothing)', }, + save: { + workingFailed: 'Failed to save working languages. Please try again.', + targetFailed: 'Failed to save translation target. Please try again.', + hotkeyRegisterFailed: 'Failed to register the translation shortcut. The preference was not saved.', + hotkeySaveFailed: 'Failed to save the translation shortcut. Please try again.', + }, howto: { title: 'How to use', step1: 'Place the text cursor in another app (Notes, mail, chat — anything with a text field).', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index b11bf9f2..27790919 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -210,6 +210,12 @@ export const ja: typeof zhCN = { desc: 'いずれかの言語を選択すると、録音中の任意のタイミングで Shift を 1 回押すだけで、停止後に転写をその言語に翻訳してカーソル位置に入力します。「無効」を選ぶと Shift は何の効果もなく、通常の整文パイプラインに進みます。', disabled: '無効(Shift で翻訳を発動しない)', }, + save: { + workingFailed: '作業言語の保存に失敗しました。もう一度お試しください。', + targetFailed: '翻訳ターゲット言語の保存に失敗しました。もう一度お試しください。', + hotkeyRegisterFailed: '翻訳ショートカットの登録に失敗しました。設定は保存されていません。', + hotkeySaveFailed: '翻訳ショートカットの保存に失敗しました。もう一度お試しください。', + }, howto: { title: '使い方', step1: '別のアプリの入力欄でカーソルにフォーカス(メモ、メール、チャットウィンドウなど)。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 4d042413..a517a03f 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -210,6 +210,12 @@ export const ko: typeof zhCN = { desc: '하나의 언어를 선택하면 녹음 중 임의의 시점에 Shift 를 한 번 눌러 정지 후 전사를 해당 언어로 번역하여 커서 위치에 삽입합니다. "비활성화"를 선택하면 Shift 는 효과가 없으며 일반 정리 파이프라인을 따릅니다.', disabled: '비활성화 (Shift 로 번역 발동 안 함)', }, + save: { + workingFailed: '작업 언어 저장에 실패했습니다. 다시 시도하세요.', + targetFailed: '번역 대상 언어 저장에 실패했습니다. 다시 시도하세요.', + hotkeyRegisterFailed: '번역 단축키 등록에 실패했습니다. 설정은 저장되지 않았습니다.', + hotkeySaveFailed: '번역 단축키 저장에 실패했습니다. 다시 시도하세요.', + }, howto: { title: '사용 방법', step1: '다른 앱의 입력 상자에서 커서에 포커스합니다(메모, 메일, 채팅 창 모두 가능).', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index ea1653ea..db99cebc 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -206,6 +206,12 @@ export const zhCN = { desc: '选了某个语言后,录音过程中任意时刻按一下 Shift,停止后就会把转写翻译成该语言再插入到光标位置。选「不启用」则 Shift 没有任何效果,走普通润色管线。', disabled: '不启用(Shift 按下不触发翻译)', }, + save: { + workingFailed: '工作语言保存失败,请重试。', + targetFailed: '翻译目标语言保存失败,请重试。', + hotkeyRegisterFailed: '翻译快捷键注册失败,未继续保存。', + hotkeySaveFailed: '翻译快捷键保存失败,请重试。', + }, howto: { title: '使用方法', step1: '在另一个 app 的输入框里聚焦光标(备忘录、邮件、聊天窗口都行)。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 9a9f2a90..e457f8cb 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -208,6 +208,12 @@ export const zhTW: typeof zhCN = { desc: '選了某個語言後,錄音過程中任意時刻按一下 Shift,停止後就會把轉寫翻譯成該語言再插入到光標位置。選「不啓用」則 Shift 沒有任何效果,走普通潤色管線。', disabled: '不啓用(Shift 按下不觸發翻譯)', }, + save: { + workingFailed: '工作語言保存失敗,請重試。', + targetFailed: '翻譯目標語言保存失敗,請重試。', + hotkeyRegisterFailed: '翻譯快捷鍵註冊失敗,未繼續保存。', + hotkeySaveFailed: '翻譯快捷鍵保存失敗,請重試。', + }, howto: { title: '使用方法', step1: '在另一個 app 的輸入框裏聚焦光標(備忘錄、郵件、聊天窗口都行)。', diff --git a/openless-all/app/src/pages/Translation.tsx b/openless-all/app/src/pages/Translation.tsx index c8452d91..bea8deb4 100644 --- a/openless-all/app/src/pages/Translation.tsx +++ b/openless-all/app/src/pages/Translation.tsx @@ -4,6 +4,7 @@ // - 选一个翻译目标语言(单选;选"不启用"则 Shift 不触发翻译) // - 看完整使用说明(怎么触发、按钮位置、胶囊显示) +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, PageHeader } from './_atoms'; import { SUPPORTED_LANGUAGES } from '../lib/types'; @@ -11,10 +12,53 @@ import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { formatComboLabel } from '../lib/hotkey'; import { ShortcutRecorder } from '../components/ShortcutRecorder'; import { setTranslationHotkey } from '../lib/ipc'; +import type { UserPreferences, ShortcutBinding } from '../lib/types'; + +type SaveState = 'idle' | 'saving' | 'saved' | 'failed'; export function Translation() { const { t } = useTranslation(); - const { prefs, updatePrefs: savePrefs } = useHotkeySettings(); + const { prefs, refresh, updatePrefs: savePrefs } = useHotkeySettings(); + const [saveState, setSaveState] = useState('idle'); + const [saveMessage, setSaveMessage] = useState(''); + const statusTimer = useRef(null); + + useEffect(() => () => { + if (statusTimer.current !== null) window.clearTimeout(statusTimer.current); + }, []); + + const showSaveStatus = (state: SaveState, message: string, temporary = false) => { + if (statusTimer.current !== null) { + window.clearTimeout(statusTimer.current); + statusTimer.current = null; + } + setSaveState(state); + setSaveMessage(message); + if (temporary) { + statusTimer.current = window.setTimeout(() => { + setSaveState('idle'); + setSaveMessage(''); + statusTimer.current = null; + }, 1600); + } + }; + + const persistPrefs = async ( + resolveNext: (current: UserPreferences) => UserPreferences, + failureMessage: string, + ) => { + showSaveStatus('saving', t('common.saving')); + try { + await savePrefs(resolveNext); + showSaveStatus('saved', t('common.saved'), true); + } catch (error) { + console.error('[translation] failed to save preferences', error); + showSaveStatus('failed', failureMessage); + await refresh().catch(refreshError => { + console.warn('[translation] failed to refresh preferences after save error', refreshError); + }); + } + }; if (!prefs) { return ( @@ -31,16 +75,38 @@ export function Translation() { ); } - const onWorkingLanguagesChange = (workingLanguages: string[]) => - savePrefs({ ...prefs, workingLanguages }); + const onWorkingLanguagesChange = (workingLanguages: string[]) => { + void persistPrefs( + current => ({ ...current, workingLanguages }), + t('translation.save.workingFailed'), + ); + }; const toggleWorkingLanguage = (lang: string) => { const next = prefs.workingLanguages.includes(lang) ? prefs.workingLanguages.filter(l => l !== lang) : [...prefs.workingLanguages, lang]; onWorkingLanguagesChange(next); }; - const onTargetChange = (translationTargetLanguage: string) => - savePrefs({ ...prefs, translationTargetLanguage }); + const onTargetChange = (translationTargetLanguage: string) => { + void persistPrefs( + current => ({ ...current, translationTargetLanguage }), + t('translation.save.targetFailed'), + ); + }; + const onTranslationHotkeySave = async (binding: ShortcutBinding) => { + showSaveStatus('saving', t('common.saving')); + try { + await setTranslationHotkey(binding); + } catch (error) { + console.error('[translation] failed to register translation hotkey', error); + showSaveStatus('failed', t('translation.save.hotkeyRegisterFailed')); + return; + } + await persistPrefs( + current => ({ ...current, translationHotkey: binding }), + t('translation.save.hotkeySaveFailed'), + ); + }; const triggerLabel = formatComboLabel(prefs.dictationHotkey); const translationHotkeyLabel = formatComboLabel(prefs.translationHotkey); @@ -55,6 +121,24 @@ export function Translation() { />
+ {saveState !== 'idle' && ( +
+ {saveMessage} +
+ )} {/* 1. 工作语言 */} @@ -142,10 +226,7 @@ export function Translation() {
{ - await setTranslationHotkey(binding); - await savePrefs({ ...prefs, translationHotkey: binding }); - }} + onSave={onTranslationHotkeySave} />