☰
wp-calypso 的 Babel 插件 babel-plugin-preserve-i18n:在压缩产物中保住 WordPress 翻译调用
2026/10/7 20:59:45 网站建设 项目流程
  • 前端
  • CMS

【免费下载链接】wp-calypso

The JavaScript and API powered WordPress.com

项目地址:https://gitcode.com/gh_mirrors/wp/wp-calypso
点击查看免费下载

本篇文章围绕 wp-calypso 仓库中的 Babel 插件 @automattic/babel-plugin-preserve-i18n 展开,介绍它如何在构建与压缩阶段保留__( 'Hello' )这类 WordPress i18n 函数调用语法,使 WP i18n 提取工具在产物(包括被 Terser 压缩后的 bundle)中仍能识别并抽取翻译字符串。读完本文,你将掌握该插件的设计动机、AST 变换原理、scope 绑定维护细节,以及它在 Calypso 共享 Babel 配置中的真实接入方式。

一、插件要解决什么问题

Calypso 的代码中大量使用 WordPress 官方的国际化函数,例如:

import { __, _x } from '@wordpress/i18n'; __( 'Hello' ); _x( 'World' );

在正常编译流程中,import语句会被 Babel 降级为require/ CommonJS 形式,进入生产 bundle 后,__、_x这类本地变量名往往会被压缩器(如 Terser)重命名成a、b之类的短名。一旦调用点变成a( 'Hello' ),WordPress 的 i18n 提取工具(例如 WP-CLI 的i18n make-pot,或 Calypso 自己基于 babel-plugin-i18n-calypso 的 POT 生成流程)就无法再通过__( '字符串' )的固定模式扫描到待翻译文本,导致字符串漏翻。

babel-plugin-preserve-i18n的思路与社区中的babel-plugin-optimize-react(对 React 导入做类似变换)一脉相承:通过别名化导入、再用const重新声明原名,把翻译函数名钉死在产物中。插件 README 中给出了完整的目标输出:

import { __ as alias__, _x as alias_x } from '@wordpress/i18n'; const __ = alias__; const _x = alias_x; __( 'Hello' ); _x( 'World' );

变换后,调用点的写法__( 'Hello' )与变换前逐字一致,依旧可以在输出 bundle 中被 i18n 工具扫描出来;而 Terser 压缩时若配合mangle.reserved选项将__、_x等名字加入保留名单,则即使经过 minify,这个可识别的调用模式依然完整存在(见 README.md)。

二、源码实现:AST 变换的完整流程

插件的主入口是 src/index.js,其 package.json 将main指向该文件。整个实现只有两个核心部分:一个负责收集并别名化导入的函数,以及一个挂载在ImportDeclaration上的 visitor。

1. 翻译函数白名单

const i18nImports = new Set( [ '__', '_n', '_nx', '_x' ] );

插件只对来自@wordpress/i18n的这四个命名导入做处理,覆盖了 WordPress i18n 中最常用的四个形态:__( text )(普通翻译)、_x( text, context )(带上下文)、_n( single, plural, number )(复数)、_nx( single, plural, number, context )(复数 + 上下文)。不在集合内的导入(如sprintf、isRTL等)原样保留,不做任何干预。

2. 收集导入并生成别名(collectAllImportsAndAliasThem)

