Angular 18+ 高级教程 – 国际化 Internationalization i18n
介绍
先讲讲名词。
Internationalization 的缩写是 i18n,中文叫国际化。
Globalization 是 Internationalization 的同义词,都是指国际化。
Localization 的缩写是 l10n,中文叫本地化。
i18n vs l10n
一个国际化,一个本地化,它俩有什么区别,又有什么关系呢?
我们来看一个具体的例子
上图是苹果公司给美国人访问的官网,内容是 iPhone 16 Pro 的售价。
文字使用的是美式英文 (en-US),价钱使用的是美金 (USD)。
好,我们再看另外两张图
中国人看的是简体中文 (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 哦。
它会做几件事:
package.json
安装了 @angular/localize package。
注意看,它是安装到了 devDependencies 里哦。
这也意味着,Angular i18n 是在 compile 阶段完成的,而不是在 runtime。
angular.json
多了一个 polyfill。我们刚说 i18n 发生在 compile 阶段,但也不完全。有一小部分还是需要 runtime 配合完成的。
这个 polyfill 就用在这些地方。
tsconfig
main.ts
还需要 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)
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 (日文)
Setup angular.json and build application
翻译完后,就到了最后环节,ng build。
在 build 之前,我们需要修改一下 angular.json。
告知 Angular 原文、支持的译文、还有它们的文件路径。
还需要设置 projects.i18n.architect.options.localize: true
接着就可以 build 了
ng build --localize
Warning
它出现 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 就好)。继续
一共 build 出了 3 个 folder "en-US"、"ja-JP"、"zh-Hans-CN"。
每一个 folder 里都有各自的 index.html,main.js 等等。
所有的代码都一摸一样,除了这一句
这一段符文是 unicode,对应的文字是
合起来就是 こんにちは世界,日文 Hello World 的意思。
这段是简体中文 Hello World 的 unicode。
Run application
打开 dist\i18n\browser,然后 Open with Live Server
效果
总结
以上就是 Angular i18n 最简单的 step by step 过程。
有很多细节和玩法我都还没有讲到,下面我们一个一个补上,Let's go 。
i18n 标签 (attribute) 的日常用法
我们逐一看看 i18n 标签的各种日常使用方式。
i18n 习性 の tree shaking
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 有着一模一样的文字
它们会被放到同一个 <trans-unit> 里,只需要翻译一次。
Description, meaning, translate ID
<h1 i18n="description for this translate">Home</h1>
标签值可以用来描述要翻译的文字。
这行字会被填入 <trans-unit> 里。
除了 description,我们还可以加入 meaning / title。
<h1 i18n="meaning for this translate|description for this translate">Home</h1>
使用 pipe symbol | 作为分隔符,前面一段是 meaning,后一段是 description。
meaning 除了是一个描述以外,它还有 unique 功能。
我们举一个例子。
"Home" 这个英文字,可以被翻译成 "家",也可以被翻译成 "首页"。
具体要翻译成哪一个,还得看它的上下文。
也就是说,上一 part 我们提到的特性 -- same word same translate 这个潜规则是不能满足所有场景的。
当出现这种情况时,meaning 就可以用于区分相同的文字。
<h1 i18n="Header navigation|">Home</h1>
<h1 i18n="A song name|">Home</h1>
效果
虽然文字相同,但是 meaning 不同,所以生成出了 2 个 <trans-unit>。
最后说一说 translate ID。
这个 ID 是 unique,Angular i18n 会依据文字和 meaning 自动生成对应的 ID。
如果我们想自己管理,自己 hardcode 写一个也可以。
<h1 i18n="@@id-1">Home</h1>
<h1 i18n="@@id-2">Home</h1>
效果
虽然文字一样,meaning 一样,但 ID 不同,所以肯定会分开两个 <trans-unit>。
With HTML and interpolation
<p i18n>Copyright 2024 <a href="/">{{ companyName() }}</a>. All rights reserved</p>
p 里头包含了 <a> 和 {{ interpolation }},这些都是 OK 的。
翻译的时候,针对文字翻就可以了,其它的不要乱改哦。
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 就可以了。
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 大于小于这些都不支持)
翻译文档长这样
同样的,我们只翻译文字就好,其它的不要乱改。
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>
当 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 引入的类型就是为了它
$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
把原本的 true,改成 array,array 里只能放一个 locale。
接着 ng serve 就可以了。
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/">
<base href> 有啥用,可以看这篇。
为什么 Angular i18n 要在 base href 加上 locale 呢?
因为它想让我们更方便的部署,我拿 ASP.NET Core 来举例。
ASP.NET Core 常规做法是把 ng build 的产物通通放到 wwwroot folder 里
然后在 program.cs 做 routing
简单说就是,当用户访问 /zh-Hans-CN/**/* 就会访问到 /zh-Hans-CN/index.html。
index.html
polyfills.js 结合 base href 后的路径是 /en-US/polyfills-js
从 wwwroot 往下 "\en-US\polyfills.js",这个路径是正确的。
假如 base href 是 "/",那路径就变成了 "/polyfills.js"。
那这个文档就要在 wwwroot\polyfills.js 才能拿到。
由此可见,加上 base href 会比较合理方便。
如果我们不喜欢它自作主张,也可以去 angular.json 里配置
这样 ng build 出来的 index.html 就变成 <base href="/" > 了。
DatePipe with Locale
DatePipe 会依据 locale 而变化,比如
<p>{{ today() | date }}</p>
在 zh-Hans 的情况下,它的效果是
用 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 /.\
}
效果
里面包含了 formatDate 需要用到的日期格式和语言。
我们再试试看 find 其它 locale
console.log(ɵfindLocaleData('ja'));
报错了
原因很简单,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'));
效果
CurrencyPipe with Locale
不同国家使用不同的货币,locale 除了语言,日期格式,当然也包括货币。
上一 part,我们拿 locale data 查看时,其实货币资料也包含在内。
我们来试试 CurrencyPipe
<p>{{ 500 | currency }}</p>
效果
夷...怎么不是人民币
因为 Github Issue – Currency pipe and locale
简单说就是,他们觉得自动换 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 讲解过了。
另外,getLocaleCurrencyName/Code/Symbol 目前已是废弃的状态。
Angular 建议我们使用原生的 Intl 去实现 name 和 symbol
getLocaleCurrencyCode 无法用 Intl 去实现,Angular 的建议是让我们自己写一个 mapping list 。
第二种方法就是听从 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
Angular 18+ 高级教程 – 国际化 Internationalization i18n的更多相关文章
- java框架篇---Struts2 本地化/国际化(i18n)
国际化(i18n)是规划和实施的产品和服务,使他们能很容易地适应特定的本地语言和文化的过程中,这个过程被称为本地化.国际化的过程有时也被称为翻译或本地化启用.国际化是缩写i18n,因为我和两端用n字打 ...
- Siki_Unity_2-9_C#高级教程(未完)
Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...
- java框架篇---Struts2 本地化/国际化(i18n)(转)
源地址:https://www.cnblogs.com/oumyye/p/4368453.html 国际化(i18n)是规划和实施的产品和服务,使他们能很容易地适应特定的本地语言和文化的过程中,这个过 ...
- Pandas之:Pandas高级教程以铁达尼号真实数据为例
Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...
- Java国际化(i18n)
Java国际化(i18n) 最近在做一个网站国际化的功能.用Java做开发,使用spring+velocity. Java提供了对i18n的支持,spring对其做了集成,可以很方便的配置.主要思想就 ...
- ios cocopods 安装使用及高级教程
CocoaPods简介 每种语言发展到一个阶段,就会出现相应的依赖管理工具,例如Java语言的Maven,nodejs的npm.随着iOS开发者的增多,业界也出现了为iOS程序提供依赖管理的工具,它的 ...
- 【读书笔记】.Net并行编程高级教程(二)-- 任务并行
前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...
- 【读书笔记】.Net并行编程高级教程--Parallel
一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机 ...
- 国际化支持(I18N)
本章译者:@nixil 使用国际化支持(I18N)能够使你的应用根据用户所在地区的不同选择不同的语言.下面介绍如何在引用中使用国际化. 只允许使用UTF-8 Play只支持UTF-8一种字符编码.这是 ...
- 分享25个新鲜出炉的 Photoshop 高级教程
网络上众多优秀的 Photoshop 实例教程是提高 Photoshop 技能的最佳学习途径.今天,我向大家分享25个新鲜出炉的 Photoshop 高级教程,提高你的设计技巧,制作时尚的图片效果.这 ...
随机推荐
- 字符—字符与整数的关系&&常用的库函数_C
// Code file created by C Code Develop #include "ccd.h" #include "stdio.h" #incl ...
- 【Dos-BatchPrograming】04
--1.PING 主机联通性检测 Microsoft Windows [版本 10.0.19041.746] (c) 2020 Microsoft Corporation. 保留所有权利. C:\Us ...
- 哈哈哈,我就说未来要研发无人的AI潜艇嘛 —— 说啥来啥 —— AI驱动的无人潜艇
相关: 沉默5个月后,美国对华发出挑战书,万没想到,中方打法早就变了
- 模仿学习算法:Data Aggregation Approach: DAGGER算法——Mixing policy
论文: <A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning> ...
- 洛谷P5250 【深基17.例5】木材仓库
[深基17.例5]木材仓库 题目描述 博艾市有一个木材仓库,里面可以存储各种长度的木材,但是保证没有两个木材的长度是相同的.作为仓库负责人,你有时候会进货,有时候会出货,因此需要维护这个库存.有不超过 ...
- 基础数据结构->set&&map
set&&map BEGIN:惜墨如金 set用法 基本用法 #include<bits/stdc++.h> using namespace std; void the_s ...
- Java开发环境安装笔记
目录 JDK的版本 Java 8 Java 9 Java 11 (LTS) Java 17 (LTS) Java 21 JDK的环境变量设置 JAVAHOME 和 PATH 环境变量 JAVAPATH ...
- JavaScript魔法:在线Excel附件上传与下载的完美解决方案
最新技术资源(建议收藏) https://www.grapecity.com.cn/resources/ 前言 在本地使用Excel时,经常会有需要在Excel中添加一些附件文件的需求,例如在Exce ...
- springcloud经验
> 码云地址:https://gitee.com/lpxs/lp-springcloud.git > 有问题可以多沟通:136358344@qq.com. 架构演化的步骤 在确定使用S ...
- tomcat发布两个项目报错webAppKey重复设置
两个项目的web.xml中都有一个日志监听器配置 <listener> <listener-class> org.springframework.web.util.Log4jC ...