{
+ list.iter().map(|s| s.to_string()).collect()
+ }
+
+ #[test]
+ fn requires_the_modifier() {
+ assert!(!translation_effective(
+ false,
+ "English",
+ &langs(&["简体中文"])
+ ));
+ }
+
+ #[test]
+ fn unset_target_language_is_not_translation() {
+ // 用户没在翻译页选目标语言就按 Shift:此前胶囊照样显示「正在翻译」,
+ // 而后端走的是普通润色。
+ assert!(!translation_effective(true, "", &langs(&["简体中文"])));
+ assert!(!translation_effective(true, " ", &langs(&["简体中文"])));
+ }
+
+ #[test]
+ fn target_equal_to_the_only_working_language_is_a_no_op() {
+ // 工作语言只有中文、目标也是中文 —— 源语言必定就是目标语言,翻译是空操作。
+ assert!(!translation_effective(
+ true,
+ "简体中文",
+ &langs(&["简体中文"])
+ ));
+ // 前后空白不该让它逃过判定。
+ assert!(!translation_effective(
+ true,
+ " 简体中文 ",
+ &langs(&["简体中文"])
+ ));
+ }
+
+ #[test]
+ fn simplified_to_traditional_still_translates() {
+ // 简体/繁体是语言列表里两个独立条目,简→繁是真实转换,不能按「同一种中文」拦掉。
+ assert!(translation_effective(
+ true,
+ "繁体中文",
+ &langs(&["简体中文"])
+ ));
+ }
+
+ #[test]
+ fn multiple_working_languages_are_never_blocked() {
+ // 中/英双语用户把目标设成英文是正常用法(说中文出英文),源语言无法预先判定,
+ // 不能因为目标语言出现在工作语言里就拦。
+ assert!(translation_effective(
+ true,
+ "English",
+ &langs(&["简体中文", "English"])
+ ));
+ }
+
+ #[test]
+ fn empty_working_languages_still_translates() {
+ assert!(translation_effective(true, "English", &[]));
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts
index a21c84899..44a308f3a 100644
--- a/openless-all/app/src/i18n/en.ts
+++ b/openless-all/app/src/i18n/en.ts
@@ -572,6 +572,7 @@ export const en: typeof zhCN = {
title: 'Translation target language',
desc: 'Press Shift during recording to trigger translation. "Disabled" makes Shift a no-op.',
disabled: 'Disabled (Shift does nothing)',
+ sameAsWorking: 'The target matches your only working language, so translation cannot take effect — Shift will just run a normal polish. Pick a different target, or add another working language above.',
},
save: {
workingFailed: 'Failed to save working languages. Please try again.',
diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts
index d990bdef2..a6cfdf845 100644
--- a/openless-all/app/src/i18n/ja.ts
+++ b/openless-all/app/src/i18n/ja.ts
@@ -574,6 +574,7 @@ export const ja: typeof zhCN = {
title: '翻訳ターゲット言語',
desc: '録音中に Shift で翻訳を起動。「無効」で Shift 無効化。',
disabled: '無効(Shift で翻訳を発動しない)',
+ sameAsWorking: 'ターゲット言語が唯一の作業言語と同じため、翻訳は発動しません(Shift を押しても通常の整文になります)。別のターゲットを選ぶか、上で作業言語を追加してください。',
},
save: {
workingFailed: '作業言語の保存に失敗しました。もう一度お試しください。',
diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts
index d761444e7..cc6dad6bf 100644
--- a/openless-all/app/src/i18n/ko.ts
+++ b/openless-all/app/src/i18n/ko.ts
@@ -574,6 +574,7 @@ export const ko: typeof zhCN = {
title: '번역 대상 언어',
desc: '녹음 중 Shift 로 번역 실행. "비활성화" 시 Shift 무효.',
disabled: '비활성화 (Shift 로 번역 발동 안 함)',
+ sameAsWorking: '대상 언어가 유일한 작업 언어와 같아 번역이 실행되지 않습니다. Shift 를 눌러도 일반 정리로 처리됩니다. 다른 대상 언어를 고르거나 위에서 작업 언어를 추가하세요.',
},
save: {
workingFailed: '작업 언어 저장에 실패했습니다. 다시 시도하세요.',
diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts
index b4515d57a..37121aa04 100644
--- a/openless-all/app/src/i18n/zh-CN.ts
+++ b/openless-all/app/src/i18n/zh-CN.ts
@@ -570,6 +570,7 @@ export const zhCN = {
title: '翻译目标语言',
desc: '录音时按 Shift 触发翻译。选「不启用」则 Shift 无效。',
disabled: '不启用(Shift 按下不触发翻译)',
+ sameAsWorking: '目标语言与你唯一的工作语言相同,翻译不会生效:按 Shift 仍按普通润色处理。换一个目标语言,或在上方多勾选一个工作语言。',
},
save: {
workingFailed: '工作语言保存失败,请重试。',
diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts
index 490c9a9ce..784bc65a5 100644
--- a/openless-all/app/src/i18n/zh-TW.ts
+++ b/openless-all/app/src/i18n/zh-TW.ts
@@ -572,6 +572,7 @@ export const zhTW: typeof zhCN = {
title: '翻譯目標語言',
desc: '錄音時按 Shift 觸發翻譯。選「不啟用」則 Shift 無效。',
disabled: '不啓用(Shift 按下不觸發翻譯)',
+ sameAsWorking: '目標語言與你唯一的工作語言相同,翻譯不會生效:按 Shift 仍按普通潤色處理。換一個目標語言,或在上方多勾選一個工作語言。',
},
save: {
workingFailed: '工作語言保存失敗,請重試。',
diff --git a/openless-all/app/src/lib/translationTarget.test.ts b/openless-all/app/src/lib/translationTarget.test.ts
new file mode 100644
index 000000000..66b335a55
--- /dev/null
+++ b/openless-all/app/src/lib/translationTarget.test.ts
@@ -0,0 +1,42 @@
+import { isTranslationEnabled, isTranslationTargetRedundant } from './translationTarget';
+
+function assert(condition: boolean, message: string) {
+ if (!condition) throw new Error(message);
+}
+
+// 未选目标语言 = 功能未启用。
+assert(isTranslationEnabled('') === false, 'empty target should read as disabled');
+assert(isTranslationEnabled(' ') === false, 'blank target should read as disabled');
+assert(isTranslationEnabled('English') === true, 'a chosen target should read as enabled');
+
+// 目标 = 唯一工作语言:翻译是空操作,页面必须提示。
+assert(
+ isTranslationTargetRedundant('简体中文', ['简体中文']) === true,
+ 'target equal to the only working language should be flagged redundant',
+);
+assert(
+ isTranslationTargetRedundant(' 简体中文 ', ['简体中文']) === true,
+ 'surrounding whitespace should not hide a redundant target',
+);
+
+// 简→繁是真实转换,不能误判成空操作。
+assert(
+ isTranslationTargetRedundant('繁体中文', ['简体中文']) === false,
+ 'simplified to traditional is a real conversion',
+);
+
+// 多工作语言不拦:说中文出英文是正常用法。
+assert(
+ isTranslationTargetRedundant('English', ['简体中文', 'English']) === false,
+ 'multiple working languages should never be flagged',
+);
+
+// 没选目标语言时走「未启用」提示,不该同时报「冗余」。
+assert(
+ isTranslationTargetRedundant('', ['简体中文']) === false,
+ 'an unset target is disabled, not redundant',
+);
+assert(
+ isTranslationTargetRedundant('English', []) === false,
+ 'no working languages means nothing to compare against',
+);
diff --git a/openless-all/app/src/lib/translationTarget.ts b/openless-all/app/src/lib/translationTarget.ts
new file mode 100644
index 000000000..1e167c73c
--- /dev/null
+++ b/openless-all/app/src/lib/translationTarget.ts
@@ -0,0 +1,24 @@
+// 翻译目标语言的可用性判定,与后端 `types.rs::translation_effective` 保持同一套规则。
+// 后端在按下翻译修饰键时用它决定是否进入翻译管线;这里只负责在翻译页提前把「设了但
+// 不会生效」的组合告诉用户,避免出现「按了 Shift 却什么也没翻」的沉默失败。
+
+/** 未选择目标语言 = 翻译功能未启用(Shift 无效)。 */
+export function isTranslationEnabled(translationTargetLanguage: string): boolean {
+ return translationTargetLanguage.trim() !== '';
+}
+
+/**
+ * 目标语言与用户「唯一的」工作语言相同 —— 源语言必定就是目标语言,翻译是可证的空操作。
+ *
+ * 工作语言有多个时返回 false:中/英双语用户把目标设成英文是正常用法(说中文出英文),
+ * 源语言无法预先判定,不能拦。简体/繁体是语言列表里两个独立条目,按字面比较即可,
+ * 简→繁不会被误判成空操作。
+ */
+export function isTranslationTargetRedundant(
+ translationTargetLanguage: string,
+ workingLanguages: readonly string[],
+): boolean {
+ const target = translationTargetLanguage.trim();
+ if (target === '') return false;
+ return workingLanguages.length === 1 && workingLanguages[0].trim() === target;
+}
diff --git a/openless-all/app/src/pages/Translation.tsx b/openless-all/app/src/pages/Translation.tsx
index cb54afa7c..f38cb8250 100644
--- a/openless-all/app/src/pages/Translation.tsx
+++ b/openless-all/app/src/pages/Translation.tsx
@@ -10,6 +10,7 @@ import { Card, PageHeader } from './_atoms';
import { SavedToast } from '../components/SavedToast';
import { SelectLite } from '../components/ui/SelectLite';
import { SUPPORTED_LANGUAGES } from '../lib/types';
+import { isTranslationEnabled, isTranslationTargetRedundant } from '../lib/translationTarget';
import { useHotkeySettings } from '../state/HotkeySettingsContext';
import { formatComboLabel } from '../lib/hotkey';
import type { UserPreferences } from '../lib/types';
@@ -119,7 +120,13 @@ export function Translation() {
const triggerLabel = formatComboLabel(prefs.dictationHotkey);
const translationHotkeyLabel = formatComboLabel(prefs.translationHotkey);
- const enabled = prefs.translationTargetLanguage.trim() !== '';
+ // 「已启用」= 选了目标语言 **且** 该目标真的会触发翻译。目标等于唯一工作语言时后端
+ // 走普通润色,状态灯不能还亮着说已启用(否则用户按 Shift 什么都没发生,无从排查)。
+ const redundantTarget = isTranslationTargetRedundant(
+ prefs.translationTargetLanguage,
+ prefs.workingLanguages,
+ );
+ const enabled = isTranslationEnabled(prefs.translationTargetLanguage) && !redundantTarget;
const targetOptions = useMemo(() => ([
{ value: '', label: t('translation.target.disabled') },
@@ -230,6 +237,23 @@ export function Translation() {
ariaLabel={t('translation.target.title')}
style={{ width: '100%', maxWidth: 360, fontSize: 13 }}
/>
+ {redundantTarget && (
+
+ {t('translation.target.sameAsWorking')}
+
+ )}
{/* 3. 使用方法 */}