mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
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:
parent
6cda7f891d
commit
9b7094ea4a
6
packages/shared/data/cache/cacheSchemas.ts
vendored
6
packages/shared/data/cache/cacheSchemas.ts
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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 }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user