fix: i18n and lint

This commit is contained in:
kangfenmao 2025-03-22 22:28:02 +08:00
parent 13747a585a
commit 16ac419b9b
15 changed files with 129 additions and 101 deletions

View File

@ -1,50 +1,51 @@
/** /**
* OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts * OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
*/ */
// OCOOL API KEY // OCOOL API KEY
const OCOOL_API_KEY = process.env.OCOOL_API_KEY; const OCOOL_API_KEY = process.env.OCOOL_API_KEY
const INDEX = [ const INDEX = [
// 语言的名称 代码 用来翻译的模型 // 语言的名称 代码 用来翻译的模型
{name: "France", code: "fr-fr", model: "qwen2.5-32b-instruct"}, { name: 'France', code: 'fr-fr', model: 'qwen2.5-32b-instruct' },
{name: "Spanish", code: "es-es", model: "qwen2.5-32b-instruct"}, { name: 'Spanish', code: 'es-es', model: 'qwen2.5-32b-instruct' },
{name: "Portuguese", code: "pt-pt", model: "qwen2.5-72b-instruct"}, { name: 'Portuguese', code: 'pt-pt', model: 'qwen2.5-72b-instruct' },
{name: "Greek", code: "el-gr", model: "qwen-turbo" }, { name: 'Greek', code: 'el-gr', model: 'qwen-turbo' }
] ]
const fs = require("fs"); const fs = require('fs')
import OpenAI from "openai"; import OpenAI from 'openai'
const zh = JSON.parse(fs.readFileSync("src/renderer/src/i18n/locales/zh-cn.json", 'utf8')) as object const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
const openai = new OpenAI({ const openai = new OpenAI({
apiKey: OCOOL_API_KEY, apiKey: OCOOL_API_KEY,
baseURL: "https://one.ocoolai.com/v1" baseURL: 'https://one.ocoolai.com/v1'
}); })
// 递归遍历翻译 // 递归遍历翻译
async function translate(zh:object, obj:object, target: string, model:string, updateFile){ async function translate(zh: object, obj: object, target: string, model: string, updateFile) {
const texts: {[key:string]:string} = {} const texts: { [key: string]: string } = {}
for(let e in zh){ for (const e in zh) {
if(typeof zh[e]=="object"){ // 遍历下一层 if (typeof zh[e] == 'object') {
if(!obj[e] || typeof obj[e]!="object") obj[e] = {} // 遍历下一层
await translate(zh[e], obj[e], target, model, updateFile) if (!obj[e] || typeof obj[e] != 'object') obj[e] = {}
}else{ await translate(zh[e], obj[e], target, model, updateFile)
// 加入到本层待翻译列表 } else {
if(!obj[e] || typeof obj[e]!="string"){ // 加入到本层待翻译列表
texts[e] = zh[e] if (!obj[e] || typeof obj[e] != 'string') {
} texts[e] = zh[e]
} }
} }
if(Object.keys(texts).length>0){ }
const completion = await openai.chat.completions.create({ if (Object.keys(texts).length > 0) {
model: model, const completion = await openai.chat.completions.create({
response_format: {type:"json_object"}, model: model,
messages: [ response_format: { type: 'json_object' },
{role:"user", content:` messages: [
{
role: 'user',
content: `
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on Russian language corpora, you are proficient in using the Russian language. You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on Russian language corpora, you are proficient in using the Russian language.
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the Russian language. Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the Russian language.
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted. When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
@ -53,22 +54,28 @@ Output in JSON.
INPUT INPUT
###################################################### ######################################################
${JSON.stringify({ ${JSON.stringify({
"confirm": "确定要备份数据吗?", confirm: '确定要备份数据吗?',
"select_model": "选择模型", select_model: '选择模型',
"title": "文件", title: '文件',
"deeply_thought": "已深度思考(用时 {{secounds}} 秒)", deeply_thought: '已深度思考(用时 {{secounds}} 秒)'
})} })}
###################################################### ######################################################
MAKE SURE TO OUTPUT IN Russian. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE. MAKE SURE TO OUTPUT IN Russian. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
###################################################### ######################################################
`}, `
{role:"assistant", content:JSON.stringify({ },
"confirm": "Подтвердите резервное копирование данных?", {
"select_model": "Выберите Модель", role: 'assistant',
"title": "Файл", content: JSON.stringify({
"deeply_thought": "Глубоко продумано (заняло {{seconds}} секунд)" confirm: 'Подтвердите резервное копирование данных?',
})}, select_model: 'Выберите Модель',
{role:"user", content:` title: 'Файл',
deeply_thought: 'Глубоко продумано (заняло {{seconds}} секунд)'
})
},
{
role: 'user',
content: `
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on ${target} language corpora, you are proficient in using the ${target} language. You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on ${target} language corpora, you are proficient in using the ${target} language.
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the ${target} language. Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the ${target} language.
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted. When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
@ -80,43 +87,44 @@ ${JSON.stringify(texts)}
###################################################### ######################################################
MAKE SURE TO OUTPUT IN ${target}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE. MAKE SURE TO OUTPUT IN ${target}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
###################################################### ######################################################
`} `
]
})
// 添加翻译后的键值,并打印错译漏译内容
try{
const result = JSON.parse(completion.choices[0].message.content!)
for(let e in texts){
if(result[e] && typeof result[e] === 'string'){
obj[e] = result[e]
}else{
console.log("[warning]", `missing value "${e}" in ${target} translation`)
}
}
}catch(e){
console.log("[error]", e)
for(let e in texts){
console.log("[warning]", `missing value "${e}" in ${target} translation`)
}
} }
} ]
// 删除多余的键值 })
for(let e in obj){ // 添加翻译后的键值,并打印错译漏译内容
if(!zh[e]){ try {
delete obj[e] const result = JSON.parse(completion.choices[0].message.content!)
for (const e in texts) {
if (result[e] && typeof result[e] === 'string') {
obj[e] = result[e]
} else {
console.log('[warning]', `missing value "${e}" in ${target} translation`)
} }
}
} catch (e) {
console.log('[error]', e)
for (const e in texts) {
console.log('[warning]', `missing value "${e}" in ${target} translation`)
}
} }
// 更新文件 }
updateFile() // 删除多余的键值
for (const e in obj) {
if (!zh[e]) {
delete obj[e]
}
}
// 更新文件
updateFile()
} }
(async ()=>{ ;(async () => {
for(let {name, code, model} of INDEX){ for (const { name, code, model } of INDEX) {
const obj = fs.existsSync(`src/renderer/src/i18n/locales/${code}.json`) const obj = fs.existsSync(`src/renderer/src/i18n/locales/${code}.json`)
? JSON.parse(fs.readFileSync(`src/renderer/src/i18n/locales/${code}.json`, 'utf8')) ? JSON.parse(fs.readFileSync(`src/renderer/src/i18n/locales/${code}.json`, 'utf8'))
: {} : {}
await translate(zh, obj, name, model, ()=>{ await translate(zh, obj, name, model, () => {
fs.writeFileSync(`src/renderer/src/i18n/locales/${code}.json`, JSON.stringify(obj, null, 4), 'utf8') fs.writeFileSync(`src/renderer/src/i18n/locales/${code}.json`, JSON.stringify(obj, null, 4), 'utf8')
}) })
} }
})() })()

View File

@ -1,8 +1,12 @@
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { LanguageVarious } from '@renderer/types' import { LanguageVarious } from '@renderer/types'
import { ConfigProvider, theme } from 'antd' import { ConfigProvider, theme } from 'antd'
import elGR from 'antd/locale/el_GR'
import enUS from 'antd/locale/en_US' import enUS from 'antd/locale/en_US'
import esES from 'antd/locale/es_ES'
import frFR from 'antd/locale/fr_FR'
import jaJP from 'antd/locale/ja_JP' import jaJP from 'antd/locale/ja_JP'
import ptPT from 'antd/locale/pt_PT'
import ruRU from 'antd/locale/ru_RU' import ruRU from 'antd/locale/ru_RU'
import zhCN from 'antd/locale/zh_CN' import zhCN from 'antd/locale/zh_CN'
import zhTW from 'antd/locale/zh_TW' import zhTW from 'antd/locale/zh_TW'
@ -53,7 +57,14 @@ function getAntdLocale(language: LanguageVarious) {
return ruRU return ruRU
case 'ja-JP': case 'ja-JP':
return jaJP return jaJP
case 'el-GR':
return elGR
case 'es-ES':
return esES
case 'fr-FR':
return frFR
case 'pt-PT':
return ptPT
default: default:
return zhCN return zhCN
} }

View File

@ -1,5 +1,6 @@
import i18n from 'i18next' import i18n from 'i18next'
import { initReactI18next } from 'react-i18next' import { initReactI18next } from 'react-i18next'
import elGR from './locales/el-gr.json' import elGR from './locales/el-gr.json'
import enUS from './locales/en-us.json' import enUS from './locales/en-us.json'
import esES from './locales/es-es.json' import esES from './locales/es-es.json'
@ -19,7 +20,7 @@ const resources = {
'pt-PT': ptPT, 'pt-PT': ptPT,
'ru-RU': ruRU, 'ru-RU': ruRU,
'zh-CN': zhCN, 'zh-CN': zhCN,
'zh-TW': zhTW, 'zh-TW': zhTW
} }
export const getLanguage = () => { export const getLanguage = () => {

View File

@ -556,7 +556,7 @@
"string": "文本" "string": "文本"
}, },
"pinned": "已固定", "pinned": "已固定",
"rerank_model": "重排模型", "rerank_model": "重排模型",
"rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})", "rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})",
"rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加", "rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"search": "搜索模型...", "search": "搜索模型...",

View File

@ -565,7 +565,7 @@
"string": "文字" "string": "文字"
}, },
"pinned": "已固定", "pinned": "已固定",
"rerank_model": "重排模型", "rerank_model": "重排模型",
"rerank_model_support_provider": "目前重排序模型僅支持部分服務商 ({{provider}})", "rerank_model_support_provider": "目前重排序模型僅支持部分服務商 ({{provider}})",
"rerank_model_tooltip": "在設定->模型服務中點擊管理按鈕添加", "rerank_model_tooltip": "在設定->模型服務中點擊管理按鈕添加",
"search": "搜尋模型...", "search": "搜尋模型...",

