第一次在 Angular 框架上落地国际化方案,将 Eoapi 的经验分享给大家~
项目背景
- 需要支持 Web 和 Electron 桌面端
- 技术栈
- Angular 14
- Electron 19
- NG-ZORR 13.0.6
可能遇到的困难
- 各语言下样式不统一,例如阿拉伯文字展示从右到左,英文比中文长等等
- 变量本地化,例如初始化 API 数据、日志、时间戳、金额等单位
- 翻译成本高
- 协作流程复杂
- 技术标准的制定、落地
调研
结论
先上表格看结论,满分三颗星,最终选定 I18n 方案。
一直在运行时语言包和编译手段两种方案徘徊,最后还是选定了使用 I18n 方案,有以下几点考虑:
- 对代码侵入性低,不影响现有开发模式
- 方案成熟,经过 angular 团队/社区验证,提前考虑到了各种条件
- 生成的文件 xlf 遵循国际规范 XLIFF,较通用,即使技术变迁,语言包仍通用
虽然无法支持同一个 URL 动态切换语言包,多套代码方案在桌面端实现不常规(可能需要把各种语言的安装包打进去),但考虑需求上切换语言包是一个超低频的操作以及使用编译手段可以提高良好的开发体验,还是为 i18n 方案所折服。
其实无论是运行时方案和编译手段,只要官方支持,完全可以实现相互转换,期待后续 Angular 支持将语言包反向编译成代码后支持动态切换语言包,那就是我理想中的完美方案了。
分析
在线翻译 API
切换语言包的时候再调用 OpenAPI 翻译当前页面,类似于 google 翻译。
实现难度一般,只要实现了此工具,可以兼容各种语言。
翻译 API 不一定稳定,同时免费的有次数限制,适合表达精确度要求不高,需要支持多语言的静态页。
自研语言编译器
使用编译手段提取,例如识别到代码中的中文,提取出来成为语言包,打包的时候
感兴趣可以看这篇,原理类似: https://juejin.cn/post/6844904042489970695#heading-3
不改变原有开发的模式,没有学习成本。
对代码有一定的侵入性,同时针对不同母语的代码有不同的语言匹配规则。
自研的实现成本较高,可能无法配合市面上热门翻译平台打通流程,工具生态成熟需要时间沉淀。
和自研编译提取语言文本类似,使用一些标识符标记哪些字段需要翻译,同时提供一些额外的附加规则,例如同词不同含义的标记。
较为成熟,代码侵入性低,有一定的开发学习成本。
官方文档推荐的实践都是不同的语言包打包成不同的代码,然后通过 nginx 映射到不同的代码,这适合 web,但在桌面端实现比较麻烦,无法动态加载文件。
理想情况是 angular 方案下支持一套代码加载语言包的国际化方案(one bundle for all languages),官方关于动态切换语言包的讨论:
https://github.com/angular/angular/issues/24549#issuecomment-398371120
https://github.com/angular/angular/issues/38953#issuecomment-862492065
截至本方案完成,Angular 只支持打包成多种语言包文件,动态切换语言包目前处于提案通过阶段,未来可期。
运行时语言包
支持 Angular 的方案有:
我认为这种方案最大的问题是开发体验较差,虽然很多工具可以使用 VSCode 插件显示【默认语言文本】抹平差异,但源码仍然是各种变量。
其次是切换语言包是一个低频操作,一般用户长期只会选择一种语言包,运行时方案也会增加不必要的内存占用(当然你要说可以忽略不计那我也不和你辩,算你赢)。
多套代码
没什么技术难点,同步成本高,灵活性高。
技术实现
以下代码配置的源码都在这个仓库,有需要可以拉下来部署看看具体效果。
https://github.com/eolinker/eoapi
国际化主要分为几个流程:
语言包
语言包配置
命令行配置
使用 angular 提供的指令 ng extract-i18n,同时制定语言包生成的目录 src/locale
ng extract-i18n --output-path src/locale
运行后就会生成一个默认语言包 message.xlf
建议使用如下 npm 命令封装,运行 npm run lang:gen 即可生成语言包文件
"scripts": {
"lang:gen": "ng extract-i18n --output-path src/locale",
....
angular.json 配置
想要加其他语言包,从默认语言包 message.xlf 复制一个文件,然后配置一下就可以支持多语言。
文件名语言 ID 命名请参考(en-US、zh-Hans)请参考:国际化方案
在 angular.json 中告诉构建工具你语言包叫啥,位置在哪
{
...
"projects": {
"eoapi": {
...
"i18n": {
"sourceLocale": "zh-Hans",//中文语言包
"locales": {
"en-US": "src/locale/messages.en-US.xlf"//英文语言包
}
},
Ant-Design 配置
https://ng.ant.design/docs/i18n/zh
/** 导入需要使用的语言包 **/
import { LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import en from '@angular/common/locales/en';
import zh from '@angular/common/locales/zh';
registerLocaleData(en);
registerLocaleData(zh);
/** 配置 ng-zorro-antd 国际化 **/
import { en_US, NZ_I18N, zh_CN } from 'ng-zorro-antd/i18n';
...
{
provide: NZ_I18N,
useFactory: (localId: string) => {
switch (localId) {
case 'zh':
return zh_CN;
default:
return en_US;
}
},
deps: [LOCALE_ID],
}
调试
官方原话:由于 i18n 的部署复杂性和最小化重建时间的需要,开发服务器一次仅支持本地化单个语言环境
我来翻译一下:你这需求也不常见,实现太麻烦了,你调试麻烦点就麻烦点吧,构建时间多快呀,我是为你好。
总而言之就是 Angular 调试时只会支持一种语言包,同时意味着你没法直接运行时测试切换语言包的效果(当然可以通过运行两个服务间接实现),关系不大。
你可以使用下面两种配置进行调试:
调试时可以通过设置 "localize":false 在调试时禁用本地化
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"localize":false,
"aot":true,//本地化一定要开启 AOT
...
2. angular.json指定使用某种语言包
{
...
"projects": {
"eoapi": {
...
"architect": {
"build": {
"options": {
"localize":true,//开启本地化
"aot": true,
...
},
"configurations": {
"web": {
//指定某个运行环境下的语言包,传入上面 i18n 定义的语言包名称
"localize": ["zh-Hans"],
...
}
}
},
"serve": {
...
"configurations": {
"web": {
"browserTarget": "eoapi:build:web"
}
...
运行时使用命令制定使用 --configuration web 即可
ng serve -c web -o
**构建**
Angular i18n 方案会打包成两个文件夹,所以需要注意资源在代码里面的相对地址。
**web**
和之前区别不大,主要是通过 index.html 里面的 <base> 标签来指定基础路径,打包后的代码结果如图:
![1-7.png](https://ucc.alicdn.com/pic/developer-ecology/bade5d8e5ebc49a19d88e6ca96ca88b4.png)
英语语言包的 href="en-US"
!DOCTYPE html>
Eoapi
...
![1-8.png](https://ucc.alicdn.com/pic/developer-ecology/1694ee6d19ab462abcbd69106172a371.png)
**桌面端**
Electron 启动需要指定语言
https://github.com/electron/electron/issues/5649
桌面端使用 file 协议,需要将 base href 改成相对路径 ./。
即使使用 ng build --base-href ./,打包后还是会自动在 href 前加语言标识
所以需要在 angular.json 18n 指定 baseHref 为空字符串
"projects": {
"eoapi": {
"root": "",
"i18n": {
"sourceLocale": {
"code": "zh-Hans",
"baseHref": ""//.
},
"locales": {
"en-US": {
"baseHref": "",
"translation": "src/locale/messages.en-US.xlf"
}
}
},
**Build.js 命令**
一般情况下 Angular 框架会帮我们处理好,但是我们产品涉及到 Web 和桌面端,有两套打包逻辑,所以使用了 Node 脚本去执行 Build 命令,在执行 ng build 命令前修改 angular.json 文件。
//change angular.json
const fs = require('fs');
const { execSync } = require('child_process');
class webPlatformBuilder {
resetBuildConfig(json) {
delete json.projects.eoapi.i18n.sourceLocale.baseHref;
Object.keys(json.projects.eoapi.i18n.locales).forEach((val) => {
delete json.projects.eoapi.i18n.locales[val].baseHref;
});
return json;
}
executeBuild() {
execSync('ng build -c production', { stdio: 'inherit' });
}
}
class appPlatformBuilder {
resetBuildConfig(json) {
...
}
executeBuild() {
...
}
}
class PlatformBuilder {
constructor(platForm) {
switch (platForm) {
case 'web': {
this.instance = new webPlatformBuilder();
break;
}
case 'app': {
this.instance = new appPlatformBuilder();
break;
}
}
}
build() {
const filePath = '../angular.json';
let buildConfigJson = require(filePath);
buildConfigJson = this.instance.resetBuildConfig(buildConfigJson);
let that=this;
fs.writeFile(filePath, JSON.stringify(buildConfigJson), function (err) {
if (err) {
console.error('build/beforeBuild.js:', err);
}
that.instance.executeBuild();
});
}
}
**部署**
**Vercel**
vercel 不支持多项目配置,所以只能重定向到固定 Language 而不是用户配置的 Language。
{
"rewrites": [
{
"source": "/:path((?!en/).*)",
"destination": "/en/:path*"
},
{
"source": "/:path((?!zh/).*)",
"destination": "/zh/:path*"
}
]
}
这个方案表现还是不好,拿不到一些初始化的路由信息,所以我手动写了个脚本生产空白 HTML,再手动重定向到相应位置。
fs.writeFile(
'./dist/index.html',
`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Eoapi - Easy & Open Source API Ecosystem</title>
<script>
let lang=window.location.href.includes("/en")?'en':'zh';
try{
lang=JSON.parse(window.localStorage.getItem("LOCAL_SETTINGS_KEY"))["eoapi-language"]=='en-US'?'en':'zh';
}catch(e){
}
let baseDir="/"+lang+'/'
let search={};
if(window.location.search){
window.location.href=baseDir+window.location.search;
}else{
window.location.href=baseDir;
}
</script>
</head>
<body></body>
</html>
`,
() => {}
);
**Nginx**
如果使用 Nginx 部署,可以按照官方提供的文件配置。
> https://angular.cn/guide/i18n-common-deploy#configure-a-server
**翻译流程工具调研**
- https://github.com/alibaba/kiwi
- https://github.com/mozilla/pontoon/(开源)
- https://www.volcengine.com/product/i18ntranslate
**Poeditor**
> https://poeditor.com
免费,体验一般,数据没有丢失,协作流程不够清晰,自动翻译用不了
![1-9.png](https://ucc.alicdn.com/pic/developer-ecology/cf60082f22474be1999dcab58e8472b9.png)
**Crowdin(electron 官方使用)**
> https://zh.crowdin.com/
体验好,协作清晰,但未来可能付费
![1-10.png](https://ucc.alicdn.com/pic/developer-ecology/adc74d310cdd406ea9fcb9f183276908.png)
# 结语
整个 Angular 落地国际化的方案讲完了,国际化不止涉及到技术实现,还包括翻译协作流程,如图是一个常见的翻译流程:
![1-11.png](https://ucc.alicdn.com/pic/developer-ecology/25775b1aeeea4b2fb6f65716534a71dd.png)
以及整个技术团队的国际化技术规范,毕竟翻译不止涉及到 UI,还有各种时间数据储存的格式等等数据规范。
感谢你的阅读,上面实践都落地到开源项目 Eoapi,欢迎关注~
在线体验功能
Github:https://github.com/eolinker/eoapi
# 资料
- Angular 项目 国际化方案
- 设计以人为本的国际化(i18n) 工程方案
- 国际化与本地化
- 前端国际化
- Angular internationalization (i18n) tutorial - Localizely
- https://cloud.tencent.com/developer/section/1489559
- Taobao FED | 淘系前端团队