首页演练场案例展示应用文档博客
    • English英语
      EN
    • русский俄语
      RU
    • 日本語日语
      JA
    • français法语
      FR
    • 한국어韩语
      KO
    • 中文中文
      ZH
    • español西班牙语
      ES
    • Deutsch德语
      DE
    • العربية阿拉伯语
      AR
    • italiano意大利语
      IT
    • British English英国英语
      EN-GB
    • português葡萄牙语
      PT
    • हिन्दी印地语
      HI
    • Türkçe土耳其语
      TR
    • polski波兰语
      PL
    • Indonesia印度尼西亚语
      ID
    • Tiếng Việt越南语
      VI
    • українська乌克兰语
      UK
    /
    Alt+←
    什么是国际化?
    SEO和国际化
    指南
    • 使用next-i18next的i18n
    • 使用next-intl的i18n
    在你的方案中使用Intlayer
    • 自动化 next-i18next
    • 自动化 react-i18next
    • 自动化 next-intl
    • 自动化 react-intl
    • 自动化 vue-i18n
    比较
    • next-i18next vs next-intl vs Intlayer
    • react-i18next vs react-intl vs Intlayer
    文档
    1. Blog
    2. Compiler vs declarative i18n
    Creation:2025-11-24Last update:2025-11-24
    将此文档参考到您的 AI 助手
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    使用您最喜欢的AI助手总结文档,并引用此页面和AI提供商

    此页面的内容已使用 AI 翻译。

    查看英文原文的最新版本
    编辑此文档

    如果您有改善此文档的想法,请随时通过在GitHub上提交拉取请求来贡献。

    文档的 GitHub 链接
    Copy

    复制文档 Markdown 到剪贴板

    支持与反对编译器驱动国际化的观点

    如果你从事网页应用开发超过十年,你会知道国际化(i18n)一直是一个难点。它通常是没人愿意做的任务, 提取字符串、管理 JSON 文件,以及处理复数规则。

    最近,一波新的“基于编译器”的国际化(i18n)工具涌现,承诺让这份痛苦消失。它们的宣传非常诱人:只需在组件中编写文本,构建工具会处理剩下的一切。无需键名,无需导入,纯粹是魔法。

    但正如软件工程中的所有抽象一样,魔法是有代价的。

    在这篇博客文章中,我们将探讨从声明式库向基于编译器方法的转变,它们引入的隐藏架构债务,以及为什么"无聊"的方式可能仍然是专业应用的最佳选择。

    目录

    国际化的简史

    要理解我们现在所处的位置,必须回顾我们从哪里开始。

    大约在2011年至2012年,JavaScript的生态环境截然不同。我们今天所熟知的打包工具(如Webpack、Vite)还不存在,或者还处于初期阶段。那时,我们是在浏览器中手动拼接脚本。在那个时代,像i18next这样的库诞生了。

    它们以当时唯一可行的方式解决了问题:运行时字典。你将一个庞大的JSON对象加载到内存中,然后通过函数动态查找键。这种方式可靠、明确,并且在任何地方都能工作。

    快进到今天。我们拥有强大的编译器(如SWC、基于Rust的打包工具),它们可以在毫秒级别解析抽象语法树(AST)。这种能力催生了一个新想法:为什么我们还要手动管理键?为什么编译器不能直接识别文本“Hello World”,并替我们替换它?

    于是,基于编译器的国际化(i18n)诞生了。

    基于编译器的 i18n 示例:

    • Paraglide(经过 Tree-shaking 的模块,将每条消息编译成一个小型的 ESM 函数,使打包工具能够自动剔除未使用的语言和键。你导入消息作为函数,而不是通过字符串键查找。)
    • LinguiJS(宏到函数的编译器,在构建时将类似 <Trans> 的消息宏重写为普通的 JS 函数调用。你可以使用 ICU/MessageFormat 语法,且运行时开销非常小。)
    • Lingo.dev(专注于自动化本地化流程,在构建 React 应用时直接注入翻译内容。它可以使用 AI 自动生成翻译,并直接集成到 CI/CD 流程中。)
    • Wuchale(以 Svelte 为先的预处理器,提取 .svelte 文件中的内联文本并将其编译为零包装的翻译函数。它避免使用字符串键,并将内容提取逻辑完全与主应用运行时分离。)
    • Intlayer(编译器 / 提取 CLI,解析你的组件,生成类型化字典,并可选择性地重写代码以使用显式的 Intlayer 内容。目标是在保持声明式、框架无关核心的同时,利用编译器提升开发效率。)

    声明式 i18n 示例:

    • i18next / react-i18next / next-i18next(成熟的行业标准,使用运行时 JSON 字典和丰富的插件生态系统)
    • react-intl(FormatJS 库的一部分,专注于标准 ICU 消息语法和严格的数据格式化)
    • next-intl(专为 Next.js 优化,集成了 App Router 和 React Server Components)
    • vue-i18n / @nuxt/i18n(标准的 Vue 生态系统解决方案,提供组件级翻译块和紧密的响应式集成)
    • svelte-i18n(围绕 Svelte stores 的轻量级封装,用于响应式运行时翻译)
    • angular-translate(传统的动态翻译库,依赖运行时键查找而非构建时合并)
    • angular-i18n(Angular 原生的预编译方法,在构建时将 XLIFF 文件直接合并到模板中)
    • Tolgee(结合声明式代码和上下文 SDK,实现 UI 中的“点击翻译”编辑)
    • Intlayer(基于每个组件的方法,使用内容声明文件,实现原生的 tree-shaking 和 TypeScript 校验)

    Intlayer 编译器

    虽然 Intlayer 本质上是一种鼓励对内容采取 声明式方法 的解决方案,但它包含了一个编译器,以帮助加快开发速度或促进快速原型设计。

    Intlayer 编译器会遍历你的 React、Vue 或 Svelte 组件的 AST(抽象语法树),以及其他 JavaScript/TypeScript 文件。它的作用是检测硬编码字符串,并将它们提取到专门的 .content 声明中。

    更多详情,请查阅文档:Intlayer 编译器文档

    编译器的魅力(“魔法”方法)

    这种新方法之所以流行是有原因的。对于开发者来说,这种体验令人难以置信。

    1. 速度与“流畅感”

    当你进入状态时,停下来思考一个语义化的变量名(如 home_hero_title_v2)会打断你的思路。使用编译器方法,你只需输入 <p>Welcome back</p>,然后继续前进。摩擦为零。

    2. 旧代码救援任务

    想象一下,继承了一个拥有5000个组件且没有任何翻译的大型代码库。用手动基于键的系统来改造它,将是一场持续数月的噩梦。基于编译器的工具则作为一种救援策略,能够即时提取成千上万的字符串,而你无需手动触碰任何文件。

    3. AI 时代

    这是一个我们不应忽视的现代优势。AI 编码助手(如 Copilot 或 ChatGPT)自然生成标准的 JSX/HTML。它们并不知道你特定的翻译键模式。

    • 声明式(Declarative): 你必须重写 AI 的输出,将文本替换为键。
    • 编译器(Compiler): 你只需复制粘贴 AI 的代码,它就能直接工作。

    现实检验:为什么“魔法”是危险的

    虽然“魔法”很吸引人,但抽象层会泄露。依赖构建工具来理解人类意图会引入架构上的脆弱性。

    启发式脆弱性(猜测游戏)

    编译器必须猜测什么是内容,什么是代码。这会导致一些边缘情况,最终你会发现自己在“与工具斗争”。

    考虑以下场景:

    • <span className="active"></span> 会被提取吗?(它是字符串,但很可能是类名)。
    • <span status="pending"></span> 会被提取吗?(它是一个属性值)。
    • <span>{"Hello World"}</span> 会被提取吗?(它是一个 JS 表达式)。
    • <span>Hello {name}. How are you?</span> 会被提取吗?(插值很复杂)。
    • <span aria-label="Image of cat"></span> 会被提取吗?(无障碍属性需要翻译)。
    • <span data-testid="my-element"></span> 会被提取吗?(测试 ID 不应被翻译)。
    • <MyComponent errorMessage="An error occurred" /> 会被提取吗?
    • <p>This is a paragraph{" "}\n containing multiple lines</p> 会被提取吗?
    • <p>{getStatusMessage()}</p> 函数结果会被提取吗?
    • <div>{isLoading ? "The page is loading" : <MyComponent/>} </div> 会被提取吗?
    • 像 <span>AX-99</span> 这样的产品 ID 会被提取吗?

    你不可避免地会添加特定注释(如 // ignore-translation,或特定属性如 data-compiler-ignore="true")来防止破坏你的应用逻辑。

    Intlayer 如何处理这种复杂性?

    Intlayer 使用混合方法来检测字段是否应被提取用于翻译,旨在尽量减少误报:

    1. AST 分析: 它检查元素类型(例如,区分 reactNode、label 或 title 属性)。
    2. 模式识别: 它检测字符串是否首字母大写或包含空格,这表明它更可能是人类可读的文本,而非代码标识符。

    动态数据的硬性限制

    编译器提取依赖于静态分析。它必须在代码中看到字面字符串,才能生成稳定的 ID。 如果您的 API 返回一个错误代码字符串,比如 server_error,您无法通过编译器进行翻译,因为编译器在构建时并不知道该字符串的存在。您被迫为动态数据构建一个二级的“仅运行时”系统。

    缺乏分块

    某些编译器不会按页面对翻译内容进行分块。如果您的编译器为每种语言生成一个大型 JSON 文件(例如 ./lang/en.json、./lang/fr.json 等),那么访问单个页面时,您很可能会加载所有页面的内容。此外,每个使用这些内容的组件可能会被注入远多于所需的内容,可能导致性能问题。

    还要注意动态加载翻译内容。如果不这样做,你将会加载当前语言之外的所有语言内容。

    为了说明这个问题,考虑一个有10个页面和10种语言(全部100%独特)的站点。你将会加载另外99个页面的内容(10 × 10 - 1)。

    “Chunk爆炸”和网络瀑布效应

    为了解决chunking问题,一些解决方案提供了按组件甚至按键的chunking。然而,这个问题只是部分解决了。这些解决方案的卖点通常是说“你的内容会被tree-shake”。

    实际上,如果你静态加载内容,你的解决方案会tree-shake未使用的内容,但你仍然会加载所有语言的内容到你的应用中。

    那么为什么不动态加载呢?是的,在这种情况下你会加载比必要内容更多的内容,但这并非没有权衡。

    动态加载内容会将每一块内容隔离到它自己的代码块中,只有在组件渲染时才会加载。这意味着你每个文本块都会发起一次 HTTP 请求。页面上有 1000 个文本块?→ 你将向服务器发起 1000 次 HTTP 请求。为了限制影响并优化应用的首次渲染时间,你需要插入多个 Suspense 边界或骨架加载器(Skeleton Loaders)。

    注意:即使使用 Next.js 和 SSR,组件在加载后仍会被水合(hydrated),因此 HTTP 请求仍然会被发起。

    解决方案?采用允许声明作用域内容声明的方案,比如 i18next、next-intl 或 intlayer。

    注意:i18next 和 next-intl 需要你为每个页面手动管理命名空间 / 消息导入,以优化你的包大小。你应该使用类似 rollup-plugin-visualizer(vite)、@next/bundle-analyzer(next.js)或 webpack-bundle-analyzer(React CRA / Angular / 等)这样的包分析工具来检测是否有未使用的翻译污染了你的包。

    运行时性能开销

    为了使翻译具有响应性(以便切换语言时能即时更新),编译器通常会向每个组件注入状态管理钩子。

    • 代价: 如果你渲染一个包含 5,000 个项目的列表,你实际上是在为文本初始化 5,000 个 useState 和 useEffect 钩子。React 必须同时识别并重新渲染所有这 5,000 个消费者。这会导致巨大的“主线程”阻塞,在切换语言时冻结 UI。这会消耗大量内存和 CPU 周期,而声明式库(通常使用单一 Context 提供者)则能节省这些资源。
    注意,这个问题在 React 以外的其他框架中也类似。

    陷阱:供应商锁定

    选择 i18n 解决方案时要小心,确保它允许提取或迁移翻译键。

    在声明式库的情况下,您的源代码明确包含了您的翻译意图:这些就是您的键,您可以控制它们。如果您想更换库,通常只需要更新导入即可。

    而采用编译器方法时,您的源代码可能只是纯英文文本,没有任何翻译逻辑的痕迹:所有内容都隐藏在构建工具配置中。如果该插件不再维护或您想更换解决方案,您可能会陷入困境。没有简单的“弹出”方式:您的代码中没有可用的键,您可能需要为新库重新生成所有翻译。

    一些解决方案还提供翻译生成服务。没有更多积分了?就没有更多翻译了。

    编译器通常会对文本进行哈希处理(例如,"Hello World" -> x7f2a)。你的翻译文件看起来像 { "x7f2a": "Hola Mundo" }。陷阱在于:如果你更换了库,新库会看到 "Hello World" 并查找该键。但它找不到,因为你的翻译文件充满了哈希值(x7f2a)。

    平台锁定

    通过选择基于编译器的方法,你会将自己锁定在底层平台上。例如,某些编译器并非适用于所有打包工具(如 Vite、Turbopack 或 Metro)。这可能会使未来的迁移变得困难,你可能需要采用多种解决方案来覆盖所有应用程序。

    另一面:声明式方法的风险

    公平地说,传统的声明式方法也并不完美。它有自己的一些“坑”。

    1. 命名空间地狱: 你经常需要手动管理加载哪些 JSON 文件(common.json、dashboard.json、footer.json)。如果你忘记加载其中一个,用户就会看到原始的键名。
    2. 过度获取(Over-fetching): 如果配置不当,很容易在初始加载时意外加载所有页面的所有翻译键,导致包体积膨胀。
    3. 同步漂移(Sync Drift): 通常情况下,某些键会在使用它们的组件被删除后仍留在 JSON 文件中。你的翻译文件会无限增长,充满“僵尸键”。

    Intlayer 的折中方案

    这正是像 Intlayer 这样的工具试图创新的地方。Intlayer 理解虽然编译器功能强大,但隐式魔法是危险的。

    Intlayer 提供了一种混合方法,让你能够同时享受两种方法的优势:声明式内容管理,同时兼容其编译器以节省开发时间。

    即使你不使用 Intlayer 编译器,Intlayer 也提供了一个 transform 命令(也可以通过 VSCode 扩展访问)。它不仅仅是在隐藏的构建步骤中做魔法,而是可以实际重写你的组件代码。它会扫描你的文本,并将其替换为代码库中显式的内容声明。

    这让你同时拥有两者的优点:

    1. 细粒度控制: 你可以将翻译内容保持在组件附近(提升模块化和 tree-shaking 效果)。
    2. 安全性: 翻译变成显式代码,而不是隐藏的构建时魔法。
    3. 无锁定: 由于代码被转换成你仓库中的声明式结构,你可以轻松按下 Tab 键,或使用 IDE 的 copilot 来生成内容声明,而不是将逻辑隐藏在 webpack 插件中。

    结论

    那么,你应该选择哪种方式呢?

    如果你正在构建 MVP,或者想快速推进:
    基于编译器的方法是一个有效的选择。它允许你非常快速地开发。你不需要担心文件结构或键值。你只需构建即可。技术债务是“未来的你”需要解决的问题。

    如果你是初级开发者,或者不在意优化:
    如果你想减少手动管理,基于编译器的方法可能是最好的。你不需要自己处理键或翻译文件, 只需编写文本,编译器会自动完成剩下的工作。这减少了设置工作量和与手动步骤相关的常见国际化错误。

    如果你正在对一个已有数千个组件需要重构的现有项目进行国际化:
    基于编译器的方法在这里可以是一个务实的选择。初始的提取阶段可以节省数周甚至数月的手动工作。然而,建议考虑使用像 Intlayer 的 transform 命令这样的工具,它可以提取字符串并将其转换为显式的声明式内容声明。这让你既能享受自动化的速度,又能保持声明式方法的安全性和可移植性。你可以两者兼得:快速的初始迁移而不会带来长期的架构债务。

    如果你正在构建一个专业的企业级应用程序: 魔法通常不是一个好主意。你需要控制。

    • 你需要处理来自后端的动态数据。
    • 你需要确保在低端设备上的性能(避免钩子爆炸)。 /// 你需要确保不会永远被锁定在特定的构建工具中。

    对于专业应用,声明式内容管理(如 Intlayer 或成熟的库)仍然是黄金标准。它将关注点分离,保持架构清晰,并确保你的应用多语言能力不依赖于“黑盒”编译器去猜测你的意图。

    什么是国际化?
    Alt+→

    在此页面

      讨论是匿名的,并会定期审查以解决常见问题。欢迎分享功能想法、对文档的反馈或任何与 Intlayer 相关的内容, 我们会利用这些意见来制定路线图并改进产品。