Vscode 国际化提效插件开发

639 阅读4分钟

国际化插件开发

  • multi-language

背景

1、写完国际化后,不知道当前国际化内容的实际中文意思,容易混淆

2、国际化重复编写,无法确切知道位置及代码提示

3、编写变量时,一时想不起变量的英文名称

功能

  • 用于国际化翻译、支持国际化已有翻译自动补全
  • 支持翻译中英文hover提示
  • 支持通过菜单栏、快捷键快速翻译(接入百度api)
  • 支持模版语法,在输入$t时快速补全
  • 支持通过中文模糊查找已有翻译

国际化文件结构限制

为什么会有限制?

因为里面的核心逻辑都是围绕读写国际化文件而来,而动态去读取国际化文件内容限制于commonJs 规范,因此需要将es6 转换成commonJs规范

因此文件结构符合 a.js b.js c.js d.js. 入口文件满足 index.js 即可

a.js
const a = {
    xxx: '',
}
export default {
    a
}

b.js
const b = {
    xxx: '',
}
export default {
    b
}

c.json

{
    xxx: ''
}

d.js

export const d1 = {
    xxx: ''
}

export const d2 = {
    xxx: ''
}

index.js // 入口文件

import a from './a'
import b from './b'
import c from './c'
import { d1, d2 } from './d'

export {
    a,
    b,
    c,
    d1,
    d2
}

代码介绍

1、通过脚手架快速构建

npm install -g yo generator-code

yo code

2、结构介绍

 package.json

//  注意版本号不能太高,不然很多版本vscode将无法安装,我的默认脚手架是1.62.0的
"engines": {
    "vscode": "^1.60.0"
},
// 加入命令 ctrl + shift + p 会提示
"contributes": {
    "commands": [
      {
        "command": "translate.zntoen",
        "title": "翻译"
      },
      {
        "command": "read.file",
        "title": "重新扫描国际化页面"
      },
      {
        "command": "fuzzy.match",
        "title": "国际化查找"
      }
    ],
},
// 代码片段
"snippets": [
  {
    "language": "javascript",
    "path": "./snippets/index.json"
  }
],
// 百度翻译api
"configuration": [
  {
    "title": "translateNamed",
    "properties": {
      "translate.appid": {
        "type": "string",
        "default": "自己的appid",
        "description": "百度翻译API-appid"
      },
      "translate.secret": {
        "type": "string",
        "default": "自己的密钥",
        "description": "百度翻译API-密钥"
      }
    }
  }
],
// 将翻译加入到菜单中
"menus": {
  "editor/context": [
    {
      "when": "editorHasSelection",
      "command": "translate.zntoen",
      "group": "navigation"
    }
  ]
}

 入口

const vscode = require('vscode');
// 获取项目地址
const { getProjectPath } = require('./src/utils')
// 注册hover方法的主要逻辑 (主要的思路是通过解析国际化内容匹配国际化文件中的内容)
const provideHover = require('./src/provideHover')
// 模糊匹配(中文匹配,去查找国际化文件中的内容)
const fuzzyMatch = require('./src/fuzzyMatch')
// 翻译
const disposable = require('./src/translate')
// 遍历文件内容,对文件进行转换(目前编写时发现只支持commonJs 导致需要将es6 导入导出转换成 commonJs 规范,因此会对文件有一定的限制)
const { reReadFiles } = require('./src/reReadFile')

