Angular 18+ 高级教程 – 国际化 Internationalization i18n

介绍

先讲讲名词。

Internationalization 的缩写是 i18n,中文叫国际化。

Globalization 是 Internationalization 的同义词,都是指国际化。

Localization 的缩写是 l10n,中文叫本地化。

i18n vs l10n

一个国际化,一个本地化,它俩有什么区别,又有什么关系呢?

我们来看一个具体的例子

Angular 18+ 高级教程 – 国际化 Internationalization i18n

上图是苹果公司给美国人访问的官网,内容是 iPhone 16 Pro 的售价。

文字使用的是美式英文 (en-US),价钱使用的是美金 (USD)。

好,我们再看另外两张图

Angular 18+ 高级教程 – 国际化 Internationalization i18n

图一是苹果公司给中国人看的官网,图二则是给日本人看的官网

中国人看的是简体中文 (zh-Hans-CN) 和人民币 (CNY)。

日本人看的是日文 (ja-JP) 和日元 (JPY)。

三个网站销售的都是 iPhone 16 Pro。网站设计、排版都一模一样。

唯一的区别就是,网站会依据不同的国家,显示对应的语言和货币。

像这样一个网站,我们就可以说:苹果公司的官网支持国际化,同时也落实了本地化。

所谓支持国际化,意思是,网站架构有能力 handle 不同的语言,货币,时区。(设计,功能全都一样,就语言,货币,时区不同)

所谓落实本地化,意思是,网站不仅有能力 handle 不同的语言,货币,时区,而且它确实做出来了。

国际化指的是一个方案 / preparation,本地化则是具体的实现。

Angular i18n

Angular 有 built-in 的 i18n 方案。我们使用 Angular 就能做出像苹果公司那样支持国际化的网站。

本篇会 step by step 教 i18n,但不会讲解原理,也不会逛源码,开始吧 🚀。

 

参考

YouTube – Introduction to Internationalization in Angular

Docs – Angular Internationalization

 

Angular i18n step by step

一步一步来

创建一个新项目

ng new i18n --routing=false --ssr=false --skip-tests --style=scss

安装 @angular/localizepackage

ng add @angular/localize

提醒:是 ng add 不是 yarn add 哦。

它会做几件事:

  1. package.json

    Angular 18+ 高级教程 – 国际化 Internationalization i18n

    安装了 @angular/localize package。

    注意看,它是安装到了 devDependencies 里哦。

    这也意味着,Angular i18n 是在 compile 阶段完成的,而不是在 runtime。

  2. angular.json

    Angular 18+ 高级教程 – 国际化 Internationalization i18n

    多了一个 polyfill。我们刚说 i18n 发生在 compile 阶段,但也不完全。有一小部分还是需要 runtime 配合完成的。

    这个 polyfill 就用在这些地方。

  3. tsconfig

    Angular 18+ 高级教程 – 国际化 Internationalization i18n

    main.ts

    Angular 18+ 高级教程 – 国际化 Internationalization i18n

    还需要 TypeScript 配合,因为 runtime 会用到一些全局变量。

i18n Hello World

App Template

<h1 i18n>Hello World</h1>

注意看,这个 h1 有一个 "i18n" 标签 (attribute)。

它用来表示,这个 "Hello World" 待会儿需要被翻译成其它语言。

注:这里给的是最简单的例子,下面还会有比较复杂的玩法,我们先过一轮简单的。

Generate translation files

执行 command

ng extract-i18n --output-path src/locale

上面我们说了,Angular i18n 发生在 compile 阶段。

这个 command 会创建一个 folder (src/locale) 和一个 file (messages.xlf)

Angular 18+ 高级教程 – 国际化 Internationalization i18n

messages.xlf 是要给翻译小姐姐使用的。

它长这样

<?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">   <!-- 1. source-language="en-US" 表示我们的 source code 写的是美式英文-->   <file source-language="en-US" datatype="plaintext" original="ng2.template">     <body>       <!-- 2. 每一句要翻译的文字都有一个独一无二的 ID 代号 -->       <trans-unit id="4584092443788135411" datatype="html">         <!-- 3. source 就是我们要翻译的文字,也就是上面 App Template 里的 <h1 i18n>Hello World</h1> -->         <source>Hello World</source>         <context-group purpose="location">           <!-- 4. location 表明这个要翻译的文字,它来自哪一个 file 和哪一行 -->           <context context-type="sourcefile">src/app/app.component.html</context>           <context context-type="linenumber">1</context>         </context-group>       </trans-unit>     </body>   </file> </xliff>

