Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).',
Expand Down
6 changes: 6 additions & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ export const ja: typeof zhCN = {
desc: 'いずれかの言語を選択すると、録音中の任意のタイミングで Shift を 1 回押すだけで、停止後に転写をその言語に翻訳してカーソル位置に入力します。「無効」を選ぶと Shift は何の効果もなく、通常の整文パイプラインに進みます。',
disabled: '無効(Shift で翻訳を発動しない)',
},
save: {
workingFailed: '作業言語の保存に失敗しました。もう一度お試しください。',
targetFailed: '翻訳ターゲット言語の保存に失敗しました。もう一度お試しください。',
hotkeyRegisterFailed: '翻訳ショートカットの登録に失敗しました。設定は保存されていません。',
hotkeySaveFailed: '翻訳ショートカットの保存に失敗しました。もう一度お試しください。',
},
howto: {
title: '使い方',
step1: '別のアプリの入力欄でカーソルにフォーカス(メモ、メール、チャットウィンドウなど)。',
Expand Down
6 changes: 6 additions & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ export const ko: typeof zhCN = {
desc: '하나의 언어를 선택하면 녹음 중 임의의 시점에 Shift 를 한 번 눌러 정지 후 전사를 해당 언어로 번역하여 커서 위치에 삽입합니다. "비활성화"를 선택하면 Shift 는 효과가 없으며 일반 정리 파이프라인을 따릅니다.',
disabled: '비활성화 (Shift 로 번역 발동 안 함)',
},
save: {
workingFailed: '작업 언어 저장에 실패했습니다. 다시 시도하세요.',
targetFailed: '번역 대상 언어 저장에 실패했습니다. 다시 시도하세요.',
hotkeyRegisterFailed: '번역 단축키 등록에 실패했습니다. 설정은 저장되지 않았습니다.',
hotkeySaveFailed: '번역 단축키 저장에 실패했습니다. 다시 시도하세요.',
},
howto: {
title: '사용 방법',
step1: '다른 앱의 입력 상자에서 커서에 포커스합니다(메모, 메일, 채팅 창 모두 가능).',
Expand Down
6 changes: 6 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ export const zhCN = {
desc: '选了某个语言后,录音过程中任意时刻按一下 Shift,停止后就会把转写翻译成该语言再插入到光标位置。选「不启用」则 Shift 没有任何效果,走普通润色管线。',
disabled: '不启用(Shift 按下不触发翻译)',
},
save: {
workingFailed: '工作语言保存失败,请重试。',
targetFailed: '翻译目标语言保存失败,请重试。',
hotkeyRegisterFailed: '翻译快捷键注册失败,未继续保存。',
hotkeySaveFailed: '翻译快捷键保存失败,请重试。',
},
howto: {
title: '使用方法',
step1: '在另一个 app 的输入框里聚焦光标(备忘录、邮件、聊天窗口都行)。',
Expand Down
6 changes: 6 additions & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ export const zhTW: typeof zhCN = {
desc: '選了某個語言後,錄音過程中任意時刻按一下 Shift,停止後就會把轉寫翻譯成該語言再插入到光標位置。選「不啓用」則 Shift 沒有任何效果,走普通潤色管線。',
disabled: '不啓用(Shift 按下不觸發翻譯)',
},
save: {
workingFailed: '工作語言保存失敗,請重試。',
targetFailed: '翻譯目標語言保存失敗,請重試。',
hotkeyRegisterFailed: '翻譯快捷鍵註冊失敗,未繼續保存。',
hotkeySaveFailed: '翻譯快捷鍵保存失敗,請重試。',
},
howto: {
title: '使用方法',
step1: '在另一個 app 的輸入框裏聚焦光標(備忘錄、郵件、聊天窗口都行)。',
Expand Down
99 changes: 90 additions & 9 deletions openless-all/app/src/pages/Translation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,61 @@
// - 选一个翻译目标语言(单选;选"不启用"则 Shift 不触发翻译)
// - 看完整使用说明(怎么触发、按钮位置、胶囊显示)

import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, PageHeader } from './_atoms';
import { SUPPORTED_LANGUAGES } from '../lib/types';
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<SaveState>('idle');
const [saveMessage, setSaveMessage] = useState('');
const statusTimer = useRef<number | null>(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 (
Expand All @@ -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);
Expand All @@ -55,6 +121,24 @@ export function Translation() {
/>

<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{saveState !== 'idle' && (
<div
role={saveState === 'failed' ? 'alert' : 'status'}
style={{
padding: '8px 12px',
borderRadius: 10,
border: saveState === 'failed'
? '0.5px solid rgba(239,68,68,0.22)'
: '0.5px solid rgba(37,99,235,0.16)',
background: saveState === 'failed' ? 'rgba(239,68,68,0.07)' : 'rgba(37,99,235,0.06)',
color: saveState === 'failed' ? 'var(--ol-red, #ef4444)' : 'var(--ol-blue)',
fontSize: 11.5,
lineHeight: 1.5,
}}
>
{saveMessage}
</div>
)}

{/* 1. 工作语言 */}
<Card>
Expand Down Expand Up @@ -142,10 +226,7 @@ export function Translation() {
</div>
<ShortcutRecorder
value={prefs.translationHotkey}
onSave={async binding => {
await setTranslationHotkey(binding);
await savePrefs({ ...prefs, translationHotkey: binding });
}}
onSave={onTranslationHotkeySave}
/>
</Card>

Expand Down