国际化插件开发
- 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
}