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

View File

@ -1,3 +1,4 @@
import type { TranslateLanguageCode } from '@types'
import * as z from 'zod' import * as z from 'zod'
import type { PreferenceSchemas } from './preferenceSchemas' import type { PreferenceSchemas } from './preferenceSchemas'
@ -93,3 +94,7 @@ export type AutoDetectionMethod = z.infer<typeof AutoDetectionMethodSchema>
export const isAutoDetectionMethod = (method: string): method is AutoDetectionMethod => { export const isAutoDetectionMethod = (method: string): method is AutoDetectionMethod => {
return AutoDetectionMethodSchema.safeParse(method).success 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( const getLanguageByLangcode = useCallback(
(langCode: string) => { (langCode: string) => {
if (!isLoaded) {
logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.')
return UNKNOWN
}
const result = translateLanguages.find((item) => item.langCode === langCode) const result = translateLanguages.find((item) => item.langCode === langCode)
if (result) { if (result) {
return result return result
} else { } else {
logger.warn(`Unknown language ${langCode}`) if (!isLoaded) {
logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.')
} else {
logger.warn(`Unknown language ${langCode}`)
}
return UNKNOWN return UNKNOWN
} }
}, },

View File

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