diff --git a/docs/technical/db.settings.md b/docs/technical/db.settings.md new file mode 100644 index 0000000000..1d63098851 --- /dev/null +++ b/docs/technical/db.settings.md @@ -0,0 +1,11 @@ +# 数据库设置字段 + +此文档包含部分字段的数据类型说明。 + +## 字段 + +| 字段名 | 类型 | 说明 | +| ------------------------------ | ------------------------------ | ------------ | +| `translate:target:language` | `LanguageCode` | 翻译目标语言 | +| `translate:source:language` | `LanguageCode` | 翻译源语言 | +| `translate:bidirectional:pair` | `[LanguageCode, LanguageCode]` | 双向翻译对 | diff --git a/src/renderer/src/components/Popups/TextEditPopup.tsx b/src/renderer/src/components/Popups/TextEditPopup.tsx index 46bca109fc..ab7bf40cb6 100644 --- a/src/renderer/src/components/Popups/TextEditPopup.tsx +++ b/src/renderer/src/components/Popups/TextEditPopup.tsx @@ -3,6 +3,7 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import { fetchTranslate } from '@renderer/services/ApiService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' +import { getLanguageByLangcode } from '@renderer/utils/translate' import { Modal, ModalProps } from 'antd' import TextArea from 'antd/es/input/TextArea' import { TextAreaProps } from 'antd/lib/input' @@ -111,7 +112,7 @@ const PopupContainer: React.FC = ({ } try { - const assistant = getDefaultTranslateAssistant(targetLanguage, textValue) + const assistant = getDefaultTranslateAssistant(getLanguageByLangcode(targetLanguage), textValue) const translatedText = await fetchTranslate({ content: textValue, assistant }) if (isMounted.current) { setTextValue(translatedText) diff --git a/src/renderer/src/components/TranslateButton.tsx b/src/renderer/src/components/TranslateButton.tsx index 78dd1b7d34..d52448b488 100644 --- a/src/renderer/src/components/TranslateButton.tsx +++ b/src/renderer/src/components/TranslateButton.tsx @@ -3,6 +3,7 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import { fetchTranslate } from '@renderer/services/ApiService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' +import { getLanguageByLangcode } from '@renderer/utils/translate' import { Button, Tooltip } from 'antd' import { Languages } from 'lucide-react' import { FC, useEffect, useState } from 'react' @@ -54,7 +55,7 @@ const TranslateButton: FC = ({ text, onTranslated, disabled, style, isLoa setIsTranslating(true) try { - const assistant = getDefaultTranslateAssistant(targetLanguage, text) + const assistant = getDefaultTranslateAssistant(getLanguageByLangcode(targetLanguage), text) const translatedText = await fetchTranslate({ content: text, assistant }) onTranslated(translatedText) } catch (error) { @@ -75,7 +76,7 @@ const TranslateButton: FC = ({ text, onTranslated, disabled, style, isLoa return ( {isTranslating ? : } diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts index 9a85b68ecc..cbac95aafa 100644 --- a/src/renderer/src/config/translate.ts +++ b/src/renderer/src/config/translate.ts @@ -1,136 +1,159 @@ import i18n from '@renderer/i18n' +import { Language } from '@renderer/types' -export interface TranslateLanguageOption { - value: string - langCode?: string - label: string - emoji: string +export const ENGLISH: Language = { + value: 'English', + langCode: 'en-us', + label: () => i18n.t('languages.english'), + emoji: '🇬🇧' } -export const TranslateLanguageOptions: TranslateLanguageOption[] = [ - { - value: 'English', - langCode: 'en-us', - label: i18n.t('languages.english'), - emoji: '🇬🇧' - }, - { - value: 'Chinese (Simplified)', - langCode: 'zh-cn', - label: i18n.t('languages.chinese'), - emoji: '🇨🇳' - }, - { - value: 'Chinese (Traditional)', - langCode: 'zh-tw', - label: i18n.t('languages.chinese-traditional'), - emoji: '🇭🇰' - }, - { - value: 'Japanese', - langCode: 'ja-jp', - label: i18n.t('languages.japanese'), - emoji: '🇯🇵' - }, - { - value: 'Korean', - langCode: 'ko-kr', - label: i18n.t('languages.korean'), - emoji: '🇰🇷' - }, - - { - value: 'French', - langCode: 'fr-fr', - label: i18n.t('languages.french'), - emoji: '🇫🇷' - }, - { - value: 'German', - langCode: 'de-de', - label: i18n.t('languages.german'), - emoji: '🇩🇪' - }, - { - value: 'Italian', - langCode: 'it-it', - label: i18n.t('languages.italian'), - emoji: '🇮🇹' - }, - { - value: 'Spanish', - langCode: 'es-es', - label: i18n.t('languages.spanish'), - emoji: '🇪🇸' - }, - { - value: 'Portuguese', - langCode: 'pt-pt', - label: i18n.t('languages.portuguese'), - emoji: '🇵🇹' - }, - { - value: 'Russian', - langCode: 'ru-ru', - label: i18n.t('languages.russian'), - emoji: '🇷🇺' - }, - { - value: 'Polish', - langCode: 'pl-pl', - label: i18n.t('languages.polish'), - emoji: '🇵🇱' - }, - { - value: 'Arabic', - langCode: 'ar-ar', - label: i18n.t('languages.arabic'), - emoji: '🇸🇦' - }, - { - value: 'Turkish', - langCode: 'tr-tr', - label: i18n.t('languages.turkish'), - emoji: '🇹🇷' - }, - { - value: 'Thai', - langCode: 'th-th', - label: i18n.t('languages.thai'), - emoji: '🇹🇭' - }, - { - value: 'Vietnamese', - langCode: 'vi-vn', - label: i18n.t('languages.vietnamese'), - emoji: '🇻🇳' - }, - { - value: 'Indonesian', - langCode: 'id-id', - label: i18n.t('languages.indonesian'), - emoji: '🇮🇩' - }, - { - value: 'Urdu', - langCode: 'ur-pk', - label: i18n.t('languages.urdu'), - emoji: '🇵🇰' - }, - { - value: 'Malay', - langCode: 'ms-my', - label: i18n.t('languages.malay'), - emoji: '🇲🇾' - } -] - -export const translateLanguageOptions = (): typeof TranslateLanguageOptions => { - return TranslateLanguageOptions.map((option) => { - return { - value: option.value, - label: option.label, - emoji: option.emoji - } - }) +export const CHINESE_SIMPLIFIED: Language = { + value: 'Chinese (Simplified)', + langCode: 'zh-cn', + label: () => i18n.t('languages.chinese'), + emoji: '🇨🇳' } + +export const CHINESE_TRADITIONAL: Language = { + value: 'Chinese (Traditional)', + langCode: 'zh-tw', + label: () => i18n.t('languages.chinese-traditional'), + emoji: '🇭🇰' +} + +export const JAPANESE: Language = { + value: 'Japanese', + langCode: 'ja-jp', + label: () => i18n.t('languages.japanese'), + emoji: '🇯🇵' +} + +export const KOREAN: Language = { + value: 'Korean', + langCode: 'ko-kr', + label: () => i18n.t('languages.korean'), + emoji: '🇰🇷' +} + +export const FRENCH: Language = { + value: 'French', + langCode: 'fr-fr', + label: () => i18n.t('languages.french'), + emoji: '🇫🇷' +} + +export const GERMAN: Language = { + value: 'German', + langCode: 'de-de', + label: () => i18n.t('languages.german'), + emoji: '🇩🇪' +} + +export const ITALIAN: Language = { + value: 'Italian', + langCode: 'it-it', + label: () => i18n.t('languages.italian'), + emoji: '🇮🇹' +} + +export const SPANISH: Language = { + value: 'Spanish', + langCode: 'es-es', + label: () => i18n.t('languages.spanish'), + emoji: '🇪🇸' +} + +export const PORTUGUESE: Language = { + value: 'Portuguese', + langCode: 'pt-pt', + label: () => i18n.t('languages.portuguese'), + emoji: '🇵🇹' +} + +export const RUSSIAN: Language = { + value: 'Russian', + langCode: 'ru-ru', + label: () => i18n.t('languages.russian'), + emoji: '🇷🇺' +} + +export const POLISH: Language = { + value: 'Polish', + langCode: 'pl-pl', + label: () => i18n.t('languages.polish'), + emoji: '🇵🇱' +} + +export const ARABIC: Language = { + value: 'Arabic', + langCode: 'ar-ar', + label: () => i18n.t('languages.arabic'), + emoji: '🇸🇦' +} + +export const TURKISH: Language = { + value: 'Turkish', + langCode: 'tr-tr', + label: () => i18n.t('languages.turkish'), + emoji: '🇹🇷' +} + +export const THAI: Language = { + value: 'Thai', + langCode: 'th-th', + label: () => i18n.t('languages.thai'), + emoji: '🇹🇭' +} + +export const VIETNAMESE: Language = { + value: 'Vietnamese', + langCode: 'vi-vn', + label: () => i18n.t('languages.vietnamese'), + emoji: '🇻🇳' +} + +export const INDONESIAN: Language = { + value: 'Indonesian', + langCode: 'id-id', + label: () => i18n.t('languages.indonesian'), + emoji: '🇮🇩' +} + +export const URDU: Language = { + value: 'Urdu', + langCode: 'ur-pk', + label: () => i18n.t('languages.urdu'), + emoji: '🇵🇰' +} + +export const MALAY: Language = { + value: 'Malay', + langCode: 'ms-my', + label: () => i18n.t('languages.malay'), + emoji: '🇲🇾' +} + +export const LanguagesEnum = { + enUS: ENGLISH, + zhCN: CHINESE_SIMPLIFIED, + zhTW: CHINESE_TRADITIONAL, + jaJP: JAPANESE, + koKR: KOREAN, + frFR: FRENCH, + deDE: GERMAN, + itIT: ITALIAN, + esES: SPANISH, + ptPT: PORTUGUESE, + ruRU: RUSSIAN, + plPL: POLISH, + arAR: ARABIC, + trTR: TURKISH, + thTH: THAI, + viVN: VIETNAMESE, + idID: INDONESIAN, + urPK: URDU, + msMY: MALAY +} as const + +export const translateLanguageOptions: Language[] = Object.values(LanguagesEnum) diff --git a/src/renderer/src/databases/index.ts b/src/renderer/src/databases/index.ts index aa765db05b..6c23a115a5 100644 --- a/src/renderer/src/databases/index.ts +++ b/src/renderer/src/databases/index.ts @@ -3,7 +3,7 @@ import { FileMetadata, KnowledgeItem, QuickPhrase, TranslateHistory } from '@ren import type { Message as NewMessage, MessageBlock } from '@renderer/types/newMessage' import { Dexie, type EntityTable } from 'dexie' -import { upgradeToV5, upgradeToV7 } from './upgrades' +import { upgradeToV5, upgradeToV7, upgradeToV8 } from './upgrades' // Database declaration (move this to its own module also) export const db = new Dexie('CherryStudio') as Dexie & { @@ -74,4 +74,17 @@ db.version(7) }) .upgrade((tx) => upgradeToV7(tx)) +db.version(8) + .stores({ + // Re-declare all tables for the new version + files: 'id, name, origin_name, path, size, ext, type, created_at, count', + topics: '&id', // Correct index for topics + settings: '&id, value', + knowledge_notes: '&id, baseId, type, content, created_at, updated_at', + translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt', + quick_phrases: 'id', + message_blocks: 'id, messageId, file.id' // Correct syntax with comma separator + }) + .upgrade((tx) => upgradeToV8(tx)) + export default db diff --git a/src/renderer/src/databases/upgrades.ts b/src/renderer/src/databases/upgrades.ts index cb1e770db0..5543dde4ab 100644 --- a/src/renderer/src/databases/upgrades.ts +++ b/src/renderer/src/databases/upgrades.ts @@ -1,7 +1,7 @@ import Logger from '@renderer/config/logger' -import type { LegacyMessage as OldMessage, Topic } from '@renderer/types' -import { FileTypes } from '@renderer/types' // Import FileTypes enum -import { WebSearchSource } from '@renderer/types' +import { LanguagesEnum } from '@renderer/config/translate' +import type { LanguageCode, LegacyMessage as OldMessage, Topic } from '@renderer/types' +import { FileTypes, WebSearchSource } from '@renderer/types' // Import FileTypes enum import type { BaseMessageBlock, CitationMessageBlock, @@ -308,3 +308,78 @@ export async function upgradeToV7(tx: Transaction): Promise { Logger.log('DB migration to version 7 finished successfully.') } + +export async function upgradeToV8(tx: Transaction): Promise { + Logger.log('DB migration to version 8 started') + + const langMap: Record = { + english: 'en-us', + chinese: 'zh-cn', + 'chinese-traditional': 'zh-tw', + japanese: 'ja-jp', + korean: 'ko-kr', + french: 'fr-fr', + german: 'de-de', + italian: 'it-it', + spanish: 'es-es', + portuguese: 'pt-pt', + russian: 'ru-ru', + polish: 'pl-pl', + arabic: 'ar-ar', + turkish: 'tr-tr', + thai: 'th-th', + vietnamese: 'vi-vn', + indonesian: 'id-id', + urdu: 'ur-pk', + malay: 'ms-my' + } + + const settingsTable = tx.table('settings') + const defaultPair: [LanguageCode, LanguageCode] = [LanguagesEnum.enUS.langCode, LanguagesEnum.zhCN.langCode] + const originSource = (await settingsTable.get('translate:source:language'))?.value + const originTarget = (await settingsTable.get('translate:target:language'))?.value + const originPair = (await settingsTable.get('translate:bidirectional:pair'))?.value + let newSource, newTarget, newPair + Logger.log('originSource: %o', originSource) + if (originSource === 'auto') { + newSource = 'auto' + } else { + newSource = langMap[originSource] + if (!newSource) { + newSource = LanguagesEnum.enUS.langCode + } + } + + Logger.log('originTarget: %o', originTarget) + newTarget = langMap[originTarget] + if (!newTarget) { + newTarget = LanguagesEnum.zhCN.langCode + } + + Logger.log('originPair: %o', originPair) + newPair = [langMap[originPair[0]], langMap[originPair[1]]] + if (!newPair[0] || !newPair[1]) { + newPair = defaultPair + } + + Logger.log('DB migration to version 8: %o', { newSource, newTarget, newPair }) + + await settingsTable.put({ id: 'translate:bidirectional:pair', value: newPair }) + await settingsTable.put({ id: 'translate:source:language', value: newSource }) + await settingsTable.put({ id: 'translate:target:language', value: newTarget }) + + const histories = tx.table('translate_history') + + for (const history of await histories.toArray()) { + try { + await tx.table('translate_history').put({ + ...history, + sourceLanguage: langMap[history.sourceLanguage], + targetLanguage: langMap[history.targetLanguage] + }) + } catch (error) { + console.error('Error upgrading history:', error) + } + } + Logger.log('DB migration to version 8 finished.') +} diff --git a/src/renderer/src/hooks/useMessageOperations.ts b/src/renderer/src/hooks/useMessageOperations.ts index 559b4ad879..d8ac8aac60 100644 --- a/src/renderer/src/hooks/useMessageOperations.ts +++ b/src/renderer/src/hooks/useMessageOperations.ts @@ -19,7 +19,7 @@ import { updateMessageAndBlocksThunk, updateTranslationBlockThunk } from '@renderer/store/thunk/messageThunk' -import type { Assistant, Model, Topic } from '@renderer/types' +import type { Assistant, LanguageCode, Model, Topic } from '@renderer/types' import type { Message, MessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { abortCompletion } from '@renderer/utils/abortController' @@ -195,9 +195,9 @@ export function useMessageOperations(topic: Topic) { const getTranslationUpdater = useCallback( async ( messageId: string, - targetLanguage: string, + targetLanguage: LanguageCode, sourceBlockId?: string, - sourceLanguage?: string + sourceLanguage?: LanguageCode ): Promise<((accumulatedText: string, isComplete?: boolean) => void) | null> => { if (!topic.id) return null diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index fe550510a0..98095a81e1 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -37,6 +37,7 @@ import type { MessageInputBaseParams } from '@renderer/types/newMessage' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { formatQuotedText } from '@renderer/utils/formats' import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input' +import { getLanguageByLangcode } from '@renderer/utils/translate' import { documentExts, imageExts, textExts } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { Button, Tooltip } from 'antd' @@ -253,7 +254,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = try { setIsTranslating(true) - const translatedText = await translateText(text, targetLanguage) + const translatedText = await translateText(text, getLanguageByLangcode(targetLanguage)) translatedText && setText(translatedText) setTimeout(() => resizeTextArea(), 0) } catch (error) { diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index dcd86288e1..a8d55bbe80 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -2,7 +2,7 @@ import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } fro import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import { isVisionModel } from '@renderer/config/models' -import { TranslateLanguageOptions } from '@renderer/config/translate' +import { translateLanguageOptions } from '@renderer/config/translate' import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useChatContext } from '@renderer/hooks/useChatContext' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' @@ -13,7 +13,7 @@ import { translateText } from '@renderer/services/TranslateService' import store, { RootState } from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' import { selectMessagesForTopic } from '@renderer/store/newMessage' -import type { Assistant, Model, Topic } from '@renderer/types' +import type { Assistant, Language, Model, Topic } from '@renderer/types' import { type Message, MessageBlockType } from '@renderer/types/newMessage' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, classNames } from '@renderer/utils' import { copyMessageAsPlainText } from '@renderer/utils/copy' @@ -153,12 +153,12 @@ const MessageMenubar: FC = (props) => { }, [message.id, startEditing]) const handleTranslate = useCallback( - async (language: string) => { + async (language: Language) => { if (isTranslating) return setIsTranslating(true) const messageId = message.id - const translationUpdater = await getTranslationUpdater(messageId, language) + const translationUpdater = await getTranslationUpdater(messageId, language.langCode) if (!translationUpdater) return try { await translateText(mainTextContent, language, translationUpdater) @@ -457,10 +457,10 @@ const MessageMenubar: FC = (props) => { backgroundClip: 'border-box' }, items: [ - ...TranslateLanguageOptions.map((item) => ({ - label: item.emoji + ' ' + item.label, - key: item.value, - onClick: () => handleTranslate(item.value) + ...translateLanguageOptions.map((item) => ({ + label: item.emoji + ' ' + item.label(), + key: item.langCode, + onClick: () => handleTranslate(item) })), ...(hasTranslationBlocks ? [ diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 67c3ba7b97..cc56f72c05 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -8,6 +8,7 @@ import { isSupportedFlexServiceTier, isSupportedReasoningEffortOpenAIModel } from '@renderer/config/models' +import { translateLanguageOptions } from '@renderer/config/translate' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useTheme } from '@renderer/context/ThemeProvider' import { useAssistant } from '@renderer/hooks/useAssistant' @@ -44,14 +45,7 @@ import { setShowTranslateConfirm, setThoughtAutoCollapse } from '@renderer/store/settings' -import { - Assistant, - AssistantSettings, - CodeStyleVarious, - MathEngine, - ThemeMode, - TranslateLanguageVarious -} from '@renderer/types' +import { Assistant, AssistantSettings, CodeStyleVarious, MathEngine, ThemeMode } from '@renderer/types' import { modalConfirm } from '@renderer/utils' import { getSendMessageShortcutLabel } from '@renderer/utils/input' import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd' @@ -625,14 +619,10 @@ const SettingsTab: FC = (props) => { {t('settings.input.target_language')} setTargetLanguage(value as TranslateLanguageVarious)} - options={[ - { value: 'chinese', label: t('settings.input.target_language.chinese') }, - { value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') }, - { value: 'english', label: t('settings.input.target_language.english') }, - { value: 'japanese', label: t('settings.input.target_language.japanese') }, - { value: 'russian', label: t('settings.input.target_language.russian') } - ]} + onChange={(value) => setTargetLanguage(value)} + options={translateLanguageOptions.map((item) => { + return { value: item.langCode, label: item.emoji + ' ' + item.label() } + })} /> diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index 60d152a353..af57c21f47 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -7,6 +7,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import TranslateButton from '@renderer/components/TranslateButton' import { isMac } from '@renderer/config/constant' import { getProviderLogo } from '@renderer/config/providers' +import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' @@ -543,7 +544,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { try { setIsTranslating(true) - const translatedText = await translateText(painting.prompt, 'english') + const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS) updatePaintingState({ prompt: translatedText }) } catch (error) { console.error('Translation failed:', error) diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index 51edb4244a..f595ef76de 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -12,6 +12,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import TranslateButton from '@renderer/components/TranslateButton' import { isMac } from '@renderer/config/constant' import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models' +import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' @@ -302,7 +303,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { try { setIsTranslating(true) - const translatedText = await translateText(painting.prompt, 'english') + const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS) updatePaintingState({ prompt: translatedText }) } catch (error) { console.error('Translation failed:', error) diff --git a/src/renderer/src/pages/paintings/TokenFluxPage.tsx b/src/renderer/src/pages/paintings/TokenFluxPage.tsx index 85a39df438..40e1c2f2c0 100644 --- a/src/renderer/src/pages/paintings/TokenFluxPage.tsx +++ b/src/renderer/src/pages/paintings/TokenFluxPage.tsx @@ -4,6 +4,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import TranslateButton from '@renderer/components/TranslateButton' import { isMac } from '@renderer/config/constant' import { getProviderLogo } from '@renderer/config/providers' +import { LanguagesEnum } from '@renderer/config/translate' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' import { useRuntime } from '@renderer/hooks/useRuntime' @@ -255,7 +256,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => { try { setIsTranslating(true) - const translatedText = await translateText(painting.prompt, 'english') + const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS) updatePaintingState({ prompt: translatedText }) } catch (error) { console.error('Translation failed:', error) diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 8d4e7116b6..ae1ee408a3 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -4,7 +4,7 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon' import { HStack } from '@renderer/components/Layout' import { isEmbeddingModel } from '@renderer/config/models' import { TRANSLATE_PROMPT } from '@renderer/config/prompts' -import { translateLanguageOptions } from '@renderer/config/translate' +import { LanguagesEnum, translateLanguageOptions } from '@renderer/config/translate' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' @@ -15,13 +15,14 @@ import { getDefaultTranslateAssistant } from '@renderer/services/AssistantServic import { getModelUniqId, hasModel } from '@renderer/services/ModelService' import { useAppDispatch } from '@renderer/store' import { setTranslateModelPrompt } from '@renderer/store/settings' -import type { Model, TranslateHistory } from '@renderer/types' +import type { Language, LanguageCode, Model, TranslateHistory } from '@renderer/types' import { runAsyncFunction, uuid } from '@renderer/utils' import { createInputScrollHandler, createOutputScrollHandler, detectLanguage, - determineTargetLanguage + determineTargetLanguage, + getLanguageByLangcode } from '@renderer/utils/translate' import { Button, Dropdown, Empty, Flex, Modal, Popconfirm, Select, Space, Switch, Tooltip } from 'antd' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' @@ -35,7 +36,7 @@ import styled from 'styled-components' let _text = '' let _result = '' -let _targetLanguage = 'english' +let _targetLanguage = LanguagesEnum.enUS const TranslateSettings: FC<{ visible: boolean @@ -46,8 +47,8 @@ const TranslateSettings: FC<{ setIsBidirectional: (value: boolean) => void enableMarkdown: boolean setEnableMarkdown: (value: boolean) => void - bidirectionalPair: [string, string] - setBidirectionalPair: (value: [string, string]) => void + bidirectionalPair: [Language, Language] + setBidirectionalPair: (value: [Language, Language]) => void translateModel: Model | undefined onModelChange: (model: Model) => void allModels: Model[] @@ -71,7 +72,7 @@ const TranslateSettings: FC<{ const { t } = useTranslation() const { translateModelPrompt } = useSettings() const dispatch = useAppDispatch() - const [localPair, setLocalPair] = useState<[string, string]>(bidirectionalPair) + const [localPair, setLocalPair] = useState<[Language, Language]>(bidirectionalPair) const [showPrompt, setShowPrompt] = useState(false) const [localPrompt, setLocalPrompt] = useState(translateModelPrompt) @@ -94,7 +95,7 @@ const TranslateSettings: FC<{ return } setBidirectionalPair(localPair) - db.settings.put({ id: 'translate:bidirectional:pair', value: localPair }) + db.settings.put({ id: 'translate:bidirectional:pair', value: [localPair[0].langCode, localPair[1].langCode] }) db.settings.put({ id: 'translate:scroll:sync', value: isScrollSyncEnabled }) db.settings.put({ id: 'translate:markdown:enabled', value: enableMarkdown }) db.settings.put({ id: 'translate:model:prompt', value: localPrompt }) @@ -189,16 +190,16 @@ const TranslateSettings: FC<{ setLocalPair([localPair[0], value])} - options={translateLanguageOptions().map((lang) => ({ - value: lang.value, + value={localPair[1].langCode} + onChange={(value) => setLocalPair([localPair[0], getLanguageByLangcode(value)])} + options={translateLanguageOptions.map((lang) => ({ + value: lang.langCode, label: ( {lang.emoji} -
{lang.label}
+
{lang.label()}
) }))} @@ -275,7 +276,6 @@ const TranslateSettings: FC<{ const TranslatePage: FC = () => { const { t } = useTranslation() const { shikiMarkdownIt } = useCodeStyle() - const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) const [text, setText] = useState(_text) const [result, setResult] = useState(_result) const [renderedMarkdown, setRenderedMarkdown] = useState('') @@ -286,10 +286,14 @@ const TranslatePage: FC = () => { const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false) const [isBidirectional, setIsBidirectional] = useState(false) const [enableMarkdown, setEnableMarkdown] = useState(false) - const [bidirectionalPair, setBidirectionalPair] = useState<[string, string]>(['english', 'chinese']) + const [bidirectionalPair, setBidirectionalPair] = useState<[Language, Language]>([ + LanguagesEnum.enUS, + LanguagesEnum.zhCN + ]) const [settingsVisible, setSettingsVisible] = useState(false) - const [detectedLanguage, setDetectedLanguage] = useState(null) - const [sourceLanguage, setSourceLanguage] = useState('auto') + const [detectedLanguage, setDetectedLanguage] = useState(null) + const [sourceLanguage, setSourceLanguage] = useState('auto') + const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) const contentContainerRef = useRef(null) const textAreaRef = useRef(null) const outputTextRef = useRef(null) @@ -329,8 +333,8 @@ const TranslatePage: FC = () => { const saveTranslateHistory = async ( sourceText: string, targetText: string, - sourceLanguage: string, - targetLanguage: string + sourceLanguage: LanguageCode, + targetLanguage: LanguageCode ) => { const history: TranslateHistory = { id: uuid(), @@ -364,7 +368,7 @@ const TranslatePage: FC = () => { setLoading(true) try { // 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测 - let actualSourceLanguage: string + let actualSourceLanguage: Language if (sourceLanguage === 'auto') { actualSourceLanguage = await detectLanguage(text) setDetectedLanguage(actualSourceLanguage) @@ -389,7 +393,7 @@ const TranslatePage: FC = () => { return } - const actualTargetLanguage = result.language as string + const actualTargetLanguage = result.language as Language if (isBidirectional) { setTargetLanguage(actualTargetLanguage) } @@ -405,7 +409,7 @@ const TranslatePage: FC = () => { } }) - await saveTranslateHistory(text, translatedText, actualSourceLanguage, actualTargetLanguage) + await saveTranslateHistory(text, translatedText, actualSourceLanguage.langCode, actualTargetLanguage.langCode) setLoading(false) } catch (error) { console.error('Translation error:', error) @@ -432,7 +436,7 @@ const TranslatePage: FC = () => { const onHistoryItemClick = (history: TranslateHistory) => { setText(history.sourceText) setResult(history.targetText) - setTargetLanguage(history.targetLanguage) + setTargetLanguage(getLanguageByLangcode(history.targetLanguage)) } useEffect(() => { @@ -460,20 +464,32 @@ const TranslatePage: FC = () => { useEffect(() => { runAsyncFunction(async () => { const targetLang = await db.settings.get({ id: 'translate:target:language' }) - targetLang && setTargetLanguage(targetLang.value) + targetLang && setTargetLanguage(getLanguageByLangcode(targetLang.value)) const sourceLang = await db.settings.get({ id: 'translate:source:language' }) - sourceLang && setSourceLanguage(sourceLang.value) + sourceLang && + setSourceLanguage(sourceLang.value === 'auto' ? sourceLang.value : getLanguageByLangcode(sourceLang.value)) const bidirectionalPairSetting = await db.settings.get({ id: 'translate:bidirectional:pair' }) if (bidirectionalPairSetting) { const langPair = bidirectionalPairSetting.value + let source: undefined | Language + let target: undefined | Language + if (Array.isArray(langPair) && langPair.length === 2 && langPair[0] !== langPair[1]) { - setBidirectionalPair(langPair as [string, string]) + source = getLanguageByLangcode(langPair[0]) + target = getLanguageByLangcode(langPair[1]) + } + + if (source && target) { + setBidirectionalPair([source, target]) } else { - const defaultPair: [string, string] = ['english', 'chinese'] + const defaultPair: [Language, Language] = [LanguagesEnum.enUS, LanguagesEnum.zhCN] setBidirectionalPair(defaultPair) - db.settings.put({ id: 'translate:bidirectional:pair', value: defaultPair }) + db.settings.put({ + id: 'translate:bidirectional:pair', + value: [defaultPair[0].langCode, defaultPair[1].langCode] + }) } } @@ -489,7 +505,7 @@ const TranslatePage: FC = () => { }, []) const onKeyDown = (e: React.KeyboardEvent) => { - const isEnterPressed = e.keyCode == 13 + const isEnterPressed = e.key === 'Enter' if (isEnterPressed && !e.shiftKey && !e.ctrlKey && !e.metaKey) { e.preventDefault() onTranslate() @@ -501,32 +517,37 @@ const TranslatePage: FC = () => { // 获取当前语言状态显示 const getLanguageDisplay = () => { - if (isBidirectional) { - return ( - - - {`${t(`languages.${bidirectionalPair[0]}`)} ⇆ ${t(`languages.${bidirectionalPair[1]}`)}`} - - - ) + try { + if (isBidirectional) { + return ( + + + {`${bidirectionalPair[0].label()} ⇆ ${bidirectionalPair[1].label()}`} + + + ) + } + } catch (error) { + console.error('Error getting language display:', error) + setBidirectionalPair([LanguagesEnum.enUS, LanguagesEnum.zhCN]) } return ( { - setSourceLanguage(value) + onChange={(value: LanguageCode | 'auto') => { + if (value !== 'auto') setSourceLanguage(getLanguageByLangcode(value)) + else setSourceLanguage('auto') db.settings.put({ id: 'translate:source:language', value }) }} options={[ { value: 'auto', label: detectedLanguage - ? `${t('translate.detected.language')} (${t(`languages.${detectedLanguage.toLowerCase()}`)})` + ? `${t('translate.detected.language')} (${detectedLanguage.label()})` : t('translate.detected.language') }, - ...translateLanguageOptions().map((lang) => ({ - value: lang.value, + ...translateLanguageOptions.map((lang) => ({ + value: lang.langCode, label: ( {lang.emoji} - {lang.label} + {lang.label()} ) })) diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index e8ec416b1e..0d216aa3aa 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -2,7 +2,7 @@ import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@ import i18n from '@renderer/i18n' import store from '@renderer/store' import { addAssistant } from '@renderer/store/assistants' -import type { Agent, Assistant, AssistantSettings, Model, Provider, Topic } from '@renderer/types' +import type { Agent, Assistant, AssistantSettings, Language, Model, Provider, Topic } from '@renderer/types' import { uuid } from '@renderer/utils' export function getDefaultAssistant(): Assistant { @@ -28,7 +28,7 @@ export function getDefaultAssistant(): Assistant { } } -export function getDefaultTranslateAssistant(targetLanguage: string, text: string): Assistant { +export function getDefaultTranslateAssistant(targetLanguage: Language, text: string): Assistant { const translateModel = getTranslateModel() const assistant: Assistant = getDefaultAssistant() assistant.model = translateModel @@ -39,7 +39,7 @@ export function getDefaultTranslateAssistant(targetLanguage: string, text: strin assistant.prompt = store .getState() - .settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage) + .settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value) .replaceAll('{{text}}', text) return assistant } diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index 00a09acf54..4bb92f38b0 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -1,6 +1,6 @@ import Logger from '@renderer/config/logger' import db from '@renderer/databases' -import { upgradeToV7 } from '@renderer/databases/upgrades' +import { upgradeToV7, upgradeToV8 } from '@renderer/databases/upgrades' import i18n from '@renderer/i18n' import store from '@renderer/store' import { setWebDAVSyncState } from '@renderer/store/backup' @@ -637,7 +637,7 @@ export function stopAutoSync() { export async function getBackupData() { return JSON.stringify({ time: new Date().getTime(), - version: 4, + version: 5, localStorage, indexedDB: await backupDatabase() }) @@ -674,6 +674,12 @@ export async function handleData(data: Record) { }) } + if (data.version === 4) { + await db.transaction('rw', db.tables, async (tx) => { + await upgradeToV8(tx) + }) + } + window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' }) setTimeout(() => window.api.reload(), 1000) return diff --git a/src/renderer/src/services/TranslateService.ts b/src/renderer/src/services/TranslateService.ts index 513fa3ef36..1fb25d1a86 100644 --- a/src/renderer/src/services/TranslateService.ts +++ b/src/renderer/src/services/TranslateService.ts @@ -1,12 +1,13 @@ import i18n from '@renderer/i18n' import store from '@renderer/store' +import { Language } from '@renderer/types' import { fetchTranslate } from './ApiService' import { getDefaultTranslateAssistant } from './AssistantService' export const translateText = async ( text: string, - targetLanguage: string, + targetLanguage: Language, onResponse?: (text: string, isComplete: boolean) => void ) => { const translateModel = store.getState().llm.translateModel diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index f0c0cb2680..5f1d84bfae 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -54,7 +54,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 119, + version: 120, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index a305d03735..53c28a84ba 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -5,7 +5,7 @@ import { SYSTEM_MODELS } from '@renderer/config/models' import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import db from '@renderer/databases' import i18n from '@renderer/i18n' -import { Assistant, Provider, WebSearchProvider } from '@renderer/types' +import { Assistant, LanguageCode, Provider, WebSearchProvider } from '@renderer/types' import { getDefaultGroupName, getLeadingEmoji, runAsyncFunction, uuid } from '@renderer/utils' import { UpgradeChannel } from '@shared/config/constant' import { isEmpty } from 'lodash' @@ -897,6 +897,7 @@ const migrateConfig = { }, '65': (state: RootState) => { try { + // @ts-ignore expect error state.settings.targetLanguage = 'english' return state } catch (error) { @@ -1736,6 +1737,25 @@ const migrateConfig = { } catch (error) { return state } + }, + '120': (state: RootState) => { + try { + const langMap: Record = { + english: 'en-us', + chinese: 'zh-cn', + 'chinese-traditional': 'zh-tw', + japanese: 'ja-jp', + russian: 'ru-ru' + } + + const origin = state.settings.targetLanguage + const newLang = langMap[origin] + if (newLang) state.settings.targetLanguage = newLang + else state.settings.targetLanguage = 'en-us' + return state + } catch (error) { + return state + } } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 771e7cf349..71b99b9463 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -201,7 +201,7 @@ export const initialState: SettingsState = { assistantsTabSortType: 'list', sendMessageShortcut: 'Enter', language: navigator.language as LanguageVarious, - targetLanguage: 'english' as TranslateLanguageVarious, + targetLanguage: 'en-us', proxyMode: 'system', proxyUrl: undefined, userName: '', diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 21eb4bbc99..b90e41b79b 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -340,16 +340,7 @@ export enum ThemeMode { export type LanguageVarious = 'zh-CN' | 'zh-TW' | 'el-GR' | 'en-US' | 'es-ES' | 'fr-FR' | 'ja-JP' | 'pt-PT' | 'ru-RU' -export type TranslateLanguageVarious = - | 'chinese' - | 'chinese-traditional' - | 'greek' - | 'english' - | 'spanish' - | 'french' - | 'japanese' - | 'portuguese' - | 'russian' +export type TranslateLanguageVarious = LanguageCode export type CodeStyleVarious = 'auto' | string @@ -489,12 +480,41 @@ export type GenerateImageResponse = { images: string[] } +export type LanguageCode = + | 'en-us' + | 'zh-cn' + | 'zh-tw' + | 'ja-jp' + | 'ko-kr' + | 'fr-fr' + | 'de-de' + | 'it-it' + | 'es-es' + | 'pt-pt' + | 'ru-ru' + | 'pl-pl' + | 'ar-ar' + | 'tr-tr' + | 'th-th' + | 'vi-vn' + | 'id-id' + | 'ur-pk' + | 'ms-my' + +// langCode应当能够唯一确认一种语言 +export type Language = { + value: string + langCode: LanguageCode + label: () => string + emoji: string +} + export interface TranslateHistory { id: string sourceText: string targetText: string - sourceLanguage: string - targetLanguage: string + sourceLanguage: LanguageCode + targetLanguage: LanguageCode createdAt: string } diff --git a/src/renderer/src/utils/translate.ts b/src/renderer/src/utils/translate.ts index bd50733482..77a88928b9 100644 --- a/src/renderer/src/utils/translate.ts +++ b/src/renderer/src/utils/translate.ts @@ -1,13 +1,15 @@ +import { LanguagesEnum } from '@renderer/config/translate' +import { Language, LanguageCode } from '@renderer/types' import { franc } from 'franc-min' import React, { MutableRefObject } from 'react' /** * 使用Unicode字符范围检测语言 * 适用于较短文本的语言检测 - * @param {string} text 需要检测语言的文本 - * @returns {string} 检测到的语言代码 + * @param text 需要检测语言的文本 + * @returns 检测到的语言 */ -export const detectLanguageByUnicode = (text: string): string => { +export const detectLanguageByUnicode = (text: string): Language => { const counts = { zh: 0, ja: 0, @@ -40,8 +42,8 @@ export const detectLanguageByUnicode = (text: string): string => { } } - if (totalChars === 0) return 'en' - let maxLang = 'en' + if (totalChars === 0) return LanguagesEnum.enUS + let maxLang = '' let maxCount = 0 for (const [lang, count] of Object.entries(counts)) { @@ -52,73 +54,68 @@ export const detectLanguageByUnicode = (text: string): string => { } if (maxCount / totalChars < 0.3) { - return 'en' + return LanguagesEnum.enUS + } + + switch (maxLang) { + case 'zh': + return LanguagesEnum.zhCN + case 'ja': + return LanguagesEnum.jaJP + case 'ko': + return LanguagesEnum.koKR + case 'ru': + return LanguagesEnum.ruRU + case 'ar': + return LanguagesEnum.arAR + case 'en': + return LanguagesEnum.enUS + default: + console.error(`Unknown language: ${maxLang}`) + return LanguagesEnum.enUS } - return maxLang } /** * 检测输入文本的语言 - * @param {string} inputText 需要检测语言的文本 - * @returns {Promise} 检测到的语言代码 + * @param inputText 需要检测语言的文本 + * @returns 检测到的语言 */ -export const detectLanguage = async (inputText: string): Promise => { +export const detectLanguage = async (inputText: string): Promise => { const text = inputText.trim() - if (!text) return 'any' - let code: string + if (!text) return LanguagesEnum.zhCN + let lang: Language // 如果文本长度小于20个字符,使用Unicode范围检测 if (text.length < 20) { - code = detectLanguageByUnicode(text) + lang = detectLanguageByUnicode(text) } else { // franc 返回 ISO 639-3 代码 const iso3 = franc(text) - const isoMap: Record = { - cmn: 'zh', - jpn: 'ja', - kor: 'ko', - rus: 'ru', - ara: 'ar', - spa: 'es', - fra: 'fr', - deu: 'de', - ita: 'it', - por: 'pt', - eng: 'en', - pol: 'pl', - tur: 'tr', - tha: 'th', - vie: 'vi', - ind: 'id', - urd: 'ur', - zsm: 'ms' + const isoMap: Record = { + cmn: LanguagesEnum.zhCN, + jpn: LanguagesEnum.jaJP, + kor: LanguagesEnum.koKR, + rus: LanguagesEnum.ruRU, + ara: LanguagesEnum.arAR, + spa: LanguagesEnum.esES, + fra: LanguagesEnum.frFR, + deu: LanguagesEnum.deDE, + ita: LanguagesEnum.itIT, + por: LanguagesEnum.ptPT, + eng: LanguagesEnum.enUS, + pol: LanguagesEnum.plPL, + tur: LanguagesEnum.trTR, + tha: LanguagesEnum.thTH, + vie: LanguagesEnum.viVN, + ind: LanguagesEnum.idID, + urd: LanguagesEnum.urPK, + zsm: LanguagesEnum.msMY } - code = isoMap[iso3] || 'en' + lang = isoMap[iso3] || LanguagesEnum.enUS } - // 映射到应用使用的语言键 - const languageMap: Record = { - zh: 'chinese', - ja: 'japanese', - ko: 'korean', - ru: 'russian', - es: 'spanish', - fr: 'french', - de: 'german', - it: 'italian', - pt: 'portuguese', - ar: 'arabic', - en: 'english', - pl: 'polish', - tr: 'turkish', - th: 'thai', - vi: 'vietnamese', - id: 'indonesian', - ur: 'urdu', - ms: 'malay' - } - - return languageMap[code] || 'english' + return lang } /** @@ -127,10 +124,13 @@ export const detectLanguage = async (inputText: string): Promise => { * @param languagePair 配置的语言对 * @returns 目标语言 */ -export const getTargetLanguageForBidirectional = (sourceLanguage: string, languagePair: [string, string]): string => { - if (sourceLanguage === languagePair[0]) { +export const getTargetLanguageForBidirectional = ( + sourceLanguage: Language, + languagePair: [Language, Language] +): Language => { + if (sourceLanguage.langCode === languagePair[0].langCode) { return languagePair[1] - } else if (sourceLanguage === languagePair[1]) { + } else if (sourceLanguage.langCode === languagePair[1].langCode) { return languagePair[0] } return languagePair[0] !== sourceLanguage ? languagePair[0] : languagePair[1] @@ -142,8 +142,8 @@ export const getTargetLanguageForBidirectional = (sourceLanguage: string, langua * @param languagePair 配置的语言对 * @returns 是否在语言对中 */ -export const isLanguageInPair = (sourceLanguage: string, languagePair: [string, string]): boolean => { - return [languagePair[0], languagePair[1]].includes(sourceLanguage) +export const isLanguageInPair = (sourceLanguage: Language, languagePair: [Language, Language]): boolean => { + return [languagePair[0].langCode, languagePair[1].langCode].includes(sourceLanguage.langCode) } /** @@ -155,11 +155,11 @@ export const isLanguageInPair = (sourceLanguage: string, languagePair: [string, * @returns 处理结果对象 */ export const determineTargetLanguage = ( - sourceLanguage: string, - targetLanguage: string, + sourceLanguage: Language, + targetLanguage: Language, isBidirectional: boolean, - bidirectionalPair: [string, string] -): { success: boolean; language?: string; errorType?: 'same_language' | 'not_in_pair' } => { + bidirectionalPair: [Language, Language] +): { success: boolean; language?: Language; errorType?: 'same_language' | 'not_in_pair' } => { if (isBidirectional) { if (!isLanguageInPair(sourceLanguage, bidirectionalPair)) { return { success: false, errorType: 'not_in_pair' } @@ -169,7 +169,7 @@ export const determineTargetLanguage = ( language: getTargetLanguageForBidirectional(sourceLanguage, bidirectionalPair) } } else { - if (sourceLanguage === targetLanguage) { + if (sourceLanguage.langCode === targetLanguage.langCode) { return { success: false, errorType: 'same_language' } } return { success: true, language: targetLanguage } @@ -228,3 +228,21 @@ export const createOutputScrollHandler = ( handleScrollSync(e.currentTarget, inputEl, isProgrammaticScrollRef) } } + +/** + * 根据语言代码获取对应的语言对象 + * @param langcode - 语言代码 + * @returns 返回对应的语言对象,如果找不到则返回英语(enUS) + * @example + * ```typescript + * const language = getLanguageByLangcode('zh-cn') // 返回中文语言对象 + * ``` + */ +export const getLanguageByLangcode = (langcode: LanguageCode): Language => { + const result = Object.values(LanguagesEnum).find((item) => item.langCode === langcode) + if (!result) { + console.error(`Language not found for langcode: ${langcode}`) + return LanguagesEnum.enUS + } + return result +} diff --git a/src/renderer/src/windows/mini/translate/TranslateWindow.tsx b/src/renderer/src/windows/mini/translate/TranslateWindow.tsx index 26e83fcf17..9cfde74bd3 100644 --- a/src/renderer/src/windows/mini/translate/TranslateWindow.tsx +++ b/src/renderer/src/windows/mini/translate/TranslateWindow.tsx @@ -1,13 +1,14 @@ import { SwapOutlined } from '@ant-design/icons' import Scrollbar from '@renderer/components/Scrollbar' -import { TranslateLanguageOptions } from '@renderer/config/translate' +import { LanguagesEnum, translateLanguageOptions } from '@renderer/config/translate' import db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' import { fetchTranslate } from '@renderer/services/ApiService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' -import { Assistant } from '@renderer/types' +import { Assistant, Language } from '@renderer/types' import { runAsyncFunction } from '@renderer/utils' -import { Select, Space } from 'antd' +import { getLanguageByLangcode } from '@renderer/utils/translate' +import { Select } from 'antd' import { isEmpty } from 'lodash' import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' @@ -18,11 +19,11 @@ interface Props { text: string } -let _targetLanguage = 'chinese' +let _targetLanguage = (await db.settings.get({ id: 'translate:target:language' }))?.value || LanguagesEnum.zhCN const Translate: FC = ({ text }) => { const [result, setResult] = useState('') - const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) + const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) const { translateModel } = useDefaultModel() const { t } = useTranslation() const translatingRef = useRef(false) @@ -37,8 +38,7 @@ const Translate: FC = ({ text }) => { try { translatingRef.current = true - const targetLang = await db.settings.get({ id: 'translate:target:language' }) - const assistant: Assistant = getDefaultTranslateAssistant(targetLang?.value || targetLanguage, text) + const assistant: Assistant = getDefaultTranslateAssistant(targetLanguage, text) // const message: Message = { // id: uuid(), // role: 'user', @@ -64,7 +64,7 @@ const Translate: FC = ({ text }) => { useEffect(() => { runAsyncFunction(async () => { const targetLang = await db.settings.get({ id: 'translate:target:language' }) - targetLang && setTargetLanguage(targetLang.value) + targetLang && setTargetLanguage(getLanguageByLangcode(targetLang.value)) }) }, []) @@ -91,22 +91,17 @@ const Translate: FC = ({ text }) => { ({ - value: lang.value, + options={translateLanguageOptions.map((lang) => ({ + value: lang.langCode, label: ( {lang.emoji} - {lang.label} + {lang.label()} ) }))} - onChange={(value) => handleChangeLanguage(value, alterLanguage)} + onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)} disabled={isLoading} />