前端小白使用ChatGPT实现沉浸式翻译的核心功能

365 阅读3分钟

作为一个后端开发, 一直比较好奇沉浸式翻译的效果是如何实现的, 所以咱也当了回甲方, 让 ChatGPT 帮我实现想要的效果

场景

为了锻炼英文阅读能力, 在英文文章阅读时还是期望能多读外文, 如果一把梭翻译一遍, 普遍是只读中文不看英文了, 那么也就起不到我们锻炼英文能力的要求了.

需求

如果我遇到了一些不认识或者不确定意思的单词,我希望得到一个贴合当前语境的翻译,这样既能理解文章,又能在语境中学习单词。

需求拆解

如果鼠标在某个单词上方悬浮超过 xx 毫秒, 那么则结合当前语境翻译这个单词及这个句子.

好的, 需求有了, 思路也有了, 具体咋做咱就不知道了, 现在轮到我最强的小弟 ChatGPT 大显身手了

效果图

image.png

具体功能点:

  1. 关键词的翻译, 译文紧跟在关键词后面
  2. 当前语句的翻译

实现此效果的完整 ChatGPT 对话: 实现沉浸式翻译的核心功能

广告

推荐一个 ChatGPT Plus 拼车服务, 客服wx

  1. 无需科学上网, 国内网络直接使用
  2. 会话隔离, 别人看不到你的聊天记录
  3. 手机/电脑 不限使用设备

最终完整代码

import type { PlasmoCSConfig } from "plasmo"
import { generate } from 'short-uuid'

export const config: PlasmoCSConfig = {
    matches: ["<all_urls>"],
    all_frames: true
}

const loading = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><radialGradient id="a11" cx=".66" fx=".66" cy=".3125" fy=".3125" gradientTransform="scale(1.5)"><stop offset="0" stop-color="#85A2B6"></stop><stop offset=".3" stop-color="#85A2B6" stop-opacity=".9"></stop><stop offset=".6" stop-color="#85A2B6" stop-opacity=".6"></stop><stop offset=".8" stop-color="#85A2B6" stop-opacity=".3"></stop><stop offset="1" stop-color="#85A2B6" stop-opacity="0"></stop></radialGradient><circle transform-origin="center" fill="none" stroke="url(#a11)" stroke-width="15" stroke-linecap="round" stroke-dasharray="200 1000" stroke-dashoffset="0" cx="100" cy="100" r="70"><animateTransform type="rotate" attributeName="transform" calcMode="spline" dur="2" values="360;0" keyTimes="0;1" keySplines="0 0 1 1" repeatCount="indefinite"></animateTransform></circle><circle transform-origin="center" fill="none" opacity=".2" stroke="#85A2B6" stroke-width="15" stroke-linecap="round" cx="100" cy="100" r="70"></circle></svg>
`
const cssCode = `
.ct-loading {
    width: 1em;
    display: inline-block;
}

