├── .gitignore ├── .gitattributes ├── assets ├── icon.png ├── example.gif └── example_1.gif ├── .vscodeignore ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── tslint.json ├── src ├── translator │ ├── interface.ts │ ├── BaiduTranslator.ts │ ├── YoudaoTranslator.ts │ └── ABaseTranslatorAbstract.ts ├── utils │ ├── index.ts │ └── request.ts ├── test │ ├── extension.test.ts │ └── index.ts ├── SpeakPanel.ts ├── TranslatorX.ts └── extension.ts ├── speakApp ├── index.html └── speak.js ├── CHANGELOG.md ├── README.md ├── tsconfig.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtaox/vscode-extension-translatorX/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtaox/vscode-extension-translatorX/HEAD/assets/example.gif -------------------------------------------------------------------------------- /assets/example_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtaox/vscode-extension-translatorX/HEAD/assets/example_1.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | tslint.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "eg2.tslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | // "no-duplicate-variable": true, 6 | // "curly": true, 7 | "class-name": true, 8 | // "semicolon": [ 9 | // true, 10 | // "always" 11 | // ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } -------------------------------------------------------------------------------- /src/translator/interface.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString } from 'vscode' 2 | 3 | 4 | interface StandardResultInterface { 5 | markdown: MarkdownString, 6 | replaceableArr: Array 7 | } 8 | 9 | interface TranslatorXFetchResult { 10 | translateResult: Array, 11 | replaceableArr: Array 12 | } 13 | 14 | export { 15 | StandardResultInterface, 16 | TranslatorXFetchResult 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /speakApp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SpeakApp 8 | 9 | 10 | 13 |

SpeakApp

