☰
Lingui 包体积优化全指南:从紧凑 ID 到移除生产环境编译器
2026/10/12 3:48:38 网站建设 项目流程
  • 开发工具
  • 前端

【免费下载链接】js-lingui

🌍 📖 A readable, automated, and optimized (2 kb) internationalization for JavaScript

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

导读:本文基于 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、commentLingui 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 的包体积优化可以概括为两条主线:

  1. ID 化:宏把消息替换为紧凑 ID,消除「源码字符串 + catalog key + catalog value」的三重冗余;
  2. 预编译:构建期完成消息编译,生产环境完全不带编译器,descriptorFields负责精确控制哪些字段进入产物。

配套纪律是:构建前先extract-template,保证 catalog 与源码同步,避免生产环境泄露原始 ID。若需要 CMS 或 OTA 这类运行时注入场景,再通过i18n.setMessagesCompiler(compileMessage)显式带回编译器,同时接受体积的相应增长。理解了 dev/prod 的差异与这套配置矩阵,你就能在包体积、可调试性和动态能力之间找到适合自己项目的平衡点。

  • 开发工具
  • 前端

【免费下载链接】js-lingui

🌍 📖 A readable, automated, and optimized (2 kb) internationalization for JavaScript

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

相关推荐

上一篇:Reactide项目搜索功能:快速定位文件与代码片段
下一篇:2025 必学工具 Modin:一行代码让 Pandas 提速 4 倍的秘密

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

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

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

立即咨询