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
This commit is contained in:
Phantom 2026-01-04 13:25:08 +08:00 committed by GitHub
parent acd1ecc09c
commit 680bda3993
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 81 additions and 48 deletions

View File

@ -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
}

View File

@ -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<AssistantSettings>

View File

@ -83,9 +83,7 @@ const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguage
const assistant: Assistant = getDefaultAssistant()
assistant.model = model
assistant.settings = {
temperature: 0.7
}
assistant.settings = {}
assistant.prompt = LANG_DETECT_PROMPT.replace('{{list_lang}}', listLangText).replace('{{input}}', text)
const onChunk: (chunk: Chunk) => 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
}
}

View File

@ -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<Props> = ({ action, scrollToBottom }) => {
const { t } = useTranslation()
const { translateModelPrompt, language } = useSettings()
const { language } = useSettings()
const { getLanguageByLangcode, isLoaded: isLanguagesLoaded } = useTranslate()
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.zhCN)
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(() => {
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<TranslateLanguage>(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<Assistant | null>(null)
const topicRef = useRef<Topic | null>(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<Props> = ({ 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<Props> = ({ 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] })