写在前面
在日常冲浪和技术学习中,我们常常会遇到需要翻译网页部分内容的情况。整页翻译虽然方便,但有时会破坏页面布局,翻译技术术语也不够精准。作为一个开发者,我希望能有一款"指哪打哪"的翻译工具。于是,我利用trae打造了一款小巧灵活的 Chrome 翻译插件,它通过双击或鼠标悬停的方式,实现精准的划词翻译。
一句话总结
这是一个对网页任意元素精准翻译的chrome 插件。
代码解读
在 background.js 中,最关键的部分是如何优雅地处理网络请求。
javascript
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log(`background`)
if (request.action === 'translate') {
console.log('Background收到翻译请求:', request.url); // 添加日志用于调试
fetch(request.url)
.then(response => {
console.log('API响应状态:', response.status);
return response.json();
})
.then(data => sendResponse({success: true, data}))
.catch(error => {
console.error('翻译请求错误:', error);
sendResponse({success: false, error: error.message});
});
return true; // 保持消息通道开放以支持异步响应
}
});
content.js是核心代码
javascript
/**
* 判断文本是否为英文
* @param {string} text 要检测的文本
* @param {object} options 可选参数
* @param {boolean} options.allowMixed 是否允许混合少量非英文字符,默认不允许
* @param {number} options.maxNonEnglish 允许的最大非英文字符比例(0-1),默认0
* @returns {boolean} 是否为英文文本
*/
function isEnglishText(text, options = {}) {
// 处理空值或非字符串输入
if (!text || typeof text !== 'string') {
return false;
}
// 解析参数
const { allowMixed = false, maxNonEnglish = 0 } = options;
// 英文文本的基本正则表达式(包含大小写字母、数字、常见英文标点和空白)
const englishPattern = /^[a-zA-Z0-9\s!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]+$/;
// 严格模式:完全匹配英文模式
if (!allowMixed) {
return englishPattern.test(text);
}
// 宽松模式:允许一定比例的非英文字符
const nonEnglishChars = text.match(/[^a-zA-Z0-9\s!"#$%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]/g) || [];
const nonEnglishRatio = nonEnglishChars.length / text.length;
return nonEnglishRatio <= maxNonEnglish;
}
// 等待页面加载完成
console.log('等待页面加载完成,脚本开始执行,准备注册事件监听');
// document.addEventListener('DOMContentLoaded', processPage);
if (document.readyState === 'complete' || document.readyState === 'interactive') {
// processPage();
// 监听鼠标事件
setupLinkHoverTranslation()
} else {
// document.addEventListener('DOMContentLoaded', processPage);
}
// 监听DOM变化,处理动态加载内容
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(node => {
if (mutation.nodeType === Node.ELEMENT_NODE) {
processElement(node);
}
});
}
});
});
// 配置并启动观察器
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
// 处理整个页面
function processPage() {
const begin = new Date()
console.log(`processPage 开始处理整个页面 ${begin}`)
processElement(document.body);
}
// 处理单个元素
function processElement(element) {
const textNodes = getTextNodes(element);
textNodes.forEach(processTextNode);
}
// 获取所有文本节点
function getTextNodes(element) {
const nodes = [];
const ignoreTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'IMG', 'VIDEO', 'AUDIO'];
function traverse(node) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0) {
if (!ignoreTags.includes(node.parentNode.tagName)) {
nodes.push(node);
}
} else if (node.nodeType === Node.ELEMENT_NODE && !ignoreTags.includes(node.tagName)) {
node.childNodes.forEach(traverse);
}
}
traverse(element);
return nodes;
}
// 处理文本节点
function processTextNode(node) {
const text = node.textContent.trim();
if (text.length < 5) return; // 忽略过短文本
// 使用franc库检测语言
// console.log(`franc 检测语言 ${text}`)
const lang = isEnglishText(text);
if (lang) {
// 检查是否已经翻译过
if (node.nextSibling && node.nextSibling.classList?.contains('translation-result')) {
return;
}
translateText(text,node)
}
}
// 插入翻译结果
function insertTranslation(node, translatedText) {
const translationElement = document.createElement('div');
translationElement.className = 'translation-result';
translationElement.textContent = translatedText;
node.parentNode.insertBefore(translationElement, node.nextSibling);
}
// 生成随机数(百度翻译API需要)
function generateSalt() {
return Math.floor(Math.random() * 10000000000);
}
// 调用百度翻译API
async function translateText(text,node) {
// From storage get Baidu translate configuration
const { baiduAppId, baiduSecretKey } = await chrome.storage.sync.get(['baiduAppId', 'baiduSecretKey']);
if (!baiduAppId || !baiduSecretKey) {
console.error('未设置百度翻译App ID或密钥,请在插件设置中配置');
return '需要百度翻译App ID和密钥,请在插件设置中配置';
}
try {
const salt = generateSalt();
let sign = md5(baiduAppId + text + salt + baiduSecretKey);
sign = sign.toLowerCase()
const encodedText = encodeURIComponent(text);
// Update the Baidu API URL from http to https
const url = `https://api.fanyi.baidu.com/api/trans/vip/translate?q=${encodedText}&from=en&to=zh&appid=${baiduAppId}&salt=${salt}&sign=${sign}`;
chrome.runtime.sendMessage({
action: 'translate',
url: url
}, (response) => {
// console.log('收到background响应:', response); // 添加日志
if (response.success) {
// 处理翻译结果(取消注释并完善)
// console.log('翻译结果:', response.data);
if(response.data.error_code=='54003' || response.data.error_code=='54001'){
// 等待1s
sleep(1000).then(()=>{
translateText(text,node)
})
}else if(response.data.error_code=='54004'){
console.log(response.data.error_msg)
}else{
// 将翻译结果显示到页面上的代码
const translatedText = response.data.trans_result.map(item => item.dst).join('\n');
console.log(`${text} ==> ${translatedText}`)
if (typeof node === 'function') {
node(translatedText);
}else if(node!=null){
insertTranslation(node, translatedText); // 插入翻译结果到页面
}
}
} else {
console.error('翻译失败:', response.error);
}
});
} catch (error) {
console.error('翻译API调用失败:', error);
return '翻译失败: ' + error.message;
}
}
// 简化的翻译测试函数
function testTranslation() {
console.log('开始翻译测试');
const testText = 'Hello world';
console.log('待翻译文本:', testText);
translateText(testText,null)
}
// 页面加载完成后执行测试
window.addEventListener('load', function() {
console.log('页面加载完成,准备执行翻译');
// testTranslation();
// 添加双击事件监听
document.addEventListener('dblclick', function(event) {
let target = event.target;
// debugger
// 检查是否是翻译结果元素
if (target.classList.contains('translation-result')) {
// 如果点击的是翻译结果,隐藏它
// console.log(`隐藏当前元素`)
if(target.style.display!='none'){
target.style.display = 'none';
}else{
target.style.display = 'block';
}
return;
}
// 判断是否已有翻译结果,若有则判断是否显示
const tmpChildren = target.children;
if(tmpChildren && tmpChildren?.length==1 && tmpChildren[0]?.classList?.contains('translation-result')){
// console.log(`已有翻译结果,判断是否显示`)
if(tmpChildren[0].style.display=='none' || tmpChildren[0].style.display==''){
// console.log(`翻译结果被隐藏,显示它`)
tmpChildren[0].style.display = 'block';
}else{
// console.log(`翻译结果已显示,隐藏它`)
tmpChildren[0].style.display = 'none';
}
return
}
// target = target.parentNode
console.log(`翻译当前元素`)
// 如果不是翻译结果元素,尝试获取文本
let text = '';
console.log(`target:${JSON.stringify(target)}`)
if (target.nodeType === Node.TEXT_NODE) {
text = target.textContent.trim();
} else {
// 如果是元素节点,获取其文本内容
text = target.innerText.trim();
// 如果文本太短,尝试获取父节点的文本
if (text.length < 5) {
// text = target.parentNode.textContent.trim();
return
}
}
// 检查文本是否有效且需要翻译
// target = target.parentNode
if (isEnglishText(text,{allowMixed:true,maxNonEnglish:1})) {
// 检查是否已经有翻译结果
const children = target.children;
if(children && children?.length==1 && children[0]?.classList?.contains('translation-result')){
console.log(`已有翻译结果且被隐藏,显示它`)
children[0].style.display = 'block';
return
}
// TODO 如果关闭页面初始化翻译,只在点击元素的时候翻译,那这个地方就将翻译的内容放到了target下一个元素了,而不是放到target孩子元素中,这个地方需要修改一下
translateText(text, target);
}
});
});
源代码
完整源代码已上传至GitHub:项目地址
总结
这个项目虽然小巧,但完整地演示了一个 Chrome 扩展的核心开发流程。
对于想要学习 Chrome 扩展开发和处理网络请求的前端伙伴来说,这是一个非常好的练手项目。欢迎大家前往 GitHub 仓库 克隆代码,尝试运行并提出宝贵的建议。如果你觉得有用,别忘了点个 Star ⭐️!