從零到一開發vscode外掛變數翻譯

kerin發表於2021-12-31

需求的起因是英語渣在開發的過程中經常遇到一個變數知道中文叫啥,但是英文單詞可能就忘了,或者不知道,這個時候,我之前做法是開啟瀏覽器,開啟谷歌翻譯,輸入中文,複製英文,然後切回vscode,貼上結果。實在是太麻煩了,年輕的時候還好,記性好,英文單詞大部分都能記住,但隨著年紀越來越大,頭髮越來越少,記性也是越來越差,上面的步驟重複的次數也越來越多,所以痛定思痛,就開發了這款外掛。因為自己也是這幾天從零開始學習的外掛開發,所以本文完全的記錄了一個小白開發的外掛開發之路,內容主要是實戰類的介紹,主要從四個方面介紹來完整的展示整個外掛從功能設計到釋出的完整歷程。

  1. 功能設計
  2. 環境搭建
  3. 外掛功能開發
  4. 外掛打包釋出

功能設計

功能主要是兩個功能,中譯英,其他語言翻譯成中文

  1. 將中文變數替換為翻譯後的英文變數,多個單詞需要自動駝峰,解決的場景就是日常開發經常碰到的“英語卡殼”
  2. 劃詞翻譯,自動將各種語言翻譯成中文,這解決的場景是有時候看國外專案原始碼的註釋碰到不會的單詞不知道啥意思影響效率

環境搭建

上手的第一步,環境搭建

  1. 安裝腳手架, yogenerator-code,這兩個工具可以幫助我們快速構建專案,詳情可見 Github

    //安裝
    yarn global add yo generator-code
  2. 安裝vsce,vsce可用來將開發的程式碼打包成.vsix字尾的檔案,方便上傳至微軟外掛商店或者本地安裝

    yarn global add vsce
  3. 生成並初始化專案,初始化資訊根據自己情況填寫

    //初始化生成專案
    yo code

    到這一步後,選擇直接開啟,Open with code

    image-20211230215507341開啟後會自動建立一個工作區,並生成這些檔案,可根據自己需要對檔案進行刪減,完成這步後,我們可以直接進行開發與除錯了

    image-20211230215823987

    如何進行除錯?

    執行與除錯皮膚點選Run Extention,或者快捷鍵F5,mac可以直接點選觸控欄的除錯按鈕

    image-20211230220226620

    開啟後會彈出一個新的vscode視窗,這個新的視窗就是你的測試環境(擴充套件開發宿主),你做的外掛功能就是在這個新的視窗測試,列印的訊息在前一個視窗的除錯控制檯中,比如自帶的例子,在我們新視窗 cmd/ctrl+shift+p後輸入Hello world後會在前一個視窗的控制檯列印一些資訊

    image-20211230221707832

    到這裡,開發準備環境就準備好了,下一步就是開始正式的外掛功能開發

外掛功能開發

外掛開發中,有兩個重要的檔案,一個是 package.json,一個是 extention.js

重要檔案說明

package.json

image-20211230222536296

  • activationEvents用來註冊啟用事件,用來表明什麼情況下會啟用extention.js中的active函式。常見的有onLanguage,onCommand...更多資訊可檢視vscode文件activationEvents
  • main表示外掛的主入口
  • contributes用來註冊命令(commands),繫結快捷鍵(keybindings),配置設定項(configuration)等等,更多可配置項可看文件
extention.js

extention.js主要作用是作為外掛功能的實現點,通過active,deactive函式,以及vscode提供的api以及一些事件鉤子來完成外掛功能的開發

實現翻譯功能