14 | 15 | 16 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | const reg = /^[^\u4e00-\u9fa5\w]*|[^\u4e00-\u9fa5\w]*$/g 4 | 5 | export const md5 = (content: string) => { 6 | return crypto.createHash('md5').update(content).digest('hex') 7 | } 8 | 9 | // 有道api 获取签名 10 | export const getYoudaoSign = ({ appKey, q, salt, secret }: any): string => { 11 | return md5(appKey + q + salt + secret) 12 | } 13 | 14 | export const trim = (str: string): string => { 15 | return str.trim().replace(reg, '') 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | # 0.8.0 - 2019-4-20 4 | 5 | - 默认开启翻译 6 | 7 | - 添加手动配置功能 8 | 9 | - 添加有道翻译(需要注册[有道智云](https://ai.youdao.com/),申请appkey和secret) 10 | 11 | > 因为publish的时候遇到了token错误、permission denied等问题,导致publish操作失败了多次,但版本号却一直在累加,所以发布成功后直接到了`0.8.0`,好坑😅 12 | 13 | # 0.1.2 - 2018-7-6 14 | 15 | - 支持了untitled类型临时文件的翻译 16 | 17 | # 0.1.1 - 2018-6-29 18 | 19 | - 修复了打开vsc没有activeTextEditor的情况下,导致命令注册失败的bug 20 | 21 | ## 0.0.6 - 2018-6-26 22 | 23 | - 修复了在某些新版本会提示`command not found`的bug 24 | 25 | ## 0.0.4 - 2018-6-26 26 | - 修改显示名称 27 | - 修改最低兼容版本 28 | 29 | ## 0.0.1 - 2018-6-26 30 | - 发布第一个版本 31 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", function () { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", function() { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { URL } from 'url'; 3 | 4 | const API_BAIDU = 'https://sp1.baidu.com/5b11fzupBgM18t7jm9iCKT-xh_/sensearch/selecttext' 5 | 6 | interface RequestOption { 7 | methods: string, 8 | } 9 | 10 | const SUCCESS_STATUS: string = 'OK' 11 | const ERROR_MSG: string = '网络异常' 12 | 13 | const fetch = (url: string, params: any = {}, options?: RequestOption): Promise => { 14 | return axios 15 | .get(url) 16 | .then(({statusText, data}) => { 17 | console.log(data) 18 | if (statusText === SUCCESS_STATUS) { 19 | return data 20 | } else { 21 | console.log(statusText) 22 | return { 23 | error: ERROR_MSG 24 | } 25 | } 26 | }) 27 | .catch(() => ({error: ERROR_MSG})) 28 | } 29 | 30 | export default fetch -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-translatorx 2 | 3 | [![version](https://vsmarketplacebadge.apphb.com/version/jiangtao.vscode-translatorx.svg)](https://marketplace.visualstudio.com/items?itemName=jiangtao.vscode-translatorx) 4 | 5 | 6 | - [x] 支持百度翻译 7 | - [x] 支持有道翻译(需要注册[有道智云](https://ai.youdao.com/),申请appkey和secret) 8 | - [x] 支持翻译结果快速替换 9 | 10 | 11 | ## 使用 12 | 13 | `ctrl + shift + p`调用出命令面板,键入`translatorX`选择开启或关闭插件. 14 | 15 | ![example](https://github.com/jtaox/vscode-extension-translatorX/blob/master/assets/example.gif?raw=true) 16 | 17 | ![example](https://github.com/jtaox/vscode-extension-translatorX/blob/master/assets/example_1.gif?raw=true) 18 | 19 | 20 | 在`设置->扩展->TranslatorX`可单独开启或关闭有道翻译、百度翻译。如果开启有道翻译,需在设置中配置有道api所需的 appKey 和 secret,获取方式查看[https://ai.youdao.com/](https://ai.youdao.com/) 21 | 22 | 当开启有道翻译时,可通过快捷键shift + cmd + R 快速替换翻译结果,替换规则:`result[i++ % result.length]` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | /* Strict Type-Checking Option */ 12 | "strict": true, /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | // "noUnusedLocals": true /* Report errors on unused locals. */ 15 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 16 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 17 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | ".vscode-test" 22 | ] 23 | } -------------------------------------------------------------------------------- /src/translator/BaiduTranslator.ts: -------------------------------------------------------------------------------- 1 | import ABaseTranslatorAbstract from './ABaseTranslatorAbstract' 2 | 3 | class BadiduTranslator extends ABaseTranslatorAbstract { 4 | 5 | apiUrl = 'https://sp1.baidu.com/5b11fzupBgM18t7jm9iCKT-xh_/sensearch/selecttext' 6 | configSection = 'baidu' 7 | 8 | getParams(word: string): any { 9 | return { 10 | _: Date.now().toString(), 11 | q: word 12 | } 13 | } 14 | 15 | getResultTitle(): string { 16 | return 'baidu:' 17 | } 18 | 19 | parseRawResult(result: any) { 20 | const { errno, data } = result 21 | if (errno !== 0) { 22 | return 'api码错误' + errno 23 | } 24 | 25 | const { result: reqResult } = data 26 | 27 | if (typeof reqResult === 'string') return [[reqResult]] 28 | 29 | const res = reqResult.map(({pre,cont}: any) => { 30 | return [`*${pre}*`, cont] 31 | }) 32 | 33 | return res 34 | 35 | } 36 | 37 | getReplaceableResult(rawResult: any): Array { 38 | return [] 39 | } 40 | 41 | } 42 | 43 | export default BadiduTranslator -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /speakApp/speak.js: -------------------------------------------------------------------------------- 1 | function Speak() { 2 | this.audio = document.querySelector("#speakAudio"); 3 | this.addListener(); 4 | } 5 | 6 | Speak.prototype.play = function(url) { 7 | let audio = new Audio(); 8 | audio.src = url; 9 | setTimeout(() => { 10 | audio.play(); 11 | }, 200); 12 | }; 13 | 14 | Speak.prototype.stop = function() { 15 | audio.pause(); 16 | audio.currentTime = 0; 17 | }; 18 | 19 | Speak.prototype.addListener = function() { 20 | // addEventListener 21 | }; 22 | 23 | function SpeakControler() { 24 | this.commandMap = new Map(); 25 | } 26 | 27 | SpeakControler.prototype.register = function(cmd, fun) { 28 | this.commandMap.set(cmd, fun); 29 | }; 30 | 31 | SpeakControler.prototype.execute = function(action) { 32 | const { command, args } = action; 33 | if (this.commandMap.has(command)) { 34 | this.commandMap.get(command)(args); 35 | } 36 | }; 37 | 38 | const speak = new Speak(); 39 | const speakControler = new SpeakControler(); 40 | 41 | speakControler.register("play", ({ url }) => speak.play(url)); 42 | 43 | window.addEventListener("message", event => { 44 | const message = event.data; 45 | const { command, args } = message; 46 | console.log(JSON.stringify(message), "message"); 47 | speakControler.execute({ 48 | command, 49 | args 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: watch" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/SpeakPanel.ts: -------------------------------------------------------------------------------- 1 | import { window, WebviewPanel, ViewColumn, Uri } from "vscode" 2 | import { join } from "path"; 3 | import { readFileSync } from "fs"; 4 | 5 | const SEPAK_APP_DIR = 'speakApp' 6 | 7 | class SpeakPanel { 8 | private static panel: SpeakPanel | undefined 9 | private webviewPanel: WebviewPanel 10 | private extensionPath: string 11 | 12 | constructor(extensionPath: string) { 13 | this.extensionPath = extensionPath 14 | 15 | this.webviewPanel = window.createWebviewPanel("SpeakPanel", "SpeakPanel", { 16 | preserveFocus: false, 17 | viewColumn: ViewColumn.Active 18 | }, { 19 | localResourceRoots: [Uri.file(join(extensionPath, 'speakApp'))], 20 | enableScripts: true 21 | }) 22 | 23 | this.inintWebview() 24 | } 25 | 26 | private inintWebview(): void { 27 | const html = readFileSync(join(this.extensionPath, SEPAK_APP_DIR, 'index.html'), 'utf8') 28 | this.webviewPanel.webview.html = this.templateFormat(html) 29 | } 30 | 31 | public play(url: string): void { 32 | this.postMessage({ command: 'play', args: { url } }) 33 | } 34 | 35 | public static getInstance(extensionPath: string): SpeakPanel { 36 | if (!SpeakPanel.panel) { 37 | SpeakPanel.panel = new SpeakPanel(extensionPath) 38 | } 39 | 40 | return SpeakPanel.panel 41 | } 42 | 43 | private postMessage(data: any) { 44 | console.log(data, ' post message ') 45 | this.webviewPanel.webview.postMessage(data) 46 | } 47 | 48 | private templateFormat(template: string) { 49 | const scriptUrlReg = /\$\{scriptUrl\}/g 50 | 51 | const tp = template.replace(scriptUrlReg, this.getVSResource('speak.js')) 52 | 53 | return tp 54 | } 55 | 56 | private getVSResource(fileName: string): string { 57 | const resourcePath = Uri.file(join(this.extensionPath, SEPAK_APP_DIR, fileName)) 58 | 59 | return resourcePath.with({ scheme: 'vscode-resource' }).toString() 60 | } 61 | } 62 | 63 | export default SpeakPanel 64 | -------------------------------------------------------------------------------- /src/translator/YoudaoTranslator.ts: -------------------------------------------------------------------------------- 1 | import ABaseTranslatorAbstract from './ABaseTranslatorAbstract' 2 | import { getYoudaoSign } from './../utils' 3 | 4 | class YoudaoTranslator extends ABaseTranslatorAbstract { 5 | apiUrl = 'https://openapi.youdao.com/api' 6 | configSection = 'youdao' 7 | 8 | getParams(q: string): any { 9 | const youdaoConfig = this.getConfig(this.configSection) || {} 10 | const { secret, appKey } = youdaoConfig 11 | const salt = Date.now().toString() 12 | 13 | const sign = getYoudaoSign({appKey, q, salt, secret}) 14 | 15 | if ( !secret || !appKey ) { 16 | throw new Error('请配置有道api secret、appKey') 17 | } 18 | 19 | return { 20 | appKey, 21 | secret, 22 | salt, 23 | sign, 24 | q, 25 | from: 'auto', 26 | to: 'auto', 27 | } 28 | } 29 | 30 | getResultTitle(): string { 31 | return 'youdao:' 32 | } 33 | 34 | getSpeakUrl(result: any): string { 35 | return result.tSpeakUrl 36 | } 37 | 38 | parseRawResult(result: any) { 39 | const { web = [], query = '', translation = [], errorCode = '', basic = {} } = result 40 | const { explains } = basic 41 | 42 | if (errorCode != 0) { 43 | return '❌' + errorCode 44 | } 45 | 46 | const arrayResult: Array = [] 47 | 48 | if (translation && translation.length) { 49 | arrayResult.push(...translation.map((item: string) => `- ${item}`)) 50 | } 51 | 52 | if (explains && explains.length) { 53 | arrayResult.push('基本释义:') 54 | arrayResult.push(...explains.map((item: string) => `- ${item}`)) 55 | } 56 | 57 | if (web && web.length) { 58 | arrayResult.push('网络释义:') 59 | web.forEach((item: any) => { 60 | arrayResult.push([item.key, item.value.join('、')]) 61 | }) 62 | } 63 | 64 | 65 | return arrayResult 66 | 67 | } 68 | 69 | getReplaceableResult(rawResult: any): Array { 70 | return rawResult.translation || [] 71 | } 72 | 73 | } 74 | 75 | export default YoudaoTranslator -------------------------------------------------------------------------------- /src/TranslatorX.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { URL } from 'url' 3 | import { md5 } from './utils' 4 | import BaiduTranslator from './translator/BaiduTranslator' 5 | import YoudaoTranslator from './translator/YoudaoTranslator' 6 | import { TranslatorXFetchResult } from './translator/interface' 7 | import { MarkdownString, workspace, WorkspaceConfiguration } from 'vscode'; 8 | 9 | const baiduApi = 'https://sp1.baidu.com/5b11fzupBgM18t7jm9iCKT-xh_/sensearch/selecttext' 10 | const youdaoApi = 'https://openapi.youdao.com/api' 11 | 12 | interface YoudaoConfig { 13 | secret: string, 14 | appKey: string, 15 | enable: boolean 16 | } 17 | 18 | class TranslatorX { 19 | private conf: any 20 | private state: boolean 21 | private baiduTranslator: BaiduTranslator 22 | private youdaoTranslator: YoudaoTranslator 23 | 24 | public constructor() { 25 | this.conf = this.getWordSpaceConfiguration() 26 | 27 | this.state = this.getTrarnslatorXState() 28 | 29 | 30 | this.baiduTranslator = new BaiduTranslator() 31 | this.youdaoTranslator = new YoudaoTranslator() 32 | 33 | workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this) 34 | 35 | } 36 | 37 | private onDidChangeConfiguration() { 38 | this.conf = this.getWordSpaceConfiguration() 39 | } 40 | 41 | private getWordSpaceConfiguration(): WorkspaceConfiguration { 42 | return workspace.getConfiguration("TranslatorX") 43 | } 44 | 45 | public getTrarnslatorXState(): boolean { 46 | return this.conf.get('enable') 47 | } 48 | 49 | public setTranslatorXState(b: boolean) { 50 | this.conf.update('enable', b, true) 51 | this.state = b 52 | } 53 | 54 | public getYoudaoApiConfig(): YoudaoConfig { 55 | return this.conf.get('youdao') 56 | } 57 | 58 | async fetch(params: { 59 | word: string 60 | }): Promise { 61 | 62 | const result = [] 63 | const replaceable = [] 64 | const status = this.getTrarnslatorXState() 65 | 66 | if (status && this.youdaoTranslator.getStatus()) { 67 | const { markdown, replaceableArr } = await this.youdaoTranslator.fetchStandardResult(params.word) 68 | result.push(markdown) 69 | replaceable.push(...replaceableArr) 70 | } 71 | 72 | if (status && this.baiduTranslator.getStatus()) { 73 | const { markdown, replaceableArr } = await this.baiduTranslator.fetchStandardResult(params.word) 74 | result.push(markdown) 75 | replaceable.push(...replaceableArr) 76 | } 77 | 78 | return { 79 | translateResult: result, 80 | replaceableArr: replaceable 81 | } 82 | } 83 | 84 | } 85 | 86 | export default TranslatorX 87 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import * as vscode from "vscode" 3 | import TranslatorX from './TranslatorX' 4 | import { trim } from './utils' 5 | import SpeakPanel from './SpeakPanel' 6 | 7 | export function activate(context: vscode.ExtensionContext) { 8 | 9 | let { window, languages, commands, } = vscode 10 | 11 | let gReplaceableArr: Array = [] 12 | let gReplaceableIndex: number = 0 13 | // let gRepTimer: number = null 14 | 15 | let translatorX = new TranslatorX() 16 | 17 | let enable = vscode.commands.registerCommand("extension.enable", () => { 18 | translatorX.setTranslatorXState(true) 19 | }) 20 | 21 | let test = vscode.commands.registerCommand("extension.test", (args) => { 22 | SpeakPanel.getInstance(context.extensionPath).play(decodeURIComponent(args.speakUrl)) 23 | }) 24 | 25 | let disable = vscode.commands.registerCommand("extension.disable", () => { 26 | translatorX.setTranslatorXState(false) 27 | }) 28 | 29 | let replaceWithTranslationResults = vscode.commands.registerTextEditorCommand('extension.replaceWithTranslationResults', (textEditor, edit) => { 30 | let editor = window.activeTextEditor 31 | if (!editor) return 32 | const selection = editor && editor.selection 33 | 34 | edit.replace(selection, gReplaceableArr[gReplaceableIndex % gReplaceableArr.length]) 35 | gReplaceableIndex++ 36 | }) 37 | 38 | let provider = { 39 | async provideHover(document: any, position: any, token: any) { 40 | let editor = window.activeTextEditor 41 | if (!editor) return 42 | const selection = editor && editor.selection 43 | 44 | // range可能为空 45 | let range = document.getWordRangeAtPosition(position) 46 | let string = range ? document.getText(range) : "" 47 | 48 | if (selection && !selection.isEmpty) { 49 | let text = document.getText(selection) 50 | // position在selection范围内 51 | // (selection.contains(range) || ~string.indexOf(text)) && (string = text) 52 | if (selection.contains(range) || ~string.indexOf(text)) string = text 53 | } 54 | 55 | string = trim(string) 56 | if (!string) return 57 | 58 | string = string.replace(/\n/g,"") 59 | 60 | const { replaceableArr, translateResult } = await translatorX.fetch({ word: string }) 61 | 62 | gReplaceableArr = replaceableArr 63 | gReplaceableIndex = 0 64 | 65 | if (translateResult) { 66 | let hover = new vscode.Hover(translateResult) 67 | return hover 68 | } 69 | 70 | } 71 | } 72 | 73 | languages.registerHoverProvider({ scheme: 'file', language: '*' }, provider) 74 | languages.registerHoverProvider({ scheme: 'untitled' }, provider) 75 | 76 | context.subscriptions.push(enable) 77 | context.subscriptions.push(disable) 78 | context.subscriptions.push(test) 79 | context.subscriptions.push(replaceWithTranslationResults) 80 | } 81 | 82 | 83 | 84 | 85 | 86 | export function deactivate() {} 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-translatorx", 3 | "displayName": "TranslatorX", 4 | "description": "translator", 5 | "version": "0.8.0", 6 | "publisher": "jiangtao", 7 | "icon": "assets/icon.png", 8 | "engines": { 9 | "vscode": "^1.18.1" 10 | }, 11 | "galleryBanner": { 12 | "color": "#232323", 13 | "theme": "dark" 14 | }, 15 | "keywords": [ 16 | "translate" 17 | ], 18 | "categories": [ 19 | "Other" 20 | ], 21 | "bugs": { 22 | "url": "https://github.com/jtaox/vscode-extension-translatorX/issues", 23 | "email": "jtaobox@126.com" 24 | }, 25 | "activationEvents": [ 26 | "*" 27 | ], 28 | "license": "MIT", 29 | "homepage": "https://github.com/jtaox/vscode-extension-translatorX", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/jtaox/vscode-extension-translatorX.git" 33 | }, 34 | "main": "./out/extension", 35 | "contributes": { 36 | "commands": [ 37 | { 38 | "command": "extension.enable", 39 | "title": "启用translatorX", 40 | "category": "translatorX" 41 | }, 42 | { 43 | "command": "extension.disable", 44 | "title": "禁用translatorX", 45 | "category": "translatorX" 46 | }, 47 | { 48 | "command": "extension.replaceWithTranslationResults", 49 | "title": "替换选中内容", 50 | "category": "translatorX" 51 | }, 52 | { 53 | "command": "extension.test", 54 | "title": "test", 55 | "category": "translatorX" 56 | } 57 | ], 58 | "configuration": { 59 | "type": "object", 60 | "title": "TranslatorX", 61 | "properties": { 62 | "TranslatorX.enable": { 63 | "type": "boolean", 64 | "default": true, 65 | "description": "TranslatorX全局开关" 66 | }, 67 | "TranslatorX.baidu.enable": { 68 | "type": "boolean", 69 | "default": true, 70 | "description": "TranslatorX 百度翻译功能" 71 | }, 72 | "TranslatorX.youdao.enable": { 73 | "type": "boolean", 74 | "default": true, 75 | "description": "TranslatorX 有道翻译功能" 76 | }, 77 | "TranslatorX.youdao.secret": { 78 | "type": "string", 79 | "default": "", 80 | "description": "有道api secret" 81 | }, 82 | "TranslatorX.youdao.appKey": { 83 | "type": "string", 84 | "default": "", 85 | "description": "有道api appkey" 86 | } 87 | } 88 | }, 89 | "keybindings": [ 90 | { 91 | "command": "extension.replaceWithTranslationResults", 92 | "key": "shift+alt+r", 93 | "mac": "shift+cmd+r" 94 | } 95 | ] 96 | }, 97 | "scripts": { 98 | "vscode:prepublish": "npm run compile", 99 | "compile": "tsc -p ./", 100 | "watch": "tsc -watch -p ./", 101 | "postinstall": "node ./node_modules/vscode/bin/install", 102 | "test": "npm run compile && node ./node_modules/vscode/bin/test" 103 | }, 104 | "devDependencies": { 105 | "typescript": "^2.6.1", 106 | "vscode": "^1.1.6", 107 | "tslint": "^5.8.0", 108 | "@types/node": "^7.0.43", 109 | "@types/mocha": "^2.2.42" 110 | }, 111 | "dependencies": { 112 | "axios": "^0.21.1" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/translator/ABaseTranslatorAbstract.ts: -------------------------------------------------------------------------------- 1 | import { URL } from "url" 2 | import fetch from "./../utils/request" 3 | import { StandardResultInterface } from './interface' 4 | import { MarkdownString, workspace, WorkspaceConfiguration, Uri } from 'vscode' 5 | 6 | const EXTENSION_NAME = 'TranslatorX' 7 | const ICON_PLAY = '🗣' 8 | const ICON_STOP = '◼︎' 9 | 10 | abstract class TranslatorAbstract { 11 | 12 | /** 13 | * api地址 14 | */ 15 | abstract apiUrl: string 16 | 17 | /** 18 | * 配置命名空间 19 | */ 20 | abstract configSection: string 21 | 22 | 23 | constructor () { 24 | } 25 | 26 | /** 27 | * 获取参数 28 | * @param word 要翻译的文字 29 | */ 30 | abstract getParams(word: string): any 31 | 32 | abstract parseRawResult(result: any): Array | string 33 | 34 | /** 35 | * 用户通过快捷键可以替换的内容数组 由子类实现 36 | * @param rawResult 接口返回内容 37 | */ 38 | abstract getReplaceableResult(rawResult: any): Array 39 | 40 | /** 41 | * 获取当前翻译设置状态 42 | */ 43 | getStatus(): boolean { 44 | const currentConfig = this.getConfig(this.configSection) || {} 45 | 46 | return Boolean(currentConfig.enable) 47 | } 48 | 49 | /** 50 | * 用户选中的文字 51 | */ 52 | private selectWord: string = '' 53 | 54 | /** 55 | * 获取翻译结果title 56 | * 也就是hover上显示的标题 57 | * 子类可以通过覆盖改方法修改默认title 58 | */ 59 | protected getResultTitle (): string { 60 | return this.selectWord + '翻译结果' 61 | } 62 | 63 | /** 64 | * 获取播放url,子类可选实现 65 | */ 66 | protected getSpeakUrl(result: any): string | undefined { 67 | return undefined 68 | } 69 | 70 | get workspaceConfiguration() { 71 | return workspace.getConfiguration(EXTENSION_NAME) 72 | } 73 | 74 | getConfig (section: string): any { 75 | return this.workspaceConfiguration.get(section) 76 | } 77 | 78 | /** 79 | * 获取完整url 80 | * @param url 接口地址 81 | * @param params 参数 82 | */ 83 | public getUrlWithParams(url: string, params: any = {}): string { 84 | const urlObj = new URL(url) 85 | 86 | Object.keys(params).forEach(key => { 87 | urlObj.searchParams.append(key, params[key]) 88 | }) 89 | 90 | return urlObj.toString() 91 | } 92 | 93 | public fetchTranslationResult(word: string): Promise { 94 | this.selectWord = word 95 | let params = undefined 96 | let errorMsg = null 97 | 98 | try { 99 | params = this.getParams(word) 100 | } catch (error) { 101 | errorMsg = error.message 102 | } 103 | 104 | if (params) { 105 | return fetch(this.getUrlWithParams(this.apiUrl, params)) 106 | } else { 107 | return Promise.resolve({ error: errorMsg }) 108 | } 109 | 110 | } 111 | 112 | public async fetchStandardResult (word: string): Promise { 113 | const rawResult = await this.fetchTranslationResult(word) 114 | 115 | const title = this.getResultTitle() 116 | const speakUrl = this.getSpeakUrl(rawResult) 117 | 118 | const ms = new MarkdownString(title) 119 | ms.appendText('\n\n') 120 | 121 | if (rawResult.error) { 122 | return {markdown: ms.appendMarkdown(`❌ ${ rawResult.error }`), replaceableArr: []} 123 | } 124 | 125 | const result = this.parseRawResult(rawResult) 126 | 127 | if (!Array.isArray(result)) { 128 | return {markdown: ms.appendMarkdown(`* ${result} *`), replaceableArr: []} 129 | } 130 | 131 | result.forEach((record: Array) => { 132 | if (Array.isArray(record)) { 133 | record.forEach(item => ms.appendMarkdown(`- ${item}`) && ms.appendText(' ')) 134 | } else { 135 | ms.appendMarkdown(record) 136 | } 137 | ms.appendText('\n\n') 138 | }) 139 | 140 | if (speakUrl) { 141 | ms.appendMarkdown(this.getSpeakActionMarkdown(speakUrl)) 142 | ms.isTrusted = true 143 | } 144 | 145 | const replaceable = this.getReplaceableResult(rawResult) 146 | 147 | return {markdown: ms, replaceableArr: replaceable} 148 | } 149 | 150 | private getSpeakActionMarkdown(speakUrl: string): string { 151 | const url = Uri.parse( 152 | `command:extension.test?${encodeURIComponent(JSON.stringify({ speakUrl }))}` 153 | ); 154 | console.log(url.toString()) 155 | return `[${ICON_PLAY}](${url}) ` 156 | } 157 | } 158 | 159 | export default TranslatorAbstract 160 | --------------------------------------------------------------------------------