From 829fea3c141c2d707ad89054531530c9af7b40f7 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Thu, 7 May 2026 20:58:08 +0800 Subject: [PATCH] Expose translation save failures before users trust stale settings Translation settings were optimistically updated without awaiting or surfacing write failures, so users could see enabled language and shortcut states that were never persisted. This keeps the fix inside the Translation page: all three save paths now show saving/saved/failed feedback, catch failed writes, and refresh preferences after failed preference persistence to roll back local state. Constraint: Follow the new beta-first PR workflow from #327. Rejected: Refactor HotkeySettingsContext queue behavior | issue explicitly excludes queue redesign. Confidence: high Scope-risk: narrow Directive: Keep Translation page save feedback local unless SelectionAsk receives the same treatment in its own issue. Tested: npm run build Tested: git diff --check Related: #314 --- openless-all/app/src/i18n/en.ts | 6 ++ openless-all/app/src/i18n/ja.ts | 6 ++ openless-all/app/src/i18n/ko.ts | 6 ++ openless-all/app/src/i18n/zh-CN.ts | 6 ++ openless-all/app/src/i18n/zh-TW.ts | 6 ++ openless-all/app/src/pages/Translation.tsx | 99 ++++++++++++++++++++-- 6 files changed, 120 insertions(+), 9 deletions(-) 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} />