翻譯這裡主要是使用了兩個服務,谷歌和百度翻譯。

  1. 谷歌翻譯參考了別人的做法,使用google-translate-token獲取到token,然後構造請求url,再處理返回的body,拿到返回結果。這裡還有一個沒搞懂的地方就是請求url的生成很迷,不知道這一塊是啥意思。

    const qs = require('querystring');
    const got = require('got');
    const safeEval = require('safe-eval');
    const googleToken = require('google-translate-token');
    const languages = require('../utils/languages.js');
    const config = require('../config/index.js');
    
    // 獲取請求url
    async function getRequestUrl(text, opts) {
        let token = await googleToken.get(text);
        const data = {
            client: 'gtx',
            sl: opts.from,
            tl: opts.to,
            hl: opts.to,
            dt: ['at', 'bd', 'ex', 'ld', 'md', 'qca', 'rw', 'rm', 'ss', 't'],
            ie: 'UTF-8',
            oe: 'UTF-8',
            otf: 1,
            ssel: 0,
            tsel: 0,
            kc: 7,
            q: text
        };
        data[token.name] = token.value;
        const requestUrl = `${config.googleBaseUrl}${qs.stringify(data)}`;
        return requestUrl;
    }
    
    //處理返回的body
    async function handleBody(url, opts) {
        const result = await got(url);
        let resultObj = {
            text: '',
            from: {
                language: {
                    didYouMean: false,
                    iso: ''
                },
                text: {
                    autoCorrected: false,
                    value: '',
                    didYouMean: false
                }
            },
            raw: ''
        };
    
        if (opts.raw) {
            resultObj.raw = result.body;
        }
        const body = safeEval(result.body);
    
        // console.log('body', body);
        body[0].forEach(function(obj) {
            if (obj[0]) {
                resultObj.text += obj[0];
            }
        });
    
        if (body[2] === body[8][0][0]) {
            resultObj.from.language.iso = body[2];
        } else {
            resultObj.from.language.didYouMean = true;
            resultObj.from.language.iso = body[8][0][0];
        }
    
        if (body[7] && body[7][0]) {
            let str = body[7][0];
    
            str = str.replace(/<b><i>/g, '[');
            str = str.replace(/<\/i><\/b>/g, ']');
    
            resultObj.from.text.value = str;
    
            if (body[7][5] === true) {
                resultObj.from.text.autoCorrected = true;
            } else {
                resultObj.from.text.didYouMean = true;
            }
        }
        return resultObj;
    }
    
    //翻譯
    async function translate(text, opts) {
        opts = opts || {};
        opts.from = opts.from || 'auto';
        opts.to = opts.to || 'en';
    
        opts.from = languages.getCode(opts.from);
        opts.to = languages.getCode(opts.to);
    
        try {
            const requestUrl = await getRequestUrl(text, opts);
            const result = await handleBody(requestUrl, opts);
            return result;
        } catch (error) {
            console.log(error);
        }
    }
    
    // 獲取翻譯結果
    const getGoogleTransResult = async(originText, ops = {}) => {
        const { from, to } = ops;
        try {
            const result = await translate(originText, { from: from || config.defaultFrom, to: to || defaultTo });
            console.log('谷歌翻譯結果', result.text);
            return result;
        } catch (error) {
            console.log(error);
            console.log('翻譯失敗');
        }
    }
    
    module.exports = getGoogleTransResult;
  1. 百度翻譯,百度翻譯的比較簡單,申請服務,獲得appid和key,然後構造請求url直接請求就行,不知道如何申請的,可檢視我之前的一篇文章Electron+Vue從零開始打造一個本地檔案翻譯器進行申請

    const md5 = require("md5");
    const axios = require("axios");
    const config = require('../config/index.js');
    axios.defaults.withCredentials = true;
    axios.defaults.crossDomain = true;
    axios.defaults.headers.post["Content-Type"] =
        "application/x-www-form-urlencoded";
    
    // 百度翻譯
    async function getBaiduTransResult(text = "", opt = {}) {
        const { from, to, appid, key } = opt;
        try {
            const q = text;
            const salt = parseInt(Math.random() * 1000000000);
            let str = `${appid}${q}${salt}${key}`;
            const sign = md5(str);
            const query = encodeURI(q);
            const params = `q=${query}&from=${from}&to=${to}&appid=${appid}&salt=${salt}&sign=${sign}`;
            const url = `${config.baiduBaseUrl}${params}`;
            console.log(url);
            const res = await axios.get(url);
            console.log('百度翻譯結果', res.data.trans_result[0]);
            return res.data.trans_result[0];
        } catch (error) {
            console.log({ error });
        }
    }
    
    module.exports = getBaiduTransResult;

獲取選中的文字

使用事件鉤子onDidChangeTextEditorSelection,獲取選中的文字

    onDidChangeTextEditorSelection(({ textEditor, selections }) => {
        text = textEditor.document.getText(selections[0]);
    })

配置項的獲取更新

通過vscode.workspace.getConfiguration獲取到工作區的配置項,然後通過事件鉤子onDidChangeConfiguration監聽配置項的變動。

獲取更新配置項

const { getConfiguration } = vscode.workspace;
const config = getConfiguration();

//注意get裡面的引數其實就是package.json配置項裡面的contributes.configuration.properties.xxx
const isCopy = config.get(IS_COPY);
const isReplace = config.get(IS_REPLACE);
const isHump = config.get(IS_HUMP);
const service = config.get(SERVICE);
const baiduAppid = config.get(BAIDU_APPID);
const baiduKey = config.get(BAIDU_KEY);

