- 前端
- CMS
【免费下载链接】wp-calypso
The JavaScript and API powered WordPress.com
本篇文章围绕 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 上的包名 |
version | 1.0.0 | 当前版本 |
description | A Babel plugin to preserves translation functions even when minified. | 一句话概括插件目标 |
main | src/index.js | 入口即插件实现 |
license | GPL-2.0-or-later | 开源许可证 |
publishConfig.access | public | 允许公开发布 |
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头。整体链路可以概括为:
- 源码写入
__( 'Hello' ); - Babel(默认 preset,含 preserve-i18n)把 import 别名化并插入
const重声明,调用点写法原样保留; - 生产构建经 Terser 压缩时配合
mangle.reserved保留翻译函数名; - 构建产物(bundle)中依然存在可被扫描的
__( 'Hello' )调用模式; - 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
相关推荐
wp-calypso 国际化构建基石:babel-plugin-i18n-calypso 提取 translate 调用生成 POT 全解析
wp calypso 国际化构建基石:babel plugin i18n calypso 提取 translate 调用生成 POT 全解析 本文围绕 pack
前端CMSWordPress Gutenberg 的 Babel 插件 babel-plugin-makepot:从 JS 源码自动生成 gettext POT 翻译模板
WordPress Gutenberg 的 Babel 插件 babel plugin makepot:从 JS 源码自动生成 gettext POT 翻译模板
后端前端探索Babel插件魔法:`babel-plugin-macros`
探索Babel插件魔法: babel plugin macros 项目简介 Babel https://babeljs.io/ 是JavaScript的编译器,
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考