news 2026/9/9 20:00:57

Angular 国际化标记机制深解:认识 @angular/localize 与 $localize 标签模板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Angular 国际化标记机制深解:认识 @angular/localize 与 $localize 标签模板

Angular 国际化标记机制深解:认识 @angular/localize 与 $localize 标签模板

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

导读

@angular/localize是 Angular 官方用于“应用本地化(localization)”的辅助包,其核心是把代码中需要翻译的字符串,通过一个名为$localize的模板字符串标签(tagged template)进行“标记”,再由构建期工具或运行期函数统一完成翻译替换。本文以仓库中 packages/localize/PACKAGE.md 为骨架,结合@angular/localize包的源码实现,讲清楚$localize为什么能同时扮演“运行时翻译函数”与“可被静态后处理工具替换的全局标记”,并给出元数据块、占位符命名、三种处理场景等完整语法与工程实践。

读完本文,你将掌握:如何为组件模板中的i18n文本与代码中的 TS 字符串统一打上翻译标记、$localize的编译期/运行期两种翻译路径如何工作、以及loadTranslations等运行时 API 的正确使用方式。


一、包的定位与安装前提

在 Angular 仓库中,packages/localize对应发布到 npm 上的@angular/localize包。官方 PACKAGE 文档对它的定位只有一句话,但信息量很足:

@angular/localize包包含用于本地化你的应用的辅助函数与工具。

注意这里的“工具”是实打实的:查看 packages/localize/package.json 可以看到,该包内置了三组命令行工具:

"bin": { "localize-translate": "./tools/bundles/src/translate/cli.js", "localize-extract": "./tools/bundles/src/extract/cli.js", "localize-migrate": "./tools/bundles/src/migrate/cli.js" }

分别对应翻译替换、消息提取、代码迁移三个场景,它们的实现位于 packages/localize/tools/ 目录。

官方文档特别强调安装方式与时机:

  • 只有当你的应用需要把文本标记为可翻译时,才应执行ng add @angular/localize
  • 从 package.json 的ng-add配置可以看出,该包通过 Angular CLI 的ng add安装时会被保存到devDependencies"save": "devDependencies"),因为它的角色更偏向构建期工具链而非运行期业务依赖。

二、核心机制:用模板字符串标签$localize标记可翻译文本

整个本地化方案围绕一个概念展开:使用被称为$localize的模板字符串标签(template literal tag)来“标记”代码中的字符串。在 index.ts 的全局类型声明中,$localize被定义为ɵLocalizeFn

const $localize: ɵLocalizeFn;

它接收模板字符串的静态片段与插值表达式,返回处理后的字符串。用法是最直观的模板标签形式:

const message = $localize`Hello, World!`;

这段代码并不“翻译”任何东西,它只是在告诉本地化工具链:“这个字符串需要进入待翻译清单”。至于翻译何时、以何种方式发生,则取决于下面要讲的$localize的双重身份。


三、$localize的双重身份:运行函数与“压缩不死的”全局标记

PACKAGE 文档点出了这一设计最精妙之处,也是整个方案的根基:

这个$localize标识符可以是一个真实的函数,能够在浏览器里于运行时完成翻译;但更重要的是,它是一个能够扛过代码压缩(minification)的全局标识符

这意味着它同时具备两种完全不同的用途:

  1. 运行时翻译函数:代码中真正调用它,翻译在浏览器端执行;
  2. 静态标记:它仅仅作为源码中的“路标”,供某个静态后处理工具在部署前把原文替换成译文。

“全局标识符在压缩后依然存活”是后一种用途成立的前提——正因为压缩器无法重命名全局引用,后处理工具才能稳定地识别出每一处标记。文档给出了一个非常直观的例子:源码中写的:

warning = $localize`${this.process} is not right`;

经过编译期替换后,可能变成:

warning = '' + this.process + ", n'est pas bon.";

此时代码里所有对$localize的引用都被彻底移除,渲染翻译后的文本零运行时开销(zero runtime cost)。这就是$localize被设计成“既可运行、又可作为纯标记被擦除”的根本原因——它把选择权交给了构建管道。

从源码看,运行期翻译时的函数本体位于 localize.ts:

