基于 VSCode 的国际化解决方案 - Internationalization Workbench (i18n-workbench)
概要
工作台(Workbench)指的是桌面开发环境。工作台的目标是通过提供一个通用的创建、管理和导航工作空间资源的模型,载体目前是VSCode
设计动机
国际化的困扰
文案提取、文案翻译、系统中语法转换过程繁琐低效,使得开发效率降低,并且扰人心态。现有的解决国际化工具的模式包括:
- cli工具
- 基于VSCode实现的交互式插件 网络上关于有关这些模式的工具介绍有很多,这里就不再赘述细节。总体来说,这些工具存在以下问题:
- 文案提取&文案转换不支持复杂的组合方式(模板字符&静态文案的拼接),需要人工干预。
- 整体交互链路过于繁琐,提效不明显。
- 由于整体交互链路的影响,很多文案提取的key都为随机。
- 隐式问题过多。
设定目标
由VSCode做载体,实现一个拟工作台的交互式插件。保证文案提取、文案翻译、文案插入、替换文档资源文案整条交互链路流畅、可靠、高效、易拓展的前提,针对当前工作空间中的文档资源、国际化资源做出创建与管理。
- 工作空间:表示当前VSCode的工作空间
- 文档资源:表示VSCode打开的当前文档或者资源管理器中的所有资源
- 国际化资源:表示项目中的国际化文件
设计细节
插件类图展示
解析本地国际化资源
将本地工作空间中的国际化文件解析为对象数据类型
支持的类型
- ecmascript
- json
根据全局设置的国际化资源的目录结构设置不同的模式
file(文件模式):
- locales
- zh-CN.js
- en-US.js
dir(目录模式):
- locales
- zh-CN
- common.js
- table.js
- en-US
- common.js
- table.js
使用类图中的Parser
对资源文件进行解析
- ecmascript:使用babel将代码统一输出为CJS(export default {} > module.exports = {}),使用node的Module模块对其转换后的代码编译最终得到module对象
- json:JSON.parse暴力解析
// ecmascript:
const code = readFileSync('locales/zh-CN.js', 'utf-8')
const ast = parse(texts, {
sourceType: 'module'
})
traverse(ast, {
ExportDefaultDeclaration(path: NodePath<{ declaration: any }>) {
path.replaceWith(
expressionStatement(
assignmentExpression(
'=',
memberExpression(identifier('module'), identifier('exports')),
objectExpression(path.node.declaration.properties),
)
)
)
}
})
const { code } = generate(ast)
const module = new Module('module')
module._compile(code, 'module')
console.log(module.exports) // {}
// json
const code = readFileSync(locales/zh-CN.js', 'utf-8')
console.log(JSON.parse(text)) // {}
提取文档中的文案
使用@vue/compiler & @babel全家桶将当前文档资源中需国际化的文案提取 支持的类型:
- sfc
- tsx&jsx
使用类图中的Extractor
对文档中的文案进行提取
template
对于 VUE SFC 内部模板语法解析得到的 AST 和 JS 的 AST 区别很大,主要是没有像
@babel/traverse
等处理工具,有的只是vue-template-compiler
或者@vue/compiler-*
这样的解析工具。所以只能对template-ast树进行递归,提取ELEMENT、TEXT、SIMPLE_EXPRESSION
类型中符合条件的文案。
包含文案的AST节点类型:
export enum NodeTypes{
// 元素节点,包括template元素
ELEMENT = 1,
// 文本类型,包括代码里的一切空白字符,例如换行,空格等
// <span title="文本类型">文本类型</span>
TEXT = 2,
// 表达式,包括模板字符串等
// <span :title="`${data}表达式`">{{ "表达式" + `${data}表达式`}}</span>
SIMPLE_EXPRESSION = 4,
// 插值
INTERPOLATION = 5,
// 普通属性
ATTRIBUTE = 6,
// 指令的值
DIRECTIVE = 7,
}
script
script
就是解析<script>
标签内部 JS 得到的内容,只不过@vue/compiler-*
没有将这部分直接解析成 AST,也就是我们还需要利用@babel/parser
去解析,并使用@babel/traverse
去遍历AST节点。
// 伪代码
traverse(ast, {
StringLiteral: (path) => {
const { value, start, end } = path.node
if (!start || !end) return
if (path.findParent(p => p.isImportDeclaration())) return
const range = new Range(
document.positionAt(offset + start + 1),
document.positionAt(offset + end - 1)
)
words.push({
id: this.id,
text: value,
start,
end,
range,
isSetup,
type: 'js-string'
})
},
TemplateLiteral: (path) => {
if (path.findParent(p => p.isImportDeclaration())) return
const value = path.get('quasis').map(item => ({
text: item.node.value.raw,
start: item.node.start
}))
value.forEach(item => {
this.getShouldExtractedText(item.text).forEach(t => {
if (item.start) {
const start = source.indexOf(t, item.start)
const end = start + t.length
const range = new Range(
document.positionAt(start + offset),
document.positionAt(end + offset)
)
words.push({
id: this.id,
text: t,
start,
end,
range,
isDynamic: true,
isSetup,
type: 'js-template'
})
}
})
})
}
})
创建工作台页面
使用VSCode内置API创建工作台,即 Webview
使用类图中的Workbench
作为Webview创建与通信的桥梁
出于安全考虑,Webview默认无法直接访问本地资源,它在一个孤立的上下文中运行,想要加载本地图片、js、css等必须通过特殊的vscode-resource:
协议,网页里面所有的静态资源都要转换成这种格式,否则无法被正常加载。
class Workbench{
private constructor() {
this.panel = window.createWebviewPanel('workbench', '工作台', ViewColumn.Beside, {
enableScripts: true,
retainContextWhenHidden: true,
})
this.panel.iconPath = Uri.file(
join(Config.extensionPath, 'resources/workbench.svg')
)
this.panel.webview.html = getHtmlForWebview(Config.extensionPath, 'resources/workbench/index.html')
this.panel.webview.onDidReceiveMessage((message) => this.handleMessages(message), null, this.disposables)
this.panel.onDidDispose(() => this.dispose(), null, this.disposables)
}
private handleMessages(message: Message) {
const { type, data } = message
switch (type) {
case EventTypes.READY:
this.panel.webview.postMessage({
type: EventTypes.CONFIG,
data: this.config
})
break
case EventTypes.SAVE:
CurrentFile.write(data)
break
case EventTypes.TRANSLATE_SINGLE:
this.translateSignal(data)
break
}
}
}
vite & vue 作为工作台的载体,以下简单页面的展示
插入国际化资源文件
将当前工作台中所编辑的文案插入国际化文件
支持的类型:
- ecmascript
- json
使用类图中的Inserter
对上述工作台中所处理好的数据进行插入
1、转换工作台的数据
[ { key: "common.confirm", insertPaths: { "zh-CN": "/xxx/zh-CN/common.ts" "en-US": "/ccc/en-US/common.ts" }, languages: { "zh-CN": "确认" "en-US": "confirm" } }]
转换为:
{
"/xxx/zh-CN/common.ts": {
flattenData: {
"common.confirm":"确认",
"common.cancel":"取消"
},
unFlattenData: {
common: {
confirm: "确认",
cancel: "取消"
}
}
},
"/ccc/en-US/common.ts": {
// 同上
}
}
2、合并工作台的数据与Parser
类中所提取的数据
{
"common.confirm": "确认",
"common.cancel": "取消"
} // 步骤1的数据
{
"common.add": "添加",
"common.delete": "删除"
} // Parser类中的数据
合并为:
{
common: {
confirm: "确认",
cancel: "取消",
add: "添加",
delete: "删除"
}
}
3、根据路径进行插入,实现逻辑涉及到一些@babel/traverse
与JSON.stringify
的一些操作,不做阐述
替换当前文档中的文案
将当前文档中的文案使用与国际化资源中的Key和国际化调用函数名称($i18n、$t)做一个转换
使用类图中的Replacer
处理文档中的文案
1、根据Parser类中所提取的数据转为为 text: key 的形式
{
common: {
confirm: "确认",
cancel: "取消",
add: "添加",
delete: "删除"
}
}
转为:
{
"确认": "common.confirm",
"取消": "common.cancel",
"添加": "common.add",
"删除": "common.delete"
}
2、使用类图中的Extractor
模块提取的文案数据进行替换
Interface
export type ExtractorType = 'html-attribute' | 'html-attribute-template' | 'html-inline' | 'html-inline-template' | 'js-string' | 'js-template' | 'jsx-text'
export interface ExtractorResult {
id: ExtractorId
text: string
start: number
end: number
range: Range
isDynamic?: boolean
isSetup?: boolean // script setup
isJsx?: boolean
fullText?: string
fullStart?: number
fullEnd?: number
fullRange?: Range
attrName?: string // when the type is html-attribute
type: ExtractorType
}
3、使用类图中的Refactor
对文案数据进行一个重构
具体重构文案的逻辑不做阐述噢
switch (type) {
case 'html-inline':
break
case 'html-inline-template':
break
case 'html-attribute':
break
case 'html-attribute-template':
break
case 'js-string':
break
case 'js-template':
break
case 'jsx-text':
break
default:
break
}
如何使用
设置语言目录
不出意外导入插件后就会自动识别目录了,如果出了意外那就
Ctrl + P
输入手动设置语言目录 | i18n-workbench.config-locales
目录结构
file(文件模式):
- locales
- zh-CN.js
- en-US.js
dir(目录模式):
- locales
- zh-CN
- common.js
- table.js
- en-US
- common.js
- table.js
打开翻译工作台
Ctrl + P 输入打开翻译工作台 | i18n-workbench.open-workbench
替换当前文档
Ctrl + P 输入替换当前文档 | i18n-workbench.replace-with
未来展望
1、单包架构变为多包,核心功能(提取、替换、翻译)抽离,使用客户端软件作为工作台载体,同时支持VSCode。
2、增强插件功能
结尾
此插件未发布VSCode拓展商店。
仓库地址:github.com/yzydevelope…
下载地址:i18n-workbench-vscode-extension
希望此插件能够帮助到大家,或者有想法的小伙伴可以基于此插件做些定制化的功能,提高团队效率。再或者可以从意识层面启发大家可以从哪些方面入手去解决问题。
如果你遇到了难以解决的问题
或者有想法
,那你可以加我微信DK573432332
,要是可以贡献 PR 那真的太棒了 ????