使用electron重构翻译软件

之前做了一个翻译软件专门用来实时翻译和提供提示词扩展,借助了智谱清言的智能体。

用了一段时间后发现还是有比较明显的缺陷。比如生成的文本没法直观地与原文对照。

碍于我py代码水平实在不咋样,改功能只能通过问ai,这样效率太低了。所以打算换成前端语言来重构。

electron是一个桌面端开发框架

electron打包包体大小基本都100m起步,毕竟每个软件都自带一个小浏览器。理论上来说我这种小工具不适合用electron开发。

功能需求

  • 置顶
  • 关键词翻译
  • 鼠标选中输入的提示词时,会在结果页面高亮显示对应的翻译结果(或者相反)

Electron Fiddle

因为我的需求只是做一个应用,所以根据官方推荐,安装了Electron Fiddle,直接能使用Electron。并且我看了下介绍这个也可以打包应用,那么有何不可呢。

官网:https://www.electronjs.org/fiddle

下载完直接双击就能打开

image-20240920180052253

开发

经过一晚上的拷打ai,终于完成了初步的效果实现。以下是代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单翻译软件</title>
<!-- 引入 styles.css 文件 -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h2>百度API翻译</h2>
<div class="input-output-container">
<div id="inputField" class="input-field" contenteditable="true"></div>
<div id="outputWindow" class="output-window" contenteditable="false" spellcheck="false">
<!-- 翻译结果将显示在这里 -->
</div>
</div>
<!-- 引入 renderer.js 文件 -->
<script src="renderer.js"></script>
</body>
</html>

main.js

const { app, BrowserWindow,ipcMain,Menu} = require('electron');
let mainWindow;
let isAlwaysOnTop = false; // 跟踪窗口是否置顶
const https = require('https');


//创建窗口代码
function createWindow() {
mainWindow = new BrowserWindow({
width: 730,
height: 400,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
}
);

mainWindow.loadFile('index.html');
mainWindow.on('closed', function () {
mainWindow = null;
});

//创建菜单功能函数
function updateMenu() {
const menuTemplate = [
{ role: 'reload' },
{ role: 'toggledevtools' },
{
label: isAlwaysOnTop ? '📌置顶(c+t)' : '置顶(c+t)',
type: 'checkbox',
accelerator: 'CommandOrControl+T',
checked: isAlwaysOnTop,
click: () => {
isAlwaysOnTop = !isAlwaysOnTop;
mainWindow.setAlwaysOnTop(isAlwaysOnTop);
updateMenu(); // 重新构建菜单
}
}
];

const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
}
updateMenu(); // 初始构建菜单

}

app.on('ready', createWindow);

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', function () {
if (mainWindow === null) {
createWindow();
}
});

// 处理翻译请求
ipcMain.on('translate', (event, text) => {
const appid = '20240409002018364';
const secretKey = 'hKiK_xVALpvs0qXdLCp7';
const from = 'auto'; // 源语言
const to = 'en'; // 目标语言
const salt = (new Date).getTime();
const query = text;

// 使用MD5生成签名
const md5 = require('md5');
const sign = md5(appid + query + salt + secretKey);

// 构建请求参数
const params = {
q: query,
appid: appid,
salt: salt,
from: from,
to: to,
sign: sign
};

// 将参数转换为URL编码的字符串
const queryStr = Object.keys(params).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');

// 发送请求到百度翻译API
const options = {
hostname: 'api.fanyi.baidu.com',
path: '/api/trans/vip/translate?' + queryStr,
method: 'GET'
};

const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
// 解析返回的JSON数据
const result = JSON.parse(data);
if (result && result.trans_result) {
// 发送翻译结果回渲染进程
event.reply('translated', result.trans_result[0].dst);
}
});
});

req.on('error', (e) => {
console.error(e);
});

req.end();
});

renderer.js

const { ipcRenderer } = require('electron');

document.addEventListener('DOMContentLoaded', () => {
const inputField = document.querySelector('.input-field');
const outputWindow = document.querySelector('.output-window');

// 实时翻译功能
inputField.addEventListener('input', () => {
const text = inputField.innerText; // 使用innerText获取div的内容
if (text) {
// 向主进程发送翻译请求
ipcRenderer.send('translate', text);
}
});

// 监听来自主进程的翻译结果
ipcRenderer.on('translated', (event, translatedText) => {
// 清空输出窗口
outputWindow.innerHTML = '';
// 将翻译文本分割成单词并包裹在span标签中
const words = translatedText.split(','); // 假设单词之间是用,分隔的
words.forEach((word, index) => {
const span = document.createElement('span');
span.className = 'word'; // 应用CSS样式
span.textContent = word + ','; // 添加单词和空格
span.dataset.index = index; // 添加数据属性以跟踪单词索引
span.addEventListener('mouseover', highlightCorrespondingWord); // 为span添加鼠标悬停事件监听器
span.addEventListener('mouseout', removeHighlight); // 为span添加鼠标移出事件监听器
outputWindow.appendChild(span);
});
});
});

function highlightCorrespondingWord(event) {
const highlightedWord = event.target.textContent.trim().replace(/, /g, '');
const spans = document.querySelectorAll('.output-window .word');
const index = event.target.dataset.index; // 获取单词的索引

// 高亮显示outputWindow中的单词
spans.forEach(span => {
if (span.dataset.index === index) {
span.classList.add('active');
}
});

// 高亮显示inputField中的对应单词
const inputWords = inputField.innerText.split(',');
const correspondingWord = inputWords[index];
const range = document.createRange();
const sel = window.getSelection();
let found = false;

for (let i = 0; i < inputWords.length; i++) {
const word = inputWords[i];
if (word === correspondingWord && !found) {
found = true;
range.setStart(inputField.childNodes[0], inputField.childNodes[0].textContent.indexOf(word));
range.setEnd(inputField.childNodes[0], inputField.childNodes[0].textContent.indexOf(word) + word.length);
sel.removeAllRanges();
sel.addRange(range);
break;
}
}
}