//更新使用update方法,第三個引數為true代表應用到全域性
config.update(SERVICE, selectedItem, true);

監聽配置項的變動

const { getConfiguration, onDidChangeConfiguration } = vscode.workspace;
const config = getConfiguration();

//監聽變動
const disposeConfig = onDidChangeConfiguration(() => {
  config = getConfiguration();
})

監聽個別配置項的變動

const disposeConfig = onDidChangeConfiguration((e) => {
    if (e && e.affectsConfiguration(BAIDU_KEY)) {
        //幹些什麼
    }
})

獲取當前開啟的編輯器物件

vscode.window.activeTextEditor代表當前開啟的編輯器,如果切換標籤頁,而沒有設定監聽,那麼這個這個物件不會自動更新,所以需要使用onDidChangeActiveTextEditor來監聽,並替換之前的編輯器物件

const { activeTextEditor, onDidChangeActiveTextEditor } = vscode.window;
let active = activeTextEditor;
const edit = onDidChangeActiveTextEditor((textEditor) => {
  console.log('activeEditor改變了');
  //更換開啟的編輯器物件
  if (textEditor) {
      active = textEditor;
  }
})

劃詞翻譯懸浮提示

通過vscode.languages.registerHoverProvider註冊一個Hover,然後通過activeTextEditor拿到選中的詞語進行翻譯,然後再通過new vscode.Hover將翻譯結果懸浮提示

// 劃詞翻譯檢測
const disposeHover = vscode.languages.registerHoverProvider("*", {
    async provideHover(document, position, token) {
        const service = config.get(SERVICE);
        const baiduAppid = config.get(BAIDU_APPID);
        const baiduKey = config.get(BAIDU_KEY);

        let response, responseText;
        const selected = document.getText(active.selection);
        // 谷歌翻譯
        if (service === 'google') {
            response = await getGoogleTransResult(selected, { from: 'auto', to: 'zh-cn' });
            responseText = response.text;
        }

        // 百度翻譯
        if (service === 'baidu') {
            response = await getBaiduTransResult(selected, { from: "auto", to: "zh", appid: baiduAppid, key: baiduKey });
            responseText = response.dst;
        }
        // 懸浮提示
        return new vscode.Hover(`${responseText}`);
    }
})

替換選中的文字

獲取到activeTextEditor,呼叫他的edit方法,然後使用回撥中的replace

//是否替換原文
if (isReplace) {
  let selectedItem = active.selection;
  active.edit(editBuilder => {
    editBuilder.replace(selectedItem, result)
  })
}

複製到剪貼簿

使用vscode.env.clipboard.writeText;

// 是否複製翻譯結果
if (isCopy) {
  vscode.env.clipboard.writeText(result);
}

駝峰處理

function toHump(str) {
    if (!str) {
        return
    }
    const strArray = str.split(' ');
    const firstLetter = [strArray.shift()];
    const newArray = strArray.map(item => {
        return `${item.substring(0,1).toUpperCase()}${item.substring(1)}`;
    })
    const result = firstLetter.concat(newArray).join('');
    return result;
}

module.exports = toHump;

快捷鍵繫結

通過vscode.commands.registerCommand註冊繫結之前package.json中設定的keybindings,需要注意的是registerCommand的第一個引數需要與keybindings的command保持一致才能繫結

registerCommand('translateVariable.toEN', async() => {
  //do something
})


//package.json
"keybindings": [{
  "key": "ctrl+t",
  "mac": "cmd+t",
  "when": "editorTextFocus",
  "command": "translateVariable.toEN"
}],

外掛打包釋出

打包

vsce package

打包後會在目錄下生成.vsix字尾的外掛

釋出

外掛釋出主要是把打包的vsix字尾外掛,傳入微軟vscode外掛商店,當然也能本地安裝使用。

傳入商店

釋出到線上需要到微軟外掛商店管理頁面,建立釋出者資訊,如果沒有微軟賬號,需要去申請。

image-20211229224224632

建立完成後,選擇釋出到vscode商店

image-20211229224338826

本地安裝

本地是可以直接安裝.vsix字尾外掛的,找到外掛選單

image-20211229224545688

選擇從VSIX安裝,安裝上面打包的外掛就好了

image-20211229224658287

最後

vscode的中文資料有點少,這次開發大多數時間都在看英文文件,以及上外網尋找資料,真的英語太重要了,後面得多學點英語了,希望後面我使用自己做的這個外掛的次數會越來越少,專案已開源,使用說明與原始碼傳送門

相關文章