From d94f73b5ca053a86d5f6dc9f6e1ed69341a8cb7f Mon Sep 17 00:00:00 2001 From: icarus Date: Wed, 15 Oct 2025 01:42:16 +0800 Subject: [PATCH] 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 --- .../data/preference/preferenceSchemas.ts | 6 ++ .../shared/data/preference/preferenceTypes.ts | 5 + src/renderer/src/hooks/useTranslate.ts | 11 +-- .../action/components/ActionTranslate.tsx | 91 ++++++++----------- 4 files changed, 52 insertions(+), 61 deletions(-) diff --git a/packages/shared/data/preference/preferenceSchemas.ts b/packages/shared/data/preference/preferenceSchemas.ts index 3d94a68370..d8c8523553 100644 --- a/packages/shared/data/preference/preferenceSchemas.ts +++ b/packages/shared/data/preference/preferenceSchemas.ts @@ -401,6 +401,8 @@ export interface PreferenceSchemas { 'translate.settings.auto_detection_method': PreferenceTypes.AutoDetectionMethod 'translate.settings.enable_markdown': boolean 'translate.settings.scroll_sync': boolean + // new preference + 'translate.settings.target_langs': PreferenceTypes.TargetLangs // redux/settings/customCss 'ui.custom_css': string // redux/settings/navbarPosition @@ -664,6 +666,10 @@ export const DefaultPreferences: PreferenceSchemas = { 'translate.settings.auto_detection_method': 'franc', 'translate.settings.enable_markdown': false, 'translate.settings.scroll_sync': false, + 'translate.settings.target_langs': { + alter: 'zh-cn', + target: 'en-us' + }, 'ui.custom_css': '', 'ui.navbar.position': 'top', 'ui.sidebar.icons.invisible': [], diff --git a/packages/shared/data/preference/preferenceTypes.ts b/packages/shared/data/preference/preferenceTypes.ts index d0c990032f..42607930ba 100644 --- a/packages/shared/data/preference/preferenceTypes.ts +++ b/packages/shared/data/preference/preferenceTypes.ts @@ -1,3 +1,4 @@ +import type { TranslateLanguageCode } from '@types' import * as z from 'zod' import type { PreferenceSchemas } from './preferenceSchemas' @@ -93,3 +94,7 @@ export type AutoDetectionMethod = z.infer export const isAutoDetectionMethod = (method: string): method is AutoDetectionMethod => { return AutoDetectionMethodSchema.safeParse(method).success } +export type TargetLangs = { + target: TranslateLanguageCode + alter: TranslateLanguageCode +} diff --git a/src/renderer/src/hooks/useTranslate.ts b/src/renderer/src/hooks/useTranslate.ts index cc8ec9e312..9d6df35133 100644 --- a/src/renderer/src/hooks/useTranslate.ts +++ b/src/renderer/src/hooks/useTranslate.ts @@ -33,16 +33,15 @@ export default function useTranslate() { const getLanguageByLangcode = useCallback( (langCode: string) => { - if (!isLoaded) { - logger.verbose('Translate languages are not loaded yet. Return UNKNOWN.') - return UNKNOWN - } - const result = translateLanguages.find((item) => item.langCode === langCode) if (result) { return result } 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 } }, diff --git a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx index d46dbebd0a..b38ef3aba9 100644 --- a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx @@ -4,17 +4,15 @@ import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import CopyButton from '@renderer/components/CopyButton' import LanguageSelect from '@renderer/components/LanguageSelect' -import { LanguagesEnum, UNKNOWN } from '@renderer/config/translate' -import db from '@renderer/databases' +import { UNKNOWN } from '@renderer/config/translate' import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import useTranslate from '@renderer/hooks/useTranslate' import MessageContent from '@renderer/pages/home/Messages/MessageContent' 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 { abortCompletion } from '@renderer/utils/abortController' import { detectLanguage } from '@renderer/utils/translate' -import { defaultLanguage } from '@shared/config/constant' import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes' import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react' import type { FC } from 'react' @@ -34,11 +32,9 @@ const logger = loggerService.withContext('ActionTranslate') const ActionTranslate: FC = ({ action, scrollToBottom }) => { const { t } = useTranslation() - const [language] = usePreference('app.language') const [translateModelPrompt] = usePreference('feature.translate.model_prompt') - - const [targetLanguage, setTargetLanguage] = useState(LanguagesEnum.enUS) - const [alterLanguage, setAlterLanguage] = useState(LanguagesEnum.zhCN) + const [targetLangs, setTargetLangs] = usePreference('translate.settings.target_langs') + const { target: targetLanguage, alter: alterLanguage } = targetLangs const [error, setError] = useState('') const [showOriginal, setShowOriginal] = useState(false) @@ -53,36 +49,6 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { const topicRef = useRef(null) 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 useEffect(() => { if (initialized.current || !action.selectedText) return @@ -90,14 +56,22 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { runAsyncFunction(async () => { // 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 // Initialize topic topicRef.current = getDefaultTopic(currentAssistant.id) }) - }, [action, targetLanguage, translateModelPrompt]) + }, [action, getLanguageByLangcode, targetLanguage, translateModelPrompt]) const fetchResult = useCallback(async () => { if (!assistantRef.current || !topicRef.current || !action.selectedText) return @@ -130,24 +104,31 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { return } - let translateLang: TranslateLanguage + let translateLang: TranslateLanguageCode if (sourceLanguageCode === UNKNOWN.langCode) { logger.debug('Unknown source language. Just use target language.') translateLang = targetLanguage } else { logger.debug('Detected Language: ', { sourceLanguage: sourceLanguageCode }) - if (sourceLanguageCode === targetLanguage.langCode) { + if (sourceLanguageCode === targetLanguage) { translateLang = alterLanguage } else { translateLang = targetLanguage } } - const assistant = await getDefaultTranslateAssistant(translateLang, action.selectedText) - assistantRef.current = assistant + let assistant: TranslateAssistant + 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) - }, [action, targetLanguage, alterLanguage, scrollToBottom]) + }, [action.selectedText, getLanguageByLangcode, scrollToBottom, targetLanguage, alterLanguage]) useEffect(() => { fetchResult() @@ -161,11 +142,11 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { return lastAssistantMessage ? : null }, [allMessages]) - const handleChangeLanguage = (targetLanguage: TranslateLanguage, alterLanguage: TranslateLanguage) => { - setTargetLanguage(targetLanguage) - setAlterLanguage(alterLanguage) - - db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage.langCode, alterLanguage.langCode] }) + const handleChangeLanguage = (targetLanguage: TranslateLanguageCode, alterLanguage: TranslateLanguageCode) => { + setTargetLangs({ + target: targetLanguage, + alter: alterLanguage + }) } const handlePause = () => { @@ -191,24 +172,24 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)} + onChange={(value) => handleChangeLanguage(value, alterLanguage)} disabled={isLoading} /> handleChangeLanguage(targetLanguage, getLanguageByLangcode(value))} + onChange={(value) => handleChangeLanguage(targetLanguage, value)} disabled={isLoading} />