refactor: simplify translate action by removing dual language selector

Remove the confusing "smart translation" feature that had two language
selectors (target + alternative). The UI was misleading as users expected
a source-to-target translation flow, but the feature auto-switched between
languages based on detected source language.

Now uses a single target language selector for clearer UX.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kangfenmao 2026-01-07 17:08:41 +08:00
parent d0a1512f23
commit 44d814cfd6
12 changed files with 22 additions and 122 deletions

View File

@ -2938,9 +2938,6 @@
"summary": "Summarize",
"translate": "Translate"
},
"translate": {
"smart_translate_tips": "Smart Translation: Content will be translated to the target language first; content already in the target language will be translated to the alternative language"
},
"window": {
"c_copy": "C: Copy",
"esc_close": "Esc: Close",
@ -4969,7 +4966,6 @@
"traceWindow": "Call Chain Window"
},
"translate": {
"alter_language": "Alternative Language",
"any": {
"language": "Any language"
},

View File

@ -2938,9 +2938,6 @@
"summary": "总结",
"translate": "翻译"
},
"translate": {
"smart_translate_tips": "智能翻译:内容将优先翻译为目标语言;内容已是目标语言的,将翻译为备选语言"
},
"window": {
"c_copy": "C 复制",
"esc_close": "Esc 关闭",
@ -4969,7 +4966,6 @@
"traceWindow": "调用链窗口"
},
"translate": {
"alter_language": "备用语言",
"any": {
"language": "任意语言"
},

View File

@ -2938,9 +2938,6 @@
"summary": "總結",
"translate": "翻譯"
},
"translate": {
"smart_translate_tips": "智慧翻譯:內容將優先翻譯為目標語言;內容已是目標語言時,將翻譯為備用語言"
},
"window": {
"c_copy": "C 複製",
"esc_close": "Esc 關閉",
@ -4969,7 +4966,6 @@
"traceWindow": "呼叫鏈視窗"
},
"translate": {
"alter_language": "備用語言",
"any": {
"language": "任意語言"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Zusammenfassen",
"translate": "Übersetzen"
},
"translate": {
"smart_translate_tips": "Intelligente Übersetzung: Inhalt wird bevorzugt in Zielsprache übersetzt; wenn Inhalt bereits in Zielsprache, Übersetzung in Alternativsprache"
},
"window": {
"c_copy": "C zum Kopieren",
"esc_close": "Esc Schließen",
@ -4969,7 +4966,6 @@
"traceWindow": "Aufrufkette-Fenster"
},
"translate": {
"alter_language": "Alternative Sprache",
"any": {
"language": "Beliebige Sprache"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Σύνοψη",
"translate": "Μετάφραση"
},
"translate": {
"smart_translate_tips": "Έξυπνη μετάφραση: το περιεχόμενο θα μεταφραστεί προτεραιακά στη στόχος γλώσσα· αν το περιεχόμενο είναι ήδη στη στόχος γλώσσα, θα μεταφραστεί στην εναλλακτική γλώσσα"
},
"window": {
"c_copy": "Αντιγραφή C",
"esc_close": "Esc Κλείσιμο",
@ -4969,7 +4966,6 @@
"traceWindow": "Παράθυρο αλυσίδας κλήσης"
},
"translate": {
"alter_language": "Εναλλακτική γλώσσα",
"any": {
"language": " οποιαδήποτε γλώσσα"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Resumen",
"translate": "Traducir"
},
"translate": {
"smart_translate_tips": "Traducción inteligente: el contenido se traducirá primero al idioma de destino; si el contenido ya está en el idioma de destino, se traducirá al idioma alternativo"
},
"window": {
"c_copy": "C Copiar",
"esc_close": "Esc Cerrar",
@ -4969,7 +4966,6 @@
"traceWindow": "Ventana de cadena de llamadas"
},
"translate": {
"alter_language": "Idioma alternativo",
"any": {
"language": "cualquier idioma"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Résumé",
"translate": "Traduire"
},
"translate": {
"smart_translate_tips": "Traduction intelligente : le contenu sera d'abord traduit dans la langue cible ; si le contenu est déjà dans la langue cible, il sera traduit dans la langue secondaire"
},
"window": {
"c_copy": "C Copier",
"esc_close": "Esc Fermer",
@ -4969,7 +4966,6 @@
"traceWindow": "Fenêtre de chaîne d'appel"
},
"translate": {
"alter_language": "Langue de secours",
"any": {
"language": "langue arbitraire"
},

View File

@ -2938,9 +2938,6 @@
"summary": "要約",
"translate": "翻訳"
},
"translate": {
"smart_translate_tips": "スマート翻訳:内容は優先的に目標言語に翻訳されます。すでに目標言語の場合は、備用言語に翻訳されます。"
},
"window": {
"c_copy": "Cでコピー",
"esc_close": "Escで閉じる",
@ -4969,7 +4966,6 @@
"traceWindow": "呼び出しチェーンウィンドウ"
},
"translate": {
"alter_language": "備用言語",
"any": {
"language": "任意の言語"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Resumir",
"translate": "Traduzir"
},
"translate": {
"smart_translate_tips": "Tradução inteligente: o conteúdo será priorizado para tradução no idioma de destino; se o conteúdo já estiver no idioma de destino, será traduzido para o idioma alternativo"
},
"window": {
"c_copy": "C Copiar",
"esc_close": "Esc Fechar",
@ -4969,7 +4966,6 @@
"traceWindow": "Janela de rastreamento"
},
"translate": {
"alter_language": "Idioma alternativo",
"any": {
"language": "qualquer idioma"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Rezumat",
"translate": "Tradu"
},
"translate": {
"smart_translate_tips": "Traducere inteligentă: Conținutul va fi tradus mai întâi în limba țintă; conținutul aflat deja în limba țintă va fi tradus în limba alternativă"
},
"window": {
"c_copy": "C: Copiază",
"esc_close": "Esc: Închide",
@ -4969,7 +4966,6 @@
"traceWindow": "Fereastră lanț de apelare"
},
"translate": {
"alter_language": "Limbă alternativă",
"any": {
"language": "Orice limbă"
},

View File

@ -2938,9 +2938,6 @@
"summary": "Суммаризировать",
"translate": "Перевести"
},
"translate": {
"smart_translate_tips": "Смарт-перевод: содержимое будет переведено на целевой язык; содержимое уже на целевом языке будет переведено на альтернативный язык"
},
"window": {
"c_copy": "C - копировать",
"esc_close": "Esc - закрыть",
@ -4969,7 +4966,6 @@
"traceWindow": "Окно цепочки вызовов"
},
"translate": {
"alter_language": "Альтернативный язык",
"any": {
"language": "Любой язык"
},

View File

@ -10,13 +10,12 @@ import useTranslate from '@renderer/hooks/useTranslate'
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { pauseTrace } from '@renderer/services/SpanManagerService'
import type { Assistant, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
import type { Assistant, Topic, TranslateLanguage } from '@renderer/types'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import type { ActionItem } from '@renderer/types/selectionTypes'
import { abortCompletion } from '@renderer/utils/abortController'
import { detectLanguage } from '@renderer/utils/translate'
import { Tooltip } from 'antd'
import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react'
import { ChevronDown } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -46,8 +45,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
}
})
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
const [error, setError] = useState('')
const [showOriginal, setShowOriginal] = useState(false)
const [status, setStatus] = useState<'preparing' | 'streaming' | 'finished'>('preparing')
@ -61,27 +58,22 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const targetLangRef = useRef(targetLanguage)
// It's called only in initialization.
// It will change target/alter language, so fetchResult will be triggered. Be careful!
const updateLanguagePair = useCallback(async () => {
// It will change target language, so fetchResult will be triggered. Be careful!
const updateTargetLanguage = useCallback(async () => {
// Only called is when languages loaded.
// It ensure we could get right language from getLanguageByLangcode.
if (!isLanguagesLoaded) {
logger.silly('[updateLanguagePair] Languages are not loaded. Skip.')
logger.silly('[updateTargetLanguage] Languages are not loaded. Skip.')
return
}
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
const savedTargetLang = await db.settings.get({ id: 'translate:target:language' })
if (biDirectionLangPair && biDirectionLangPair.value[0]) {
const targetLang = getLanguageByLangcode(biDirectionLangPair.value[0])
if (savedTargetLang && savedTargetLang.value) {
const targetLang = getLanguageByLangcode(savedTargetLang.value)
setTargetLanguage(targetLang)
targetLangRef.current = targetLang
}
if (biDirectionLangPair && biDirectionLangPair.value[1]) {
const alterLang = getLanguageByLangcode(biDirectionLangPair.value[1])
setAlterLanguage(alterLang)
}
}, [getLanguageByLangcode, isLanguagesLoaded])
// Initialize values only once
@ -91,7 +83,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
return
}
// Only try to initialize when languages loaded, so updateLanguagePair would not fail.
// Only try to initialize when languages loaded, so updateTargetLanguage would not fail.
if (!isLanguagesLoaded) {
logger.silly('[initialize] Languages not loaded. Skip initialization.')
return
@ -104,10 +96,10 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
}
logger.silly('[initialize] Start initialization.')
// Initialize language pair.
// Initialize target language.
// It will update targetLangRef, so we could get latest target language in the following code
await updateLanguagePair()
logger.silly('[initialize] UpdateLanguagePair completed.')
await updateTargetLanguage()
logger.silly('[initialize] updateTargetLanguage completed.')
// Initialize assistant
const currentAssistant = getDefaultTranslateAssistant(targetLangRef.current, action.selectedText)
@ -117,12 +109,12 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
// Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id)
setInitialized(true)
}, [action.selectedText, initialized, isLanguagesLoaded, updateLanguagePair])
}, [action.selectedText, initialized, isLanguagesLoaded, updateTargetLanguage])
// Try to initialize when:
// 1. action.selectedText change (generally will not)
// 2. isLanguagesLoaded change (only initialize when languages loaded)
// 3. updateLanguagePair change (depend on translateLanguages and isLanguagesLoaded)
// 3. updateTargetLanguage change (depend on translateLanguages and isLanguagesLoaded)
useEffect(() => {
initialize()
}, [initialize])
@ -146,35 +138,11 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
setError(error.message)
}
let sourceLanguageCode: TranslateLanguageCode
try {
sourceLanguageCode = await detectLanguage(action.selectedText)
} catch (err) {
onError(err instanceof Error ? err : new Error('An error occurred'))
logger.error('Error detecting language:', err as Error)
return
}
let translateLang: TranslateLanguage
if (sourceLanguageCode === UNKNOWN.langCode) {
logger.debug('Unknown source language. Just use target language.')
translateLang = targetLanguage
} else {
logger.debug('Detected Language: ', { sourceLanguage: sourceLanguageCode })
if (sourceLanguageCode === targetLanguage.langCode) {
translateLang = alterLanguage
} else {
translateLang = targetLanguage
}
}
const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText)
const assistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText)
assistantRef.current = assistant
logger.debug('process once')
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
}, [action, targetLanguage, alterLanguage, scrollToBottom, initialized])
}, [action, targetLanguage, scrollToBottom, initialized])
useEffect(() => {
fetchResult()
@ -213,15 +181,14 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const isPreparing = status === 'preparing'
const isStreaming = status === 'streaming'
const handleChangeLanguage = (targetLanguage: TranslateLanguage, alterLanguage: TranslateLanguage) => {
const handleChangeLanguage = (newTargetLanguage: TranslateLanguage) => {
if (!initialized) {
return
}
setTargetLanguage(targetLanguage)
targetLangRef.current = targetLanguage
setAlterLanguage(alterLanguage)
setTargetLanguage(newTargetLanguage)
targetLangRef.current = newTargetLanguage
db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage.langCode, alterLanguage.langCode] })
db.settings.put({ id: 'translate:target:language', value: newTargetLanguage.langCode })
}
const handlePause = () => {
@ -244,36 +211,17 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
<>
<Container>
<MenuContainer>
<Tooltip placement="bottom" title={t('translate.any.language')} arrow>
<Globe size={16} style={{ flexShrink: 0 }} />
</Tooltip>
<ArrowRightToLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
<Tooltip placement="bottom" title={t('translate.target_language')} arrow>
<LanguageSelect
value={targetLanguage.langCode}
style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }}
style={{ minWidth: 100, maxWidth: 200, flex: 'auto' }}
listHeight={160}
title={t('translate.target_language')}
optionFilterProp="label"
onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)}
onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value))}
disabled={isStreaming}
/>
</Tooltip>
<ArrowRightFromLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
<Tooltip placement="bottom" title={t('translate.alter_language')} arrow>
<LanguageSelect
value={alterLanguage.langCode}
style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }}
listHeight={160}
title={t('translate.alter_language')}
optionFilterProp="label"
onChange={(value) => handleChangeLanguage(targetLanguage, getLanguageByLangcode(value))}
disabled={isStreaming}
/>
</Tooltip>
<Tooltip placement="bottom" title={t('selection.action.translate.smart_translate_tips')} arrow>
<QuestionIcon size={14} style={{ marginLeft: 4 }} />
</Tooltip>
<Spacer />
<OriginalHeader onClick={() => setShowOriginal(!showOriginal)}>
<span>
@ -393,9 +341,5 @@ const ErrorMsg = styled.div`
const Spacer = styled.div`
flex-grow: 0.5;
`
const QuestionIcon = styled(CircleHelp)`
cursor: pointer;
color: var(--color-text-3);
`
export default ActionTranslate