diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts index 3aebd51b60..fa35acea1a 100644 --- a/src/renderer/src/config/translate.ts +++ b/src/renderer/src/config/translate.ts @@ -1,18 +1,6 @@ import i18n from '@renderer/i18n' -type LanguageOption = { - value: string - label: string - emoji: string - style?: React.CSSProperties -} - -export const TranslateLanguageOptions: LanguageOption[] = [ - { - value: 'auto-detect', - label: i18n.t('languages.auto-detect'), - emoji: '🌐' - }, +export const TranslateLanguageOptions = [ { value: 'english', label: i18n.t('languages.english'), @@ -75,17 +63,12 @@ export const TranslateLanguageOptions: LanguageOption[] = [ } ] -export const translateLanguageOptions = (): LanguageOption[] => { +export const translateLanguageOptions = (): typeof TranslateLanguageOptions => { return TranslateLanguageOptions.map((option) => { return { value: option.value, label: i18n.t(`languages.${option.value}`), - emoji: option.emoji, - style: { - display: 'flex', - alignItems: 'center', - gap: '8px' - } + emoji: option.emoji } }) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index e4531cac85..d9d124d0c6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})." }, "languages": { - "auto-detect": "Auto Detect", "arabic": "Arabic", "chinese": "Chinese", "chinese-traditional": "Traditional Chinese", @@ -1764,6 +1763,17 @@ "processing": "Translation in progress...", "scroll_sync.disable": "Disable synced scroll", "scroll_sync.enable": "Enable synced scroll", + "bidirectional.disable": "Disable bidirectional translation", + "bidirectional.enable": "Enable bidirectional translation", + "language.same": "Source and target languages are the same", + "language.not_pair": "Source language is different from the set language", + "settings": { + "title": "Translation Settings", + "model": "Model Settings", + "model_desc": "Model used for translation service", + "bidirectional": "Bidirectional Translation Settings", + "scroll_sync": "Scroll Sync Settings" + }, "title": "Translation", "tooltip.newline": "Newline", "menu": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 203aba979d..6cb36f8af1 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。" }, "languages": { - "auto-detect": "自動検出", "arabic": "アラビア語", "chinese": "中国語", "chinese-traditional": "繁体字中国語", @@ -1764,6 +1763,17 @@ "processing": "翻訳中...", "scroll_sync.disable": "關閉滾動同步", "scroll_sync.enable": "開啟滾動同步", + "bidirectional.disable": "双方向翻訳を無効にする", + "bidirectional.enable": "双方向翻訳を有効にする", + "language.same": "ソース言語と目標言語が同じです", + "language.not_pair": "ソース言語が設定された言語と異なります", + "settings": { + "title": "翻訳設定", + "model": "モデル設定", + "model_desc": "翻訳サービスで使用されるモデル", + "bidirectional": "双方向翻訳設定", + "scroll_sync": "スクロール同期設定" + }, "title": "翻訳", "tooltip.newline": "改行", "menu": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 80ae7eead2..85d2061e54 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})" }, "languages": { - "auto-detect": "Автоопределение", "arabic": "Арабский", "chinese": "Китайский", "chinese-traditional": "Китайский традиционный", @@ -1765,6 +1764,17 @@ "processing": "Перевод в процессе...", "scroll_sync.disable": "Отключить синхронизацию прокрутки", "scroll_sync.enable": "Включить синхронизацию прокрутки", + "bidirectional.disable": "Отключить двунаправленный перевод", + "bidirectional.enable": "Включить двунаправленный перевод", + "language.same": "Исходный и целевой языки совпадают", + "language.not_pair": "Исходный язык отличается от настроенного", + "settings": { + "title": "Настройки перевода", + "model": "Настройки модели", + "model_desc": "Модель, используемая для службы перевода", + "bidirectional": "Настройки двунаправленного перевода", + "scroll_sync": "Настройки синхронизации прокрутки" + }, "title": "Перевод", "tooltip.newline": "Перевести", "menu": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e2077e39fa..a9d1286431 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -558,7 +558,6 @@ "urls": "网址" }, "languages": { - "auto-detect": "自动检测", "arabic": "阿拉伯文", "chinese": "简体中文", "chinese-traditional": "繁体中文", @@ -1767,6 +1766,17 @@ "processing": "翻译中...", "scroll_sync.disable": "关闭滚动同步", "scroll_sync.enable": "开启滚动同步", + "bidirectional.disable": "关闭双向翻译", + "bidirectional.enable": "开启双向翻译", + "language.same": "源语言和目标语言相同", + "language.not_pair": "源语言与设置的语言不同", + "settings": { + "title": "翻译设置", + "model": "模型设置", + "model_desc": "翻译服务使用的模型", + "bidirectional": "双向翻译设置", + "scroll_sync": "滚动同步设置" + }, "title": "翻译", "tooltip.newline": "换行" }, diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 90b4e260c3..70b34c7d19 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -558,7 +558,6 @@ "dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})" }, "languages": { - "auto-detect": "自動偵測", "arabic": "阿拉伯文", "chinese": "簡體中文", "chinese-traditional": "繁體中文", @@ -1765,6 +1764,17 @@ "processing": "翻譯中...", "scroll_sync.disable": "關閉滾動同步", "scroll_sync.enable": "開啟滾動同步", + "bidirectional.disable": "關閉雙向翻譯", + "bidirectional.enable": "開啟雙向翻譯", + "language.same": "源語言和目標語言相同", + "language.not_pair": "源語言與設定的語言不同", + "settings": { + "title": "翻譯設定", + "model": "模型設定", + "model_desc": "翻譯服務使用的模型", + "bidirectional": "雙向翻譯設定", + "scroll_sync": "滾動同步設定" + }, "title": "翻譯", "tooltip.newline": "換行", "menu": { diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 698734a76b..fd160aad5f 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -1,51 +1,253 @@ import { CheckOutlined, DeleteOutlined, HistoryOutlined, SendOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import CopyIcon from '@renderer/components/Icons/CopyIcon' -import { isLocalAi } from '@renderer/config/env' +import { HStack } from '@renderer/components/Layout' +import { isEmbeddingModel } from '@renderer/config/models' import { translateLanguageOptions } from '@renderer/config/translate' import db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' -import i18n from '@renderer/i18n' +import { useProviders } from '@renderer/hooks/useProvider' import { fetchTranslate } from '@renderer/services/ApiService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' -import type { Assistant, TranslateHistory } from '@renderer/types' +import { getModelUniqId, hasModel } from '@renderer/services/ModelService' +import type { Model, TranslateHistory } from '@renderer/types' import { runAsyncFunction, uuid } from '@renderer/utils' -import { Button, Dropdown, Empty, Flex, Popconfirm, Select, Space, Tooltip } from 'antd' +import { Button, Divider, Empty, Flex, Modal, Popconfirm, Select, Space, Switch, Tooltip } from 'antd' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import dayjs from 'dayjs' import { useLiveQuery } from 'dexie-react-hooks' -import { isEmpty } from 'lodash' -import { Mouse, Settings2, TriangleAlert } from 'lucide-react' -import { FC, useEffect, useRef, useState } from 'react' +import { find, isEmpty, sortBy } from 'lodash' +import { Settings2, TriangleAlert } from 'lucide-react' +import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' import styled from 'styled-components' let _text = '' let _result = '' let _targetLanguage = 'english' +const TranslateSettings: FC<{ + visible: boolean + onClose: () => void + isScrollSyncEnabled: boolean + setIsScrollSyncEnabled: (value: boolean) => void + isBidirectional: boolean + setIsBidirectional: (value: boolean) => void + bidirectionalPair: [string, string] + setBidirectionalPair: (value: [string, string]) => void + translateModel: Model | undefined + onModelChange: (model: Model) => void + allModels: Model[] + selectOptions: any[] +}> = ({ + visible, + onClose, + isScrollSyncEnabled, + setIsScrollSyncEnabled, + isBidirectional, + setIsBidirectional, + bidirectionalPair, + setBidirectionalPair, + translateModel, + onModelChange, + allModels, + selectOptions +}) => { + const { t } = useTranslation() + const [localPair, setLocalPair] = useState<[string, string]>(bidirectionalPair) + + const defaultTranslateModel = useMemo( + () => (hasModel(translateModel) ? getModelUniqId(translateModel) : undefined), + [translateModel] + ) + + useEffect(() => { + setLocalPair(bidirectionalPair) + }, [bidirectionalPair, visible]) + + const handleRemoveModel = () => { + db.settings.put({ id: 'translate:model', value: null }) + } + + const handleSave = () => { + if (localPair[0] === localPair[1]) { + window.message.warning({ + content: t('translate.language.same'), + key: 'translate-message' + }) + return + } + setBidirectionalPair(localPair) + db.settings.put({ id: 'translate:bidirectional:pair', value: localPair }) + db.settings.put({ id: 'translate:scroll:sync', value: isScrollSyncEnabled }) + window.message.success({ + content: t('message.save.success.title'), + key: 'translate-settings-save' + }) + onClose() + } + + return ( + {t('translate.settings.title')}} + open={visible} + onCancel={onClose} + footer={[ + , + + ]} + width={420}> + +
+
{t('translate.settings.model')}
+ + setLocalPair([value, localPair[1]])} + options={translateLanguageOptions().map((lang) => ({ + value: lang.value, + label: ( + + + {lang.emoji} + + {lang.label} + + ) + }))} + /> + + { + setTargetLanguage(value) + db.settings.put({ id: 'translate:target:language', value }) + }} + options={translateLanguageOptions().map((lang) => ({ + value: lang.value, + label: ( + + + {lang.emoji} + + {lang.label} + + ) + }))} + /> + ) } return ( @@ -239,94 +548,70 @@ const TranslatePage: FC = () => { {t('translate.history.title')} - {!isEmpty(translateHistory) && ( - - - - )} + + + - {translateHistory && translateHistory.length ? ( + {translateHistory && translateHistory.length > 0 ? ( - {translateHistory.map((item) => ( - , - danger: true, - onClick: () => deleteHistory(item.id) - } - ] - }}> - onHistoryItemClick(item)}> - - {item.sourceText} - {item.targetText} - {dayjs(item.createdAt).format('MM/DD HH:mm')} - - - + {translateHistory.map((history) => ( + onHistoryItemClick(history)}> + {history.sourceText} + + {dayjs(history.createdAt).format('MM-DD HH:mm')} +