From 9b7094ea4a2f6f738fe6d39157f783ed653fcd4f Mon Sep 17 00:00:00 2001 From: icarus Date: Wed, 15 Oct 2025 00:16:04 +0800 Subject: [PATCH] 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 --- packages/shared/data/cache/cacheSchemas.ts | 6 ++ packages/shared/data/cache/cacheValueTypes.ts | 7 +- .../src/pages/translate/TranslatePage.tsx | 69 +++++------------ .../src/pages/translate/TranslateSettings.tsx | 62 +++++---------- src/renderer/src/utils/translate.ts | 75 +++++++++++-------- 5 files changed, 90 insertions(+), 129 deletions(-) diff --git a/packages/shared/data/cache/cacheSchemas.ts b/packages/shared/data/cache/cacheSchemas.ts index 908700ae6e..e88dcc9ca9 100644 --- a/packages/shared/data/cache/cacheSchemas.ts +++ b/packages/shared/data/cache/cacheSchemas.ts @@ -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 diff --git a/packages/shared/data/cache/cacheValueTypes.ts b/packages/shared/data/cache/cacheValueTypes.ts index eca43337c6..1f1b67c945 100644 --- a/packages/shared/data/cache/cacheValueTypes.ts +++ b/packages/shared/data/cache/cacheValueTypes.ts @@ -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 +} diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 117e191219..393fcdb5d6 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -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('') 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(null) const [sourceLanguage, setSourceLanguage] = useState(_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 ( @@ -392,10 +357,14 @@ const TranslatePage: FC = () => { ) + } 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} /> diff --git a/src/renderer/src/pages/translate/TranslateSettings.tsx b/src/renderer/src/pages/translate/TranslateSettings.tsx index 912232c693..eb9b3147c2 100644 --- a/src/renderer/src/pages/translate/TranslateSettings.tsx +++ b/src/renderer/src/pages/translate/TranslateSettings.tsx @@ -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<{ { - 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 }) }} /> { - 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 }) }} /> diff --git a/src/renderer/src/utils/translate.ts b/src/renderer/src/utils/translate.ts index 64e3e2947c..e14503527d 100644 --- a/src/renderer/src/utils/translate.ts +++ b/src/renderer/src/utils/translate.ts @@ -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 }