Angular i18n 会扫描我们所有的文件,然后提取出需要翻译的部分,接着制作出 messages.xlf。

Translate

接着,我们把 messages.xlf 寄给翻译小姐姐。

她会替我们翻译出不同语言的版本,比如

messages-zh-Hans-CN.xlf (简体中文)

<?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">   <file source-language="en-US" datatype="plaintext" original="ng2.template">     <body>       <trans-unit id="4584092443788135411" datatype="html">         <source>Hello World</source>         <!-- 1. 添加了简体中文 -->         <target>你好,世界</target>         <context-group purpose="location">           <context context-type="sourcefile">src/app/app.component.html</context>           <context context-type="linenumber">1</context>         </context-group>       </trans-unit>     </body>   </file> </xliff>

messages.ja-JP.xlf (日文)

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Setup angular.json and build application

翻译完后,就到了最后环节,ng build。

在 build 之前,我们需要修改一下 angular.json。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

告知 Angular 原文、支持的译文、还有它们的文件路径。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

"i18n": {   // 原文是美式英文   "sourceLocale": "en-US",   "locales": {     // 支持简体中文     "zh-Hans-CN": {       // 简体中文翻译文档在这儿       "translation": "src/locale/messages.zh-Hans-CN.xlf"     },     // 支持日文     "ja-JP": {       // 日文翻译文档在这儿       "translation": "src/locale/messages.ja-JP.xlf"     }   } },

View Code

还需要设置 projects.i18n.architect.options.localize: true

Angular 18+ 高级教程 – 国际化 Internationalization i18n

接着就可以 build 了

ng build --localize

Warning 

Angular 18+ 高级教程 – 国际化 Internationalization i18n

它出现 warning 是因为我写的 locale ID 它不支持。

一个完整的 locale ID 应该是 "语言-国家",比如:zh-Hans 代表简体中文,CN 代表中国,但它只支持写语言 zh-Hans,-CN 不行。

相关 Github Issue – Unable to find zh-Hant-TW or zh-TW locale in @angular/common

不碍事儿,程序员不关心警告 (如果出现 error,请把国家 -CN 删掉,改成 zh-Hans 就好)。继续

Angular 18+ 高级教程 – 国际化 Internationalization i18n

一共 build 出了 3 个 folder "en-US"、"ja-JP"、"zh-Hans-CN"。

每一个 folder 里都有各自的 index.html,main.js 等等。

所有的代码都一摸一样,除了这一句

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Angular 18+ 高级教程 – 国际化 Internationalization i18n

这一段符文是 unicode,对应的文字是

Angular 18+ 高级教程 – 国际化 Internationalization i18n

合起来就是 こんにちは世界,日文 Hello World 的意思。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

这段是简体中文 Hello World 的 unicode。 

Run application

打开 disti18nbrowser,然后 Open with Live Server

Angular 18+ 高级教程 – 国际化 Internationalization i18n

效果

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Angular 18+ 高级教程 – 国际化 Internationalization i18n

总结

以上就是 Angular i18n 最简单的 step by step 过程。

有很多细节和玩法我都还没有讲到,下面我们一个一个补上,Let's go 🚀。

 

i18n 标签 (attribute) 的日常用法

我们逐一看看 i18n 标签的各种日常使用方式。

i18n 习性 の tree shaking

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Test Template 里有 i18n 标签。

但是,Test 组件没有被任何其它组件使用。

问:ng extract-i18n 会扫描到这个标签吗?

答:不会,因为扫描是有 tree shaking 概念的。 

i18n 习性 の same word same translate

<h1 i18n>Hello World</h1> <h1 i18n>Hello World</h1>

两个 h1 有着一模一样的文字

Angular 18+ 高级教程 – 国际化 Internationalization i18n

它们会被放到同一个 <trans-unit> 里,只需要翻译一次。

Description, meaning, translate ID

<h1 i18n="description for this translate">Home</h1>

标签值可以用来描述要翻译的文字。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

这行字会被填入 <trans-unit> 里。

除了 description,我们还可以加入 meaning / title。

<h1 i18n="meaning for this translate|description for this translate">Home</h1>

使用 pipe symbol | 作为分隔符,前面一段是 meaning,后一段是 description。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

meaning 除了是一个描述以外,它还有 unique 功能。

我们举一个例子。

"Home" 这个英文字,可以被翻译成 "家",也可以被翻译成 "首页"。

具体要翻译成哪一个,还得看它的上下文。

也就是说,上一 part 我们提到的特性 -- same word same translate 这个潜规则是不能满足所有场景的。

当出现这种情况时,meaning 就可以用于区分相同的文字。

<h1 i18n="Header navigation|">Home</h1> <h1 i18n="A song name|">Home</h1>

效果

Angular 18+ 高级教程 – 国际化 Internationalization i18n

虽然文字相同,但是 meaning 不同,所以生成出了 2 个 <trans-unit>。

最后说一说 translate ID。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

这个 ID 是 unique,Angular i18n 会依据文字和 meaning 自动生成对应的 ID。

如果我们想自己管理,自己 hardcode 写一个也可以。

<h1 i18n="@@id-1">Home</h1> <h1 i18n="@@id-2">Home</h1>

效果

Angular 18+ 高级教程 – 国际化 Internationalization i18n

虽然文字一样,meaning 一样,但 ID 不同,所以肯定会分开两个 <trans-unit>。

With HTML and interpolation

<p i18n>Copyright © 2024 <a href="/">{{ companyName() }}</a>. All rights reserved</p>

p 里头包含了 <a> 和 {{ interpolation }},这些都是 OK 的。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

翻译的时候,针对文字翻就可以了,其它的不要乱改哦。

Translate attribute value

title、aria-label 这些 element attribute value 也可以被 translate。

<button i18n-aria-label aria-label="Example icon button with a vertical three dot icon" mat-icon-button>   <mat-icon>more_vert</mat-icon> </button>

在 i18n 标签后面加上指定的 attribute name 作为 suffix 就可以了。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Without element

假如没有 element,就只有 text,那 i18n 标签要放哪里呢?

答案是 <ng-container>

<ng-container i18n>Hello World</ng-container>

ICU expressions

ICU expressions 被用于 conditional 翻译。

我们看例子来理解

handle conditional number – plural

<p i18n>{ peopleCount(), plural, =0 { no person } =1 { one person } other { {{ peopleCount() }} people } }</p>

它的语法是这样的

{ 参数一,参数二,参数三 }

参数一是 condition,peopleCount 是一个组件 property Signal<number>。

参数二有 2 个值可以填,一个是 plural,一个是 select。

plural 就是针对数字来做判断,select 则是针对 string 来做判断,select 下面会教,我们先看 plural。

参数三是不同数字下要呈现的文字。

在 runtime 阶段,假如 peopleCount = 0,那就会显示 "no person"。

假如 peopleCount = 1,那就会显示 "one person"。

peopleCount = 其它数字,则会显示 "{{ peopleCount() }} people"。

用 @if 实现的话,长这样

<p i18n>   @if(peopleCount() === 0) {     no person   }   @else if (peopleCount() === 1) {     one person   }   @else {     {{ peopleCount() }} people   } </p>

显然使用 ICU expressions 会更精简一些。(但 plural 的表达式是有限的,它只能写 =1,=5,不可以写 >5,<3 大于小于这些都不支持)

翻译文档长这样

Angular 18+ 高级教程 – 国际化 Internationalization i18n

同样的,我们只翻译文字就好,其它的不要乱改。

handle conditional string – select

select 和 plural 结构是一样的,只是前者针对 string,后者针对 number。

export class AppComponent {   readonly gender = signal<'male' | 'female' | null>(null); }

<p i18n>{ gender(), select, male { male } female { female } other { other } }</p>

Angular 18+ 高级教程 – 国际化 Internationalization i18n

当 gender 是 "male" 时,显示 "男"。

当 gender 是 "female" 时,显示 "女"。

当 gender 不是 "male" 也不是 "female" (e.g. null) 时,显示 "其它"。

Translate in script

上面我们讲的都是在 HTML 里做翻译。

如果我的文字写在 script 里头呢?如何打上 i18n 标签?

答案是使用 $localize

export class AppComponent {   constructor() {     const value = $localize`Hello World`;     console.log(value);   } }

$localize 是一个全局变量

main.ts 引入的类型就是为了它

Angular 18+ 高级教程 – 国际化 Internationalization i18n

$localize 等价于 HTML 的 i18n 标签,用法也大同小异,生成出来的翻译文档也是一样的。

下面这个是 meaning, description, id 的写法

const value = $localize`:meaning|description@@id:Hello World`;

用 : 分号做分割。

唯一比较大的区别是,$localize 不支持 ICU expressions。

假如我们需要 conditional 就用一般的 if else swtich 来完成就可以了,比如:

// <p i18n>{ peopleCount(), plural, =0 { no person } =1 { one person } other { {{ peopleCount() }} people } }</p>  const peopleCount = signal(0); const value =   peopleCount() === 0     ? $localize`no person`     : peopleCount() === 1     ? $localize`one person`     : $localize`${peopleCount()} people`;  console.log(value);

总结

以上便是 i18n 标签的日常用法。

 

ng serve for i18n application

上面我们讲的都是 ng build 最终的发布。

那在 development 阶段,ng serve 是否可以开启 i18n application?

可以,但只能选定其中一个 locale。

去 angular.json 指定 locale

Angular 18+ 高级教程 – 国际化 Internationalization i18n

把原本的 true,改成 array,array 里只能放一个 locale。

接着 ng serve 就可以了。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Get current locale ID

通过 inject LOCALE_ID,我们可以获知当前是什么 locale。

export class AppComponent {   constructor() {     console.log(inject(LOCALE_ID)); // zh-Hans   } }

在没有 i18n 的情况下,它的默认值是 "en-US" (提醒:它不是依据游览器 settings 哦,它是 hardcode en-US)。

 

关于 base href

所有 build 出来的 index.html 都带有 <base href="/locale/">

Angular 18+ 高级教程 – 国际化 Internationalization i18n

<base href> 有啥用,可以看这篇

为什么 Angular i18n 要在 base href 加上 locale 呢?

因为它想让我们更方便的部署,我拿 ASP.NET Core 来举例。

ASP.NET Core 常规做法是把 ng build 的产物通通放到 wwwroot folder 里

Angular 18+ 高级教程 – 国际化 Internationalization i18n

然后在 program.cs 做 routing

Angular 18+ 高级教程 – 国际化 Internationalization i18n

简单说就是,当用户访问 /zh-Hans-CN/**/* 就会访问到 /zh-Hans-CN/index.html。

index.html 

Angular 18+ 高级教程 – 国际化 Internationalization i18n

polyfills.js 结合 base href 后的路径是 /en-US/polyfills-js

Angular 18+ 高级教程 – 国际化 Internationalization i18n

从 wwwroot 往下 "en-USpolyfills.js",这个路径是正确的。

假如 base href 是 "/",那路径就变成了 "/polyfills.js"。

那这个文档就要在 wwwrootpolyfills.js 才能拿到。

由此可见,加上 base href 会比较合理方便。

如果我们不喜欢它自作主张,也可以去 angular.json 里配置

Angular 18+ 高级教程 – 国际化 Internationalization i18n

这样 ng build 出来的 index.html 就变成 <base href="/" > 了。

 

DatePipe with Locale

DatePipe 会依据 locale 而变化,比如

<p>{{ today() | date }}</p>

在 zh-Hans 的情况下,它的效果是

Angular 18+ 高级教程 – 国际化 Internationalization i18n

用 formatDate 也是同理

constructor() {   const today = new Date();   const format = 'mediumDate';   const locale = inject(LOCALE_ID);   console.log(formatDate(today, format, locale)); // 2024年9月17日 }

formatDate 底层是如何做到 translate 的呢?

首先,它并不是使用游览器原生的 Intl,Angular 自己写了一套逻辑 (为什么 Angular 要自己写一套,不使用原生的?我不太清楚,可能是当时 Inlt 支持度还不高?不管怎样,目前我的感觉是,以后 Angular 很可能会改用原生的 Intl)。

通过 ɵfindLocaleData (formatDate 底层用的就是它) 我们可以拿到许多翻译内容

import { ɵfindLocaleData } from '@angular/core';  constructor() {   console.log(ɵfindLocaleData('zh-Hans')); // 注:这里不能是 zh-Hans-CN 哦,因为 Angular 的 locale data 没有 zh-Hans-CN 只有 zh-Hans /. }

效果

Angular 18+ 高级教程 – 国际化 Internationalization i18n

里面包含了 formatDate 需要用到的日期格式和语言。

我们再试试看 find 其它 locale

console.log(ɵfindLocaleData('ja')); 

报错了

Angular 18+ 高级教程 – 国际化 Internationalization i18n

原因很简单,Angular 默认是不会加载所有 locale 资料的。zh-Hans 之所以可以 find 到是因为我们做了 i18n,并指定了 ng serve 是 zh-Hans。

它不自动加载,但我们可以手动替它加载。

import { registerLocaleData,  } from '@angular/common';  import jaLocaleData from '@angular/common/locales/ja'; registerLocaleData(jaLocaleData, 'ja');

import 日文资料,然后 register 到 localeData 里。

这样就可以 find 到了

console.log(ɵfindLocaleData('ja')); 

效果

Angular 18+ 高级教程 – 国际化 Internationalization i18n

 

CurrencyPipe with Locale

不同国家使用不同的货币,locale 除了语言,日期格式,当然也包括货币。

上一 part,我们拿 locale data 查看时,其实货币资料也包含在内。

Angular 18+ 高级教程 – 国际化 Internationalization i18n  Angular 18+ 高级教程 – 国际化 Internationalization i18n

我们来试试 CurrencyPipe

<p>{{ 500 | currency }}</p>

效果

Angular 18+ 高级教程 – 国际化 Internationalization i18n

夷...怎么不是人民币❓🤔

因为 Github Issue – Currency pipe and locale

Angular 18+ 高级教程 – 国际化 Internationalization i18n

简单说就是,他们觉得自动换 currency symbol 但不换 value 是不合理的,所以干脆把职责全交给开发人员。

我们有两种方法可以做到 currency pipe with locale。

第一种是使用 getLocaleCurrencyName/Code/Symbol 函数

import { getLocaleCurrencyCode, getLocaleCurrencyName, getLocaleCurrencySymbol } from '@angular/common';  const code = getLocaleCurrencyCode('zh-Hans'); const name = getLocaleCurrencyName('zh-Hans'); const symbol = getLocaleCurrencySymbol('zh-Hans'); console.log([code, name, symbol]); // ['CNY', '人民币', '¥']

这三个函数底层用的是 ɵfindLocaleData 函数,这个我们上一 part 讲解过了。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

另外,getLocaleCurrencyName/Code/Symbol 目前已是废弃的状态。

Angular 建议我们使用原生的 Intl 去实现 name 和 symbol

Angular 18+ 高级教程 – 国际化 Internationalization i18n

Angular 18+ 高级教程 – 国际化 Internationalization i18n

getLocaleCurrencyCode 无法用 Intl 去实现,Angular 的建议是让我们自己写一个 mapping list 😮。

Angular 18+ 高级教程 – 国际化 Internationalization i18n

第二种方法就是听从 Angular 的建议,使用原生的 Intl。

const locale = 'zh-Hans'; const code = getLocaleCurrencyCode(locale)!; // Intl 没法从 zh-Hans 生成 CNY,我们只能自己写 mapping list 或者继续用它废弃的接口  const symbolFormatter = new Intl.NumberFormat(locale, {   style: 'currency',   currency: code,   currencyDisplay: 'symbol' }); const symbol = symbolFormatter.formatToParts(0).find(part => part.type === 'currency')!.value; // ¥  const displayNames = new Intl.DisplayNames([locale], { type: 'currency' }); const name = displayNames.of(code);  console.log([code, symbol, name]); // ['CNY', '¥', '人民币']

 

 

总结

本篇简单介绍了 Angular i18n 方案,没有深入讲解原理,也没有逛源码。

因为我个人从来没有在项目中使用过它,希望未来有机会吧,到时再深入研究研究。

另外,日常项目中,我使用的是 ASP.NET Core – Globalization & Localization

对比它俩,最大的区别是,ASP.NET Core 的翻译文档是拆散的,每一个页面,甚至每一个组件都有一个翻译文档。

而不像 Angular 那样把整个项目每一个组件资料通通放到了同一个文档里。

感觉 Angular 维护起来可能会比较乱,尤其是当网站或应用程序内容有更动的时候。

 

 

目录

上一篇 Angular 18+ 高级教程 – Memory leak, unsubscribe, onDestroy

下一篇 TODO

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻

 

发表评论

相关文章