From 680bda3993ced26b028cdafbb61fadc5a4a70822 Mon Sep 17 00:00:00 2001 From: Phantom Date: Sun, 4 Jan 2026 13:25:08 +0800 Subject: [PATCH] fix(translate): Fix ActionTranslate duplicate execution and getLanguageByLangcode logic (#12241) * refactor(translate): remove default temperature setting * refactor(translate): Remove temperature setting from language detection prompt * refactor: Set default translate languages from user settings Initialize target language from user's language setting, falling back to zh-CN if unknown. Set alter language to en-US by default. Remove redundant logic that was duplicated in the effect. * fix(translate): translate action won't trigger twice * fix(translate): Fix getLanguageByLangcode to return built-in language when not loaded Previously, the function would always return UNKNOWN if the languages were not loaded, even for built-in language codes. This change ensures built-in languages are returned if found, regardless of the load state. * fix: Expose translation languages loaded state and fix initialization Add `isLoaded` flag to `useTranslate` hook to track when translation languages are available. Use this flag to prevent premature initialization in `ActionTranslate` component, ensuring language lookups succeed before creating assistants and topics. Add error logging for failed custom language loading and update fallback warning messages for better debugging. * fix: set initialized when finished --- src/renderer/src/hooks/useTranslate.ts | 11 +- src/renderer/src/services/AssistantService.ts | 1 - src/renderer/src/utils/translate.ts | 5 +- .../action/components/ActionTranslate.tsx | 112 ++++++++++++------ 4 files changed, 81 insertions(+), 48 deletions(-) diff --git a/src/renderer/src/hooks/useTranslate.ts b/src/renderer/src/hooks/useTranslate.ts index 6191560d3d..8b4f7147bb 100644 --- a/src/renderer/src/hooks/useTranslate.ts +++ b/src/renderer/src/hooks/useTranslate.ts @@ -36,18 +36,16 @@ export default function useTranslate() { const getLanguageByLangcode = useCallback( (langCode: string) => { - if (!isLoaded) { - logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.') - return UNKNOWN - } - const result = translateLanguages.find((item) => item.langCode === langCode) + if (result) { return result + } else if (!isLoaded) { + logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.') } else { logger.warn(`Unknown language ${langCode}`) - return UNKNOWN } + return UNKNOWN }, [isLoaded, translateLanguages] ) @@ -63,6 +61,7 @@ export default function useTranslate() { prompt, settings, translateLanguages, + isLoaded, getLanguageByLangcode, updateSettings: handleUpdateSettings } diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index 6f4ec188da..97f2a6f179 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -116,7 +116,6 @@ export function getDefaultTranslateAssistant( // disable reasoning if it could be disabled, otherwise no configuration const reasoningEffort = supportedOptions?.includes('none') ? 'none' : 'default' const settings = { - temperature: 0.7, reasoning_effort: reasoningEffort, ..._settings } satisfies Partial diff --git a/src/renderer/src/utils/translate.ts b/src/renderer/src/utils/translate.ts index 4e01649369..d724bdb35e 100644 --- a/src/renderer/src/utils/translate.ts +++ b/src/renderer/src/utils/translate.ts @@ -83,9 +83,7 @@ const detectLanguageByLLM = async (inputText: string): Promise void = (chunk: Chunk) => { @@ -257,6 +255,7 @@ export const getTranslateOptions = async () => { })) return [...builtinLanguages, ...transformedCustomLangs] } catch (e) { + logger.error('[getTranslateOptions] Failed to get custom languages. Fallback to builtinLanguages', e as Error) return builtinLanguages } } diff --git a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx index 5e83071add..b5e0fea689 100644 --- a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx @@ -11,7 +11,6 @@ import MessageContent from '@renderer/pages/home/Messages/MessageContent' import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import type { Assistant, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types' import type { ActionItem } from '@renderer/types/selectionTypes' -import { runAsyncFunction } from '@renderer/utils' import { abortCompletion } from '@renderer/utils/abortController' import { detectLanguage } from '@renderer/utils/translate' import { Tooltip } from 'antd' @@ -32,70 +31,102 @@ const logger = loggerService.withContext('ActionTranslate') const ActionTranslate: FC = ({ action, scrollToBottom }) => { const { t } = useTranslation() - const { translateModelPrompt, language } = useSettings() + const { language } = useSettings() + const { getLanguageByLangcode, isLoaded: isLanguagesLoaded } = useTranslate() - const [targetLanguage, setTargetLanguage] = useState(LanguagesEnum.enUS) - const [alterLanguage, setAlterLanguage] = useState(LanguagesEnum.zhCN) + const [targetLanguage, setTargetLanguage] = useState(() => { + const lang = getLanguageByLangcode(language) + if (lang !== UNKNOWN) { + return lang + } else { + logger.warn('[initialize targetLanguage] Unexpected UNKNOWN. Fallback to zh-CN') + return LanguagesEnum.zhCN + } + }) + + const [alterLanguage, setAlterLanguage] = useState(LanguagesEnum.enUS) const [error, setError] = useState('') const [showOriginal, setShowOriginal] = useState(false) const [isContented, setIsContented] = useState(false) const [isLoading, setIsLoading] = useState(true) const [contentToCopy, setContentToCopy] = useState('') - const { getLanguageByLangcode } = useTranslate() // Use useRef for values that shouldn't trigger re-renders const initialized = useRef(false) const assistantRef = useRef(null) const topicRef = useRef(null) const askId = useRef('') + const targetLangRef = useRef(targetLanguage) - useEffect(() => { - runAsyncFunction(async () => { - const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' }) + // It's called only in initialization. + // It will change target/alter language, so fetchResult will be triggered. Be careful! + const updateLanguagePair = 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.') + return + } - let targetLang: TranslateLanguage - let alterLang: TranslateLanguage - - if (!biDirectionLangPair || !biDirectionLangPair.value[0]) { - const lang = getLanguageByLangcode(language) - if (lang !== UNKNOWN) { - targetLang = lang - } else { - logger.warn('Fallback to zh-CN') - targetLang = LanguagesEnum.zhCN - } - } else { - targetLang = getLanguageByLangcode(biDirectionLangPair.value[0]) - } - - if (!biDirectionLangPair || !biDirectionLangPair.value[1]) { - alterLang = LanguagesEnum.enUS - } else { - alterLang = getLanguageByLangcode(biDirectionLangPair.value[1]) - } + const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' }) + if (biDirectionLangPair && biDirectionLangPair.value[0]) { + const targetLang = getLanguageByLangcode(biDirectionLangPair.value[0]) setTargetLanguage(targetLang) - setAlterLanguage(alterLang) - }) - }, [getLanguageByLangcode, language]) + targetLangRef.current = targetLang + } - // Initialize values only once when action changes - useEffect(() => { - if (initialized.current || !action.selectedText) return - initialized.current = true + if (biDirectionLangPair && biDirectionLangPair.value[1]) { + const alterLang = getLanguageByLangcode(biDirectionLangPair.value[1]) + setAlterLanguage(alterLang) + } + }, [getLanguageByLangcode, isLanguagesLoaded]) + + // Initialize values only once + const initialize = useCallback(async () => { + if (initialized.current) { + logger.silly('[initialize] Already initialized.') + return + } + + // Only try to initialize when languages loaded, so updateLanguagePair would not fail. + if (!isLanguagesLoaded) { + logger.silly('[initialize] Languages not loaded. Skip initialization.') + return + } + + // Edge case + if (action.selectedText === undefined) { + logger.error('[initialize] No selected text.') + return + } + logger.silly('[initialize] Start initialization.') + + // Initialize language pair. + // It will update targetLangRef, so we could get latest target language in the following code + await updateLanguagePair() // Initialize assistant - const currentAssistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText) + const currentAssistant = getDefaultTranslateAssistant(targetLangRef.current, action.selectedText) assistantRef.current = currentAssistant // Initialize topic topicRef.current = getDefaultTopic(currentAssistant.id) - }, [action, targetLanguage, translateModelPrompt]) + initialized.current = true + }, [action.selectedText, isLanguagesLoaded, updateLanguagePair]) + + // 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) + useEffect(() => { + initialize() + }, [initialize]) const fetchResult = useCallback(async () => { - if (!assistantRef.current || !topicRef.current || !action.selectedText) return + if (!assistantRef.current || !topicRef.current || !action.selectedText || !initialized.current) return const setAskId = (id: string) => { askId.current = id @@ -141,6 +172,7 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText) assistantRef.current = assistant + logger.debug('process once') processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError) }, [action, targetLanguage, alterLanguage, scrollToBottom]) @@ -157,7 +189,11 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { }, [allMessages]) const handleChangeLanguage = (targetLanguage: TranslateLanguage, alterLanguage: TranslateLanguage) => { + if (!initialized.current) { + return + } setTargetLanguage(targetLanguage) + targetLangRef.current = targetLanguage setAlterLanguage(alterLanguage) db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage.langCode, alterLanguage.langCode] })