欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

基于 VSCode 的国际化解决方案 - Internationalization Workbench (i18n-workbench)

最编程 2024-04-06 20:15:28
...

概要

工作台(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对文档中的文案进行提取

Extractor.gif

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 作为工作台的载体,以下简单页面的展示

WebView

插入国际化资源文件

将当前工作台中所编辑的文案插入国际化文件

支持的类型:

  • ecmascript
  • json

使用类图中的Inserter对上述工作台中所处理好的数据进行插入

Inserter.gif

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/traverseJSON.stringify的一些操作,不做阐述

替换当前文档中的文案

将当前文档中的文案使用与国际化资源中的Key和国际化调用函数名称($i18n、$t)做一个转换

使用类图中的Replacer处理文档中的文案

Replacer.gif

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 那真的太棒了 ????