export const $localize: LocalizeFn = function ( messageParts: TemplateStringsArray, ...expressions: readonly any[] ) { if ($localize.translate) { const translation = $localize.translate(messageParts, expressions); messageParts = translation[0]; expressions = translation[1]; } let message = stripBlock(messageParts[0], messageParts.raw[0]); for (let i = 1; i < messageParts.length; i++) { message += expressions[i - 1] + stripBlock(messageParts[i], messageParts.raw[i]); } return message; };

可以看到:只有挂载了translate函数(例如通过loadTranslations注入,见下文)时才会走翻译分支;否则就按普通模板字符串拼接求值,并把每个 message part 开头的元数据块(colon block)用stripBlock剥掉。


四、与模板编译器的衔接:i18n属性最终也变成$localize

一个常被忽视的事实是:Angular 模板编译器本身并不做翻译,它只负责把模板中的i18n文本翻译成$localize标签字符串

也就是说,写模板时的做法与在 TS 代码里手动打标记完全同构。例如下面这段模板:

<h1 i18n>Hello, World!</h1>

会被编译成类似下面的指令调用序列:

ɵɵelementStart(0, 'h1'); // <h1> ɵɵi18n(1, $localize`Hello, World!`); // Hello, World! ɵɵelementEnd(); // </h1>

这一步的意义在于:无论翻译文本来自组件模板还是 TypeScript 源码,Angular 编译器完成工作之后,所有被打上i18n属性的模板文本都已经统一成了$localize标签字符串。于是后续的处理——无论是提取、编译期内联还是运行期翻译——只需面对同一种语法形态即可,模板和代码两条路径在此汇合。

这与@angular/localize依赖@angular/compiler@angular/compiler-cli(见 package.json 的peerDependencies)的事实相互印证:模板侧解析i18n的能力来自编译器,而$localize的运行时实现与工具链则由本包提供。


五、$localize的进阶语法:元数据块、占位符命名与冒号转义

PACKAGE 文档只给出了最基础的用法,但 index.ts 与 localize.ts 中的全局类型 JSDoc 把完整语法都写明了。这些语法与模板中i18n标记的写法完全一致(文档明确注明了这一点),值得完整掌握。

5.1 元数据块:meaning / description / id

可以为本地化字符串指定可选的meaningdescriptionid,做法是在消息文本前追加一段冒号分隔的元数据块,格式为:meaning|description@@id:

$localize`:meaning|description@@id:source message text`; // 也可以只提供部分元数据 $localize`:meaning|:source message text`; $localize`:description:source message text`; $localize`:@@id:source message text`;
  • meaningdescription用来辅助翻译人员理解上下文(同一句原文在不同语境下可能含义不同);
  • @@id用于强制指定该消息的消息 ID,覆盖默认的哈希 ID。

这也是为什么运行时$localize实现里需要stripBlock:处理每个 message part 时都要把这段前置的元数据块从最终渲染文本中剥离(见 localize.ts,冒号块由BLOCK_MARKER = ':'界定)。

5.2 占位符的自动命名与手动命名

如果模板字符串里含表达式,占位符会被自动命名。例如:

$localize `Hi ${name}! There are ${items.length} items.`;

会生成消息源(message-source):

Hi {$PH}! There are {$PH_1} items.

不过文档给出的推荐实践是为每个表达式手动命名占位符——方法是在表达式之后紧跟一段用:包裹的占位符名。这些占位符名随后会被从最终本地化字符串中剔除。例如把items.length命名为itemCount

$localize `There are ${items.length}:itemCount: items.`;

手动命名之所以是“推荐实践”,是因为翻译人员看到的占位符(如{$itemCount})将比自动生成的{$PH}{$PH_1}可读得多,能显著降低翻译歧义。

5.3 冒号转义:\:

冒号:是元数据块的界定符,因此当文本内容本身就出现在“可能被误认为元数据块”的位置时,就必须用反斜杠转义:

  • 带元数据块时无需转义(::写法用于在描述之后让正文以冒号开头):
// message 带元数据块,因此无需转义冒号 $localize `:some description::this message starts with a colon (:)`; // 没有元数据块,开头冒号必须转义 $localize `\:this message starts with a colon (:)`;
  • 具名替换后无需转义,匿名替换后的冒号必须转义:
