feat(translate): add bidirectional translation configuration in cache

Refactor bidirectional translation logic to use cache for state management
Simplify language pair handling by using language codes directly
This commit is contained in:
icarus 2025-10-15 00:16:04 +08:00
parent 6cda7f891d
commit 9b7094ea4a
5 changed files with 90 additions and 129 deletions

View File

@ -32,6 +32,7 @@ export type UseCacheSchema = {
'translate.output': string
'translate.detecting': boolean
'translate.translating': CacheValueTypes.CacheTranslating
'translate.bidirectional': CacheValueTypes.CacheTranslateBidirectional
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
@ -83,6 +84,11 @@ export const DefaultUseCache: UseCacheSchema = {
'translate.output': '',
'translate.detecting': false,
'translate.translating': { isTranslating: false, abortKey: null },
'translate.bidirectional': {
enabled: false,
origin: 'en-us',
target: 'zh-cn'
},
// Test keys (for dataRefactorTest window)
// TODO: remove after testing

View File

@ -1,4 +1,4 @@
import type { MinAppType, Topic, WebSearchStatus } from '@types'
import type { MinAppType, Topic, TranslateLanguageCode, WebSearchStatus } from '@types'
import type { UpdateInfo } from 'builder-util-runtime'
export type CacheAppUpdateState = {
@ -25,3 +25,8 @@ export type CacheTranslating =
isTranslating: false
abortKey: null
}
export type CacheTranslateBidirectional = {
enabled: boolean
origin: TranslateLanguageCode
target: TranslateLanguageCode
}

View File

@ -69,18 +69,15 @@ const TranslatePage: FC = () => {
const [isDetecting, setIsDetecting] = useCache('translate.detecting')
const [translatingState, setTranslatingState] = useCache('translate.translating')
const { isTranslating, abortKey } = translatingState
const [bidirectional, setBidirectional] = useCache('translate.bidirectional')
const { enabled: isBidirectional } = bidirectional
// states
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
const [copied, setCopied] = useTemporaryValue(false, 2000)
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false)
const [isBidirectional, setIsBidirectional] = useState(false)
const [enableMarkdown, setEnableMarkdown] = useState(false)
const [bidirectionalPair, setBidirectionalPair] = useState<[TranslateLanguage, TranslateLanguage]>([
LanguagesEnum.enUS,
LanguagesEnum.zhCN
])
const [settingsVisible, setSettingsVisible] = useState(false)
const [detectedLanguage, setDetectedLanguage] = useState<TranslateLanguage | null>(null)
const [sourceLanguage, setSourceLanguage] = useState<TranslateLanguage | 'auto'>(_sourceLanguage)
@ -149,12 +146,11 @@ const TranslatePage: FC = () => {
!text.trim() ||
(sourceLanguage !== 'auto' && sourceLanguage.langCode === UNKNOWN.langCode) ||
targetLanguage.langCode === UNKNOWN.langCode ||
(isBidirectional &&
(bidirectionalPair[0].langCode === UNKNOWN.langCode || bidirectionalPair[1].langCode === UNKNOWN.langCode)) ||
(isBidirectional && (bidirectional.origin === UNKNOWN.langCode || bidirectional.target === UNKNOWN.langCode)) ||
isProcessing ||
isDetecting
)
}, [bidirectionalPair, isBidirectional, isDetecting, isProcessing, sourceLanguage, targetLanguage.langCode, text])
}, [bidirectional, isBidirectional, isDetecting, isProcessing, sourceLanguage, targetLanguage.langCode, text])
// 控制翻译按钮,翻译前进行校验
const onTranslate = useCallback(async () => {
@ -184,7 +180,7 @@ const TranslatePage: FC = () => {
}
try {
const result = determineTargetLanguage(actualSourceLanguage, targetLanguage, isBidirectional, bidirectionalPair)
const result = determineTargetLanguage(actualSourceLanguage.langCode, targetLanguage.langCode, bidirectional)
if (!result.success) {
let errorMessage = ''
if (result.errorType === 'same_language') {
@ -197,7 +193,8 @@ const TranslatePage: FC = () => {
return
}
const actualTargetLanguage = result.language as TranslateLanguage
const actualTargetLanguage = getLanguageByLangcode(result.language)
if (isBidirectional) {
setTargetLanguage(actualTargetLanguage)
}
@ -230,7 +227,7 @@ const TranslatePage: FC = () => {
}
}, [
autoCopy,
bidirectionalPair,
bidirectional,
couldTranslate,
getLanguageByLangcode,
isBidirectional,
@ -254,12 +251,6 @@ const TranslatePage: FC = () => {
abortCompletion(abortKey)
}
// 控制双向翻译切换
const toggleBidirectional = (value: boolean) => {
setIsBidirectional(value)
db.settings.put({ id: 'translate:bidirectional:enabled', value })
}
// 控制历史记录点击
const onHistoryItemClick = (history: TranslateHistory) => {
setText(history.sourceText)
@ -334,32 +325,6 @@ const TranslatePage: FC = () => {
sourceLang &&
setSourceLanguage(sourceLang.value === 'auto' ? sourceLang.value : getLanguageByLangcode(sourceLang.value))
const bidirectionalPairSetting = await db.settings.get({ id: 'translate:bidirectional:pair' })
if (bidirectionalPairSetting) {
const langPair = bidirectionalPairSetting.value
let source: undefined | TranslateLanguage
let target: undefined | TranslateLanguage
if (Array.isArray(langPair) && langPair.length === 2 && langPair[0] !== langPair[1]) {
source = getLanguageByLangcode(langPair[0])
target = getLanguageByLangcode(langPair[1])
}
if (source && target) {
setBidirectionalPair([source, target])
} else {
const defaultPair: [TranslateLanguage, TranslateLanguage] = [LanguagesEnum.enUS, LanguagesEnum.zhCN]
setBidirectionalPair(defaultPair)
db.settings.put({
id: 'translate:bidirectional:pair',
value: [defaultPair[0].langCode, defaultPair[1].langCode]
})
}
}
const bidirectionalSetting = await db.settings.get({ id: 'translate:bidirectional:enabled' })
setIsBidirectional(bidirectionalSetting ? bidirectionalSetting.value : false)
const scrollSyncSetting = await db.settings.get({ id: 'translate:scroll:sync' })
setIsScrollSyncEnabled(scrollSyncSetting ? scrollSyncSetting.value : false)
@ -383,8 +348,8 @@ const TranslatePage: FC = () => {
// 获取目标语言显示
const getLanguageDisplay = () => {
try {
if (isBidirectional) {
if (isBidirectional) {
try {
return (
<Flex className="min-w-40 items-center">
<BidirectionalLanguageDisplay>
@ -392,10 +357,14 @@ const TranslatePage: FC = () => {
</BidirectionalLanguageDisplay>
</Flex>
)
} catch (error) {
logger.error('Error getting language display:', error as Error)
setBidirectional({
enabled: true,
origin: LanguagesEnum.enUS.langCode,
target: LanguagesEnum.zhCN.langCode
})
}
} catch (error) {
logger.error('Error getting language display:', error as Error)
setBidirectionalPair([LanguagesEnum.enUS, LanguagesEnum.zhCN])
}
return (
@ -766,12 +735,8 @@ const TranslatePage: FC = () => {
onClose={() => setSettingsVisible(false)}
isScrollSyncEnabled={isScrollSyncEnabled}
setIsScrollSyncEnabled={setIsScrollSyncEnabled}
isBidirectional={isBidirectional}
setIsBidirectional={toggleBidirectional}
enableMarkdown={enableMarkdown}
setEnableMarkdown={setEnableMarkdown}
bidirectionalPair={bidirectionalPair}
setBidirectionalPair={setBidirectionalPair}
translateModel={translateModel}
/>
</Container>

View File

@ -1,12 +1,12 @@
import { Button, ColFlex, Flex, HelpTooltip, RowFlex, Switch, Tooltip } from '@cherrystudio/ui'
import { useCache } from '@data/hooks/useCache'
import { usePreference } from '@data/hooks/usePreference'
import LanguageSelect from '@renderer/components/LanguageSelect'
import db from '@renderer/databases'
import useTranslate from '@renderer/hooks/useTranslate'
import type { Model, TranslateLanguage } from '@renderer/types'
import type { Model } from '@renderer/types'
import { Modal, Radio, Space } from 'antd'
import type { FC } from 'react'
import { memo, useEffect, useState } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import TranslateSettingsPopup from '../settings/TranslateSettingsPopup/TranslateSettingsPopup'
@ -17,35 +17,15 @@ const TranslateSettings: FC<{
onClose: () => void
isScrollSyncEnabled: boolean
setIsScrollSyncEnabled: (value: boolean) => void
isBidirectional: boolean
setIsBidirectional: (value: boolean) => void
enableMarkdown: boolean
setEnableMarkdown: (value: boolean) => void
bidirectionalPair: [TranslateLanguage, TranslateLanguage]
setBidirectionalPair: (value: [TranslateLanguage, TranslateLanguage]) => void
translateModel: Model | undefined
}> = ({
visible,
onClose,
isScrollSyncEnabled,
setIsScrollSyncEnabled,
isBidirectional,
setIsBidirectional,
enableMarkdown,
setEnableMarkdown,
bidirectionalPair,
setBidirectionalPair
}) => {
}> = ({ visible, onClose, isScrollSyncEnabled, setIsScrollSyncEnabled, enableMarkdown, setEnableMarkdown }) => {
const { t } = useTranslation()
const [localPair, setLocalPair] = useState<[TranslateLanguage, TranslateLanguage]>(bidirectionalPair)
const { getLanguageByLangcode } = useTranslate()
const [autoCopy, setAutoCopy] = usePreference('translate.settings.auto_copy')
const [autoDetectionMethod, setAutoDetectionMethod] = usePreference('translate.settings.auto_detection_method')
useEffect(() => {
setLocalPair(bidirectionalPair)
}, [bidirectionalPair, visible])
const [bidirectional, setBidirectional] = useCache('translate.bidirectional')
const { enabled: isBidirectional } = bidirectional
const onMoreSetting = () => {
onClose()
TranslateSettingsPopup.show()
@ -146,7 +126,7 @@ const TranslateSettings: FC<{
isSelected={isBidirectional}
color="primary"
onValueChange={(isSelected) => {
setIsBidirectional(isSelected)
setBidirectional({ ...bidirectional, enabled: isSelected })
// 双向翻译设置不需要持久化,它只是界面状态
}}
/>
@ -156,36 +136,32 @@ const TranslateSettings: FC<{
<Flex className="items-center justify-between gap-2.5">
<LanguageSelect
style={{ flex: 1 }}
value={localPair[0].langCode}
value={bidirectional.origin}
onChange={(value) => {
const newPair: [TranslateLanguage, TranslateLanguage] = [getLanguageByLangcode(value), localPair[1]]
if (newPair[0] === newPair[1]) {
if (value === bidirectional.target) {
window.toast.warning(t('translate.language.same'))
return
}
setLocalPair(newPair)
setBidirectionalPair(newPair)
db.settings.put({
id: 'translate:bidirectional:pair',
value: [newPair[0].langCode, newPair[1].langCode]
setBidirectional({
...bidirectional,
origin: value,
target: bidirectional.target
})
}}
/>
<span></span>
<LanguageSelect
style={{ flex: 1 }}
value={localPair[1].langCode}
value={bidirectional.target}
onChange={(value) => {
const newPair: [TranslateLanguage, TranslateLanguage] = [localPair[0], getLanguageByLangcode(value)]
if (newPair[0] === newPair[1]) {
if (bidirectional.origin === value) {
window.toast.warning(t('translate.language.same'))
return
}
setLocalPair(newPair)
setBidirectionalPair(newPair)
db.settings.put({
id: 'translate:bidirectional:pair',
value: [newPair[0].langCode, newPair[1].langCode]
setBidirectional({
...bidirectional,
origin: bidirectional.origin,
target: value
})
}}
/>

View File

@ -11,6 +11,7 @@ import type { Assistant, TranslateLanguage, TranslateLanguageCode } from '@rende
import type { Chunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk'
import { LANG_DETECT_PROMPT } from '@shared/config/prompts'
import type { CacheTranslateBidirectional } from '@shared/data/cache/cacheValueTypes'
import { franc } from 'franc-min'
import type { RefObject } from 'react'
import React from 'react'
@ -128,60 +129,68 @@ const detectLanguageByFranc = (inputText: string): TranslateLanguageCode => {
}
/**
*
* @param sourceLanguage
* @param languagePair
* @returns
* Determine the target language for bidirectional translation.
* When the source language matches one side of the pair, the opposite side is returned.
* @param sourceLanguage The detected source language code
* @param languagePair The configured bidirectional language pair
* @returns The target language code to translate into
*/
export const getTargetLanguageForBidirectional = (
sourceLanguage: TranslateLanguage,
languagePair: [TranslateLanguage, TranslateLanguage]
): TranslateLanguage => {
if (sourceLanguage.langCode === languagePair[0].langCode) {
return languagePair[1]
} else if (sourceLanguage.langCode === languagePair[1].langCode) {
return languagePair[0]
sourceLanguage: TranslateLanguageCode,
languagePair: CacheTranslateBidirectional
): TranslateLanguageCode => {
const { origin, target } = languagePair
if (sourceLanguage === origin) {
return target
} else if (sourceLanguage === target) {
return origin
}
return languagePair[0] !== sourceLanguage ? languagePair[0] : languagePair[1]
return origin !== sourceLanguage ? origin : target
}
/**
*
* @param sourceLanguage
* @param languagePair
* @returns
* Check if the source language is within the configured language pair
* @param sourceLanguage The detected source language code
* @param languagePair The configured bidirectional language pair
* @returns true if the source language is in the pair, false otherwise
*/
export const isLanguageInPair = (
sourceLanguage: TranslateLanguage,
languagePair: [TranslateLanguage, TranslateLanguage]
sourceLanguage: TranslateLanguageCode,
languagePair: CacheTranslateBidirectional
): boolean => {
return [languagePair[0].langCode, languagePair[1].langCode].includes(sourceLanguage.langCode)
return [languagePair.origin, languagePair.target].includes(sourceLanguage)
}
type DetermineTargetLanguageReturn =
| { success: true; language: TranslateLanguageCode; errorType?: never }
| {
success: false
errorType: 'same_language' | 'not_in_pair'
}
/**
*
* @param sourceLanguage
* @param targetLanguage
* @param isBidirectional
* @param bidirectionalPair
* @returns
* Determine the target language for translation
* @param sourceLanguage The detected source language code
* @param targetLanguage The user-set target language code
* @param bidirectional The bidirectional translation configuration
* @returns An object indicating success or failure, including the target language code if successful, or an error type if failed
*/
export const determineTargetLanguage = (
sourceLanguage: TranslateLanguage,
targetLanguage: TranslateLanguage,
isBidirectional: boolean,
bidirectionalPair: [TranslateLanguage, TranslateLanguage]
): { success: boolean; language?: TranslateLanguage; errorType?: 'same_language' | 'not_in_pair' } => {
sourceLanguage: TranslateLanguageCode,
targetLanguage: TranslateLanguageCode,
bidirectional: CacheTranslateBidirectional
): DetermineTargetLanguageReturn => {
const isBidirectional = bidirectional.enabled
if (isBidirectional) {
if (!isLanguageInPair(sourceLanguage, bidirectionalPair)) {
if (!isLanguageInPair(sourceLanguage, bidirectional)) {
return { success: false, errorType: 'not_in_pair' }
}
return {
success: true,
language: getTargetLanguageForBidirectional(sourceLanguage, bidirectionalPair)
language: getTargetLanguageForBidirectional(sourceLanguage, bidirectional)
}
} else {
if (sourceLanguage.langCode === targetLanguage.langCode) {
if (sourceLanguage === targetLanguage) {
return { success: false, errorType: 'same_language' }
}
return { success: true, language: targetLanguage }