核心函数collectAllImportsAndAliasThem( path )完成三步工作(src/index.js#L6-L38):

第一步:校验模块来源。只有t.isStringLiteral( node.source ) && node.source.value === '@wordpress/i18n'的导入声明才会进入处理逻辑,避免误伤其他包的同名函数。

第二步:遍历 specifiers 并别名化。对每个ImportSpecifier(即具名导入),要求imported与local都是Identifier(import { __ as foo }这类带本地别名的写法也能正确处理),随后:

  • 命中白名单后,记录{ original: localNode.name, aliased: 'alias' + localNode.name },即原名__的别名是alias__;
  • 用t.importSpecifier( t.identifier( 'alias' + localNode.name ), t.identifier( importedNode.name ) )替换原 specifier,把导入目标改成别名;
  • 调用path.scope.removeBinding( localNode.name )移除原本地绑定的注册;
  • 循环结束后path.scope.registerDeclaration( path )把修改后的导入声明重新注册到作用域。

第三步:返回收集到的别名列表,供 visitor 使用。

3. 在导入声明后插入 const 重声明

visitor 只监听一个节点类型:

visitor: { ImportDeclaration( path ) { const aliases = collectAllImportsAndAliasThem( path ); if ( aliases.length > 0 ) { const declarations = aliases.map( ( { original, aliased } ) => t.variableDeclarator( t.identifier( original ), t.identifier( aliased ) ) ); const aliasDeclarationNode = t.variableDeclaration( 'const', declarations ); path.insertAfter( aliasDeclarationNode ); const aliasDeclarationPath = path.getNextSibling(); path.scope.registerDeclaration( aliasDeclarationPath ); } }, },

(见 src/index.js#L40-L57)

要点拆解:

  • 当收集到至少一个别名时,把每条记录生成一个variableDeclarator( 原名, 别名 ),最终拼成一个const声明节点,例如const __ = alias__, _x = alias_x;;
  • 通过path.insertAfter把该声明紧跟在 import 语句之后插入,使__、_x以const绑定形式继续存在于模块作用域中;
  • 插入后立刻通过path.getNextSibling()拿到新节点的 Path,并registerDeclaration到作用域,保证后续对__、_x的引用能被正确解析。

这种"先removeBinding、再registerDeclaration"的作用域管理是插件正确性的关键:它让 Babel 在后续遍历中不会因绑定信息过期而报错,也确保同一文件内既有的__( 'Hello' )调用语义不发生任何变化。

4. 插件元信息

返回的对象带有name: 'babel-plugin-preserve-i18n',便于 Babel 在报错和调试信息中标识该插件。包本身以module.exports = function ( babel ) { ... }的形式导出标准的 Babel 插件工厂函数,依赖 Babel 传入的babel.types(t)完成全部节点构造。

三、包配置与工程形态

从 package.json 可以看到该包的完整工程信息:

字段值说明
name@automattic/babel-plugin-preserve-i18n发布在 npm 上的包名
version1.0.0当前版本
descriptionA Babel plugin to preserves translation functions even when minified.一句话概括插件目标
mainsrc/index.js入口即插件实现
licenseGPL-2.0-or-later开源许可证
publishConfig.accesspublic允许公开发布
devDependencies@automattic/calypso-eslint-overrides、@automattic/calypso-typescript-config(均workspace:^)仅用于仓库内 lint 与 TS 配置

仓库地址指向git+https://github.com/Automattic/wp-calypso.git,directory字段精确标注到packages/babel-plugin-preserve-i18n。其 tsconfig.json 仅扩展了 @automattic/calypso-typescript-config 的js-package.json预设——注意插件本身是纯 JavaScript 实现,TS 配置只服务于仓库统一的工程约束。

四、在 Calypso 构建管线中的真实接入

babel-plugin-preserve-i18n不是孤立的实验代码,而是 Calypso 共享 Babel 配置的一等公民。在 packages/calypso-babel-config/package.json 中它被声明为calypso-babel-config的运行时依赖,并在默认 preset 中直接启用:

plugins: [ require.resolve( '@babel/plugin-proposal-class-properties' ), [ require.resolve( '@babel/plugin-transform-runtime' ), { corejs: false, helpers: true, regenerator: false, useESModules: false, /* ... */ }, ], require.resolve( '@automattic/babel-plugin-preserve-i18n' ), require.resolve( '@emotion/babel-plugin' ), ],

(见 presets/default.js#L38-L54)

也就是说,任何基于@automattic/calypso-babel-config默认预设的 Calypso 模块(包括 client 与 packages 下的 React/JS 源码),在 Babel 编译阶段都会自动经过本插件的变换。

与之配合的还有 config.js 定义的build_pot环境:该环境启用@automattic/babel-plugin-i18n-calypso,并可通过outputPOT参数指定 POT 文件输出目录、注入content-type与x-generator头。整体链路可以概括为:

  1. 源码写入__( 'Hello' );
  2. Babel(默认 preset,含 preserve-i18n)把 import 别名化并插入const重声明,调用点写法原样保留;
  3. 生产构建经 Terser 压缩时配合mangle.reserved保留翻译函数名;
  4. 构建产物(bundle)中依然存在可被扫描的__( 'Hello' )调用模式;
  5. WP i18n 工具或build_pot环境下的 i18n-calypso 插件据此抽取全部待翻译字符串生成 POT。

这个顺序保证了"翻译字符串提取"与"代码压缩优化"两条需求不互相打架——这正是该插件在大型 JS 应用中的价值所在。

五、适用边界与使用注意事项

从源码结构与 README 可以梳理出以下几点实践须知:

  • 只处理命名导入,不处理默认导入:visitor 中仅匹配t.isImportSpecifier,import i18n from '@wordpress/i18n'这类默认导入不会触发变换;
  • 模块来源必须精确匹配:@wordpress/i18n字符串字面量是唯一目标,即使其他包也导出了__也不会被误伤;
  • 别名命名规则:别名固定为alias+ 本地名(如alias__、alias_nx),如果源码中恰巧存在同名标识符,理论上存在冲突风险,这也是使用时应留意的边界情况;
  • 与 Terser 的配合是"锦上添花"而非必需:即使不配置mangle.reserved,经插件变换后的 bundle 在未压缩状态下也完全可被 i18n 工具提取;配置mangle.reserved才能保证 minify 之后调用名不被改写;
  • 压缩阶段的保护对象是"调用模式"而非字符串本身:插件只关心函数名与调用形态的保留,字符串常量仍由常规的 i18n 工具链负责收集与去重。

如果希望在自有项目中复用它,最直接的方式是像 Calypso 一样把它加入 Babel 配置的plugins数组(本仓库为 workspace 依赖,可通过yarn workspace引用@automattic/babel-plugin-preserve-i18n),随后按上文链路配置构建与提取流程即可。

六、小结

babel-plugin-preserve-i18n是一个小而精准的 Babel 插件:它以一次 AST 变换(import 别名化 +const原名重声明 + scope 绑定维护)解决了"压缩产物中的翻译调用可被提取"这一国际化工程难题。核心证据集中在 src/index.js 的几十行实现,以及 presets/default.js 中的真实接入点;它服务的目标函数集合(__、_n、_nx、_x)与 WordPress i18n 工具的扫描模式一一对应。理解这个插件的原理,也就理解了大型 JavaScript 应用中"编译优化"与"字符串提取"如何通过作用域与 AST 操作达成平衡。

  • 前端
  • CMS

【免费下载链接】wp-calypso

The JavaScript and API powered WordPress.com

项目地址:https://gitcode.com/gh_mirrors/wp/wp-calypso
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询