// 具名替换后无需转义(:紧跟在具名块之后是合法的) $localize `${label}:label:: ${}`; // 匿名替换后冒号会被当成块起始标记,必须转义 $localize `${label}\: ${}`;

实现上,stripBlock判断是否剥块时读取的是messageParts.raw[i](保留反斜杠的原始片段),这正是反斜杠转义能生效的底层原因——见 localize.ts 中对 raw message part 的检查。


六、三种处理场景:编译期内联 / 运行期求值 / 透传求值

PACKAGE 文档与 index.ts 的 JSDoc 都一致地把本地化字符串的处理方式归纳为三种场景,理解它们就理解了整套国际化方案的取舍:

场景机制适用场景
编译期内联(compile-time inlining)由转译器在编译期转换$localize标签:移除标签,用提供的翻译集合中的译文直接替换模板字符串字面量需要零运行时成本、发布纯静态多语言包时;前文warning = '' + this.process + ", n'est pas bon.";即属此类
运行期求值(run-time evaluation)$localize是一个真实的运行函数,把模板字符串的各个部分(静态文本与表达式)替换、重排为运行期加载的翻译文本希望按需在浏览器中加载某一种语言,且允许保留标签引用
透传求值(pass-through evaluation)$localize是运行函数,但不做任何翻译,只是按原始模板字符串求值开发阶段,或当前场景不需要翻译这些标签文本

在 Angular 仓库源码中这三种路径都能找到对应物:

  • 编译期内联与消息提取由@angular/localize/tools(即 package.json 中localize-translate/localize-extract命令背后的实现,见 packages/localize/tools/)承担;
  • 运行期求值与透传求值都落在 localize.ts 的$localize函数本体上——当$localize.translate未挂载时(典型的开发态/透传态),函数直接拼接原文;挂载了翻译函数后才会进入真正的翻译分支。

七、运行时翻译 API:loadTranslationsclearTranslations

如果你的应用走“运行期求值”路线(不在编译期做死译文),就需要把翻译文件在浏览器中加载给$localize。这正是 packages/localize/src/translate.ts 里两个公开 API 的职责,它们也通过 index.ts 对外导出:

7.1loadTranslations(translations)

作用是把一组翻译挂到全局$localize对象上。其核心行为(源码中均有明确注释):

  • 翻译表以消息 ID(MessageId)为键,译文值为内容;
  • 这些消息 ID 与翻译文本的格式,与提取消息时生成的 “simple JSON” 翻译文件完全一致
  • 占位符在消息中采用{$PLACEHOLDER_NAME}语法渲染。

从 translate.ts 的源码可见两个值得注意的实现细节:

  1. 首次调用时会先把$localize.translate挂载为内部translate函数(因为只有当存在翻译时,运行期翻译才被激活);
  2. 翻译存储容器使用Object.create(null)创建的空原型对象。注释解释得很清楚:消息 ID 是逐字取自翻译文件的键,可能恰好是__proto__这类字符串,若用普通对象作为 map,对该键的赋值会触发继承的__proto__setter 造成污染甚至抛错——null 原型对象能把它当作普通键安全存储。

7.2 一个完整的运行时消息 ID 示例

翻译表键(消息 ID)与译文的形态如下(来自 translate.ts 的 JSDoc 示例)。给定模板:

<div i18n>pre<span>inner-pre<b>bold</b>inner-post</span>post</div>

它在translationsmap 中对应的条目为:

{ "2932901491976224757": "pre{$START_TAG_SPAN}inner-pre{$START_BOLD_TEXT}bold{$CLOSE_BOLD_TEXT}inner-post{$CLOSE_TAG_SPAN}post" }

可以看到,HTML 元素的开闭标签都变成了带明确语义的占位符({$START_TAG_SPAN}{$CLOSE_TAG_SPAN}等),这让译文能够在必要时重排或删除这些片段(比如某些语言里行内元素的语序与英文不同)。

7.3 与“运行时翻译”有关的三个重要约束