.ct-span {
    display: inline-block;
    margin-right: 2px;
    margin-left: 2px;
}
`

var xy = { x: 0, y: 0 }

document.head.insertAdjacentHTML('beforeend', '<style type="text/css">' + cssCode + '</style>');
// 存储已经翻译过的单词,以避免重复翻译
var translatedWords = new Set(); // 用于存储已翻译的单词及其父元素
var pool = {}

document.addEventListener('mousemove', function (event) {
    // 获取鼠标位置
    var x = event.clientX, y = event.clientY;
    xy = { x, y }

    // 获取鼠标位置下的文本范围
    var range, textNode, offset;
    range = document.caretRangeFromPoint(x, y);
    textNode = range.startContainer;
    if (!textNode) {
        return
    }
    if (textNode.parentElement.hasAttribute('data-ct-translation')) { // 翻译结果节点
        return;
    }
    if (hasPreOrCodeParent(textNode)) {
        return
    }
    offset = range.startOffset;

    var text = textNode.textContent;
    var words = text.split(/\s+/);
    for (var i = 0; i < words.length; i++) {
        var word = removeSpecialCharacters(words[i]);
        if (!isEnglishWord(word)) {
            continue;
        }
        // 检测鼠标是否在当前单词上
        var wordRange = document.createRange();
        wordRange.setStart(textNode, text.indexOf(word));
        wordRange.setEnd(textNode, text.indexOf(word) + word.length);
        var rects = wordRange.getClientRects();
        for (var j = 0; j < rects.length; j++) {
            var rect = rects[j];
            if (!(x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom)) {
                continue;
            }
            var parentElementId = textNode.parentElement.getAttribute('data-ct-id');
            if (parentElementId) {
                translatedWords[word] = parentElementId;
            } else {
                parentElementId = generate();
                textNode.parentElement.setAttribute('data-ct-id', parentElementId);
            }

            if (translatedWords.has(word + '-' + parentElementId)) {
                continue;
            }

            // 创建包装的 span 元素
            // console.log();
            var spanElement = document.createElement('span');
            const wordId = generate()
            spanElement.setAttribute('id', wordId);
            spanElement.setAttribute('data-pid', parentElementId);
            spanElement.setAttribute('data-ct-translation', '0');
            spanElement.setAttribute('data-ct-word', word);
            spanElement.setAttribute('data-ct-rect', `${rect.left},${rect.right},${rect.top},${rect.bottom}`);
            spanElement.setAttribute('data-ct-parent', getFullSentenceContainingWord(textNode.parentElement));
            wordRange.collapse(false); // 将光标移到范围的末尾
            wordRange.insertNode(spanElement);
            translatedWords.add(word + '-' + parentElementId);
            pool[wordId] = {}
            // console.log('Word under mouse:', word);
        }
    }
});

setInterval(() => {
    for (const wordId in pool) {
        const wordElement = document.getElementById(wordId);
        if (!wordElement) {
            continue;
        }
        const rect = wordElement.getAttribute('data-ct-rect').split(',').map(Number);
        const word = wordElement.getAttribute('data-ct-word');
        const pid = wordElement.getAttribute('data-pid');
        if (!isMouseOnTranslation({ left: rect[0], right: rect[1], top: rect[2], bottom: rect[3] }, xy.x, xy.y)) {
            translatedWords.delete(word + '-' + pid)
            wordElement.remove()
            delete pool[wordId]
            continue
        }
        wordElement.classList.add('ct-loading')
        wordElement.innerHTML = loading

        fetch('http://localhost:8787/translate', {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                word: word,
                sentence: wordElement.getAttribute('data-ct-parent')
            })
        }).then(res => res.json()).then(res => {
            console.log(res)
            wordElement.classList.remove('ct-loading')
            wordElement.classList.add('ct-span')
            wordElement.innerHTML = `(${res.word_result})`
            const p = document.querySelector(`[data-ct-id="${pid}"]`)
            const cp = p.cloneNode(true)
            cp.textContent = res.sentence_result
            p.after(cp)
        }).catch(e => console.log)

        wordElement.setAttribute('data-ct-translation', '1');
        delete pool[wordId]
    }
}, 1500)

// 判断鼠标是否在翻译内容上
function isMouseOnTranslation(rect, mouseX, mouseY) {
    return rect && mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom
}

function isEnglishWord(word) {
    // 使用正则表达式匹配英文单词,这里假设一个英文单词由大小写字母组成
    var regex = /^[a-zA-Z0-9]+$/;
    return regex.test(removePunctuation(word));
}


function removePunctuation(word) {
    // 使用正则表达式匹配标点符号并替换为空字符串
    return word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '');
}

function getFullSentenceContainingWord(parentNode) {
    // const tokens = parentNode.innerText.replace(/\n/, '').split('.');
    // return tokens[0];
    return parentNode.innerText.replace(/\n/, '')
}

function removeSpecialCharacters(str) {
    return str.replace(/[^a-zA-Z0-9]/g, '');
}

function hasPreOrCodeParent(node) {
    let parent = node.parentNode;

    while (parent) {
        if (parent.tagName === 'PRE' || parent.tagName === 'CODE') {
            return true;
        }
        parent = parent.parentNode;
    }

    return false;
}