mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 18:10:26 +08:00
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:
parent
7f34d084cc
commit
d94f73b5ca
@ -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': [],
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
if (!isLoaded) {
|
||||||
|
logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.')
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Unknown language ${langCode}`)
|
logger.warn(`Unknown language ${langCode}`)
|
||||||
|
}
|
||||||
return UNKNOWN
|
return UNKNOWN
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
try {
|
||||||
|
assistant = await getDefaultTranslateAssistant(getLanguageByLangcode(translateLang), action.selectedText)
|
||||||
assistantRef.current = assistant
|
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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user