mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
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:
parent
f6ffd574bf
commit
11502edad2
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -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"],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -4301,6 +4301,7 @@
|
||||
},
|
||||
"processing": "翻译中...",
|
||||
"settings": {
|
||||
"autoCopy": "翻译完成后自动复制",
|
||||
"bidirectional": "双向翻译设置",
|
||||
"bidirectional_tip": "开启后,仅支持在源语言和目标语言之间进行双向翻译",
|
||||
"model": "模型设置",
|
||||
|
||||
@ -4301,6 +4301,7 @@
|
||||
},
|
||||
"processing": "翻譯中...",
|
||||
"settings": {
|
||||
"autoCopy": "翻譯完成後自動複製",
|
||||
"bidirectional": "雙向翻譯設定",
|
||||
"bidirectional_tip": "開啟後,僅支援在源語言和目標語言之間進行雙向翻譯",
|
||||
"model": "模型設定",
|
||||
|
||||
@ -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": "Ρύθμιση μοντέλου",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "モデル設定",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Настройки модели",
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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)
|
||||
// 双向翻译设置不需要持久化,它只是界面状态
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 150,
|
||||
version: 152,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user