feat(translate): add target languages preference and refactor language handling

- Introduce TargetLangs type to manage target and alter languages
- Replace local state with preference-based language management
- Simplify language selection logic in ActionTranslate component
- Remove deprecated database storage for language pairs
This commit is contained in:
icarus 2025-10-15 01:42:16 +08:00
parent 7f34d084cc
commit d94f73b5ca
4 changed files with 52 additions and 61 deletions

View File

@ -401,6 +401,8 @@ export interface PreferenceSchemas {
'translate.settings.auto_detection_method': PreferenceTypes.AutoDetectionMethod
'translate.settings.enable_markdown': boolean
'translate.settings.scroll_sync': boolean
// new preference
'translate.settings.target_langs': PreferenceTypes.TargetLangs
// redux/settings/customCss
'ui.custom_css': string
// redux/settings/navbarPosition
@ -664,6 +666,10 @@ export const DefaultPreferences: PreferenceSchemas = {
'translate.settings.auto_detection_method': 'franc',
'translate.settings.enable_markdown': false,
'translate.settings.scroll_sync': false,
'translate.settings.target_langs': {
alter: 'zh-cn',
target: 'en-us'
},
'ui.custom_css': '',
'ui.navbar.position': 'top',
'ui.sidebar.icons.invisible': [],

View File

@ -1,3 +1,4 @@
import type { TranslateLanguageCode } from '@types'
import * as z from 'zod'
import type { PreferenceSchemas } from './preferenceSchemas'
@ -93,3 +94,7 @@ export type AutoDetectionMethod = z.infer<typeof AutoDetectionMethodSchema>
export const isAutoDetectionMethod = (method: string): method is AutoDetectionMethod => {
return AutoDetectionMethodSchema.safeParse(method).success
}
export type TargetLangs = {
target: TranslateLanguageCode
alter: TranslateLanguageCode
}

View File

@ -33,16 +33,15 @@ 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
}
},

View File

@ -4,17 +4,15 @@ import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import CopyButton from '@renderer/components/CopyButton'
import LanguageSelect from '@renderer/components/LanguageSelect'
import { LanguagesEnum, UNKNOWN } from '@renderer/config/translate'
import db from '@renderer/databases'
import { UNKNOWN } from '@renderer/config/translate'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import useTranslate from '@renderer/hooks/useTranslate'
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 { Assistant, Topic, TranslateAssistant, TranslateLanguageCode } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { abortCompletion } from '@renderer/utils/abortController'
import { detectLanguage } from '@renderer/utils/translate'
import { defaultLanguage } from '@shared/config/constant'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react'
import type { FC } from 'react'
@ -34,11 +32,9 @@ const logger = loggerService.withContext('ActionTranslate')
const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const { t } = useTranslation()
const [language] = usePreference('app.language')
const [translateModelPrompt] = usePreference('feature.translate.model_prompt')
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.zhCN)
const [targetLangs, setTargetLangs] = usePreference('translate.settings.target_langs')
const { target: targetLanguage, alter: alterLanguage } = targetLangs
const [error, setError] = useState('')
const [showOriginal, setShowOriginal] = useState(false)
@ -53,36 +49,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const topicRef = useRef<Topic | null>(null)
const askId = useRef('')
useEffect(() => {
runAsyncFunction(async () => {
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
let targetLang: TranslateLanguage
let alterLang: TranslateLanguage
if (!biDirectionLangPair || !biDirectionLangPair.value[0]) {
const lang = getLanguageByLangcode(language || navigator.language || defaultLanguage)
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])
}
setTargetLanguage(targetLang)
setAlterLanguage(alterLang)
})
}, [getLanguageByLangcode, language])
// Initialize values only once when action changes
useEffect(() => {
if (initialized.current || !action.selectedText) return
@ -90,14 +56,22 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
runAsyncFunction(async () => {
// Initialize assistant
const currentAssistant = await getDefaultTranslateAssistant(targetLanguage, action.selectedText!)
let currentAssistant: TranslateAssistant
try {
currentAssistant = await getDefaultTranslateAssistant(
getLanguageByLangcode(targetLanguage),
action.selectedText!
)
} catch (e) {
logger.error('Failed to initialize assistant', { targetLanguage, text: action.selectedText })
return
}
assistantRef.current = currentAssistant
// Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id)
})
}, [action, targetLanguage, translateModelPrompt])
}, [action, getLanguageByLangcode, targetLanguage, translateModelPrompt])
const fetchResult = useCallback(async () => {
if (!assistantRef.current || !topicRef.current || !action.selectedText) return
@ -130,24 +104,31 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
return
}
let translateLang: TranslateLanguage
let translateLang: TranslateLanguageCode
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) {
if (sourceLanguageCode === targetLanguage) {
translateLang = alterLanguage
} else {
translateLang = targetLanguage
}
}
const assistant = await getDefaultTranslateAssistant(translateLang, action.selectedText)
let assistant: TranslateAssistant
try {
assistant = await getDefaultTranslateAssistant(getLanguageByLangcode(translateLang), action.selectedText)
assistantRef.current = assistant
} catch (err) {
onError(err instanceof Error ? err : new Error('An error occurred'))
logger.error('Error when getting assistant:', err as Error)
return
}
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
}, [action, targetLanguage, alterLanguage, scrollToBottom])
}, [action.selectedText, getLanguageByLangcode, scrollToBottom, targetLanguage, alterLanguage])
useEffect(() => {
fetchResult()
@ -161,11 +142,11 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
return lastAssistantMessage ? <MessageContent key={lastAssistantMessage.id} message={lastAssistantMessage} /> : null
}, [allMessages])
const handleChangeLanguage = (targetLanguage: TranslateLanguage, alterLanguage: TranslateLanguage) => {
setTargetLanguage(targetLanguage)
setAlterLanguage(alterLanguage)
db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage.langCode, alterLanguage.langCode] })
const handleChangeLanguage = (targetLanguage: TranslateLanguageCode, alterLanguage: TranslateLanguageCode) => {
setTargetLangs({
target: targetLanguage,
alter: alterLanguage
})
}
const handlePause = () => {
@ -191,24 +172,24 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
<ArrowRightToLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
<Tooltip placement="bottom" content={t('translate.target_language')}>
<LanguageSelect
value={targetLanguage.langCode}
value={targetLanguage}
style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }}
listHeight={160}
title={t('translate.target_language')}
optionFilterProp="label"
onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)}
onChange={(value) => handleChangeLanguage(value, alterLanguage)}
disabled={isLoading}
/>
</Tooltip>
<ArrowRightFromLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
<Tooltip placement="bottom" content={t('translate.alter_language')}>
<LanguageSelect
value={alterLanguage.langCode}
value={alterLanguage}
style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }}
listHeight={160}
title={t('translate.alter_language')}
optionFilterProp="label"
onChange={(value) => handleChangeLanguage(targetLanguage, getLanguageByLangcode(value))}
onChange={(value) => handleChangeLanguage(targetLanguage, value)}
disabled={isLoading}
/>
</Tooltip>