在 Angular 框架上国际化方案

简介: 第一次在 Angular 框架上落地国际化方案,将 Eoapi 的经验分享给大家~
第一次在 Angular 框架上落地国际化方案,将 Eoapi 的经验分享给大家~

项目背景

  • 需要支持 Web 和 Electron 桌面端
  • 技术栈
  • Angular 14
  • Electron 19
  • NG-ZORR 13.0.6

可能遇到的困难

  • 各语言下样式不统一,例如阿拉伯文字展示从右到左,英文比中文长等等
  • 变量本地化,例如初始化 API 数据、日志、时间戳、金额等单位
  • 翻译成本高
  • 协作流程复杂
  • 技术标准的制定、落地

调研

结论
先上表格看结论,满分三颗星,最终选定 I18n 方案。

1-1.PNG

一直在运行时语言包和编译手段两种方案徘徊,最后还是选定了使用 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 只支持打包成多种语言包文件,动态切换语言包目前处于提案通过阶段,未来可期。

1-2.PNG

运行时语言包

支持 Angular 的方案有:

我认为这种方案最大的问题是开发体验较差,虽然很多工具可以使用 VSCode 插件显示【默认语言文本】抹平差异,但源码仍然是各种变量。

1-3.gif

其次是切换语言包是一个低频操作,一般用户长期只会选择一种语言包,运行时方案也会增加不必要的内存占用(当然你要说可以忽略不计那我也不和你辩,算你赢)。

多套代码
没什么技术难点,同步成本高,灵活性高。

技术实现

以下代码配置的源码都在这个仓库,有需要可以拉下来部署看看具体效果。

https://github.com/eolinker/eoapi

国际化主要分为几个流程:
1-4.jpg

语言包
语言包配置
命令行配置

使用 angular 提供的指令 ng extract-i18n,同时制定语言包生成的目录 src/locale

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

运行后就会生成一个默认语言包 message.xlf

1-5.png

建议使用如下 npm 命令封装,运行 npm run lang:gen 即可生成语言包文件

"scripts": {
"lang:gen": "ng extract-i18n --output-path src/locale",
....

angular.json 配置

想要加其他语言包,从默认语言包 message.xlf 复制一个文件,然后配置一下就可以支持多语言。

文件名语言 ID 命名请参考(en-US、zh-Hans)请参考:国际化方案

1-6.png

在 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 调试时只会支持一种语言包,同时意味着你没法直接运行时测试切换语言包的效果(当然可以通过运行两个服务间接实现),关系不大。

你可以使用下面两种配置进行调试:

  1. 调试时可以通过设置 "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 | 淘系前端团队
相关文章
|
6月前
|
前端开发 JavaScript 开发者
什么是 Angular 框架中的 Zone.js
什么是 Angular 框架中的 Zone.js
|
6天前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
2月前
|
前端开发 JavaScript API
React、Vue.js 和 Angular前端三大框架对比与选择
前端框架是用于构建用户界面的工具和库,它提供组件化结构、数据绑定、路由管理和状态管理等功能,帮助开发者高效地创建和维护 web 应用的前端部分。常见的前端框架如 React、Vue.js 和 Angular,能够提高开发效率并促进团队协作。
67 4
|
3月前
|
缓存 监控 前端开发
WEB前端三大主流框架:React、Vue与Angular
在Web前端开发中,React、Vue和Angular被誉为三大主流框架。它们各自具有独特的特点和优势,为开发者提供了丰富的工具和抽象,使得构建复杂的Web应用变得更加容易。
192 6
|
3月前
|
Java 数据库连接 数据库
强强联手!JSF 与 Hibernate 打造高效数据访问层,让你的应用如虎添翼,性能飙升!
【8月更文挑战第31天】本文通过具体示例详细介绍了如何在 JavaServer Faces (JSF) 应用程序中集成 Hibernate,实现数据访问层的最佳实践。首先,创建一个 JSF 项目并在 Eclipse 中配置支持 JSF 的服务器版本。接着,添加 JSF 和 Hibernate 依赖,并配置数据库连接池和 Hibernate 配置文件。然后,定义实体类 `User` 和 DAO 类 `UserDAO` 处理数据库操作。
57 0
|
3月前
|
Java Spring
🔥JSF 与 Spring 强强联手:打造高效、灵活的 Web 应用新标杆!💪 你还不知道吗?
【8月更文挑战第31天】JavaServer Faces(JSF)与 Spring 框架是常用的 Java Web 技术。本文介绍如何整合两者,发挥各自优势,构建高效灵活的 Web 应用。首先通过 `web.xml` 和 `ContextLoaderListener` 配置 Spring 上下文,在 `applicationContext.xml` 定义 Bean。接着使用 `@Autowired` 将 Spring 管理的 Bean 注入到 JSF 管理的 Bean 中。
57 0
|
3月前
|
开发者 安全 SQL
JSF安全卫士:打造铜墙铁壁,抵御Web攻击的钢铁防线!
【8月更文挑战第31天】在构建Web应用时,安全性至关重要。JavaServer Faces (JSF)作为流行的Java Web框架,需防范如XSS、CSRF及SQL注入等攻击。本文详细介绍了如何在JSF应用中实施安全措施,包括严格验证用户输入、使用安全编码实践、实施内容安全策略(CSP)及使用CSRF tokens等。通过示例代码和最佳实践,帮助开发者构建更安全的应用,保护用户数据和系统资源。
50 0
|
3月前
|
开发者 缓存 数据库
【性能奇迹】Wicket应用的极速重生:揭秘那些让开发者心跳加速的调优秘技!
【8月更文挑战第31天】在软件开发中,性能优化是确保应用快速响应和高效运行的关键。本书《性能调优:Apache Wicket应用的速度提升秘籍》详细介绍了如何优化Apache Wicket应用,包括代码优化、资源管理、数据库查询优化、缓存策略及服务器配置等方面。通过减少不必要的组件渲染、优化SQL查询、使用缓存和调整服务器设置等方法,本书帮助开发者显著提升Wicket应用的性能,确保其在高并发和数据密集型场景下的稳定性和响应速度。
41 0
|
3月前
|
JavaScript 前端开发 开发者
深入解析Angular装饰器:揭秘框架核心机制与应用——从基础用法到内部原理的全面教程
【8月更文挑战第31天】本文深入解析了Angular框架中的装饰器特性,包括其基本概念、使用方法及内部机制。装饰器作为TypeScript的关键特性,在Angular中用于定义组件、服务等。通过具体示例介绍了`@Component`和`@Injectable`装饰器的应用,展示了如何利用装饰器优化代码结构与依赖注入,帮助开发者构建高效、可维护的应用。
32 0
|
3月前
|
缓存 前端开发 JavaScript
Angular邂逅PWA:一场关于如何利用现代Web技术栈中的明星框架与渐进式理念,共同编织出具备原生应用般丝滑体验、离线访问及桌面集成能力的未来Web应用的探索之旅
【8月更文挑战第31天】本文详细介绍如何利用Angular将传统Web应用升级为渐进式Web应用(PWA),克服后者在网络依赖、设备集成及通知功能上的局限。通过具体命令行操作与代码示例,指导读者从新建Angular项目到配置`manifest.json`和服务工作进程,最终实现离线访问、主屏添加及推送通知等功能,显著提升用户体验。适合各水平开发者学习实践。
34 0