feat(translate): Auto copy when translation is done (#10032)

* feat(translate): add settings with autoCopy option to translate state

Add settings object to translate state to store user preferences like autoCopy functionality. Implement updateSettings reducer to handle settings updates.

* docs(translate): add todo comment for settings field

* refactor(translate): simplify settings update and expose in hook

Remove dependency on objectEntriesStrict and use Object.entries directly
Expose translate settings and update function in useTranslate hook

* fix(useTranslate): use dispatch to update

* feat(translate): add auto-copy setting for translated content

Add auto-copy functionality that automatically copies translated text to clipboard when enabled. Includes settings toggle in TranslateSettings component and integration with translate logic.

* chore: add tailwindcss file association to vscode settings

* feat(translate): add auto copy setting and improve switch styling

Add new translation strings for auto copy feature and apply consistent primary color to all switches in translation settings

* fix(theme): update hero UI primary color variable

Add --primary CSS variable to match --color-primary for hero UI components

* refactor(hooks): rename _updateSettings to handleUpdateSettings for clarity

Improve variable naming consistency and better reflect the function's purpose

* refactor(translate): simplify settings update using Object.assign

* fix(translate): handle clipboard write errors in copy functionality

Add error handling for clipboard operations to prevent silent failures and show user feedback when copy fails

* feat(i18n): add translation placeholders for new UI strings

Add new translation keys for front matter operations and auto-copy setting
Include additional Anthropic OAuth related messages for better user feedback

* fix(i18n): Auto update translations for PR #10032

* fix(i18n): correct translation errors in multiple language files

Fix incorrect translations and fill missing values in Japanese, Russian, Portuguese, French and Spanish localization files. Changes include correcting property name in Japanese, adding missing empty value in Russian, fixing editValue in Portuguese, correcting date and empty values in French, and fixing multiple terms in Spanish.

* fix: update error message in migration from 151 to 152

* fix(translate): await copy operation and show success message

Ensure the copy operation completes before proceeding and notify user of successful copy

* feat(translate): add delay timer for auto-copy functionality

Use setTimeoutTimer to introduce a 100ms delay before auto-copy to ensure UI stability

* fix(translate): increase modal width from 420 to 520 for better content display

* fix(ThemeProvider): ensure proper theme class is applied to body

Add logic to toggle 'light' and 'dark' classes on body element when theme changes

* fix(translate): only copy when success

* fix(translate): remove redundant error message display on translation failure

* fix(translate): handle abort and empty translation cases properly

Improve error handling for translation abort scenarios and empty responses. Show appropriate user messages when translation is aborted and properly handle NoOutputGeneratedError cases.

* fix(translate): handle translation errors by showing user-friendly message

Display a localized error message to users when translation fails instead of just logging it

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Phantom 2025-09-09 00:49:20 +08:00 committed by GitHub
parent f6ffd574bf
commit 11502edad2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 262 additions and 113 deletions

View File

@ -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"],

View File

@ -51,6 +51,13 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ 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

View File

@ -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<TranslateLanguage[]>(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<TranslateState['settings']>) => {
dispatch(updateSettings(update))
},
[dispatch]
)
return {
prompt,
settings,
translateLanguages,
getLanguageByLangcode
getLanguageByLangcode,
updateSettings: handleUpdateSettings
}
}

View File

@ -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())
}

View File

@ -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",

View File

@ -4301,6 +4301,7 @@
},
"processing": "翻译中...",
"settings": {
"autoCopy": "翻译完成后自动复制",
"bidirectional": "双向翻译设置",
"bidirectional_tip": "开启后,仅支持在源语言和目标语言之间进行双向翻译",
"model": "模型设置",

View File

@ -4301,6 +4301,7 @@
},
"processing": "翻譯中...",
"settings": {
"autoCopy": "翻譯完成後自動複製",
"bidirectional": "雙向翻譯設定",
"bidirectional_tip": "開啟後,僅支援在源語言和目標語言之間進行雙向翻譯",
"model": "模型設定",

View File

@ -2221,6 +2221,21 @@
}
},
"dragHandle": "στοιχεία σύρεσης",
"frontMatter": {
"addProperty": "Προσθήκη χαρακτηριστικού",
"addTag": "Προσθήκη ετικέτας",
"changeToBoolean": "Πλαίσιο ελέγχου",
"changeToDate": "Ημερομηνία",
"changeToNumber": "ψηφίο",
"changeToTags": "ετικέτα",
"changeToText": "κείμενο",
"changeType": "Αλλαγή τύπου",
"deleteProperty": "διαγραφή χαρακτηριστικού",
"editValue": "Επεξεργασία τιμής",
"empty": "<translate_input>\n空\n</translate_input>",
"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": "Ρύθμιση μοντέλου",

View File

@ -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",

View File

@ -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",

View File

@ -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": "モデル設定",

View File

@ -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",

View File

@ -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": "Настройки модели",

View File

@ -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 }

View File

@ -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">
<Flex vertical gap={16} style={{ marginTop: 16, paddingBottom: 20 }}>
<div>
<Flex align="center" justify="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.preview')}</div>
<Switch
checked={enableMarkdown}
onChange={(checked) => {
isSelected={enableMarkdown}
onValueChange={(checked) => {
setEnableMarkdown(checked)
db.settings.put({ id: 'translate:markdown:enabled', value: checked })
}}
@ -74,14 +77,28 @@ const TranslateSettings: FC<{
</Flex>
</div>
<div>
<HStack alignItems="center" justifyContent="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.autoCopy')}</div>
<Switch
isSelected={autoCopy}
color="primary"
onValueChange={(isSelected) => {
updateSettings({ autoCopy: isSelected })
}}
/>
</HStack>
</div>
<div>
<Flex align="center" justify="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.scroll_sync')}</div>
<Switch
checked={isScrollSyncEnabled}
onChange={(checked) => {
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 })
}}
/>
</Flex>
@ -131,9 +148,10 @@ const TranslateSettings: FC<{
</HStack>
</div>
<Switch
checked={isBidirectional}
onChange={(checked) => {
setIsBidirectional(checked)
isSelected={isBidirectional}
color="primary"
onValueChange={(isSelected) => {
setIsBidirectional(isSelected)
// 双向翻译设置不需要持久化,它只是界面状态
}}
/>

View File

@ -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
}
/**

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 150,
version: 152,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},

View File

@ -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
}
}
}

View File

@ -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<string>) => {
state.translatedContent = action.payload
},
updateSettings: (state, action: PayloadAction<Partial<TranslateState['settings']>>) => {
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