- 开发工具
- 前端
【免费下载链接】js-lingui
🌍 📖 A readable, automated, and optimized (2 kb) internationalization for JavaScript
导读:本文基于 js-lingui 仓库
website/docs/guides/optimizing-bundle-size.md展开,深入讲解 Lingui 如何通过「将消息替换为紧凑 ID」与「构建期预编译、生产环境移除消息编译器」两大策略压缩 i18n 产物体积。你将了解 dev 与 prod 行为差异的底层原理、生产环境出现原始 ID 的排查方法、动态加载翻译时的运行时编译回退方案,以及descriptorFields与setMessagesCompiler的完整配置矩阵。
为什么 i18n 会让包体积失控
构建现代应用时,国际化(i18n)很容易让产物变得臃肿。语言和消息越多,体积增长越快。以一个最简单的消息为例:
<Trans>Hello world</Trans>如果这条消息原样保留在代码和翻译数据中,你可能会得到:
- 源代码中的消息字符串
"Hello world"; - 翻译目录(catalog)中作为 key 的同一字符串;
- 默认语言目录中作为 value 的又一份相同字符串。
同一个东西出现了三份,而且每条消息都是如此。再乘以数百条消息和数种语言,体积膨胀可想而知。
Lingui 的思路是:在保证可读、可维护的前提下,尽可能把重复与冗余消灭在构建阶段——这正是其「可读、自动化、优化」设计目标在包体积上的体现。
Lingui 如何压缩产物体积
1. 用紧凑 ID 替换消息本体
构建应用时,Lingui 的宏(macro)会把:
<Trans>Hello world</Trans>转换成类似:
<Trans id="zfhb1" />消息本体不再进入产物,取而代之的是一个短 ID。这带来两个直接收益:
- 节省空间:ID 远比完整字符串短;
- 消除重复:只需要存在翻译本身,原始文本不再需要打包进产物。
从源码看,这一替换发生在宏展开阶段:packages/babel-plugin-lingui-macro/src/messageDescriptorUtils.ts中的createIdProperty会调用generateMessageId(message, context)生成 ID,然后仅当descriptorFields模式要求时才保留message等字段(详见下文「descriptorFields 字段保留规则」)。
2. 从生产环境移除消息编译器
像下面这样的消息:
"{count, plural, one {# item} other {# items}}"使用的是 ICU MessageFormat 语法,必须被编译成 Lingui 运行时可以执行的形态(即编译后的 token 数组)才能渲染。Lingui 自带消息编译器来完成这件事,但编译器本身并不小。
与其把它发给浏览器,Lingui 选择在构建期(编译 catalog 时)提前完成编译。这样生产环境就完全不需要携带编译器。
这正是无论 catalog 是什么格式(哪怕已是 JSON 而非.po)都必须执行lingui compile的原因:编译不是文件格式转换,而是把消息变换成运行时可以执行的形态。
编译产物是一个经过JSON.parse包裹的 JS 对象。以compileNamespace: "es"为例(参见 conf.md),输出类似:
/* eslint-disable */export const messages = JSON.parse("...")compileNamespace支持cjs(默认)、es、ts、json以及window.xxx/global.xxx等命名空间,可根据目标环境选择产物形态。
:::note ✅ 提示:如果你使用@lingui/vite-plugin、@lingui/loader或@lingui/metro-transformer,就不需要手动执行lingui compile——这些插件会在你 import catalog 时自动完成编译。例如packages/vite-plugin/src/index.ts中的vite-plugin-lingui-load-catalogtransform 会在加载 catalog 时调用createCompiledCatalog产出编译后的模块代码。 :::
为什么开发环境一切照常
这是 Lingui 设计的精妙之处:dev 与 prod 行为刻意不同。
开发环境下,Lingui:
- 在产物中保留原始消息(如
Hello world); - 保留消息编译器,让新消息可以立即工作。
这样迭代非常快:你可以随时新增一个<Trans>,即使尚未执行 extract 或 compile,也能立刻在浏览器里看到原文。从源码看,i18n.ts 的I18n构造函数在NODE_ENV !== "production"时会自动调用this.setMessagesCompiler(compileMessage),为未编译的消息兜底。
生产环境下,Lingui:
- 剥离全部原始消息;
- 完全移除消息编译器。
这意味着你必须提前 extract 并 compile 所有消息——否则 Lingui 无法知道如何渲染它们。
I18n._(渲染入口)的实现印证了这一约束(i18n.ts):当拿到的翻译仍是字符串(即未编译)时,若存在_messageCompiler则现场编译;若不存在(生产环境默认没有编译器),则会打印Uncompiled message detected!警告——ICU 插值、复数等特性将无法正常工作。对应的测试用例见 i18n.test.ts。
常见问题:生产环境出现奇怪的消息 ID
假设你新增了一条消息:
<Trans>This is a new message</Trans>本地一切正常,但部署后用户在界面上看到类似:
z3fd2这通常只有一个原因:构建前没有执行消息提取。
当 Lingui 编译 catalog 时,它会尝试把每个消息 ID 与源代码中的消息对应起来。如果该消息不在 catalog 里,就没有任何可回退的内容——于是原始 ID 直接出现在 UI 上。
✅ 解决方案:构建前务必先 extract
确保构建脚本先提取最新消息:
"build": "lingui extract-template && vite build"这样能保证 catalog 与源代码保持同步。
除了在脚本里串联命令,你也可以在 CI 中借助lingui check sync校验 catalog 是否与源码同步(lingui check missing校验是否有缺失翻译),做到「失同步即失败」,把问题挡在构建之前。CLI 的完整用法见 cli.md。
动态加载翻译时怎么办
Lingui 的设计面向构建期静态分析,对大多数应用很合适,但在以下场景会变得棘手:
- 从 CMS 加载翻译;
- 支持目录的 OTA(over-the-air)推送;
- 在运行时注入新消息。
在这些场景下,不能只依赖预编译 catalog,你需要重新引入运行时消息编译器:
import { compileMessage } from "@lingui/message-utils/compileMessage"; i18n.setMessagesCompiler(compileMessage);setMessagesCompiler定义在 i18n.ts,它把编译器注册到_messageCompiler,I18n._在遇到未编译字符串时会调用它现场编译。compileMessage实现在 compileMessage.ts:内部用@messageformat/parser解析消息,再经processTokens递归转换为 token 数组(字符串片段、[arg]、[arg, "plural", {one: ..., other: ...}]等形式);若解析失败会打印错误并原样返回消息,保证不崩溃。
⚠️ 请记住:这样做会让包体积再次增大,因为编译器及其依赖(如 ICU 解析器)会进入产物。对应的行为在 i18n.test.ts 中有测试覆盖。
按需配置 Lingui
descriptorFields选项控制宏转换后保留哪些描述符字段,决定最终进入产物的内容。它的完整取值(定义见 babel-plugin-lingui-macro/src/index.ts)如下:
| 取值 | 保留字段 | 典型用途 |
|---|---|---|
"auto"(默认) | 生产环境只保留id;其他环境等同"all" | 默认推荐 |
"all" | id、message、context、comment | Lingui CLI 提取消息时使用 |
"id-only" | 仅id | 生产环境最小化 |
"message" | id、message、context(不含comment) | 运行时需要访问 message 与 context |
字段保留的判定逻辑位于 messageDescriptorUtils.ts:"id-only"时message与context均被丢弃,"all"时才保留comment。"auto"的解析则依据process.env.NODE_ENV === "production"决定(index.ts),传入非法值会直接抛出Invalid descriptorFields value错误(测试见 descriptor-fields-validation.test.ts)。从 v6 起,旧的extract与stripMessageField选项已被移除,并提示改用descriptorFields。
场景一:希望生产环境表现得像开发环境
保留原始消息、上下文,并在生产环境也使用运行时编译——例如用于调试或动态 catalog。
// Macro 配置 descriptorFields: "message"; // 运行时配置 i18n.setMessagesCompiler(compileMessage);输出中会保留id、message和context。context之所以保留,是因为正确的消息 ID 生成依赖它(generateMessageId(message, context)把 context 作为 ID 计算的输入,见 messageDescriptorUtils.ts)。
场景二:追求 dev 与 prod 完全一致
希望两个环境都剥离所有字段,便于尽早发现问题。
// Macro 配置 descriptorFields: "id-only"; // 运行时配置 i18n.setMessagesCompiler(null);注意setMessagesCompiler(null)的类型签名为MessageCompiler,传null用于显式关闭编译器——此时若出现未编译消息,I18n._将直接打印警告(见上文)。就类型而言以实际发布的.d.ts为准,建议结合 TS 严格模式验证。
场景三:使用 Lingui 默认行为
什么都不改。Lingui 自动在生产环境剥离消息(descriptorFields: "auto"→ 解析为"id-only"),在开发环境保留全部字段。只需确保构建前始终执行extract-template。
宏配置的补充说明
Lingui 宏的接入方式因构建工具而异:
- 作为独立的Babel 插件;
- 作为SWC 插件;
- 通过babel-plugin-macros使用。
每种方式的配置入口略有不同。以 Vite 为例,@lingui/vite-plugin在vite-plugin-lingui-macro-transform中会依据this.environment.config.isProduction自动把descriptorFields设为"id-only"或"all"(vite-plugin/src/index.ts),与"auto"的语义保持一致。如果你使用 SWC 插件或 babel-plugin-macros,请以对应插件的文档为准。
小结
Lingui 的包体积优化可以概括为两条主线:
- ID 化:宏把消息替换为紧凑 ID,消除「源码字符串 + catalog key + catalog value」的三重冗余;
- 预编译:构建期完成消息编译,生产环境完全不带编译器,
descriptorFields负责精确控制哪些字段进入产物。
配套纪律是:构建前先extract-template,保证 catalog 与源码同步,避免生产环境泄露原始 ID。若需要 CMS 或 OTA 这类运行时注入场景,再通过i18n.setMessagesCompiler(compileMessage)显式带回编译器,同时接受体积的相应增长。理解了 dev/prod 的差异与这套配置矩阵,你就能在包体积、可调试性和动态能力之间找到适合自己项目的平衡点。
- 开发工具
- 前端
【免费下载链接】js-lingui
🌍 📖 A readable, automated, and optimized (2 kb) internationalization for JavaScript
相关推荐
Notepad++ Markdown语法高亮终极指南:10+主题提升你的写作效率
Notepad++ Markdown语法高亮终极指南:10+主题提升你的写作效率 还在为Notepad++中单调的Markdown编辑体验而烦恼吗?markdo
开发工具CLI一条命令归档全部 QQ 空间历史说说:GetQzonehistory 使用说明
一条命令归档全部 QQ 空间历史说说:GetQzonehistory 使用说明 GetQzonehistory 是一个用 Python 写的说说导出、说说备份工
网页爬虫数据分析TradingAgents-CN多智能体交易系统:零基础构建AI投资分析平台的终极指南
TradingAgents CN多智能体交易系统:零基础构建AI投资分析平台的终极指南 想要快速搭建一个专业的AI投资分析系统吗?TradingAgents C
人工智能大模型AI Agent多智能体金融科技后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考