View File

@ -15,8 +15,8 @@ import { UploadOutlined } from '@ant-design/icons'
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 TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { TranslateLanguageOptions } from '@renderer/config/translate'
import { isReasoningModel } from '@renderer/config/models' import { isReasoningModel } from '@renderer/config/models'
import { TranslateLanguageOptions } from '@renderer/config/translate'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService' import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
@ -24,7 +24,6 @@ import { translateText } from '@renderer/services/TranslateService'
import { Message, Model } from '@renderer/types' import { Message, Model } from '@renderer/types'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils'
import { withMessageThought } from '@renderer/utils/formats'
import { import {
exportMarkdownToJoplin, exportMarkdownToJoplin,
exportMarkdownToNotion, exportMarkdownToNotion,
@ -32,12 +31,13 @@ import {
exportMessageAsMarkdown, exportMessageAsMarkdown,
messageToMarkdown messageToMarkdown
} from '@renderer/utils/export' } from '@renderer/utils/export'
import { withMessageThought } from '@renderer/utils/formats'
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd' import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { clone } from 'lodash'
import { FC, memo, useCallback, useMemo, useState } from 'react' import { FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { clone } from 'lodash'
interface Props { interface Props {
message: Message message: Message
@ -258,7 +258,7 @@ const MessageMenubar: FC<Props> = (props) => {
] ]
} }
], ],
[message, messageContainerRef, onEdit, onNewBranch, t] [message, messageContainerRef, onEdit, onNewBranch, t, topic.name]
) )
const onRegenerate = async (e: React.MouseEvent | undefined) => { const onRegenerate = async (e: React.MouseEvent | undefined) => {

View File

@ -26,8 +26,8 @@ import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components' import styled from 'styled-components'
import ChatNavigation from './ChatNavigation' import ChatNavigation from './ChatNavigation'
import MessageGroup from './MessageGroup'
import MessageAnchorLine from './MessageAnchorLine' import MessageAnchorLine from './MessageAnchorLine'
import MessageGroup from './MessageGroup'
import NarrowLayout from './NarrowLayout' import NarrowLayout from './NarrowLayout'
import NewTopicButton from './NewTopicButton' import NewTopicButton from './NewTopicButton'
import Prompt from './Prompt' import Prompt from './Prompt'

View File

@ -24,6 +24,7 @@ import {
setFontSize, setFontSize,
setMathEngine, setMathEngine,
setMessageFont, setMessageFont,
setMessageNavigation,
setMessageStyle, setMessageStyle,
setMultiModelMessageStyle, setMultiModelMessageStyle,
setPasteLongTextAsFile, setPasteLongTextAsFile,
@ -31,7 +32,6 @@ import {
setRenderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens, setShowInputEstimatedTokens,
setShowMessageDivider, setShowMessageDivider,
setMessageNavigation,
setThoughtAutoCollapse setThoughtAutoCollapse
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types' import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'

View File

@ -584,10 +584,10 @@ const ModelInfo = styled.div`
.model-row { .model-row {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px;
} }
.label-column { .label-column {
width: 80px;
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -12,13 +12,13 @@ import {
setWebdavSyncInterval as _setWebdavSyncInterval, setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser setWebdavUser as _setWebdavUser
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { formatFileSize } from '@renderer/utils'
import { Button, Input, Modal, Select, Spin, Tooltip } from 'antd' import { Button, Input, Modal, Select, Spin, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import { formatFileSize } from '@renderer/utils'
interface BackupFile { interface BackupFile {
fileName: string fileName: string

View File

@ -96,13 +96,13 @@ const GeneralSettings: FC = () => {
const languagesOptions: { value: LanguageVarious; label: string; flag: string }[] = [ const languagesOptions: { value: LanguageVarious; label: string; flag: string }[] = [
{ value: 'zh-CN', label: '中文', flag: '🇨🇳' }, { value: 'zh-CN', label: '中文', flag: '🇨🇳' },
{ value: 'zh-TW', label: '中文(繁体)', flag: '🇭🇰' }, { value: 'zh-TW', label: '中文(繁体)', flag: '🇭🇰' },
{ value: 'el-GR', label: 'Ελληνικά', flag: '🇬🇷' },
{ value: 'en-US', label: 'English', flag: '🇺🇸' }, { value: 'en-US', label: 'English', flag: '🇺🇸' },
{ value: 'ja-JP', label: '日本語', flag: '🇯🇵' },
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' },
{ value: 'el-GR', label: 'Ελληνικά', flag: '🇬🇷' },
{ value: 'es-ES', label: 'Español', flag: '🇪🇸' }, { value: 'es-ES', label: 'Español', flag: '🇪🇸' },
{ value: 'fr-FR', label: 'Français', flag: '🇫🇷' }, { value: 'fr-FR', label: 'Français', flag: '🇫🇷' },
{ value: 'ja-JP', label: '日本語', flag: '🇯🇵' }, { value: 'pt-PT', label: 'Português', flag: '🇵🇹' }
{ value: 'pt-PT', label: 'Português', flag: '🇵🇹' },
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' }
] ]
return ( return (

View File

@ -168,7 +168,6 @@ const initialState: SettingsState = {
obsidianTages: '', obsidianTages: '',
joplinToken: '', joplinToken: '',
joplinUrl: '' joplinUrl: ''
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({

View File

@ -201,7 +201,16 @@ 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 = 'chinese' | 'chinese-traditional' | 'greek' | 'english' | 'spanish' | 'french' | 'japanese' | 'portuguese' | 'russian' export type TranslateLanguageVarious =
| 'chinese'
| 'chinese-traditional'
| 'greek'
| 'english'
| 'spanish'
| 'french'
| 'japanese'
| 'portuguese'
| 'russian'
export type CodeStyleVarious = BuiltinTheme | 'auto' export type CodeStyleVarious = BuiltinTheme | 'auto'