// 针对小数点对指令进行监控
function provideCompletionItemsDot(document, position) {
	const line = document.lineAt(position);
	cacheDoc = document

	// 只截取到光标位置为止,防止一些特殊情况
	const lineText = line.text

	let {
		data,
		exportObj
	} = global.filelanguageDatas || reReadFiles(document)

	// 整理对象树
	let o = {}
	exportObj.map(io => {
		if (io.key) {
			o[io.key] = io.value
		} else {
			o = {...o, ...io}
		}
	})

	if (/\$t\(\'\w+\./g.test(lineText)) {
		// 匹配二级对象
		let mtData = lineText.match(/\$t\(\'(\w+)\./)
		if (!mtData) return
		mtData = mtData[1]

		// mtData 可能是一个衍射的字段
		let hasMapFlag = false		
		data.forEach(mp => {
			for (const kmp in mp) {
				if (o[mtData] === kmp) {
					// 找到衍射的值
					hasMapFlag = true
					// 缓存对象树,从第一级开始匹配,有利于性能优化
					currentHandleData = {
						[mtData]: mp[kmp]
					}
				}
			}
		})

		if (!hasMapFlag) {
			currentHandleData = {
				[mtData]: o[mtData]
			}
		}

		if (typeof currentHandleData[mtData] === "string") {
			// 最后一级,截止处理
			return [new vscode.CompletionItem(`${mtData}(${o[mtData]})`, vscode.CompletionItemKind.Field)]
		} else {
			// 还有数据  国际化不会再出现嵌套层  从编写规范来说  就不再往后进行递归支持了
			return Object.keys(currentHandleData[mtData]).map(element => {
				return {
					label: element,
					kind: vscode.CompletionItemKind.Text,
					detail: currentHandleData[mtData][element],
					// additionalTextEdits: [new vscode.TextEdit(new vscode.Range(2, 4), '插入数据')]
				}
			})
		
		}
	} 
}

// 针对 $t 中的t 进行一级对象key值的提示
function provideCompletionItemsT(document, position) {
	const line = document.lineAt(position);
	cacheDoc = document

	// 只截取到光标位置为止,防止一些特殊情况
	const lineText = line.text


	let {
		data,
		exportObj
	} = global.filelanguageDatas || reReadFiles(document)

	console.log('--00--')
	
	// 整理对象树
	let o = {}
	exportObj.map(io => {
		if (io.key) {
			o[io.key] = io.value
		} else {
			o = {...o, ...io}
		}
	})

	if (/\$t/g.test(lineText)) { 
		let commadStr = []
		exportObj.forEach(item => {
			if (item.key) {
			  commadStr.push(item.key)
			} else {
			  for (const k in item) {
				commadStr.push(k)
			  }
			}
		})

		return commadStr.map(ei => {
			return new vscode.CompletionItem(`t('${ei}')`, vscode.CompletionItemKind.Field)
		})
	}
}


// 拦截数据
// 拦截数据 可以在提示选中后对提示的内容进行一些增减,往往自定义提示的时候非常有用
function resolveCompletionItem() {
	return null
}


// 入口函数
function activate(context) {

	const reRead = vscode.commands.registerCommand('read.file', async function () {
	  reReadFiles(cacheDoc)
	})
	// 注册重新扫描页面
	context.subscriptions.push(reRead)
	// 注册翻译 
	context.subscriptions.push(disposable)
	const TYPES = [ 'javascript', 'html', 'typescript', 'vue' ];

	// 遍历注册插件需要执行的文本类型
    TYPES.forEach(item => {
        let providerDisposable = vscode.languages.registerCompletionItemProvider(
            {
                scheme: 'file',
                language: item
            },
            {
				provideCompletionItems: provideCompletionItemsT,
				resolveCompletionItem
			}, 
			't'
        );
        context.subscriptions.push(providerDisposable); // 完成订阅

		let providerDisposable2 = vscode.languages.registerCompletionItemProvider(
            {
                scheme: 'file',
                language: item
            },
            {
				provideCompletionItems: provideCompletionItemsDot,
				resolveCompletionItem
			}, 
			'.'
        );
        context.subscriptions.push(providerDisposable2); // 完成订阅
    });

	// 注册鼠标悬停提示
	provideHover(context) 

	// 注册匹配国际字符串
	fuzzyMatch(context)
}

fuzzyMatch.js

const vscode = require('vscode');

const { reReadFiles } = require('./reReadFile')
 
const { handleBfs } = require('./utils')

const fuzzyMatch = vscode.commands.registerCommand('fuzzy.match', async function () {
    /**
     * @type {string} 选择的单词
     */
    let selectWord
    const currentEditor = vscode.window.activeTextEditor
    if (!currentEditor) return
    const currentSelect = currentEditor.document.getText(currentEditor.selection)
    if (!currentSelect) return

    // 获取页面数据 对象树
    const { dataTree } = reReadFiles()

    const bfsTree = handleBfs(dataTree)
    
    const mapArr = bfsTree.filter(item => {
        return item.label.indexOf(currentSelect) !== -1
    }).map(item => ({
        label: `this.$t('${item.lang}')`,
        description: item.label
    }))
    
    selectWord = await vscode.window.showQuickPick(mapArr, 
        { 
            picked: true,
            canPickMany: false,
            matchOnDescription: false,
            matchOnDetail: false
        }
    )

    if (selectWord) {
        currentEditor.edit(editBuilder => {
            editBuilder.replace(currentEditor.selection, selectWord.label)
        })
    }
})

module.exports = {
    fuzzyMatch
}

provideHover.js

const vscode = require('vscode');
const { getProjectPath } = require('./utils')
const hf = require('./handleFiles')

let filelanguageDatas


const reReadFiles = function (doc) {
	const projectPath = getProjectPath(doc);
	console.log(projectPath)
	// 获取项目配置文件
	const jsonConfig = require(`${projectPath}/package.json`);
	if (jsonConfig["language-config"]) {
		// 存在配置
		const {
			ppaths,
			exportObj
		} = hf.handlePaths({
			// 输入入口, 这个地方是一个汇总的入口文件
			input: jsonConfig["language-config"]["zh-input"],
		}, projectPath)
		let reBuildExportObj = []
		let checks = exportObj.every(item => item.key)
		let datas = hf.getFileData(ppaths)
		if (!checks) {
			// 说明有...
			exportObj.forEach(item => {
				if (item.key) {
					reBuildExportObj.push(item)
				} else {
					datas.filter(dot => {
						for (const key in dot) {
							if (key === item.value) {
								reBuildExportObj.push(dot[key])
							}
						}
					})
				}
			})
		}
		// 读取数据
		filelanguageDatas = {
			data: datas,
			exportObj: reBuildExportObj.length === 0 ? exportObj : reBuildExportObj
		}
		return filelanguageDatas
	} else {
		// 抛出错误
	}
}

/**
 * 鼠标悬停提示,当鼠标停在package.json的dependencies或者devDependencies时,
 * 自动显示对应包的名称、版本号和许可协议
 * @param {*} document 
 * @param {*} position 
 * @param {*} token 
 */
function provideHover(document, position, token) {
	const line		= document.lineAt(position);
	// const projectPath = getProjectPath(document);
	const lineText = line.text
	// console.log(lineText)

	const { data, exportObj } = reReadFiles(document)
    const reg = /\$t\(([\'\"])(.+)\1\)/
    if (reg.test(lineText)) {
        let m = lineText.match(reg) && lineText.match(reg)[2]
		if (m) {
			if (/\./.test(m)) {
				let hArr = m.split('.')
				let finalDes = ""
				exportObj.forEach(item => {
					if (item.key) {
						// 有映射
						if (item.key === hArr[0]) {
							data.filter(df => {
								for (const kdf in df) {
									if (kdf === item.value) {
										finalDes = getDes(hArr.slice(1), df[kdf])
										console.log(finalDes, '===')
									}
								}
							})
						}
					} else {
						for (const kk in item) {
							if (kk === hArr[0]) {
								// json
								finalDes = getDes(hArr.slice(1), item)
							}
						}
					}
				})
				
				function getDes(hArr, obj) {
					let cArr = JSON.parse(JSON.stringify(hArr))
					let s = ""
					while (cArr.length > 0) {
						s = obj[cArr.shift()]
					}
					return s
				}

				return new vscode.Hover(`* **中文名为**:${finalDes || '未找到'}`);
			}
		}
    }
}

module.exports = function(context) {


	// 注册鼠标悬停提示

	const TYPES = [
		'javascript',
		'typescript',
        'html',
        'vue'
    ];

	// 遍历注册插件需要执行的文本类型
    TYPES.forEach(item => {
        let providerDisposable = vscode.languages.registerHoverProvider(
            {
                scheme: 'file',
                language: item
            },
            {
				provideHover
			}
        );
        context.subscriptions.push(providerDisposable); // 完成订阅
    });
	// context.subscriptions.push(vscode.languages.registerHoverProvider('javascript', {
	// 	provideHover
	// }));
};

reReadFile.js


const vscode = require('vscode');
const { getProjectPath } = require('./utils')
const hf = require('./handleFiles')

const reReadFiles = function (doc) {
  if (!doc) {
    doc = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.document : null;
  }
  if (!doc) {
      throw new Error('当前激活的编辑器不是文件或者没有文件被打开!');
  }

	const projectPath = getProjectPath(doc);

	// 获取项目配置文件
	const jsonConfig = require(`${projectPath}/package.json`);
	if (jsonConfig["language-config"]) {
		// 存在配置
		const {
			ppaths,
			exportObj
		} = hf.handlePaths({
			// 输入入口, 这个地方是一个汇总的入口文件
			input: jsonConfig["language-config"]["zh-input"],
		}, projectPath)
		let reBuildExportObj = []
		let checks = exportObj.every(item => item.key)
		let datas = hf.getFileData(ppaths)
		if (!checks) {
			// 说明有...
			exportObj.forEach(item => {
				if (item.key) {
					reBuildExportObj.push(item)
				} else {
					datas.filter(dot => {
						for (const key in dot) {
							if (key === item.value) {
								reBuildExportObj.push(dot[key])
							}
						}
					})
				}
			})
		}

   		 // 组装结构树
		const dataTree = {}
		exportObj.forEach(eo => {
		if (eo.key) {
			// 有映射
			let doValues = null
			datas.forEach(dTree => {
			for (const dok in dTree) {
				if (dok === eo.value) {
				doValues = dTree[dok]
				}
			}
			})
			dataTree[eo.key] = doValues
		} else {
			datas.forEach(dTree => {
			for (const dask in dTree) {
				if (dask === eo.value) {
				for (const dasInDask in dTree[dask]) {
					dataTree[dasInDask] = dTree[dask][dasInDask]
				}
				}
			}
			})
		}
		})

		// 读取数据
		let filelanguageDatas = {
			data: datas,
			exportObj: reBuildExportObj.length === 0 ? exportObj : reBuildExportObj,
      		dataTree
		}
		console.log(filelanguageDatas, "filelanguageDatas")
    	global.filelanguageDatas = filelanguageDatas
		return filelanguageDatas
	} else {
		// 抛出错误
	}
}

module.exports = {
  reReadFiles
}

traslate.js

const vscode = require('vscode');
const api = require('./translate-api')


/**
 * 
 * @param {*} str 判断是否包含中文
 * @returns 
 */
function containChinese(str){
  if(/.*[\u4e00-\u9fa5]+.*$/.test(str)) {
    return true;
  } 
  return false;

}

async function commonTranslate(currentSelect) {

    let selectWord

    const data = await api.translate(currentSelect, 'zh', 'en')

    if(data.data.error_code) {
      handlingExceptions(data.data.error_code)
      return
    }
    
    const result = data.data.trans_result[0].dst
    // 基于空格分割
    const list = result.split(' ')
    if (list.length > 1) {
      const arr = []
      // 小驼峰
      arr.push(list.map((v, i) => {
        if (i !== 0) {
          return v.charAt(0).toLocaleUpperCase() + v.slice(1)
        }
        return v.toLocaleLowerCase()
      }).join(''))
      // 空格
      arr.push(list.map((v, i) => {
          if (i === 0) {
              return v.charAt(0).toLocaleUpperCase() + v.slice(1).toLocaleLowerCase()
          }
          return v.toLocaleLowerCase()
      }).join(' '))
      // - 号连接
      arr.push(list.map(v => v.toLocaleLowerCase()).join('-'))
      // 下划线连接
      arr.push(list.map(v => v.toLocaleLowerCase()).join('_'))
      // 大驼峰
      arr.push(list.map(v => v.charAt(0).toLocaleUpperCase() + v.slice(1)).join(''))
      selectWord = await vscode.window.showQuickPick(arr, { placeHolder: '请选择要替换的变量名' })
    } else {
      selectWord = list[0]
    }

    return selectWord
}

async function handleObjTranslate(currentSelect) {
    let selectWord

    if (currentSelect.indexOf(':') === -1) return;

    const data = await api.translate(currentSelect, 'zh', 'en')

    if(data.data.error_code) {
      handlingExceptions(data.data.error_code)
      return
    }
    
    const result = data.data.trans_result[0].dst
    // 基于空格分割
    const list = result.split(' ')
    if (list.length > 1) {
      const arr = []
      // 小驼峰
      arr.push(list.map((v, i) => {
        if (i !== 0) {
          return v.charAt(0).toLocaleUpperCase() + v.slice(1)
        }
        return v.toLocaleLowerCase()
      }).join(''))
      // 空格
      arr.push(list.map((v, i) => {
          if (i === 0) {
              return v.charAt(0).toLocaleUpperCase() + v.slice(1).toLocaleLowerCase()
          }
          return v.toLocaleLowerCase()
      }).join(' '))
      // - 号连接
      arr.push(list.map(v => v.toLocaleLowerCase()).join('-'))
      // 下划线连接
      arr.push(list.map(v => v.toLocaleLowerCase()).join('_'))
      // 大驼峰
      arr.push(list.map(v => v.charAt(0).toLocaleUpperCase() + v.slice(1)).join(''))
      selectWord = await vscode.window.showQuickPick(arr, { placeHolder: '请选择要替换的变量名' })
    } else {
      selectWord = list[0]
    }

    return selectWord
}

async function handleTranslate(currentSelect, isObj = false) {
    if (isObj) {
        // 对象
        return await handleObjTranslate(currentSelect)
    } else {
        // 普通
        return await commonTranslate(currentSelect)
    }
    
}

/**
 * 处理异常
 */
 function handlingExceptions(code) {
    const codes = {
      "52001": "请求超时,检查网络后重试" ,
      "52002": "系统错误, 查看百度翻译官网公告",
      "52003": "请检查appid或者服务是否开通",
      "54000": "必填参数为空",
      "54001": " 签名错误",
      "54003": "访问频率受限",
      "54004": "账户余额不足 ",
      "54005": "长query请求频繁, 请降低长query的发送频率,3s后再试 ",
      "58000": "客户端IP非法",
      "58001": "语言不支持",
      "58002": "服务当前已关闭, 请前往管理控制台开启服务",
      "90107": "认证未通过或未生效",
    }
    vscode.window.showWarningMessage("translateNamed: " + codes[code] || "未知异常, 可评论反馈")
  }

  const disposable = vscode.commands.registerCommand('translate.zntoen', async function () {
    /**
     * @type {string} 选择的单词
     */
    let selectWord
    const currentEditor = vscode.window.activeTextEditor
    if (!currentEditor) return
    const currentSelect = currentEditor.document.getText(currentEditor.selection)
    if (!currentSelect) return

    // 匹配单一字符串
    selectWord = await handleTranslate(currentSelect)

    if (selectWord) {
      currentEditor.edit(editBuilder => {
        editBuilder.replace(currentEditor.selection, selectWord)
      })
    }
  })

module.exports = {
  disposable
}