refactor(translate): Language Type (#7727)

* refactor(translate): 重构翻译功能使用语言枚举类型

统一翻译功能中的语言表示方式,使用枚举类型替代字符串
更新相关组件和服务以适配新的语言类型定义
添加数据库迁移脚本处理语言类型变更
添加store迁移处理语言类型变更

* refactor(translate): 移除调试用的console.log语句

* refactor(translate): 移除冗余的类型检查逻辑

* fix(db): 添加对TranslateHistory的db迁移

* fix(databases): 捕获数据库升级时的语言映射错误

添加错误处理以防止语言映射失败时中断升级过程

* fix(翻译组件): 修复语言比较和选择逻辑错误

修复语言比较时直接比较对象而非langCode的问题
更新Select组件使用langCode作为值并正确处理语言切换

* refactor(translate): 将saveTranslateHistory参数类型从Language改为LanguageCode

* refactor(hooks): 更新useMessageOperations中的语言代码类型

将targetLanguage和sourceLanguage参数类型从string更新为LanguageCode,提高类型安全性

* docs(translate): 更新JSDoc注释以使用TypeScript类型语法

* feat(备份服务): 升级数据库版本至v8并添加迁移逻辑

添加从v7到v8的数据库迁移支持
更新翻译历史记录中的语言代码映射
优化迁移过程中的日志记录和错误处理

* fix(store): 修复目标语言迁移时的默认值处理

确保在迁移配置时将旧版语言代码正确映射到新版格式,无法映射时使用默认英语

* refactor(translate): 将语言标签从字符串改为函数以支持动态翻译

* refactor(translate): 优化翻译窗口语言选择逻辑

重构翻译窗口的目标语言选择逻辑,使用语言代码获取完整语言信息
移除冗余的Space组件,简化Select选项渲染方式

* docs(技术文档): 新增数据库设置字段文档

添加数据库设置字段的说明文档,包含翻译相关字段的类型和用途

* refactor(translate): 修改db中biDirectionLangPair存储类型

将语言代码处理统一改为存储langCode而非Language对象
修改相关代码以使用getLanguageByLangcode进行转换
更新数据库升级逻辑以兼容新格式

* docs(translate): 为getLanguageByLangcode函数添加注释说明

* fix(数据库升级): 修复升级到V8时可能出现的空值访问问题

* refactor(databases): 优化语言映射错误处理逻辑

将不必要的try-catch块替换为if条件判断

* docs(technical): 修正数据库设置文档中的类型描述

* refactor: 优化语言代码处理和变量命名

* fix(ActionTranslate): 使用langCode存储双向翻译语言对

* fix(migrate): 修复错误的迁移过程

* refactor(translate): 重构语言选项从硬编码改为动态生成

将translateLanguageOptions从硬编码的数组改为通过LanguagesEnum动态生成,提高可维护性

* fix(store): 更新持久化存储版本并修复语言映射迁移问题

将持久化存储版本从119升级到120,并修复语言代码映射迁移问题。迁移过程中将旧的语言标识转换为新的标准语言代码格式。
This commit is contained in:
Phantom 2025-07-07 22:08:56 +08:00 committed by GitHub
parent 278fd931fb
commit a314a43f0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 557 additions and 357 deletions

View File

@ -0,0 +1,11 @@
# 数据库设置字段
此文档包含部分字段的数据类型说明。
## 字段
| 字段名 | 类型 | 说明 |
| ------------------------------ | ------------------------------ | ------------ |
| `translate:target:language` | `LanguageCode` | 翻译目标语言 |
| `translate:source:language` | `LanguageCode` | 翻译源语言 |
| `translate:bidirectional:pair` | `[LanguageCode, LanguageCode]` | 双向翻译对 |

View File

@ -3,6 +3,7 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { fetchTranslate } from '@renderer/services/ApiService' import { fetchTranslate } from '@renderer/services/ApiService'
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { getLanguageByLangcode } from '@renderer/utils/translate'
import { Modal, ModalProps } from 'antd' import { Modal, ModalProps } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { TextAreaProps } from 'antd/lib/input' import { TextAreaProps } from 'antd/lib/input'
@ -111,7 +112,7 @@ const PopupContainer: React.FC<Props> = ({
} }
try { try {
const assistant = getDefaultTranslateAssistant(targetLanguage, textValue) const assistant = getDefaultTranslateAssistant(getLanguageByLangcode(targetLanguage), textValue)
const translatedText = await fetchTranslate({ content: textValue, assistant }) const translatedText = await fetchTranslate({ content: textValue, assistant })
if (isMounted.current) { if (isMounted.current) {
setTextValue(translatedText) setTextValue(translatedText)

View File

@ -3,6 +3,7 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { fetchTranslate } from '@renderer/services/ApiService' import { fetchTranslate } from '@renderer/services/ApiService'
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { getLanguageByLangcode } from '@renderer/utils/translate'
import { Button, Tooltip } from 'antd' import { Button, Tooltip } from 'antd'
import { Languages } from 'lucide-react' import { Languages } from 'lucide-react'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
@ -54,7 +55,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
setIsTranslating(true) setIsTranslating(true)
try { try {
const assistant = getDefaultTranslateAssistant(targetLanguage, text) const assistant = getDefaultTranslateAssistant(getLanguageByLangcode(targetLanguage), text)
const translatedText = await fetchTranslate({ content: text, assistant }) const translatedText = await fetchTranslate({ content: text, assistant })
onTranslated(translatedText) onTranslated(translatedText)
} catch (error) { } catch (error) {
@ -75,7 +76,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
return ( return (
<Tooltip <Tooltip
placement="top" placement="top"
title={t('chat.input.translate', { target_language: t(`languages.${targetLanguage.toString()}`) })} title={t('chat.input.translate', { target_language: getLanguageByLangcode(targetLanguage).label() })}
arrow> arrow>
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text"> <ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
{isTranslating ? <LoadingOutlined spin /> : <Languages size={18} />} {isTranslating ? <LoadingOutlined spin /> : <Languages size={18} />}

View File

@ -1,136 +1,159 @@
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { Language } from '@renderer/types'
export interface TranslateLanguageOption { export const ENGLISH: Language = {
value: string
langCode?: string
label: string
emoji: string
}
export const TranslateLanguageOptions: TranslateLanguageOption[] = [
{
value: 'English', value: 'English',
langCode: 'en-us', langCode: 'en-us',
label: i18n.t('languages.english'), label: () => i18n.t('languages.english'),
emoji: '🇬🇧' emoji: '🇬🇧'
}, }
{
export const CHINESE_SIMPLIFIED: Language = {
value: 'Chinese (Simplified)', value: 'Chinese (Simplified)',
langCode: 'zh-cn', langCode: 'zh-cn',
label: i18n.t('languages.chinese'), label: () => i18n.t('languages.chinese'),
emoji: '🇨🇳' emoji: '🇨🇳'
}, }
{
export const CHINESE_TRADITIONAL: Language = {
value: 'Chinese (Traditional)', value: 'Chinese (Traditional)',
langCode: 'zh-tw', langCode: 'zh-tw',
label: i18n.t('languages.chinese-traditional'), label: () => i18n.t('languages.chinese-traditional'),
emoji: '🇭🇰' emoji: '🇭🇰'
}, }
{
export const JAPANESE: Language = {
value: 'Japanese', value: 'Japanese',
langCode: 'ja-jp', langCode: 'ja-jp',
label: i18n.t('languages.japanese'), label: () => i18n.t('languages.japanese'),
emoji: '🇯🇵' emoji: '🇯🇵'
}, }
{
export const KOREAN: Language = {
value: 'Korean', value: 'Korean',
langCode: 'ko-kr', langCode: 'ko-kr',
label: i18n.t('languages.korean'), label: () => i18n.t('languages.korean'),
emoji: '🇰🇷' emoji: '🇰🇷'
}, }
{ export const FRENCH: Language = {
value: 'French', value: 'French',
langCode: 'fr-fr', langCode: 'fr-fr',
label: i18n.t('languages.french'), label: () => i18n.t('languages.french'),
emoji: '🇫🇷' emoji: '🇫🇷'
}, }
{
export const GERMAN: Language = {
value: 'German', value: 'German',
langCode: 'de-de', langCode: 'de-de',
label: i18n.t('languages.german'), label: () => i18n.t('languages.german'),
emoji: '🇩🇪' emoji: '🇩🇪'
}, }
{
export const ITALIAN: Language = {
value: 'Italian', value: 'Italian',
langCode: 'it-it', langCode: 'it-it',
label: i18n.t('languages.italian'), label: () => i18n.t('languages.italian'),
emoji: '🇮🇹' emoji: '🇮🇹'
}, }
{
export const SPANISH: Language = {
value: 'Spanish', value: 'Spanish',
langCode: 'es-es', langCode: 'es-es',
label: i18n.t('languages.spanish'), label: () => i18n.t('languages.spanish'),
emoji: '🇪🇸' emoji: '🇪🇸'
}, }
{
export const PORTUGUESE: Language = {
value: 'Portuguese', value: 'Portuguese',
langCode: 'pt-pt', langCode: 'pt-pt',
label: i18n.t('languages.portuguese'), label: () => i18n.t('languages.portuguese'),
emoji: '🇵🇹' emoji: '🇵🇹'
}, }
{
export const RUSSIAN: Language = {
value: 'Russian', value: 'Russian',
langCode: 'ru-ru', langCode: 'ru-ru',
label: i18n.t('languages.russian'), label: () => i18n.t('languages.russian'),
emoji: '🇷🇺' emoji: '🇷🇺'
}, }
{
export const POLISH: Language = {
value: 'Polish', value: 'Polish',
langCode: 'pl-pl', langCode: 'pl-pl',
label: i18n.t('languages.polish'), label: () => i18n.t('languages.polish'),
emoji: '🇵🇱' emoji: '🇵🇱'
}, }
{
export const ARABIC: Language = {
value: 'Arabic', value: 'Arabic',
langCode: 'ar-ar', langCode: 'ar-ar',
label: i18n.t('languages.arabic'), label: () => i18n.t('languages.arabic'),
emoji: '🇸🇦' emoji: '🇸🇦'
}, }
{
export const TURKISH: Language = {
value: 'Turkish', value: 'Turkish',
langCode: 'tr-tr', langCode: 'tr-tr',
label: i18n.t('languages.turkish'), label: () => i18n.t('languages.turkish'),
emoji: '🇹🇷' emoji: '🇹🇷'
}, }
{
export const THAI: Language = {
value: 'Thai', value: 'Thai',
langCode: 'th-th', langCode: 'th-th',
label: i18n.t('languages.thai'), label: () => i18n.t('languages.thai'),
emoji: '🇹🇭' emoji: '🇹🇭'
}, }
{
export const VIETNAMESE: Language = {
value: 'Vietnamese', value: 'Vietnamese',
langCode: 'vi-vn', langCode: 'vi-vn',
label: i18n.t('languages.vietnamese'), label: () => i18n.t('languages.vietnamese'),
emoji: '🇻🇳' emoji: '🇻🇳'
}, }
{
export const INDONESIAN: Language = {
value: 'Indonesian', value: 'Indonesian',
langCode: 'id-id', langCode: 'id-id',
label: i18n.t('languages.indonesian'), label: () => i18n.t('languages.indonesian'),
emoji: '🇮🇩' emoji: '🇮🇩'
}, }
{
export const URDU: Language = {
value: 'Urdu', value: 'Urdu',
langCode: 'ur-pk', langCode: 'ur-pk',
label: i18n.t('languages.urdu'), label: () => i18n.t('languages.urdu'),
emoji: '🇵🇰' emoji: '🇵🇰'
}, }
{
export const MALAY: Language = {
value: 'Malay', value: 'Malay',
langCode: 'ms-my', langCode: 'ms-my',
label: i18n.t('languages.malay'), label: () => i18n.t('languages.malay'),
emoji: '🇲🇾' emoji: '🇲🇾'
}
]
export const translateLanguageOptions = (): typeof TranslateLanguageOptions => {
return TranslateLanguageOptions.map((option) => {
return {
value: option.value,
label: option.label,
emoji: option.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)

View File

@ -3,7 +3,7 @@ import { FileMetadata, KnowledgeItem, QuickPhrase, TranslateHistory } from '@ren
import type { Message as NewMessage, MessageBlock } from '@renderer/types/newMessage' import type { Message as NewMessage, MessageBlock } from '@renderer/types/newMessage'
import { Dexie, type EntityTable } from 'dexie' 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) // Database declaration (move this to its own module also)
export const db = new Dexie('CherryStudio') as Dexie & { export const db = new Dexie('CherryStudio') as Dexie & {
@ -74,4 +74,17 @@ db.version(7)
}) })
.upgrade((tx) => upgradeToV7(tx)) .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 export default db

View File

@ -1,7 +1,7 @@
import Logger from '@renderer/config/logger' import Logger from '@renderer/config/logger'
import type { LegacyMessage as OldMessage, Topic } from '@renderer/types' import { LanguagesEnum } from '@renderer/config/translate'
import { FileTypes } from '@renderer/types' // Import FileTypes enum import type { LanguageCode, LegacyMessage as OldMessage, Topic } from '@renderer/types'
import { WebSearchSource } from '@renderer/types' import { FileTypes, WebSearchSource } from '@renderer/types' // Import FileTypes enum
import type { import type {
BaseMessageBlock, BaseMessageBlock,
CitationMessageBlock, CitationMessageBlock,
@ -308,3 +308,78 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
Logger.log('DB migration to version 7 finished successfully.') Logger.log('DB migration to version 7 finished successfully.')
} }
export async function upgradeToV8(tx: Transaction): Promise<void> {
Logger.log('DB migration to version 8 started')
const langMap: Record<string, LanguageCode> = {
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.')
}

View File

@ -19,7 +19,7 @@ import {
updateMessageAndBlocksThunk, updateMessageAndBlocksThunk,
updateTranslationBlockThunk updateTranslationBlockThunk
} from '@renderer/store/thunk/messageThunk' } 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 type { Message, MessageBlock } from '@renderer/types/newMessage'
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { abortCompletion } from '@renderer/utils/abortController' import { abortCompletion } from '@renderer/utils/abortController'
@ -195,9 +195,9 @@ export function useMessageOperations(topic: Topic) {
const getTranslationUpdater = useCallback( const getTranslationUpdater = useCallback(
async ( async (
messageId: string, messageId: string,
targetLanguage: string, targetLanguage: LanguageCode,
sourceBlockId?: string, sourceBlockId?: string,
sourceLanguage?: string sourceLanguage?: LanguageCode
): Promise<((accumulatedText: string, isComplete?: boolean) => void) | null> => { ): Promise<((accumulatedText: string, isComplete?: boolean) => void) | null> => {
if (!topic.id) return null if (!topic.id) return null

View File

@ -37,6 +37,7 @@ import type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
import { formatQuotedText } from '@renderer/utils/formats' import { formatQuotedText } from '@renderer/utils/formats'
import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input' import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
import { getLanguageByLangcode } from '@renderer/utils/translate'
import { documentExts, imageExts, textExts } from '@shared/config/constant' import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Button, Tooltip } from 'antd' import { Button, Tooltip } from 'antd'
@ -253,7 +254,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
try { try {
setIsTranslating(true) setIsTranslating(true)
const translatedText = await translateText(text, targetLanguage) const translatedText = await translateText(text, getLanguageByLangcode(targetLanguage))
translatedText && setText(translatedText) translatedText && setText(translatedText)
setTimeout(() => resizeTextArea(), 0) setTimeout(() => resizeTextArea(), 0)
} catch (error) { } catch (error) {

View File

@ -2,7 +2,7 @@ import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } fro
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isVisionModel } from '@renderer/config/models' 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 { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
@ -13,7 +13,7 @@ import { translateText } from '@renderer/services/TranslateService'
import store, { RootState } from '@renderer/store' import store, { RootState } from '@renderer/store'
import { messageBlocksSelectors } from '@renderer/store/messageBlock' import { messageBlocksSelectors } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage' 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 { type Message, MessageBlockType } from '@renderer/types/newMessage'
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, classNames } from '@renderer/utils' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, classNames } from '@renderer/utils'
import { copyMessageAsPlainText } from '@renderer/utils/copy' import { copyMessageAsPlainText } from '@renderer/utils/copy'
@ -153,12 +153,12 @@ const MessageMenubar: FC<Props> = (props) => {
}, [message.id, startEditing]) }, [message.id, startEditing])
const handleTranslate = useCallback( const handleTranslate = useCallback(
async (language: string) => { async (language: Language) => {
if (isTranslating) return if (isTranslating) return
setIsTranslating(true) setIsTranslating(true)
const messageId = message.id const messageId = message.id
const translationUpdater = await getTranslationUpdater(messageId, language) const translationUpdater = await getTranslationUpdater(messageId, language.langCode)
if (!translationUpdater) return if (!translationUpdater) return
try { try {
await translateText(mainTextContent, language, translationUpdater) await translateText(mainTextContent, language, translationUpdater)
@ -457,10 +457,10 @@ const MessageMenubar: FC<Props> = (props) => {
backgroundClip: 'border-box' backgroundClip: 'border-box'
}, },
items: [ items: [
...TranslateLanguageOptions.map((item) => ({ ...translateLanguageOptions.map((item) => ({
label: item.emoji + ' ' + item.label, label: item.emoji + ' ' + item.label(),
key: item.value, key: item.langCode,
onClick: () => handleTranslate(item.value) onClick: () => handleTranslate(item)
})), })),
...(hasTranslationBlocks ...(hasTranslationBlocks
? [ ? [

View File

@ -8,6 +8,7 @@ import {
isSupportedFlexServiceTier, isSupportedFlexServiceTier,
isSupportedReasoningEffortOpenAIModel isSupportedReasoningEffortOpenAIModel
} from '@renderer/config/models' } from '@renderer/config/models'
import { translateLanguageOptions } from '@renderer/config/translate'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
@ -44,14 +45,7 @@ import {
setShowTranslateConfirm, setShowTranslateConfirm,
setThoughtAutoCollapse setThoughtAutoCollapse
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { import { Assistant, AssistantSettings, CodeStyleVarious, MathEngine, ThemeMode } from '@renderer/types'
Assistant,
AssistantSettings,
CodeStyleVarious,
MathEngine,
ThemeMode,
TranslateLanguageVarious
} from '@renderer/types'
import { modalConfirm } from '@renderer/utils' import { modalConfirm } from '@renderer/utils'
import { getSendMessageShortcutLabel } from '@renderer/utils/input' import { getSendMessageShortcutLabel } from '@renderer/utils/input'
import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd' import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
@ -625,14 +619,10 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<Selector <Selector
value={targetLanguage} value={targetLanguage}
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)} onChange={(value) => setTargetLanguage(value)}
options={[ options={translateLanguageOptions.map((item) => {
{ value: 'chinese', label: t('settings.input.target_language.chinese') }, return { value: item.langCode, label: item.emoji + ' ' + item.label() }
{ 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') }
]}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />

View File

@ -7,6 +7,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers' import { getProviderLogo } from '@renderer/config/providers'
import { LanguagesEnum } from '@renderer/config/translate'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
@ -543,7 +544,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
try { try {
setIsTranslating(true) setIsTranslating(true)
const translatedText = await translateText(painting.prompt, 'english') const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS)
updatePaintingState({ prompt: translatedText }) updatePaintingState({ prompt: translatedText })
} catch (error) { } catch (error) {
console.error('Translation failed:', error) console.error('Translation failed:', error)

View File

@ -12,6 +12,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models' import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models'
import { LanguagesEnum } from '@renderer/config/translate'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
@ -302,7 +303,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
try { try {
setIsTranslating(true) setIsTranslating(true)
const translatedText = await translateText(painting.prompt, 'english') const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS)
updatePaintingState({ prompt: translatedText }) updatePaintingState({ prompt: translatedText })
} catch (error) { } catch (error) {
console.error('Translation failed:', error) console.error('Translation failed:', error)

View File

@ -4,6 +4,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers' import { getProviderLogo } from '@renderer/config/providers'
import { LanguagesEnum } from '@renderer/config/translate'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
@ -255,7 +256,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
try { try {
setIsTranslating(true) setIsTranslating(true)
const translatedText = await translateText(painting.prompt, 'english') const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS)
updatePaintingState({ prompt: translatedText }) updatePaintingState({ prompt: translatedText })
} catch (error) { } catch (error) {
console.error('Translation failed:', error) console.error('Translation failed:', error)

View File

@ -4,7 +4,7 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { isEmbeddingModel } from '@renderer/config/models' import { isEmbeddingModel } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts' 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 { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useDefaultModel } from '@renderer/hooks/useAssistant'
@ -15,13 +15,14 @@ import { getDefaultTranslateAssistant } from '@renderer/services/AssistantServic
import { getModelUniqId, hasModel } from '@renderer/services/ModelService' import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setTranslateModelPrompt } from '@renderer/store/settings' 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 { runAsyncFunction, uuid } from '@renderer/utils'
import { import {
createInputScrollHandler, createInputScrollHandler,
createOutputScrollHandler, createOutputScrollHandler,
detectLanguage, detectLanguage,
determineTargetLanguage determineTargetLanguage,
getLanguageByLangcode
} from '@renderer/utils/translate' } from '@renderer/utils/translate'
import { Button, Dropdown, Empty, Flex, Modal, Popconfirm, Select, Space, Switch, Tooltip } from 'antd' import { Button, Dropdown, Empty, Flex, Modal, Popconfirm, Select, Space, Switch, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
@ -35,7 +36,7 @@ import styled from 'styled-components'
let _text = '' let _text = ''
let _result = '' let _result = ''
let _targetLanguage = 'english' let _targetLanguage = LanguagesEnum.enUS
const TranslateSettings: FC<{ const TranslateSettings: FC<{
visible: boolean visible: boolean
@ -46,8 +47,8 @@ const TranslateSettings: FC<{
setIsBidirectional: (value: boolean) => void setIsBidirectional: (value: boolean) => void
enableMarkdown: boolean enableMarkdown: boolean
setEnableMarkdown: (value: boolean) => void setEnableMarkdown: (value: boolean) => void
bidirectionalPair: [string, string] bidirectionalPair: [Language, Language]
setBidirectionalPair: (value: [string, string]) => void setBidirectionalPair: (value: [Language, Language]) => void
translateModel: Model | undefined translateModel: Model | undefined
onModelChange: (model: Model) => void onModelChange: (model: Model) => void
allModels: Model[] allModels: Model[]
@ -71,7 +72,7 @@ const TranslateSettings: FC<{
const { t } = useTranslation() const { t } = useTranslation()
const { translateModelPrompt } = useSettings() const { translateModelPrompt } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [localPair, setLocalPair] = useState<[string, string]>(bidirectionalPair) const [localPair, setLocalPair] = useState<[Language, Language]>(bidirectionalPair)
const [showPrompt, setShowPrompt] = useState(false) const [showPrompt, setShowPrompt] = useState(false)
const [localPrompt, setLocalPrompt] = useState(translateModelPrompt) const [localPrompt, setLocalPrompt] = useState(translateModelPrompt)
@ -94,7 +95,7 @@ const TranslateSettings: FC<{
return return
} }
setBidirectionalPair(localPair) 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:scroll:sync', value: isScrollSyncEnabled })
db.settings.put({ id: 'translate:markdown:enabled', value: enableMarkdown }) db.settings.put({ id: 'translate:markdown:enabled', value: enableMarkdown })
db.settings.put({ id: 'translate:model:prompt', value: localPrompt }) db.settings.put({ id: 'translate:model:prompt', value: localPrompt })
@ -189,16 +190,16 @@ const TranslateSettings: FC<{
<Flex align="center" justify="space-between" gap={10}> <Flex align="center" justify="space-between" gap={10}>
<Select <Select
style={{ flex: 1 }} style={{ flex: 1 }}
value={localPair[0]} value={localPair[0].langCode}
onChange={(value) => setLocalPair([value, localPair[1]])} onChange={(value) => setLocalPair([getLanguageByLangcode(value), localPair[1]])}
options={translateLanguageOptions().map((lang) => ({ options={translateLanguageOptions.map((lang) => ({
value: lang.value, value: lang.langCode,
label: ( label: (
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}> <span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji} {lang.emoji}
</span> </span>
<Space.Compact block>{lang.label}</Space.Compact> <Space.Compact block>{lang.label()}</Space.Compact>
</Space.Compact> </Space.Compact>
) )
}))} }))}
@ -206,16 +207,16 @@ const TranslateSettings: FC<{
<span></span> <span></span>
<Select <Select
style={{ flex: 1 }} style={{ flex: 1 }}
value={localPair[1]} value={localPair[1].langCode}
onChange={(value) => setLocalPair([localPair[0], value])} onChange={(value) => setLocalPair([localPair[0], getLanguageByLangcode(value)])}
options={translateLanguageOptions().map((lang) => ({ options={translateLanguageOptions.map((lang) => ({
value: lang.value, value: lang.langCode,
label: ( label: (
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}> <span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji} {lang.emoji}
</span> </span>
<div style={{ textAlign: 'left', flex: 1 }}>{lang.label}</div> <div style={{ textAlign: 'left', flex: 1 }}>{lang.label()}</div>
</Space.Compact> </Space.Compact>
) )
}))} }))}
@ -275,7 +276,6 @@ const TranslateSettings: FC<{
const TranslatePage: FC = () => { const TranslatePage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { shikiMarkdownIt } = useCodeStyle() const { shikiMarkdownIt } = useCodeStyle()
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
const [result, setResult] = useState(_result) const [result, setResult] = useState(_result)
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('') const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
@ -286,10 +286,14 @@ const TranslatePage: FC = () => {
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false) const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false)
const [isBidirectional, setIsBidirectional] = useState(false) const [isBidirectional, setIsBidirectional] = useState(false)
const [enableMarkdown, setEnableMarkdown] = 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 [settingsVisible, setSettingsVisible] = useState(false)
const [detectedLanguage, setDetectedLanguage] = useState<string | null>(null) const [detectedLanguage, setDetectedLanguage] = useState<Language | null>(null)
const [sourceLanguage, setSourceLanguage] = useState<string>('auto') const [sourceLanguage, setSourceLanguage] = useState<Language | 'auto'>('auto')
const [targetLanguage, setTargetLanguage] = useState<Language>(_targetLanguage)
const contentContainerRef = useRef<HTMLDivElement>(null) const contentContainerRef = useRef<HTMLDivElement>(null)
const textAreaRef = useRef<TextAreaRef>(null) const textAreaRef = useRef<TextAreaRef>(null)
const outputTextRef = useRef<HTMLDivElement>(null) const outputTextRef = useRef<HTMLDivElement>(null)
@ -329,8 +333,8 @@ const TranslatePage: FC = () => {
const saveTranslateHistory = async ( const saveTranslateHistory = async (
sourceText: string, sourceText: string,
targetText: string, targetText: string,
sourceLanguage: string, sourceLanguage: LanguageCode,
targetLanguage: string targetLanguage: LanguageCode
) => { ) => {
const history: TranslateHistory = { const history: TranslateHistory = {
id: uuid(), id: uuid(),
@ -364,7 +368,7 @@ const TranslatePage: FC = () => {
setLoading(true) setLoading(true)
try { try {
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测 // 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
let actualSourceLanguage: string let actualSourceLanguage: Language
if (sourceLanguage === 'auto') { if (sourceLanguage === 'auto') {
actualSourceLanguage = await detectLanguage(text) actualSourceLanguage = await detectLanguage(text)
setDetectedLanguage(actualSourceLanguage) setDetectedLanguage(actualSourceLanguage)
@ -389,7 +393,7 @@ const TranslatePage: FC = () => {
return return
} }
const actualTargetLanguage = result.language as string const actualTargetLanguage = result.language as Language
if (isBidirectional) { if (isBidirectional) {
setTargetLanguage(actualTargetLanguage) 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) setLoading(false)
} catch (error) { } catch (error) {
console.error('Translation error:', error) console.error('Translation error:', error)
@ -432,7 +436,7 @@ const TranslatePage: FC = () => {
const onHistoryItemClick = (history: TranslateHistory) => { const onHistoryItemClick = (history: TranslateHistory) => {
setText(history.sourceText) setText(history.sourceText)
setResult(history.targetText) setResult(history.targetText)
setTargetLanguage(history.targetLanguage) setTargetLanguage(getLanguageByLangcode(history.targetLanguage))
} }
useEffect(() => { useEffect(() => {
@ -460,20 +464,32 @@ const TranslatePage: FC = () => {
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
const targetLang = await db.settings.get({ id: 'translate:target:language' }) 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' }) 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' }) const bidirectionalPairSetting = await db.settings.get({ id: 'translate:bidirectional:pair' })
if (bidirectionalPairSetting) { if (bidirectionalPairSetting) {
const langPair = bidirectionalPairSetting.value const langPair = bidirectionalPairSetting.value
let source: undefined | Language
let target: undefined | Language
if (Array.isArray(langPair) && langPair.length === 2 && langPair[0] !== langPair[1]) { 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 { } else {
const defaultPair: [string, string] = ['english', 'chinese'] const defaultPair: [Language, Language] = [LanguagesEnum.enUS, LanguagesEnum.zhCN]
setBidirectionalPair(defaultPair) 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<HTMLTextAreaElement>) => { const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isEnterPressed = e.keyCode == 13 const isEnterPressed = e.key === 'Enter'
if (isEnterPressed && !e.shiftKey && !e.ctrlKey && !e.metaKey) { if (isEnterPressed && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault() e.preventDefault()
onTranslate() onTranslate()
@ -501,32 +517,37 @@ const TranslatePage: FC = () => {
// 获取当前语言状态显示 // 获取当前语言状态显示
const getLanguageDisplay = () => { const getLanguageDisplay = () => {
try {
if (isBidirectional) { if (isBidirectional) {
return ( return (
<Flex align="center" style={{ width: 160 }}> <Flex align="center" style={{ width: 160 }}>
<BidirectionalLanguageDisplay> <BidirectionalLanguageDisplay>
{`${t(`languages.${bidirectionalPair[0]}`)}${t(`languages.${bidirectionalPair[1]}`)}`} {`${bidirectionalPair[0].label()}${bidirectionalPair[1].label()}`}
</BidirectionalLanguageDisplay> </BidirectionalLanguageDisplay>
</Flex> </Flex>
) )
} }
} catch (error) {
console.error('Error getting language display:', error)
setBidirectionalPair([LanguagesEnum.enUS, LanguagesEnum.zhCN])
}
return ( return (
<Select <Select
style={{ width: 160 }} style={{ width: 160 }}
value={targetLanguage} value={targetLanguage.langCode}
onChange={(value) => { onChange={(value) => {
setTargetLanguage(value) setTargetLanguage(getLanguageByLangcode(value))
db.settings.put({ id: 'translate:target:language', value }) db.settings.put({ id: 'translate:target:language', value })
}} }}
options={translateLanguageOptions().map((lang) => ({ options={translateLanguageOptions.map((lang) => ({
value: lang.value, value: lang.langCode,
label: ( label: (
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}> <span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji} {lang.emoji}
</span> </span>
<Space.Compact block>{lang.label}</Space.Compact> <Space.Compact block>{lang.label()}</Space.Compact>
</Space.Compact> </Space.Compact>
) )
}))} }))}
@ -603,28 +624,29 @@ const TranslatePage: FC = () => {
<Flex align="center" gap={20}> <Flex align="center" gap={20}>
<Select <Select
showSearch showSearch
value={sourceLanguage} value={sourceLanguage !== 'auto' ? sourceLanguage.langCode : 'auto'}
style={{ width: 180 }} style={{ width: 180 }}
optionFilterProp="label" optionFilterProp="label"
onChange={(value) => { onChange={(value: LanguageCode | 'auto') => {
setSourceLanguage(value) if (value !== 'auto') setSourceLanguage(getLanguageByLangcode(value))
else setSourceLanguage('auto')
db.settings.put({ id: 'translate:source:language', value }) db.settings.put({ id: 'translate:source:language', value })
}} }}
options={[ options={[
{ {
value: 'auto', value: 'auto',
label: detectedLanguage label: detectedLanguage
? `${t('translate.detected.language')} (${t(`languages.${detectedLanguage.toLowerCase()}`)})` ? `${t('translate.detected.language')} (${detectedLanguage.label()})`
: t('translate.detected.language') : t('translate.detected.language')
}, },
...translateLanguageOptions().map((lang) => ({ ...translateLanguageOptions.map((lang) => ({
value: lang.value, value: lang.langCode,
label: ( label: (
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}> <span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji} {lang.emoji}
</span> </span>
<Space.Compact block>{lang.label}</Space.Compact> <Space.Compact block>{lang.label()}</Space.Compact>
</Space.Compact> </Space.Compact>
) )
})) }))

View File

@ -2,7 +2,7 @@ import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import { addAssistant } from '@renderer/store/assistants' 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' import { uuid } from '@renderer/utils'
export function getDefaultAssistant(): Assistant { 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 translateModel = getTranslateModel()
const assistant: Assistant = getDefaultAssistant() const assistant: Assistant = getDefaultAssistant()
assistant.model = translateModel assistant.model = translateModel
@ -39,7 +39,7 @@ export function getDefaultTranslateAssistant(targetLanguage: string, text: strin
assistant.prompt = store assistant.prompt = store
.getState() .getState()
.settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage) .settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value)
.replaceAll('{{text}}', text) .replaceAll('{{text}}', text)
return assistant return assistant
} }

View File

@ -1,6 +1,6 @@
import Logger from '@renderer/config/logger' import Logger from '@renderer/config/logger'
import db from '@renderer/databases' import db from '@renderer/databases'
import { upgradeToV7 } from '@renderer/databases/upgrades' import { upgradeToV7, upgradeToV8 } from '@renderer/databases/upgrades'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import { setWebDAVSyncState } from '@renderer/store/backup' import { setWebDAVSyncState } from '@renderer/store/backup'
@ -637,7 +637,7 @@ export function stopAutoSync() {
export async function getBackupData() { export async function getBackupData() {
return JSON.stringify({ return JSON.stringify({
time: new Date().getTime(), time: new Date().getTime(),
version: 4, version: 5,
localStorage, localStorage,
indexedDB: await backupDatabase() indexedDB: await backupDatabase()
}) })
@ -674,6 +674,12 @@ export async function handleData(data: Record<string, any>) {
}) })
} }
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' }) window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
setTimeout(() => window.api.reload(), 1000) setTimeout(() => window.api.reload(), 1000)
return return

View File

@ -1,12 +1,13 @@
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import { Language } from '@renderer/types'
import { fetchTranslate } from './ApiService' import { fetchTranslate } from './ApiService'
import { getDefaultTranslateAssistant } from './AssistantService' import { getDefaultTranslateAssistant } from './AssistantService'
export const translateText = async ( export const translateText = async (
text: string, text: string,
targetLanguage: string, targetLanguage: Language,
onResponse?: (text: string, isComplete: boolean) => void onResponse?: (text: string, isComplete: boolean) => void
) => { ) => {
const translateModel = store.getState().llm.translateModel const translateModel = store.getState().llm.translateModel

View File

@ -54,7 +54,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 119, version: 120,
blacklist: ['runtime', 'messages', 'messageBlocks'], blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate migrate
}, },

View File

@ -5,7 +5,7 @@ import { SYSTEM_MODELS } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' 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 { getDefaultGroupName, getLeadingEmoji, runAsyncFunction, uuid } from '@renderer/utils'
import { UpgradeChannel } from '@shared/config/constant' import { UpgradeChannel } from '@shared/config/constant'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
@ -897,6 +897,7 @@ const migrateConfig = {
}, },
'65': (state: RootState) => { '65': (state: RootState) => {
try { try {
// @ts-ignore expect error
state.settings.targetLanguage = 'english' state.settings.targetLanguage = 'english'
return state return state
} catch (error) { } catch (error) {
@ -1736,6 +1737,25 @@ const migrateConfig = {
} catch (error) { } catch (error) {
return state return state
} }
},
'120': (state: RootState) => {
try {
const langMap: Record<string, LanguageCode> = {
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
}
} }
} }

View File

@ -201,7 +201,7 @@ export const initialState: SettingsState = {
assistantsTabSortType: 'list', assistantsTabSortType: 'list',
sendMessageShortcut: 'Enter', sendMessageShortcut: 'Enter',
language: navigator.language as LanguageVarious, language: navigator.language as LanguageVarious,
targetLanguage: 'english' as TranslateLanguageVarious, targetLanguage: 'en-us',
proxyMode: 'system', proxyMode: 'system',
proxyUrl: undefined, proxyUrl: undefined,
userName: '', userName: '',

View File

@ -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 LanguageVarious = 'zh-CN' | 'zh-TW' | 'el-GR' | 'en-US' | 'es-ES' | 'fr-FR' | 'ja-JP' | 'pt-PT' | 'ru-RU'
export type TranslateLanguageVarious = export type TranslateLanguageVarious = LanguageCode
| 'chinese'
| 'chinese-traditional'
| 'greek'
| 'english'
| 'spanish'
| 'french'
| 'japanese'
| 'portuguese'
| 'russian'
export type CodeStyleVarious = 'auto' | string export type CodeStyleVarious = 'auto' | string
@ -489,12 +480,41 @@ export type GenerateImageResponse = {
images: string[] 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 { export interface TranslateHistory {
id: string id: string
sourceText: string sourceText: string
targetText: string targetText: string
sourceLanguage: string sourceLanguage: LanguageCode
targetLanguage: string targetLanguage: LanguageCode
createdAt: string createdAt: string
} }

View File

@ -1,13 +1,15 @@
import { LanguagesEnum } from '@renderer/config/translate'
import { Language, LanguageCode } from '@renderer/types'
import { franc } from 'franc-min' import { franc } from 'franc-min'
import React, { MutableRefObject } from 'react' import React, { MutableRefObject } from 'react'
/** /**
* 使Unicode字符范围检测语言 * 使Unicode字符范围检测语言
* *
* @param {string} text * @param text
* @returns {string} * @returns
*/ */
export const detectLanguageByUnicode = (text: string): string => { export const detectLanguageByUnicode = (text: string): Language => {
const counts = { const counts = {
zh: 0, zh: 0,
ja: 0, ja: 0,
@ -40,8 +42,8 @@ export const detectLanguageByUnicode = (text: string): string => {
} }
} }
if (totalChars === 0) return 'en' if (totalChars === 0) return LanguagesEnum.enUS
let maxLang = 'en' let maxLang = ''
let maxCount = 0 let maxCount = 0
for (const [lang, count] of Object.entries(counts)) { for (const [lang, count] of Object.entries(counts)) {
@ -52,73 +54,68 @@ export const detectLanguageByUnicode = (text: string): string => {
} }
if (maxCount / totalChars < 0.3) { 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 * @param inputText
* @returns {Promise<string>} * @returns
*/ */
export const detectLanguage = async (inputText: string): Promise<string> => { export const detectLanguage = async (inputText: string): Promise<Language> => {
const text = inputText.trim() const text = inputText.trim()
if (!text) return 'any' if (!text) return LanguagesEnum.zhCN
let code: string let lang: Language
// 如果文本长度小于20个字符使用Unicode范围检测 // 如果文本长度小于20个字符使用Unicode范围检测
if (text.length < 20) { if (text.length < 20) {
code = detectLanguageByUnicode(text) lang = detectLanguageByUnicode(text)
} else { } else {
// franc 返回 ISO 639-3 代码 // franc 返回 ISO 639-3 代码
const iso3 = franc(text) const iso3 = franc(text)
const isoMap: Record<string, string> = { const isoMap: Record<string, Language> = {
cmn: 'zh', cmn: LanguagesEnum.zhCN,
jpn: 'ja', jpn: LanguagesEnum.jaJP,
kor: 'ko', kor: LanguagesEnum.koKR,
rus: 'ru', rus: LanguagesEnum.ruRU,
ara: 'ar', ara: LanguagesEnum.arAR,
spa: 'es', spa: LanguagesEnum.esES,
fra: 'fr', fra: LanguagesEnum.frFR,
deu: 'de', deu: LanguagesEnum.deDE,
ita: 'it', ita: LanguagesEnum.itIT,
por: 'pt', por: LanguagesEnum.ptPT,
eng: 'en', eng: LanguagesEnum.enUS,
pol: 'pl', pol: LanguagesEnum.plPL,
tur: 'tr', tur: LanguagesEnum.trTR,
tha: 'th', tha: LanguagesEnum.thTH,
vie: 'vi', vie: LanguagesEnum.viVN,
ind: 'id', ind: LanguagesEnum.idID,
urd: 'ur', urd: LanguagesEnum.urPK,
zsm: 'ms' zsm: LanguagesEnum.msMY
} }
code = isoMap[iso3] || 'en' lang = isoMap[iso3] || LanguagesEnum.enUS
} }
// 映射到应用使用的语言键 return lang
const languageMap: Record<string, string> = {
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'
} }
/** /**
@ -127,10 +124,13 @@ export const detectLanguage = async (inputText: string): Promise<string> => {
* @param languagePair * @param languagePair
* @returns * @returns
*/ */
export const getTargetLanguageForBidirectional = (sourceLanguage: string, languagePair: [string, string]): string => { export const getTargetLanguageForBidirectional = (
if (sourceLanguage === languagePair[0]) { sourceLanguage: Language,
languagePair: [Language, Language]
): Language => {
if (sourceLanguage.langCode === languagePair[0].langCode) {
return languagePair[1] return languagePair[1]
} else if (sourceLanguage === languagePair[1]) { } else if (sourceLanguage.langCode === languagePair[1].langCode) {
return languagePair[0] return languagePair[0]
} }
return languagePair[0] !== sourceLanguage ? languagePair[0] : languagePair[1] return languagePair[0] !== sourceLanguage ? languagePair[0] : languagePair[1]
@ -142,8 +142,8 @@ export const getTargetLanguageForBidirectional = (sourceLanguage: string, langua
* @param languagePair * @param languagePair
* @returns * @returns
*/ */
export const isLanguageInPair = (sourceLanguage: string, languagePair: [string, string]): boolean => { export const isLanguageInPair = (sourceLanguage: Language, languagePair: [Language, Language]): boolean => {
return [languagePair[0], languagePair[1]].includes(sourceLanguage) return [languagePair[0].langCode, languagePair[1].langCode].includes(sourceLanguage.langCode)
} }
/** /**
@ -155,11 +155,11 @@ export const isLanguageInPair = (sourceLanguage: string, languagePair: [string,
* @returns * @returns
*/ */
export const determineTargetLanguage = ( export const determineTargetLanguage = (
sourceLanguage: string, sourceLanguage: Language,
targetLanguage: string, targetLanguage: Language,
isBidirectional: boolean, isBidirectional: boolean,
bidirectionalPair: [string, string] bidirectionalPair: [Language, Language]
): { success: boolean; language?: string; errorType?: 'same_language' | 'not_in_pair' } => { ): { success: boolean; language?: Language; errorType?: 'same_language' | 'not_in_pair' } => {
if (isBidirectional) { if (isBidirectional) {
if (!isLanguageInPair(sourceLanguage, bidirectionalPair)) { if (!isLanguageInPair(sourceLanguage, bidirectionalPair)) {
return { success: false, errorType: 'not_in_pair' } return { success: false, errorType: 'not_in_pair' }
@ -169,7 +169,7 @@ export const determineTargetLanguage = (
language: getTargetLanguageForBidirectional(sourceLanguage, bidirectionalPair) language: getTargetLanguageForBidirectional(sourceLanguage, bidirectionalPair)
} }
} else { } else {
if (sourceLanguage === targetLanguage) { if (sourceLanguage.langCode === targetLanguage.langCode) {
return { success: false, errorType: 'same_language' } return { success: false, errorType: 'same_language' }
} }
return { success: true, language: targetLanguage } return { success: true, language: targetLanguage }
@ -228,3 +228,21 @@ export const createOutputScrollHandler = (
handleScrollSync(e.currentTarget, inputEl, isProgrammaticScrollRef) 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
}

View File

@ -1,13 +1,14 @@
import { SwapOutlined } from '@ant-design/icons' import { SwapOutlined } from '@ant-design/icons'
import Scrollbar from '@renderer/components/Scrollbar' 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 db from '@renderer/databases'
import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { fetchTranslate } from '@renderer/services/ApiService' import { fetchTranslate } from '@renderer/services/ApiService'
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { Assistant } from '@renderer/types' import { Assistant, Language } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils' 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 { isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useRef, useState } from 'react' import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -18,11 +19,11 @@ interface Props {
text: string text: string
} }
let _targetLanguage = 'chinese' let _targetLanguage = (await db.settings.get({ id: 'translate:target:language' }))?.value || LanguagesEnum.zhCN
const Translate: FC<Props> = ({ text }) => { const Translate: FC<Props> = ({ text }) => {
const [result, setResult] = useState('') const [result, setResult] = useState('')
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) const [targetLanguage, setTargetLanguage] = useState<Language>(_targetLanguage)
const { translateModel } = useDefaultModel() const { translateModel } = useDefaultModel()
const { t } = useTranslation() const { t } = useTranslation()
const translatingRef = useRef(false) const translatingRef = useRef(false)
@ -37,8 +38,7 @@ const Translate: FC<Props> = ({ text }) => {
try { try {
translatingRef.current = true translatingRef.current = true
const targetLang = await db.settings.get({ id: 'translate:target:language' }) const assistant: Assistant = getDefaultTranslateAssistant(targetLanguage, text)
const assistant: Assistant = getDefaultTranslateAssistant(targetLang?.value || targetLanguage, text)
// const message: Message = { // const message: Message = {
// id: uuid(), // id: uuid(),
// role: 'user', // role: 'user',
@ -64,7 +64,7 @@ const Translate: FC<Props> = ({ text }) => {
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
const targetLang = await db.settings.get({ id: 'translate:target:language' }) 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<Props> = ({ text }) => {
<SwapOutlined /> <SwapOutlined />
<Select <Select
showSearch showSearch
value={targetLanguage} value={targetLanguage.langCode}
style={{ maxWidth: 200, minWidth: 130, flex: 1 }} style={{ maxWidth: 200, minWidth: 130, flex: 1 }}
optionFilterProp="label" optionFilterProp="label"
options={TranslateLanguageOptions} options={translateLanguageOptions.map((option) => ({
value: option.langCode,
label: option.emoji + ' ' + option.label()
}))}
onChange={async (value) => { onChange={async (value) => {
await db.settings.put({ id: 'translate:target:language', value }) await db.settings.put({ id: 'translate:target:language', value })
setTargetLanguage(value) setTargetLanguage(getLanguageByLangcode(value))
}} }}
optionRender={(option) => (
<Space>
<span role="img" aria-label={option.data.label}>
{option.data.emoji}
</span>
{option.label}
</Space>
)}
/> />
</MenuContainer> </MenuContainer>
<Main> <Main>

View File

@ -1,6 +1,6 @@
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import CopyButton from '@renderer/components/CopyButton' import CopyButton from '@renderer/components/CopyButton'
import { TranslateLanguageOptions, translateLanguageOptions } from '@renderer/config/translate' import { LanguagesEnum, translateLanguageOptions } from '@renderer/config/translate'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
@ -11,11 +11,11 @@ import {
getDefaultTopic, getDefaultTopic,
getTranslateModel getTranslateModel
} from '@renderer/services/AssistantService' } from '@renderer/services/AssistantService'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Language, Topic } from '@renderer/types'
import type { ActionItem } from '@renderer/types/selectionTypes' import type { ActionItem } from '@renderer/types/selectionTypes'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { abortCompletion } from '@renderer/utils/abortController' import { abortCompletion } from '@renderer/utils/abortController'
import { detectLanguage } from '@renderer/utils/translate' import { detectLanguage, getLanguageByLangcode } from '@renderer/utils/translate'
import { Select, Space, Tooltip } from 'antd' import { Select, Space, Tooltip } from 'antd'
import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react' import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -33,8 +33,8 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { translateModelPrompt, language } = useSettings() const { translateModelPrompt, language } = useSettings()
const [targetLanguage, setTargetLanguage] = useState('') const [targetLanguage, setTargetLanguage] = useState<Language>(LanguagesEnum.enUS)
const [alterLanguage, setAlterLanguage] = useState('') const [alterLanguage, setAlterLanguage] = useState<Language>(LanguagesEnum.zhCN)
const [error, setError] = useState('') const [error, setError] = useState('')
const [showOriginal, setShowOriginal] = useState(false) const [showOriginal, setShowOriginal] = useState(false)
@ -52,24 +52,24 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' }) const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
let targetLang = '' let targetLang: Language
let alterLang = '' let alterLang: Language
if (!biDirectionLangPair || !biDirectionLangPair.value[0]) { if (!biDirectionLangPair || !biDirectionLangPair.value[0]) {
const lang = TranslateLanguageOptions.find((lang) => lang.langCode?.toLowerCase() === language.toLowerCase()) const lang = translateLanguageOptions.find((lang) => lang.langCode?.toLowerCase() === language.toLowerCase())
if (lang) { if (lang) {
targetLang = lang.value targetLang = lang
} else { } else {
targetLang = 'chinese' targetLang = LanguagesEnum.zhCN
} }
} else { } else {
targetLang = biDirectionLangPair.value[0] targetLang = getLanguageByLangcode(biDirectionLangPair.value[0])
} }
if (!biDirectionLangPair || !biDirectionLangPair.value[1]) { if (!biDirectionLangPair || !biDirectionLangPair.value[1]) {
alterLang = 'english' alterLang = LanguagesEnum.enUS
} else { } else {
alterLang = biDirectionLangPair.value[1] alterLang = getLanguageByLangcode(biDirectionLangPair.value[1])
} }
setTargetLanguage(targetLang) setTargetLanguage(targetLang)
@ -120,8 +120,8 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
const sourceLanguage = await detectLanguage(action.selectedText) const sourceLanguage = await detectLanguage(action.selectedText)
let translateLang = '' let translateLang: Language
if (sourceLanguage === targetLanguage) { if (sourceLanguage.langCode === targetLanguage.langCode) {
translateLang = alterLanguage translateLang = alterLanguage
} else { } else {
translateLang = targetLanguage translateLang = targetLanguage
@ -129,7 +129,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
// Initialize prompt content // Initialize prompt content
const userContent = translateModelPrompt const userContent = translateModelPrompt
.replaceAll('{{target_language}}', translateLang) .replaceAll('{{target_language}}', translateLang.value)
.replaceAll('{{text}}', action.selectedText) .replaceAll('{{text}}', action.selectedText)
processMessages(assistantRef.current, topicRef.current, userContent, setAskId, onStream, onFinish, onError) processMessages(assistantRef.current, topicRef.current, userContent, setAskId, onStream, onFinish, onError)
@ -147,11 +147,11 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
return lastAssistantMessage ? <MessageContent key={lastAssistantMessage.id} message={lastAssistantMessage} /> : null return lastAssistantMessage ? <MessageContent key={lastAssistantMessage.id} message={lastAssistantMessage} /> : null
}, [allMessages]) }, [allMessages])
const handleChangeLanguage = (targetLanguage: string, alterLanguage: string) => { const handleChangeLanguage = (targetLanguage: Language, alterLanguage: Language) => {
setTargetLanguage(targetLanguage) setTargetLanguage(targetLanguage)
setAlterLanguage(alterLanguage) setAlterLanguage(alterLanguage)
db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage, alterLanguage] }) db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage.langCode, alterLanguage.langCode] })
} }
const handlePause = () => { const handlePause = () => {
@ -177,46 +177,46 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
<ArrowRightToLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} /> <ArrowRightToLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
<Tooltip placement="bottom" title={t('translate.target_language')} arrow> <Tooltip placement="bottom" title={t('translate.target_language')} arrow>
<Select <Select
value={targetLanguage} value={targetLanguage.langCode}
style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }} style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }}
listHeight={160} listHeight={160}
title={t('translate.target_language')} title={t('translate.target_language')}
optionFilterProp="label" optionFilterProp="label"
options={translateLanguageOptions().map((lang) => ({ options={translateLanguageOptions.map((lang) => ({
value: lang.value, value: lang.langCode,
label: ( label: (
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}> <span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji} {lang.emoji}
</span> </span>
<Space.Compact block>{lang.label}</Space.Compact> <Space.Compact block>{lang.label()}</Space.Compact>
</Space.Compact> </Space.Compact>
) )
}))} }))}
onChange={(value) => handleChangeLanguage(value, alterLanguage)} onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)}
disabled={isLoading} disabled={isLoading}
/> />
</Tooltip> </Tooltip>
<ArrowRightFromLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} /> <ArrowRightFromLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
<Tooltip placement="bottom" title={t('translate.alter_language')} arrow> <Tooltip placement="bottom" title={t('translate.alter_language')} arrow>
<Select <Select
value={alterLanguage} value={alterLanguage.langCode}
style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }} style={{ minWidth: 80, maxWidth: 200, flex: 'auto' }}
listHeight={160} listHeight={160}
title={t('translate.alter_language')} title={t('translate.alter_language')}
optionFilterProp="label" optionFilterProp="label"
options={translateLanguageOptions().map((lang) => ({ options={translateLanguageOptions.map((lang) => ({
value: lang.value, value: lang.langCode,
label: ( label: (
<Space.Compact direction="horizontal" block> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}> <span role="img" aria-label={lang.emoji} style={{ marginRight: 8 }}>
{lang.emoji} {lang.emoji}
</span> </span>
<Space.Compact block>{lang.label}</Space.Compact> <Space.Compact block>{lang.label()}</Space.Compact>
</Space.Compact> </Space.Compact>
) )
}))} }))}
onChange={(value) => handleChangeLanguage(targetLanguage, value)} onChange={(value) => handleChangeLanguage(targetLanguage, getLanguageByLangcode(value))}
disabled={isLoading} disabled={isLoading}
/> />
</Tooltip> </Tooltip>