function removeHighlight() {
const spans = document.querySelectorAll('.output-window .word');
spans.forEach(span => {
span.classList.remove('active');
});
window.getSelection().removeAllRanges();
}

// 添加一个新的CSS样式用于激活高亮显示
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
.word.active, .input-field::selection {
background-color: yellow;
}
`;
document.head.appendChild(style);

css

body {
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 0 30px 30px 30px;
}
h2 {
text-align: center;
}
.input-output-container {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 10px;
}
.input-field {
width: 45%; /* 根据需要调整宽度 */
margin-right: 10px; /* 添加一些间隔 */
min-height: 200px; /* 设置最小高度 */
padding: 10px; /* 添加内边距 */
border: 1px solid #ccc; /* 添加边框以区分区域 */
text-align: left; /* 默认情况下,输出窗口也设置为顶格居左对齐 */
overflow-y: auto; /* 如果内容超出,允许垂直滚动 */
outline: none; /* 移除焦点时的轮廓线 */
}
.output-window {
width: 45%; /* 根据需要调整宽度 */
margin-left: 10px; /* 添加一些间隔 */
border: 1px solid #ccc; /* 添加边框以区分区域 */
min-height: 200px; /* 设置最小高度 */
padding: 10px; /* 添加内边距 */
text-align: left; /* 默认情况下,输出窗口也设置为顶格居左对齐 */
}

主页面

image-20240921055513428

这次没有用智谱清言的ai翻译,而是直接采用百度翻译的api。

因为我考虑到只是关键词翻译的话不需要用到ai,而且扩展关键词功能有点鸡肋,平时也不怎么用。

亮点

  • 界面简洁(完全没有搞排版和颜色搭配,功能太少了主要是)
  • 实时翻译,省去了点翻译按钮或者是按回车的操作。
  • 鼠标移到生成的单词中会高亮显示,并且同步到对应的输入框中的词语。(我专门为了这个功能而重构的程序)

image-20240921060324363

还未完成的内容

  • 自动识别输入的是中或英文,并进行翻译…
  • 点击复制结果
  • 一键复制结果

2024年9月21日 06:04:37,该睡觉了。


最终成品

又经过了一晚上的鏖战终于达到了我满意的效果。先展示下画面和功能。

主页面

image-20240923053330639

鼠标移上来会显示按键提示

image-20240923053441013

功能

鼠标移到输出窗口的单词,可以中英对照查看,目前只支持中英文互译。

image-20240923053643542

点击单个词组可以进行单独复制。

image-20240923053649955

仔细看输出窗口的右下角有一个按钮,它可以复制所有的内容。

image-20240923053751957

制作过程遇到的问题很多,不过都一一解决了。

比如英译中的过程中,由于中文词语是一整个字符串,刚开始没做分割,没法与左侧对照显示。解决方法是用正则重新加了判断。

英译中

image-20240923053822178

关于百度翻译的api我也单独用一个config.json来收集,这样就不用担心自己的key的安全问题。用户自己申请key就能够使用。

只花了两天时间(昨天没写)就能够完成这个项目让我有了不少成就感。

成品源码就先不发了,留一个github链接

https://github.com/Niaoyu00/AI-Prompt-Word-Translation

使用

  • 我也是第一次开发electeon,直接用的Electron Fiddle,如果你不知道怎么用这些源码,可以下载个Electron Fiddle,很方便。

  • 需要配置config.json,里面输入百度翻译的key等参数。

  • 左下角可以直接添加需要的库,比如md5加密。

2024年9月23日 05:41:56

打包

前几天睡眠不太好,导致干活效率极低。今天终于恢复活力。卡了一天的打包问题也解决了。

文件打包不全的话要注意每一个需要的文件都写进files。打包没有图标的话也要注意不能只写一个assets,必须加上文件夹里面的内容。

"files": [
"assets/*",
"*.js",
"*.css",
"*.html",
"config.json"
],
"win": {
"target": "nsis",
"icon": "assets/myicon.ico"
},

下面是我的package.json内容。

参考了这篇知乎文章https://www.zhihu.com/question/55656662/answer/2968071709

{
"name": "baidu_translate",
"productName": "BaiduTranslate",
"description": "ai绘画提示词翻译软件,支持一键复制,对照显示中英翻译内容",
"keywords": [],
"main": "main.js",
"version": "1.0.0",
"author": "Niaoyu",
"scripts": {
"start": "electron .",
"build": "electron-builder build --dir"
},
"dependencies": {
"md5": "2.3.0"
},
"devDependencies": {
"electron": "31.2.1",
"electron-builder": "^25.0.5"
},
"build": {
"asar": true,
"asarUnpack": [
"node_modules/some-native-module/**/*"
],
"productName": "promptsTranslate",
"appId": "com.zhyny.BaiduTranslate",
"directories": {
"buildResources": "assets",
"output": "dist"
},
"nsis": {
"oneClick": false,
"language": "2052",
"perMachine": true,
"allowToChangeInstallationDirectory": true
},
"extraResources": [
"config.json"
],
"win": {
"target": "nsis",
"icon": "assets/myicon.ico"
},
"files": [
"assets/*",
"*.js",
"*.css","*.html","config.json"
],
"extends": null
}
}