diff --git a/.vscode/settings.json b/.vscode/settings.json index 997c26aedf..3dd634507f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,6 +28,9 @@ "source.organizeImports": "never" }, "editor.formatOnSave": true, + "files.associations": { + "*.css": "tailwindcss" + }, "files.eol": "\n", "i18n-ally.displayLanguage": "zh-cn", "i18n-ally.enabledFrameworks": ["react-i18next", "i18next"], diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx index daa2fd9011..5fe61108eb 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -51,6 +51,13 @@ export const ThemeProvider: React.FC = ({ children }) => { // Set initial theme and OS attributes on body document.body.setAttribute('os', isMac ? 'mac' : isWin ? 'windows' : 'linux') document.body.setAttribute('theme-mode', actualTheme) + if (actualTheme === ThemeMode.dark) { + document.body.classList.remove('light') + document.body.classList.add('dark') + } else { + document.body.classList.remove('dark') + document.body.classList.add('light') + } document.body.setAttribute('navbar-position', navbarPosition) // if theme is old auto, then set theme to system diff --git a/src/renderer/src/hooks/useTranslate.ts b/src/renderer/src/hooks/useTranslate.ts index 4a4dacd53c..5f5f1cbded 100644 --- a/src/renderer/src/hooks/useTranslate.ts +++ b/src/renderer/src/hooks/useTranslate.ts @@ -1,10 +1,12 @@ import { loggerService } from '@logger' import { builtinLanguages, UNKNOWN } from '@renderer/config/translate' import { useAppSelector } from '@renderer/store' +import { TranslateState, updateSettings } from '@renderer/store/translate' import { TranslateLanguage } from '@renderer/types' import { runAsyncFunction } from '@renderer/utils' import { getTranslateOptions } from '@renderer/utils/translate' import { useCallback, useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' const logger = loggerService.withContext('useTranslate') @@ -17,9 +19,12 @@ const logger = loggerService.withContext('useTranslate') */ export default function useTranslate() { const prompt = useAppSelector((state) => state.settings.translateModelPrompt) + const settings = useAppSelector((state) => state.translate.settings) const [translateLanguages, setTranslateLanguages] = useState(builtinLanguages) const [isLoaded, setIsLoaded] = useState(false) + const dispatch = useDispatch() + useEffect(() => { runAsyncFunction(async () => { const options = await getTranslateOptions() @@ -46,9 +51,18 @@ export default function useTranslate() { [isLoaded, translateLanguages] ) + const handleUpdateSettings = useCallback( + (update: Partial) => { + dispatch(updateSettings(update)) + }, + [dispatch] + ) + return { prompt, + settings, translateLanguages, - getLanguageByLangcode + getLanguageByLangcode, + updateSettings: handleUpdateSettings } } diff --git a/src/renderer/src/hooks/useUserTheme.ts b/src/renderer/src/hooks/useUserTheme.ts index 815903da73..0b1bc5fb24 100644 --- a/src/renderer/src/hooks/useUserTheme.ts +++ b/src/renderer/src/hooks/useUserTheme.ts @@ -11,6 +11,8 @@ export default function useUserTheme() { const colorPrimary = Color(theme.colorPrimary) document.body.style.setProperty('--color-primary', colorPrimary.toString()) + // overwrite hero UI primary color. + document.body.style.setProperty('--primary', colorPrimary.toString()) document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString()) document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString()) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 646a5c48cc..17213ec798 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4301,6 +4301,7 @@ }, "processing": "Translation in progress...", "settings": { + "autoCopy": "Copy after translation ", "bidirectional": "Bidirectional Translation Settings", "bidirectional_tip": "When enabled, only bidirectional translation between source and target languages is supported", "model": "Model Settings", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5122edc291..221e36f6af 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4301,6 +4301,7 @@ }, "processing": "翻译中...", "settings": { + "autoCopy": "翻译完成后自动复制", "bidirectional": "双向翻译设置", "bidirectional_tip": "开启后,仅支持在源语言和目标语言之间进行双向翻译", "model": "模型设置", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 364a5b7e3e..3f080ee249 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4301,6 +4301,7 @@ }, "processing": "翻譯中...", "settings": { + "autoCopy": "翻譯完成後自動複製", "bidirectional": "雙向翻譯設定", "bidirectional_tip": "開啟後,僅支援在源語言和目標語言之間進行雙向翻譯", "model": "模型設定", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 2ca2574bf0..b85f5c9b50 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -2221,6 +2221,21 @@ } }, "dragHandle": "στοιχεία σύρεσης", + "frontMatter": { + "addProperty": "Προσθήκη χαρακτηριστικού", + "addTag": "Προσθήκη ετικέτας", + "changeToBoolean": "Πλαίσιο ελέγχου", + "changeToDate": "Ημερομηνία", + "changeToNumber": "ψηφίο", + "changeToTags": "ετικέτα", + "changeToText": "κείμενο", + "changeType": "Αλλαγή τύπου", + "deleteProperty": "διαγραφή χαρακτηριστικού", + "editValue": "Επεξεργασία τιμής", + "empty": "\n空\n", + "moreActions": "Περισσότερες ενέργειες", + "propertyName": "όνομα χαρακτηριστικού" + }, "image": { "placeholder": "προσθήκη εικόνας" }, @@ -3681,15 +3696,19 @@ "apikey": "Κλειδί API", "auth_failed": "Αποτυχία πιστοποίησης ταυτότητας Anthropic", "auth_method": "Τρόπος πιστοποίησης", + "auth_success": "Η πιστοποίηση OAuth της Anthropic ήταν επιτυχής", "authenticated": "Επαληθευμένο", "authenticating": "Επαλήθευση σε εξέλιξη", "cancel": "Ακύρωση", + "code_error": "Μη έγκυρος κωδικός εξουσιοδότησης, δοκιμάστε ξανά", "code_placeholder": "Παρακαλώ εισαγάγετε τον κωδικό εξουσιοδότησης που εμφανίζεται στον περιηγητή σας", "code_required": "Ο κωδικός εξουσιοδότησης δεν μπορεί να είναι κενός", "description": "Πιστοποίηση OAuth", "description_detail": "Για να χρησιμοποιήσετε αυτόν τον τρόπο επαλήθευσης, πρέπει να είστε συνδρομητής Claude Pro ή έκδοσης μεγαλύτερης από αυτήν", "enter_auth_code": "κωδικός εξουσιοδότησης", "logout": "Αποσύνδεση λογαριασμού", + "logout_failed": "Η αποσύνδεση απέτυχε, δοκιμάστε ξανά.", + "logout_success": "Επιτυχής αποσύνδεση από το Anthropic", "oauth": "ιστοσελίδα OAuth", "start_auth": "Έναρξη εξουσιοδότησης", "submit_code": "Ολοκληρώστε την σύνδεση" @@ -4278,6 +4297,7 @@ }, "processing": "Μεταφράζεται...", "settings": { + "autoCopy": "Μετά τη μετάφραση, αντιγράφεται αυτόματα", "bidirectional": "Ρύθμιση διπλής κατεύθυνσης μετάφρασης", "bidirectional_tip": "Όταν ενεργοποιηθεί, υποστηρίζεται μόνο διπλής κατεύθυνσης μετάφραση μεταξύ της πηγαίας και της στόχου γλώσσας", "model": "Ρύθμιση μοντέλου", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 94a6a0d0cd..95d79571a1 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -2221,6 +2221,21 @@ } }, "dragHandle": "bloque de arrastre", + "frontMatter": { + "addProperty": "Agregar atributo", + "addTag": "Añadir etiqueta", + "changeToBoolean": "Casilla de verificación", + "changeToDate": "Fecha", + "changeToNumber": "número", + "changeToTags": "etiqueta", + "changeToText": "texto", + "changeType": "cambiar tipo", + "deleteProperty": "eliminar atributo", + "editValue": "editar valor", + "empty": "vacío", + "moreActions": "Más operaciones", + "propertyName": "Nombre del atributo" + }, "image": { "placeholder": "añadir imágenes" }, @@ -3681,15 +3696,19 @@ "apikey": "Clave de API", "auth_failed": "Error de autenticación de Anthropic", "auth_method": "Método de autenticación", + "auth_success": "Autenticación OAuth de Anthropic exitosa", "authenticated": "Verificado", "authenticating": "Autenticando", "cancel": "Cancelar", + "code_error": "Código de autorización inválido, inténtalo de nuevo", "code_placeholder": "Introduzca el código de autorización que se muestra en el navegador", "code_required": "El código de autorización no puede estar vacío", "description": "Autenticación OAuth", "description_detail": "Necesitas suscribirte a Claude Pro o a una versión superior para utilizar este método de autenticación", "enter_auth_code": "Código de autorización", "logout": "Cerrar sesión", + "logout_failed": "Error al cerrar sesión, inténtalo de nuevo", + "logout_success": "Cierre de sesión exitoso en Anthropic", "oauth": "Web OAuth", "start_auth": "Comenzar autorización", "submit_code": "Iniciar sesión completado" @@ -4278,6 +4297,7 @@ }, "processing": "Traduciendo...", "settings": { + "autoCopy": "Copiar automáticamente después de completar la traducción", "bidirectional": "Configuración de traducción bidireccional", "bidirectional_tip": "Una vez activada, solo se admitirá la traducción bidireccional entre el idioma de origen y el idioma de destino", "model": "Configuración del modelo", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 0c929c697d..d3ae5f5dfb 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -2221,6 +2221,21 @@ } }, "dragHandle": "bloc de glisser-déposer", + "frontMatter": { + "addProperty": "Ajouter un attribut", + "addTag": "Ajouter une étiquette", + "changeToBoolean": "Case à cocher", + "changeToDate": "fecha", + "changeToNumber": "numérique", + "changeToTags": "étiquette", + "changeToText": "texte", + "changeType": "Modifier le type", + "deleteProperty": "Supprimer l'attribut", + "editValue": "valeur d'édition", + "empty": "vacío", + "moreActions": "Plus d'actions", + "propertyName": "Nom de l'attribut" + }, "image": { "placeholder": "ajouter une image" }, @@ -3681,15 +3696,19 @@ "apikey": "Clé API", "auth_failed": "Échec de l'authentification Anthropic", "auth_method": "Mode d'authentification", + "auth_success": "Authentification OAuth Anthropic réussie", "authenticated": "Certifié", "authenticating": "Authentification en cours", "cancel": "Annuler", + "code_error": "Code d'autorisation invalide, veuillez réessayer", "code_placeholder": "Veuillez saisir le code d'autorisation affiché dans le navigateur", "code_required": "Le code d'autorisation ne peut pas être vide", "description": "Authentification OAuth", "description_detail": "Vous devez souscrire à Claude Pro ou à une version supérieure pour pouvoir utiliser cette méthode d'authentification.", "enter_auth_code": "code d'autorisation", "logout": "Déconnexion", + "logout_failed": "Échec de la déconnexion, veuillez réessayer", + "logout_success": "Déconnexion réussie d'Anthropic", "oauth": "Authentification OAuth web", "start_auth": "Commencer l'autorisation", "submit_code": "Terminer la connexion" @@ -4278,6 +4297,7 @@ }, "processing": "en cours de traduction...", "settings": { + "autoCopy": "Copié automatiquement après la traduction", "bidirectional": "Paramètres de traduction bidirectionnelle", "bidirectional_tip": "Une fois activé, seul la traduction bidirectionnelle entre la langue source et la langue cible est prise en charge", "model": "Paramètres du modèle", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 10cd57db6a..30ea6e3411 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -2221,6 +2221,21 @@ } }, "dragHandle": "ブロックをドラッグします", + "frontMatter": { + "addProperty": "属性を追加", + "addTag": "タグを追加", + "changeToBoolean": "チェックボックス", + "changeToDate": "日付", + "changeToNumber": "数字", + "changeToTags": "タグ", + "changeToText": "テキスト", + "changeType": "種類を変更", + "deleteProperty": "削除属性", + "editValue": "編集値", + "empty": "空", + "moreActions": "その他の操作", + "propertyName": "プロパティ名" + }, "image": { "placeholder": "写真を追加します" }, @@ -4282,6 +4297,7 @@ }, "processing": "翻訳中...", "settings": { + "autoCopy": "翻訳完了後、自動的にコピー", "bidirectional": "双方向翻訳設定", "bidirectional_tip": "有効にすると、ソース言語と目標言語間の双方向翻訳のみがサポートされます", "model": "モデル設定", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4127b25391..2d315e8fc8 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -2221,6 +2221,21 @@ } }, "dragHandle": "bloco de arrastar", + "frontMatter": { + "addProperty": "Adicionar atributo", + "addTag": "Adicionar etiqueta", + "changeToBoolean": "Caixa de seleção", + "changeToDate": "Data", + "changeToNumber": "número", + "changeToTags": "etiqueta", + "changeToText": "texto", + "changeType": "Alterar tipo", + "deleteProperty": "Excluir atributo", + "editValue": "Editar valor", + "empty": "vazio", + "moreActions": "Mais ações", + "propertyName": "nome do atributo" + }, "image": { "placeholder": "adicionar imagem" }, @@ -3681,15 +3696,19 @@ "apikey": "Chave da API", "auth_failed": "Falha na autenticação da Anthropic", "auth_method": "Método de autenticação", + "auth_success": "Autenticação OAuth da Anthropic bem-sucedida", "authenticated": "[retranslating]: Verificado", "authenticating": "A autenticar", "cancel": "Cancelar", + "code_error": "Código de autorização inválido, tente novamente", "code_placeholder": "Introduza o código de autorização exibido no browser", "code_required": "O código de autorização não pode estar vazio", "description": "Autenticação OAuth", "description_detail": "Precisa de uma subscrição Claude Pro ou superior para utilizar este método de autenticação", "enter_auth_code": "Código de autorização", "logout": "Sair da sessão", + "logout_failed": "Falha ao sair da conta, tente novamente", + "logout_success": "Logout bem-sucedido do login Anthropic", "oauth": "OAuth da Página Web", "start_auth": "Iniciar autorização", "submit_code": "Concluir login" @@ -4278,6 +4297,7 @@ }, "processing": "Traduzindo...", "settings": { + "autoCopy": "Cópia automática após a tradução", "bidirectional": "Configuração de Tradução Bidirecional", "bidirectional_tip": "Quando ativado, suporta apenas tradução bidirecional entre o idioma de origem e o idioma de destino", "model": "Configuração de Modelo", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index bae0264e88..045d8d618f 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -2221,6 +2221,21 @@ } }, "dragHandle": "Перетащить блок", + "frontMatter": { + "addProperty": "Добавить атрибут", + "addTag": "Добавить метки", + "changeToBoolean": "Флажок", + "changeToDate": "Дата", + "changeToNumber": "цифры", + "changeToTags": "ярлык", + "changeToText": "текст", + "changeType": "Изменить тип", + "deleteProperty": "Удалить атрибут", + "editValue": "Редактировать значение", + "empty": "Пусто", + "moreActions": "Дополнительные действия", + "propertyName": "Имя атрибута" + }, "image": { "placeholder": "Добавить картинку" }, @@ -4282,6 +4297,7 @@ }, "processing": "Перевод в процессе...", "settings": { + "autoCopy": "Автоматически копировать после завершения перевода", "bidirectional": "Настройки двунаправленного перевода", "bidirectional_tip": "Если включено, перевод будет выполняться в обоих направлениях, исходный текст будет переведен на целевой язык и наоборот.", "model": "Настройки модели", diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index b8118e800f..552da3ca03 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -13,6 +13,7 @@ import { useDrag } from '@renderer/hooks/useDrag' import { useFiles } from '@renderer/hooks/useFiles' import { useOcr } from '@renderer/hooks/useOcr' import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue' +import { useTimer } from '@renderer/hooks/useTimer' import useTranslate from '@renderer/hooks/useTranslate' import { estimateTextTokens } from '@renderer/services/TokenService' import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService' @@ -60,10 +61,12 @@ const TranslatePage: FC = () => { // hooks const { t } = useTranslation() const { translateModel, setTranslateModel } = useDefaultModel() - const { prompt, getLanguageByLangcode } = useTranslate() + const { prompt, getLanguageByLangcode, settings } = useTranslate() + const { autoCopy } = settings const { shikiMarkdownIt } = useCodeStyle() const { onSelectFile, selecting, clearFiles } = useFiles({ extensions: [...imageExts, ...textExts] }) const { ocr } = useOcr() + const { setTimeoutTimer } = useTimer() // states // const [text, setText] = useState(_text) @@ -129,6 +132,18 @@ const TranslatePage: FC = () => { [dispatch] ) + // 控制复制行为 + const onCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(translatedContent) + setCopied(true) + } catch (error) { + logger.error('Failed to copy text to clipboard:', error as Error) + // TODO: use toast + window.message.error(t('common.copy_failed')) + } + }, [setCopied, t, translatedContent]) + /** * 翻译文本并保存历史记录,包含完整的异常处理,不会抛出异常 * @param text - 需要翻译的文本 @@ -153,7 +168,9 @@ const TranslatePage: FC = () => { try { translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100), abortKey) } catch (e) { - if (!isAbortError(e)) { + if (isAbortError(e)) { + window.message.info(t('translate.info.aborted')) + } else { logger.error('Failed to translate text', e as Error) window.message.error(t('translate.error.failed') + ': ' + formatErrorMessage(e)) } @@ -162,6 +179,15 @@ const TranslatePage: FC = () => { } window.message.success(t('translate.complete')) + if (autoCopy) { + setTimeoutTimer( + 'auto-copy', + async () => { + await onCopy() + }, + 100 + ) + } try { await saveTranslateHistory(text, translated, actualSourceLanguage.langCode, actualTargetLanguage.langCode) @@ -174,7 +200,7 @@ const TranslatePage: FC = () => { window.message.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e)) } }, - [dispatch, setTranslatedContent, setTranslating, t, translating] + [autoCopy, dispatch, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating] ) // 控制翻译按钮是否可用 @@ -237,10 +263,7 @@ const TranslatePage: FC = () => { await translate(text, actualSourceLanguage, actualTargetLanguage) } catch (error) { logger.error('Translation error:', error as Error) - window.message.error({ - content: String(error), - key: 'translate-message' - }) + window.message.error(t('translate.error.failed') + ': ' + formatErrorMessage(error)) return } finally { setTranslating(false) @@ -274,12 +297,6 @@ const TranslatePage: FC = () => { db.settings.put({ id: 'translate:bidirectional:enabled', value }) } - // 控制复制按钮 - const onCopy = () => { - navigator.clipboard.writeText(translatedContent) - setCopied(true) - } - // 控制历史记录点击 const onHistoryItemClick = ( history: TranslateHistory & { _sourceLanguage: TranslateLanguage; _targetLanguage: TranslateLanguage } diff --git a/src/renderer/src/pages/translate/TranslateSettings.tsx b/src/renderer/src/pages/translate/TranslateSettings.tsx index f26fad8114..3495cf7b57 100644 --- a/src/renderer/src/pages/translate/TranslateSettings.tsx +++ b/src/renderer/src/pages/translate/TranslateSettings.tsx @@ -1,15 +1,17 @@ +import { Switch } from '@heroui/react' import LanguageSelect from '@renderer/components/LanguageSelect' import { HStack } from '@renderer/components/Layout' import db from '@renderer/databases' import useTranslate from '@renderer/hooks/useTranslate' import { AutoDetectionMethod, Model, TranslateLanguage } from '@renderer/types' -import { Button, Flex, Modal, Radio, Space, Switch, Tooltip } from 'antd' +import { Button, Flex, Modal, Radio, Space, Tooltip } from 'antd' import { HelpCircle } from 'lucide-react' import { FC, memo, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import TranslateSettingsPopup from '../settings/TranslateSettingsPopup/TranslateSettingsPopup' +// TODO: Just don't send so many props. Migrate them to redux. const TranslateSettings: FC<{ visible: boolean onClose: () => void @@ -40,7 +42,8 @@ const TranslateSettings: FC<{ }) => { const { t } = useTranslation() const [localPair, setLocalPair] = useState<[TranslateLanguage, TranslateLanguage]>(bidirectionalPair) - const { getLanguageByLangcode } = useTranslate() + const { getLanguageByLangcode, settings, updateSettings } = useTranslate() + const { autoCopy } = settings useEffect(() => { setLocalPair(bidirectionalPair) @@ -58,15 +61,15 @@ const TranslateSettings: FC<{ onCancel={onClose} centered={true} footer={null} - width={420} + width={520} transitionName="animation-move-down">
{t('translate.settings.preview')}
{ + isSelected={enableMarkdown} + onValueChange={(checked) => { setEnableMarkdown(checked) db.settings.put({ id: 'translate:markdown:enabled', value: checked }) }} @@ -74,14 +77,28 @@ const TranslateSettings: FC<{
+
+ +
{t('translate.settings.autoCopy')}
+ { + updateSettings({ autoCopy: isSelected }) + }} + /> +
+
+
{t('translate.settings.scroll_sync')}
{ - setIsScrollSyncEnabled(checked) - db.settings.put({ id: 'translate:scroll:sync', value: checked }) + isSelected={isScrollSyncEnabled} + color="primary" + onValueChange={(isSelected) => { + setIsScrollSyncEnabled(isSelected) + db.settings.put({ id: 'translate:scroll:sync', value: isSelected }) }} />
@@ -131,9 +148,10 @@ const TranslateSettings: FC<{
{ - setIsBidirectional(checked) + isSelected={isBidirectional} + color="primary" + onValueChange={(isSelected) => { + setIsBidirectional(isSelected) // 双向翻译设置不需要持久化,它只是界面状态 }} /> diff --git a/src/renderer/src/services/TranslateService.ts b/src/renderer/src/services/TranslateService.ts index c8bdad9406..0eb7fd24f7 100644 --- a/src/renderer/src/services/TranslateService.ts +++ b/src/renderer/src/services/TranslateService.ts @@ -10,7 +10,8 @@ import { import { Chunk, ChunkType } from '@renderer/types/chunk' import { uuid } from '@renderer/utils' import { readyToAbort } from '@renderer/utils/abortController' -import { formatErrorMessage, isAbortError } from '@renderer/utils/error' +import { isAbortError } from '@renderer/utils/error' +import { NoOutputGeneratedError } from 'ai' import { t } from 'i18next' import { fetchChatCompletion } from './ApiService' @@ -18,53 +19,6 @@ import { getDefaultTranslateAssistant } from './AssistantService' const logger = loggerService.withContext('TranslateService') -// async function fetchTranslate({ assistant, onResponse, abortKey }: FetchTranslateProps) { -// const model = assistant.model - -// const provider = getProviderByModel(model) - -// if (!hasApiKey(provider)) { -// throw new Error(t('error.no_api_key')) -// } - -// const isSupportedStreamOutput = () => { -// if (!onResponse) { -// return false -// } -// return true -// } - -// const stream = isSupportedStreamOutput() -// const enableReasoning = -// ((isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) && -// assistant.settings?.reasoning_effort !== undefined) || -// (isReasoningModel(model) && (!isSupportedThinkingTokenModel(model) || !isSupportedReasoningEffortModel(model))) - -// // abort control -// const controller = new AbortController() -// const signal = controller.signal - -// // 使用 transformParameters 模块构建参数 -// const { params, modelId, capabilities } = await buildStreamTextParams(undefined, assistant, provider, { -// requestOptions: { -// signal -// } -// }) - -// const options: ModernAiProviderConfig = { -// assistant, -// streamOutput: stream, -// enableReasoning, -// model: assistant.model, -// provider: provider -// } - -// const AI = new ModernAiProvider(model, provider) - -// const result = (await AI.completions(modelId, params, options)).getText().trim() -// return result -// } - /** * 翻译文本到目标语言 * @param text - 需要翻译的文本内容 @@ -81,58 +35,55 @@ export const translateText = async ( abortKey?: string ) => { let abortError - try { - const assistant = getDefaultTranslateAssistant(targetLanguage, text) + const assistant = getDefaultTranslateAssistant(targetLanguage, text) - const signal = abortKey ? readyToAbort(abortKey) : undefined + const signal = abortKey ? readyToAbort(abortKey) : undefined - let translatedText = '' - let completed = false - const onChunk = (chunk: Chunk) => { - if (chunk.type === ChunkType.TEXT_DELTA) { - translatedText = chunk.text - } else if (chunk.type === ChunkType.TEXT_COMPLETE) { + let translatedText = '' + let completed = false + const onChunk = (chunk: Chunk) => { + if (chunk.type === ChunkType.TEXT_DELTA) { + translatedText = chunk.text + } else if (chunk.type === ChunkType.TEXT_COMPLETE) { + completed = true + } else if (chunk.type === ChunkType.ERROR) { + if (isAbortError(chunk.error)) { + abortError = chunk.error completed = true - } else if (chunk.type === ChunkType.ERROR) { - if (isAbortError(chunk.error)) { - abortError = chunk.error - completed = true - } } - onResponse?.(translatedText, completed) } + onResponse?.(translatedText, completed) + } - const options = { - signal - } satisfies FetchChatCompletionOptions + const options = { + signal + } satisfies FetchChatCompletionOptions + try { await fetchChatCompletion({ prompt: assistant.content, assistant, options, onChunkReceived: onChunk }) - - const trimmedText = translatedText.trim() - - if (!trimmedText) { - return Promise.reject(new Error(t('translate.error.empty'))) - } - - return trimmedText } catch (e) { - if (isAbortError(e)) { - window.message.info(t('translate.info.aborted')) - throw e - } else if (isAbortError(abortError)) { - window.message.info(t('translate.info.aborted')) - throw abortError - } else { - logger.error('Failed to translate', e as Error) - window.message.error(t('translate.error.failed' + ': ' + formatErrorMessage(e))) + // dismiss no output generated error. it will be thrown when aborted. + if (!NoOutputGeneratedError.isInstance(e)) { throw e } } + + if (abortError) { + throw abortError + } + + const trimmedText = translatedText.trim() + + if (!trimmedText) { + return Promise.reject(new Error(t('translate.error.empty'))) + } + + return trimmedText } /** diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 56cfcd19f9..2710ca7a4a 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 150, + version: 152, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 6ff56ec83e..6485a7a2aa 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2428,6 +2428,17 @@ const migrateConfig = { logger.error('migrate 151 error', error as Error) return state } + }, + '152': (state: RootState) => { + try { + state.translate.settings = { + autoCopy: false + } + return state + } catch (error) { + logger.error('migrate 152 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/translate.ts b/src/renderer/src/store/translate.ts index 1eada6ac7c..999f7f6580 100644 --- a/src/renderer/src/store/translate.ts +++ b/src/renderer/src/store/translate.ts @@ -3,12 +3,19 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' export interface TranslateState { translateInput: string translatedContent: string + // TODO: #9749 + settings: { + autoCopy: boolean + } } const initialState: TranslateState = { translateInput: '', - translatedContent: '' -} + translatedContent: '', + settings: { + autoCopy: false + } +} as const const translateSlice = createSlice({ name: 'translate', @@ -19,10 +26,14 @@ const translateSlice = createSlice({ }, setTranslatedContent: (state, action: PayloadAction) => { state.translatedContent = action.payload + }, + updateSettings: (state, action: PayloadAction>) => { + const update = action.payload + Object.assign(state.settings, update) } } }) -export const { setTranslateInput, setTranslatedContent } = translateSlice.actions +export const { setTranslateInput, setTranslatedContent, updateSettings } = translateSlice.actions export default translateSlice.reducer