Skip to content

创建项目

安装依赖

shell
pnpm i -g yo generate-code

初始化插件目录

shell
yo code

注:如果报错:You don't seem to have a generator with the name “generator-code” installed. 就先执行npm install generator-code.

<!-- more -->

目录结构

json
{
  "name": "i18n-automatically", // 定义了插件的包名,它必须是唯一的并且符合 npm 的包命名规范。
  "displayName": "I18n Automatically",  // 定义了插件在 Visual Studio Code 中显示的名称。
  "description": "vscode国际化多语言自动生成替换方案",
  "version": "0.0.1", 
  "engines": {
    "vscode": "^1.92.0"   // 定义了插件所支持的 Visual Studio Code 的最低版本。
  },
  "categories": [   // 定义了插件所属的分类,以便在 Visual Studio Code 中组织和展示插件。
    "Extension Packs",
    "Programming Languages",
    "Visualization",
    "Debuggers",
    "Notebooks"
  ],
  "activationEvents": [],    //  定义了激活插件的事件
  "main": "./out/extension.js",  // 定义插件的入口文件
  "contributes": {
    "commands": [
      {
        "command": "i18n-automatically.mytest",  // 注册的插件命令
        "title": "mytest"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./",
    "pretest": "npm run compile && npm run lint",
    "lint": "eslint src --ext ts",
    "test": "node ./out/test/runTest.js",
    "prepare": "husky install",
    "commitlint": "commitlint --config commitlint.config.js -e -V"
  },
  "devDependencies": {},
  "lint-staged": {
    "**/*": "prettier --write --ignore-unknown"
  }
}

运行插件

  1. F5 运行插件 vsode会打开一个新的调试窗口 20240824153005

  2. 在新的调试窗口中,按 ctrl + shift + p,打开命令面板;

  3. 输入注册的插件命令,即可执行插件。 在package.json和extension文件中定义了命令,项目的默认命令是hello word,可自行更改。

功能实现

extension.ts作为项目的入口文件,里面的两个函数定义了插件激活/销毁时要执行的功能:

js
// 模块“vscode”包含VS Code可扩展性API
// 导入模块,并在下面的代码中使用别名vscode引用它
const vscode = require("vscode");

// 当您的扩展被激活时,会调用此方法
// 您的扩展在第一次执行命令时被激活

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  // 使用控制台输出诊断信息(console.log)和错误(console.error)
  // 当您的扩展被激活时,这行代码只会执行一次
  console.log(
    'Congratulations, your extension "i18n-automatically" is now active!'
  );

  // 该命令已在package.json文件中定义
  // 现在使用registerCommand提供命令的实现
  // commandId参数必须与package.json中的命令字段匹配
  const disposable = vscode.commands.registerCommand(
    "i18n-automatically.helloWorld",
    function () {
      // The code you place here will be executed every time your command is executed

      // Display a message box to the user
      vscode.window.showInformationMessage(
        "Hello World from i18n-automatically!"
      );
    }
  );

  context.subscriptions.push(disposable);
}

// This method is called when your extension is deactivated
function deactivate() {}

module.exports = {
  // 插件激活
  activate,
  // 插件销毁
  deactivate,
};

剩下的开发和平时一样。

打包发布

账号准备

https://marketplace.visualstudio.com/manage

package.json修改

新增 publisher: publisher ID

打包命令

安装依赖

shell
pnpm i @vscode/vsce -g

打包,在根目录下执行打包命令,执行成功后,会在根目录下生成 .vsix 文件

用yarn的话,后面加参数

shell
vsce package --yarn

发布

  • 命令发布
shell
vsce publish -p YOUR_PUBLISHER_ID

参考链接

https://rackar.github.io/vscode-ext-doccn/

原理讲解( AST 自动转换国际化方案分享)

20241104194430

1. 整体思路

1.1. 技术选型

  • Vue SFC 解析: @vue/compiler-sfc

    • 用于解析 .vue 文件的模板和脚本部分
    • 分别处理 template、script、scriptSetup 块
  • Babel 工具链:

    javascript
    const parser = require('@babel/parser'); // 将代码解析为 AST
    const traverse = require('@babel/traverse'); // 遍历和修改 AST
    const t = require('@babel/types'); // AST 节点类型处理
    const generate = require('@babel/generator'); // AST 转回代码

2. 支持的文件类型

2.1. Vue 单文件组件 (.vue)

javascript
// 文件处理器
function getFileProcessor(fileExt) {
  const processors = {
    '.vue': handleVueFile,  // Vue 处理器
    ...
  };
}

// Vue 文件解析
async function handleVueFile(filePath, config) {
  const { descriptor } = parseSfc(fileContent);

  // 1. 处理模板
  if (descriptor.template) {
    processTemplate(descriptor.template.ast);
  }

  // 2. 处理脚本
  if (descriptor.script) {
    processScript(descriptor.script.content);
  }

  // 3. 处理 setup
  if (descriptor.scriptSetup) {
    processScript(descriptor.scriptSetup.content);
  }
}

2.2. JavaScript/TypeScript 文件

javascript
const processors = {
  '.js': handleJsFile, // JavaScript
  '.jsx': handleJsFile, // React JSX
  '.ts': handleJsFile, // TypeScript
  '.tsx': handleJsFile, // React TSX
};

2.3. 解析配置

javascript
// Babel 解析配置
const parseOptions = {
  sourceType: 'module',
  plugins: [
    'jsx', // 支持 JSX 语法
    'typescript', // 支持 TypeScript
    'decorators-legacy', // 支持装饰器
  ],
};

// Vue 解析配置
const vueOptions = {
  filename: 'component.vue',
  sourceMap: false,
};

3. AST 转换核心处理

核心工作流程

20241104194519

3.1. Template AST 转换

3.1.1. 节点类型处理
javascript
// 处理不同类型的模板节点
function processTemplate(templateAst, context) {
  const nodeTypeHandlers = {
    // 注释节点
    3: (node) => node.loc.source,

    // 文本节点 - 处理纯文本内容
    2: (node) => {
      if (containsChinese(node.content)) {
        const key = generateKey(context);
        context.translations.set(key, node.content.trim());
        return `{{ $t('${key}') }}`;
      }
      return node.content;
    },

    // 插值节点 - 处理 {{ }} 内的表达式
    5: (node) => {
      if (node.content.ast) {
        // 处理包含 JS 表达式的插值
        return processInterpolationNode(node, context);
      }
      return processTemplateString(node.content.content, context);
    },

    // 元素节点 - 处理标签及其属性
    1: (node) => processElementNode(node, context),
  };

  return astArrayToTemplate(templateAst, nodeHandlers);
}
3.1.2. 元素节点处理
javascript
function processElementNode(node, context) {
  let result = `<${node.tag}`;

  // 处理属性
  if (node.props) {
    for (const prop of node.props) {
      // 处理指令 (v-bind, v-on 等)
      if (prop.type === 7) {
        result += processDirective(prop, context);
      }
      // 处理普通属性
      else if (prop.type === 6) {
        result += processAttribute(prop, context);
      }
    }
  }

  // 处理子节点
  if (!node.isSelfClosing) {
    result += '>';
    if (node.children) {
      result += node.children
        .map((child) => astToTemplate(child, context))
        .join('');
    }
    result += `</${node.tag}>`;
  } else {
    result += ' />';
  }

  return result;
}
3.1.3. 指令处理
javascript
function processDirective(prop, context) {
  const name = getDirectiveName(prop);

  // 没有表达式的指令
  if (!prop.exp) return `\n${name}`;

  // 处理带表达式的指令
  if (containsChinese(prop.exp.content)) {
    // 处理包含 HTML 的情况
    if (stringWithDom(prop.exp.content)) {
      const handlerContent = prop.exp.content
        .trim()
        .replace(/^[\s\n]*[`'"]|[`'"][\s\n]*$/gm, '');
      const result = handlerDomNode(handlerContent, context);
      return `\n${name}="\`${replaceForI18nCall(result, context)}\`"`;
    }

    // 处理普通中文表达式
    const result = handlerForJs(prop.exp, context);
    return `\n${name}="${replaceForI18nCall(result, context)}"`;
  }

  return `\n${name}="${prop.exp.content}"`;
}

3.2. JavaScript AST 转换

3.2.1. AST 遍历处理
javascript
function processJsAst(context) {
  const ast = parser.parse(context.contentSource, {
    sourceType: 'module',
    plugins: ['jsx', 'typescript', 'decorators-legacy'],
  });

  traverse(ast, {
    // 处理字符串字面量
    StringLiteral(path) {
      handleChineseString(path, context);
    },

    // 处理模板字符串
    TemplateElement(path) {
      handleChineseString(path, context, true);
    },

    // 处理 JSX 文本
    JSXText(path) {
      handleChineseString(path, context);
    },

    // 处理 JSX 属性
    JSXAttribute(path) {
      handleJSXAttribute(path, context);
    },

    // 处理 JSX 表达式容器
    JSXExpressionContainer(path) {
      handleJSXExpressionContainer(path, context);
    },

    // 处理程序入口,检查 i18n 导入
    Program: {
      enter(path) {
        checkForI18nImport(path, context);
      },
    },
  });

  return generateCode(ast, context);
}
3.2.2. 中文字符串处理
javascript
function handleChineseString(path, context, isTemplateLiteral = false) {
  try {
    const value = isTemplateLiteral ? path.node.value.raw : path.node.value;

    // 跳过非中文或调试上下文
    if (!containsChinese(value) || isInDebugContext(path)) return;

    // 处理包含 HTML 的字符串
    if (stringWithDom(value)) {
      handleStringWithDom(path, context, isTemplateLiteral);
      return;
    }

    const key = generateKey(context);

    // 根据不同场景选择替换策略
    if (isTemplateLiteral) {
      handleTemplateLiteral(path, context, key);
    }
    // JSX 场景
    else if (path.type === 'JSXText' || path.parent?.type.includes('JSX')) {
      replaceWithJSXI18nCall(path, context, key);
    }
    // 普通字符串场景
    else {
      replaceWithI18nCall(path, context, key);
    }

    // 保存翻译
    context.translations.set(key, value.trim());
  } catch (error) {
    context.index--;
    customConsole.error('handleChineseString 错误:', error);
  }
}
3.2.3. 特殊场景处理
javascript
// 处理带 HTML 的字符串
function handleStringWithDom(path, context, isTemplateLiteral) {
  if (path.type === 'StringLiteral') {
    convertStringLiteralToTemplateLiteral(path, context);
  } else if (isTemplateLiteral) {
    processTemplateElement(path, context);
  }
}

// 处理模板字符串
function handleTemplateLiteral(path, context, key) {
  const templateLiteral = path.parentPath;
  const newExpression = t.callExpression(
    t.identifier(context.config.scriptI18nCall),
    [t.stringLiteral(key)],
  );

  // 保持原有表达式的顺序
  const existingExpressions = templateLiteral.node.expressions.map((exp) => ({
    node: exp,
    start: exp.start,
  }));
  existingExpressions.push({
    node: newExpression,
    start: path.node.start,
  });

  // 重新排序并更新节点
  templateLiteral.node.expressions = existingExpressions
    .sort((a, b) => a.start - b.start)
    .map((item) => item.node);
}

// 上下文检查
function isInDebugContext(path) {
  const debugContexts = [
    // 检查 console 调用
    (p) =>
      p.isCallExpression() &&
      p.get('callee').isMemberExpression() &&
      p.get('callee.object').isIdentifier({ name: 'console' }),

    // 检查 Error 抛出
    (p) =>
      p.isNewExpression() && p.get('callee').isIdentifier({ name: 'Error' }),

    // 检查断言
    (p) =>
      p.isCallExpression() && p.get('callee').isIdentifier({ name: 'assert' }),

    // 检查调试器语句
    (p) => p.isDebuggerStatement(),
  ];

  return debugContexts.some((context) => path.findParent(context));
}
处理模板字符串,为什么要重新排序?
  1. 模板字符串的结构:
javascript
// 一个模板字符串可能包含多个表达式,例如:
`Hello ${name}, your score is ${score}!`;
  1. 在 AST 中,这个模板字符串被表示为:
  • quasis(静态文本部分):["Hello ", ", your score is ", "!"]
  • expressions(表达式):[name, score]
  1. 当我们在处理国际化时,可能会遇到这样的情况:
javascript
`用户 ${name} 的分数是 ${score} 分`;
  1. 处理后我们希望变成:
javascript
`${t('user-key')} ${name} ${t('score-key')} ${score} ${t('point-key')}`;
  1. 需要排序的原因:

    • AST 遍历过程中可能不是按照代码中的顺序处理表达式的
    • 当我们添加新的 i18n 函数调用时,需要确保它们被插入到正确的位置
    • expressions 数组中表达式的顺序必须与模板字符串中出现的顺序完全匹配
  2. 来看一个具体例子:

javascript
// 原始代码
`用户 ${name} 的分数是 ${score} 分`

// AST处理过程中可能的顺序:
1. 处理 "用户"
2. 处理 "的分数是"
3. 处理 "分"

// 如果不排序,可能会变成:
`${t('point-key')} ${t('user-key')} ${name} ${t('score-key')} ${score}`

// 排序后,正确的顺序:
`${t('user-key')} ${name} ${t('score-key')} ${score} ${t('point-key')}`

通过使用 start 位置进行排序,我们可以确保表达式按照它们在原始代码中出现的顺序排列,从而保持模板字符串的语义不变。这对于保持代码的正确性和可读性都是至关重要的。