源码注释还明确了三个经常被踩坑的行为边界:

  1. 翻译函数可能重排替换表达式——translate的签名返回[TemplateStringsArray, readonly any[]],即静态片段与表达式数组都可能被重新排列(见 translate.ts 与TranslateFn定义),所以不要假设原来的插值顺序在译文中保持不变。
  2. 加载新翻译不会回溯改写已经翻译过的消息——$localize消息只在标签字符串首次被求值时处理一次;在应用生命周期后期加载新翻译,并不会改变那些已被翻译消息的输出。
  3. 翻译挂在全局对象上、跨应用共享——由于TRANSLATIONS依附于全局$localize,同一页面里运行的所有应用(例如多个独立 Angular 应用并存)会共享同一份翻译表。

7.4clearTranslations():复位运行时翻译

clearTranslations用于把所有已加载进内存的翻译清除干净(translate.ts)。实现上它把$localize.translate置回undefinedTRANSLATIONS重置为空原型对象——也就是说它会连带关闭运行期翻译开关,让$localize回到透传求值状态。当你需要在测试之间隔离翻译状态,或实现语言切换前的重置时,这个 API 就派上了用场。

import {loadTranslations, clearTranslations} from '@angular/localize'; loadTranslations({ '2932901491976224757': '译文内容...', }); // ...应用运行... clearTranslations();

八、工程集成全景:从一个标记到一条完整流水线

把上文串联起来,一个完整的本地化工作流大致是:

  1. 打标:模板中用i18n属性,TS 代码中用$localize标签,或先用localize-migrate对旧版@angular/i18n用法做迁移;
  2. 提取:构建期运行localize-extract,把所有$localize标记提取为翻译源文件(如 XLIFF、XMB 或 simple JSON);
  3. 翻译:交给翻译团队产出各语言的翻译文件;
  4. 落地二选一
    • 编译期:用localize-translate(CLI)或 Angular CLI 的--localize构建为各语言静态包——此时代码中的$localize被彻底擦除,译文内联,零运行时成本;
    • 运行期:引入@angular/localize/init(package.json 的sideEffects中专门列出了./fesm2022/init.mjs,表明该入口必须在构建时保留副作用),再调用loadTranslations()在浏览器端注入翻译。

ng add @angular/localize之所以被作为第一步推荐,正是因为它会帮你把上述工具链与入口完整接入 Angular 工程配置,避免手动接线。相关的工具实现与说明可以继续在仓库的 packages/localize/tools/ 与 packages/localize/schematics/ 中深入研读。


九、总结

@angular/localize的设计核心,是把“标记可翻译文本”与“执行翻译”彻底解耦:

  • $localize是一个合法可运行的标签函数,但它被刻意设计成“在压缩后依然存活的全局标识符”,从而也能纯粹充当静态标记,供编译期工具在部署前完成译文内联并擦除自身,达成零运行时成本;
  • Angular 模板编译器不直接翻译,而是把所有i18n文本编译为$localize标签字符串,让模板与 TS 代码在工具链中汇合于同一语法;
  • 元数据块、具名占位符与冒号转义构成了这套标记语法的完整规则;
  • 编译期内联、运行期求值、透传求值三种处理场景,加上loadTranslations/clearTranslations运行 API 与localize-*三组 CLI,共同支撑起从打标、提取到多语言产出的整条本地化流水线。

无论你的应用最终选择编译期多语言静态包,还是运行期按需加载语言,$localize都是整条链路上不可绕过的锚点——理解了它,就掌握了 Angular 本地化的底层语言。

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 19:58:20

MPU6050六轴传感器从入门到实战:驱动、校准与姿态解算全解析

简介&#xff1a;MPU6050.zip是一套面向ESP32开发者的MPU6050驱动与DMP姿态解算代码包&#xff0c;适合需要快速获取俯仰、翻滚、航偏角的嵌入式项目。资源共9个文件&#xff0c;以5个C头文件、3个C源文件和1个文本说明为主&#xff0c;包含MPU6050寄存器驱动、inv_mpu库及DMP运…

作者头像 李华
网站建设 2026/9/9 19:57:03

STM32串口奇偶校验实战:USART2配置与排坑指南

简介&#xff1a;面向STM32嵌入式开发者的一份实战工程&#xff0c;基于Cortex-M3内核的F103系列单片机&#xff0c;完整演示串口2带奇偶校验通信的配置方法。资源共78个文件&#xff0c;以头文件和C源码为主&#xff0c;包含串口、按键、LED、延时等模块驱动以及标准外设库&am…

作者头像 李华