add formatters unit test

This commit is contained in:
wxzhang 2022-08-17 18:07:59 +08:00
parent 73794587c3
commit 139e2e2282
10 changed files with 467 additions and 111 deletions

View File

@ -70,7 +70,7 @@ export default defineConfig({
"/guide/advanced/langpack.md",
"/guide/advanced/autotranslate.md",
"/guide/advanced/framework.md",
"/guide/advanced/remoteLoad.md",
"/guide/advanced/dynamic-add.md",
"/guide/advanced/lngpatch.md",
"/guide/advanced/langedit.md"
],

View File

@ -1,4 +1,4 @@
# 远程加载语言包
# 动态增加语言支持
## 前言
`voerkaI18n`默认将要翻译的文本内容经编译后保存在当`languages`文件夹下,当打包应用时会与工程一起进行打包进工程源码中。这会带来以下问题:
@ -13,7 +13,7 @@
### 准备
为说明如何从远程加载语言包,我们将假设以下的应用:
为说明如何利用远程加载语言包的机制为应用动态增加语言支持,我们将假设以下的应用:
应用`chat`,依赖于`user``manager``log`等三个库,均使用了`voerkiai18n`作为多语言解决方案
当执行完`voerkai18n compile`后,项目结构大概如下:
```javascript | pure
@ -95,14 +95,6 @@ i18nScope.registerDefaultLoader(async (language,scope)=>{
}
```
**重点:为什么要向服务器传递`scope.id`参数?**
在多包环境下,按照多包/库开发的规范,每一个库或包均具有一个**唯一的id**,默认会使用`package.json`中的`name`字段。
**例如**
- 应用`A`,依赖于包/库`X``Y``Z`,并且`A/X/Y/Z`均使用了`voerkiai18n`作为多语言解决方案
- 当应用启动时,`A/X/Y/Z`均会创建一个`i18nScope`实例,其`id`分别是`A/X/Y/Z`,然后这些`i18nScope`实例会注册到全局的`voerkaI18n`实例中(详见多库联动介绍)。
- 假如应用`A`配置支持`zh``en`两种语言,当应用要切换到`de`语言时,那么不仅是`A`应用本身需要切换到`de`语言,所依赖的库也需要切换到`de`语言。但是库`X``Y``Z`本身可能支持`de`语言,也可能不支持。如果不支持,则同样需要向服务器请求该库的翻译语言。因此,在向服务器请求时就需要带上`scope.id`,这样服务器就可以分别为应用`A`和依赖库`X``Y``Z`均准备对应的语言包了。
### 第三步:将语言包文件保存在服务器
在上一步中,我们通过`fetch(/languages/${scope.id}/${language}.json)`来传递读取语言包(您可以使用任意您喜欢的方式,如`axios`这意味着我们需要在web服务器上根据此`URL`来组织语言包,以便可以下载到语言包。比如可以这样组织:
@ -192,8 +184,8 @@ webroot
语言加载器时会传入两个参数:
| 参数 | 说明 |
| --- | --- |
| language | 要切换的此语言|
| scope |语言作用域实例,其中`scope.id`值默认等于`package.json`中的`name`字段。详见[参考](../../reference/i18nscope)。 |
| **language** | 要切换的此语言|
| **scope** |语言作用域实例,其中`scope.id`值默认等于`package.json`中的`name`字段。详见[参考](../../reference/i18nscope)。 |
- 典型的语言加载器非常简单,如下:
```javascript | pure
@ -202,8 +194,9 @@ i18nScope.registerDefaultLoader(async (language,scope)=>{
return await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
})
```
- 为什么要应用自己编写语言加载器,而不是提供约定开箱即用?
主要原因是编写语言加载器很简单,但是如何组织在服务器上的保存,想让应用开发者自行决定。比如,开发者完全可以将语言包保存在数据库中等。 另外考虑安全、兼容性等原因,因此`voerkaI18n`就将此交由开发者自行编写。
- 为什么要应用自己编写语言加载器,而不是提供开箱即用的功能?
主要原因是编写语言加载器很简单只是简单地使用HTTP从服务器上读取JSON语言包文件不存在任何难度甚至您可以直接使用上面的例子即可。
而关键是语言包在服务器上的如何组织与保存,可以让应用开发者自行决定。比如,开发者完全可以将语言包保存在数据库表中,以便能扩展其他功能。另外考虑安全、兼容性等原因,因此`voerkaI18n`就将此交由开发者自行编写。
### 编写语言切换界面
@ -242,7 +235,46 @@ i18nScope.registerDefaultLoader(async (language,scope)=>{
```
通过编写合适的语言切换界面,您可以在后期随时在线增加语种支持。
### 关于语言包补丁
语言包补丁仅对在`settings.json`配置的语言起作用,而动态增加的语种因为其语言包本身就保存在服务器,因此就不存在补丁的问题。
语言包补丁会在加载时自动合并到源码中的语言包,并且会自动在本地`localStorage`中缓存。
### `scope.id`参数
**重点:为什么要向服务器传递`scope.id`参数?**
在多包环境下,按照多包/库开发的规范,每一个库或包均具有一个**唯一的id**,默认会使用`package.json`中的`name`字段。
**例如**
- 应用`A`,依赖于包/库`X``Y``Z`,并且`A/X/Y/Z`均使用了`voerkiai18n`作为多语言解决方案
- 当应用启动时,`A/X/Y/Z`均会创建一个`i18nScope`实例,其`id`分别是`A/X/Y/Z`,然后这些`i18nScope`实例会注册到全局的`voerkaI18n`实例中(详见多库联动介绍)。
- 假如应用`A`配置支持`zh``en`两种语言,当应用要切换到`de`语言时,那么不仅是`A`应用本身需要切换到`de`语言,所依赖的库也需要切换到`de`语言。但是库`X``Y``Z`本身可能支持`de`语言,也可能不支持。如果不支持,则同样需要向服务器请求该库的翻译语言。因此,在向服务器请求时就需要带上`scope.id`,这样服务器就可以分别为应用`A`和依赖库`X``Y``Z`均准备对应的语言包了。
**按此机制如果您的应用使用了任何第三方库只要第三方库也是使用voerkai18n作为多语言解决方案那么不需要原开发者支持您自已就可以为之`增加语言支持`或者`打语言包补丁`。**
### 缓存语言包
当切换到动态增加的语言时会从远程服务器加载语言包,取决于语言包的大小,可能会产生延迟,这可能对用户体验造成不良影响。因此,您可以在客户端对语言包进行缓存。
```javascript | pure
import { i18nScope } from "./languages"
async function loadLanguageMessages(language,scope){
let messages = await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
localStorage.setItem(`voerkai18n_${scope.id}_${language}_messages`,JSON.stringify(messages));
return messages
}
i18nScope.registerDefaultLoader(async (language,scope)=>{
let message = localStorage.getItem(`voerkai18n_${scope.id}_${language}_messages`);
if(messages){
setTimeout(async ()=>{
const messages = loadLanguageMessages(language,scope)
scope.refresh()
},0)
}else{
messages = loadLanguageMessages(language,scope)
}
return messages
})
```

View File

@ -5,4 +5,9 @@
**基本思路是,应用上线后发现翻译错误时,可以在服务器上约定位置放置语言包补丁,应用会自动进行更新修复,很实用的一个特性。**
### 关于语言包补丁
语言包补丁仅对在`settings.json`配置的语言起作用,而动态增加的语种因为其语言包本身就保存在服务器,因此就不存在补丁的问题。
语言包补丁会在加载时自动合并到源码中的语言包,并且会自动在本地`localStorage`中缓存。
使用方法详见`动态加载语言包`介绍。

View File

@ -25,11 +25,11 @@ console.log(t("中华人民共和国成立于{}",1949))
`t`翻译函数是从`myapp/languages/index.js`文件导出的翻译函数,但是现在`myapp/languages`还不存在,后续会使用工具自动生成。`voerkai18n`后续会使用正则表达式对提取要翻译的文本。
## 第一步:安装命令行工具
安装`@voerkai18n/cli`到全局。
```shell
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
>pnpm add -g @voerkai18/cli
> pnpm add -g @voerkai18/cli
```
## 第二步:初始化工程
@ -72,10 +72,25 @@ console.log(t("中华人民共和国成立于{}",1949))
- `voerkai18n init`是可选的,`voerkai18n extract`也可以实现相同的功能。
- 一般情况下,您可以手工修改`settings.json`,如定义名称空间。
- `voerkai18n init`仅仅是创建`languages`文件,并且生成`settings.json`,因此您也可以自己手工创建。
## 第三步:提取文本
## 第三步:标识翻译内容
接下来在源码文件中,将所有需要翻译的内容使用`t`翻译函数进行包装,例如下:
```javascript | pure
import { t } from "<myapp>/languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")
```
`t`翻译函数只是一个普通函数,您需要为之提供执行环境,关于`t`翻译函数的更多用法见[这里](../use/t.md)
## 第四步:提取文本
接下来我们使用`voerkai18n extract`命令来自动扫描工程源码文件中的需要的翻译的文本信息。
`voerkai18n extract`命令会使用正则表达式来提取`t("提取文本")`包装的文本。
```shell
myapp>voerkai18n extract
@ -83,7 +98,7 @@ myapp>voerkai18n extract
执行`voerkai18n extract`命令后,就会在`myapp/languages`通过生成`translates/default.json``settings.json`等相关文件。
- **translates/default.json** 该文件就是需要进行翻译的文本信息。
- **translates/default.json** 该文件就是从当前工程扫描提取出来的需要进行翻译的文本信息。
- **settings.json** 语言环境的基本配置信息,可以进行修改。
@ -100,7 +115,7 @@ myapp
```
**如果略过第一步中的`voerkai18n init`,也可以使用以下命令来为创建和更新`settinbgs.json`**
**如果略过第一步中的`voerkai18n init`,也可以使用以下命令来为创建和更新`settings.json`**
```javascript | pure
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
@ -112,9 +127,9 @@ myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
- 计划支持`zh``en``de``jp`四种语言
- 默认语言是中文。(指在源码文件中我们直接使用中文即可)
- 激活语言是中文(即默认切换到中文)
- `-D`代表显示扫描调试信息
- `-D`代表显示扫描调试信息,可以显示从哪些文件提供哪些文本
## 第步:翻译文本
## 第步:翻译文本
接下来就可以分别对`language/translates`文件夹下的所有`JSON`文件进行翻译了。每个`JSON`文件大概如下:
@ -122,13 +137,13 @@ myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>"
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>"
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
@ -143,7 +158,7 @@ myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
- 如果文本内容在源代码中已修改了,则会视为新增加的内容。
- 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。
因此,反复执行`voerkai18n extract`命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
总之,反复执行`voerkai18n extract`命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
大部分国际化解决方案至此就需要交给人工进行翻译了,但是`voerkai18n`除了手动翻译外,通过`voerkai18n translate`命令来实现**调用在线翻译服务**进行自动翻译。
@ -151,9 +166,9 @@ myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
>voerkai18n translate --provider baidu --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
```
在项目文件夹下执行上面的语句将会自动调用百度的在线翻译API进行翻译以现在的翻译水平而言您只需要进行少量的微调即可。关于`voerkai18n translate`命令的使用请查阅后续介绍。
在项目文件夹下执行上面的语句,将会自动调用`百度的在线翻译API`进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于`voerkai18n translate`命令的使用请查阅后续介绍。
## 第步:编译语言包
## 第步:编译语言包
当我们完成`myapp/languages/translates`下的所有`JSON语言文件`的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续`名称空间`介绍),接下来需要对翻译后的文件进行编译。
@ -180,7 +195,7 @@ myapp> voerkai18n compile
```
## 第步:导入翻译函数
## 第步:导入翻译函数
第一步中我们在源文件中直接使用了`t`翻译函数包装要翻译的文本信息,该`t`翻译函数就是在编译环节自动生成并声明在`myapp/languages/index.js`中的。
@ -192,7 +207,7 @@ import { t } from "./languages"
但是如果源码文件很多,重次重复导入`t`函数也是比较麻烦的,所以我们也提供了一个`babel/vite`等插件来自动导入`t`函数。
## 第步:切换语言
## 第步:切换语言
当需要切换语言时,可以通过调用`change`方法来切换语言。
@ -201,7 +216,7 @@ import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// VoerkaI18n是一个全局单例可以直接访问
// 或者VoerkaI18n是一个全局单例可以直接访问
await VoerkaI18n.change("en")
```
@ -214,15 +229,19 @@ import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on((newLanguage)=>{
// 在此重新渲染界面
...
})
//
VoerkaI18n.on((newLanguage)=>{
...
// 在此重新渲染界面
...
})
```
[@voerkai18n/vue](../tools/vue.md)和[@voerkai18n/react](../use/react.md)提供了相对应的插件和库来简化重新界面更新渲染。
## 第步:语言包补丁
## 第步:语言包补丁
一般情况下,多语言的工程化过程就结束了,`voerkai18n`在多语言实践考虑得更加人性化。有没有经常发现这样的情况,当项目上线后,才发现:
- 翻译有误
@ -230,10 +249,11 @@ VoerkaI18n.on((newLanguage)=>{
- 临时要增加支持一种语言
一般碰到这种情况,只好重新打包构建工程,重新发布,整个过程繁琐而麻烦。
现在`voerkai18n`针对此问题提供了完美的解决方案,可以通过服务器来为应用打语言包补丁和增加语言支持,而不需要重新打包应用和修改应用。
方法如下:
现在`voerkai18n`针对此问题提供了完美的解决方案,可以通过服务器来为应用`打语言包补丁``动态增加语言`支持,而不需要重新打包应用和修改应用。
1. 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件
**方法如下:**
1. 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件。
```javascript | pure
import { i18nScope } from "./languages"
@ -243,7 +263,7 @@ i18nScope.registerDefaultLoader(async (language,scope)=>{
```
2. 将语言包补丁文件保存在服务器上指定的位置`/languages/<应用名称>/<语言名称>.json`即可。
3. 当应用启动后会自动从服务器上加载语言补丁包,从而实现动为语言包打补丁的功能。也可以实现动态增加临时支持一种语言的功能
3. 当应用启动后会自动从服务器上加载语言补丁包合并,从而实现动为语言包打补丁的功能。也可以实现动态增加临时支持一种语言的功能
更完整的说明详见[`动态加载语言包`](../advanced/remote-load.md)和[`语言包补丁`](../advanced/lngpatch.md)功能介绍。

View File

@ -5,6 +5,8 @@ hero:
actions:
- text: 快速入门
link: https://zhangfisher.github.io/voerka-i18n/guide/intro/get-started
- text: 交流QQ群
link: https://qm.qq.com/cgi-bin/qm/qr?k=jKyZR9KupT9Ith5ZsulB-i04OaJDkCwe&jump_from=webapi
features:
- title: 全流程支持
icon: images/flow.png

View File

@ -5,17 +5,18 @@
const { toDate,toCurrency,toNumber,formatDatetime,formatTime,Formatter } = require("../utils")
// 日期格式化器
// format取字符串"long","short","local","iso","gmt","utc"或者日期模块字符串
// { value | date } == '2022/8/15' 默认
// { value | date('long') } == '2022/8/15 12:08:32'
// { value | date('short') } == '8/15'
// { value | date('GMT') } == 'Mon, 15 Aug 2022 06:39:38 GMT'
// { value | date('ISO') } == 'Mon, 15 Aug 2022 06:39:38 ISO'
// { value | date('YYYY-MM-DD HH:mm:ss') } == '2022-8-15 12:08:32'
/**
* 日期格式化器
* format取值
* 0-local,1-long,2-short,3-iso,4-gmt,5-UTC
* 或者日期模板字符串
* 默认值是local
*/
const dateFormatter = Formatter((value,format,$config)=>{
const optionals = ["long","short","local","iso","gmt","utc"]
// 处理参数:支持大小写和数字0-long,1-short,2-local,3-iso,4-gmt,5-utc
const optionals = ["local","long","short","iso","gmt","utc"]
// 处理参数:同时支持大小写名称和数字
const optionIndex = optionals.findIndex((v,i)=>{
if(typeof(format)=="string"){
return v==format || v== format.toUpperCase()
@ -24,12 +25,12 @@ const dateFormatter = Formatter((value,format,$config)=>{
}
})
switch(optionIndex){
case 0: // long
return formatDatetime(value,$config.long)
case 1: // short
return formatDatetime(value,$config.short)
case 2: // local
case 0: // local
return value.toLocaleString()
case 1: // long
return formatDatetime(value,$config.long)
case 2: // short
return formatDatetime(value,$config.short)
case 3: // ISO
return value.toISOString()
case 4: // GMT
@ -45,7 +46,7 @@ const dateFormatter = Formatter((value,format,$config)=>{
configKey: "datetime.date"
})
// 季度格式化器 format= 0=短格式 1=长格式
const mquarterFormatter = Formatter((value,format,$config)=>{
const quarterFormatter = Formatter((value,format,$config)=>{
const month = value.getMonth() + 1
const quarter = Math.floor( ( month % 3 == 0 ? ( month / 3 ) : (month / 3 + 1 ) ))
if(format<0 && format>1) format = 0
@ -70,7 +71,7 @@ const monthFormatter = Formatter((value,format,$config)=>{
configKey: "datetime.month"
})
// 格式化器 format可以取值0,1,2也可以取字符串long,short,number
// 星期x格式化器 format可以取值0,1,2也可以取字符串long,short,number
const weekdayFormatter = Formatter((value,format,$config)=>{
const day = value.getDay()
if(typeof(format)==='string'){
@ -85,10 +86,9 @@ const weekdayFormatter = Formatter((value,format,$config)=>{
})
// 时间格式化器 format可以取值0,1,2也可以取字符串long,short,timestamp,local
// 时间格式化器 format可以取值0-local(默认),1-long,2-short,3-timestamp,也可以是一个插值表达式
const timeFormatter = Formatter((value,format,$config)=>{
const month = value.getMonth()
const optionals = ['long','short','timestamp','local'] //toLocaleTimeString
const optionals = ['local','long','short','timestamp']
const optionIndex = optionals.findIndex((v,i)=>{
if(typeof(format)=="string"){
return v==format || v== format.toUpperCase()
@ -97,21 +97,21 @@ const timeFormatter = Formatter((value,format,$config)=>{
}
})
switch(optionIndex){
case 0: // long
return formatTime(value,$config.long)
case 1: // short
return formatTime(value,$config.short)
case 2: // timestamp
return value.getTime()
case 3: // local
case 0: // local : toLocaleTimeString
return value.toLocaleTimeString()
case 1: // long
return formatTime(value,$config.long)
case 2: // short
return formatTime(value,$config.short)
case 3: // timestamp
return value.getTime()
default:
return formatTime(value,format)
}
},{
normalize: toDate,
params : ['format'],
configKey: "datetime.month"
configKey: "datetime.time"
})
// 货币格式化器, CNY $13,456.00
@ -132,8 +132,8 @@ module.exports = {
units : ["Year","Quarter","Month","Week","Day","Hour","Minute","Second","Millisecond","Microsecond"],
date :{
long : 'YYYY/MM/DD HH:mm:ss',
short : "MM/DD",
format : "long"
short : "YYYY/MM/DD",
format : "local"
},
quarter : {
names : ["Q1","Q2","Q3","Q4"],
@ -156,11 +156,16 @@ module.exports = {
},
},
currency : {
symbol : "$", // 符号
prefix : "", // 前缀
suffix : "", // 后缀
division : 3, // ,分割位
precision : 2, // 精度
units : ["Thousands","Millions","Billions","Trillions"], //千,百万,十亿,万亿
default : "{symbol}{value}",
long : "{prefix} {symbol}{value}{suffix}",
short : "{symbol}{value}",
symbol : "$", // 符号
prefix : "", // 前缀
suffix : "", // 后缀
division : 3, // ,分割位
precision : 2, // 精度
},
number : {
division : 3,

View File

@ -13,8 +13,8 @@ module.exports = {
units : CN_DATETIME_UNITS,
date :{
long : 'YYYY年MM月DD日 HH点mm分ss秒',
short : "MM/DD",
format : 'long'
short : "YYYY/MM/DD",
format : 'local'
},
quarter : {
names : ["一季度","二季度","三季度","四季度"],
@ -34,15 +34,17 @@ module.exports = {
time:{
long : "HH点mm分ss秒",
short : "HH:mm:ss",
format : 'local'
}
},
currency : {
units : ["万","亿","万亿","万万亿"]
symbol : "¥",
prefix : "",
suffix : "元",
division : 4,
precision : 2
precision : 2
},
number : {
division : 3,

View File

@ -407,22 +407,23 @@ function wrapperFormatters(scope,activeLanguage,formatters){
let wrappedFormatters = []
addDefaultFormatters(formatters)
for(let [name,args] of formatters){
if(name){
let fn = getFormatter(scope,activeLanguage,name)
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
if(!isFunction(fn)){
fn = (value) =>{
if(isFunction(value[name])){
return value[name](...args)
}else{
return value
}
}
}
fn.$name = name
wrappedFormatters.push(fn)
}
let fn = getFormatter(scope,activeLanguage,name)
let formatter
// 格式化器无效或者没有定义时,查看当前值是否具有同名的原型方法,如果有则执行调用
// 比如padStart格式化器是String的原型方法不需要配置就可以直接作为格式化器调用
if(isFunction(fn)){
formatter = (value,config) => fn.call(scope,value,...args,config)
}else{
formatter = (value) =>{
if(isFunction(value[name])){
return value[name](...args)
}else{
return value
}
}
}
formatter.$name = name
wrappedFormatters.push(formatter)
}
return wrappedFormatters
}

View File

@ -77,12 +77,12 @@ function deepMerge(toObj,formObj,options={}){
if(key in results){
if(typeof value === "object" && value !== null){
if(Array.isArray(value)){
if(options.array === 0){//替换
results[key] = value
}else if(options.array === 1){//合并
if(options.array === 1){//合并
results[key] = [...results[key],...value]
}else if(options.array === 2){//去重合并
results[key] = [...new Set([...results[key],...value])]
}else{ //默认: 替换
results[key] = value
}
}else{
results[key] = deepMerge(results[key],value,options)
@ -259,8 +259,6 @@ function formatDatetime(value,templ="YYYY/MM/DD HH:mm:ss"){
["ss", second.padStart(2, "0")], // 00-59 秒,两位数
["s", second], // 0-59 秒
["SSS", millisecond], // 000-999 毫秒,三位数
["SS", millisecond.substring(year.length - 2, year.length)], // 00-99 毫秒(十),两位数
["S",millisecond[millisecond.length - 1]], // 0-9 毫秒(百),一位数
["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写
["a", hour > 12 ? "pm" : "am"], // am / pm 上/下午,小写
]
@ -284,8 +282,6 @@ function formatTime(value,templ="HH:mm:ss"){
["ss", second.padStart(2, "0")], // 00-59 秒,两位数
["s", second], // 0-59 秒
["SSS", millisecond], // 000-999 毫秒,三位数
["SS", millisecond.substring(year.length - 2, year.length)], // 00-99 毫秒(十),两位数
["S",millisecond[millisecond.length - 1]], // 0-9 毫秒(百),一位数
["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写
["a", hour > 12 ? "pm" : "am"] // am / pm 上/下午,小写
]
@ -381,8 +377,8 @@ function replaceAll(str,findValue,replaceValue){
const formatterConfig =Object.assign({},defaultParams,getByPath(activeFormatterConfigs,opts.configKey,{}))
let finalArgs = opts.params.map(param=>getByPath(formatterConfig,param,undefined))
// 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数
for(let i =0; i<finalArgs.length-1;i++){
if(i>=args.length-1) break // 最后一参数是配置
for(let i =0; i<finalArgs.length;i++){
if(i==args.length-1) break // 最后一参数是配置
if(args[i]!==undefined) finalArgs[i] = args[i]
}
return fn(finalValue,...finalArgs,formatterConfig)

View File

@ -1,18 +1,292 @@
const {i18nScope, translate, getInterpolatedVars } = require('../packages/runtime/dist/runtime.cjs')
const dayjs = require('dayjs');
function toLanguageDict(values,startIndex=0){
return values.reduce((result,curValue,i)=>{
result[i+startIndex] = curValue;
return result
},{})
}
function toLanguageIdMap(values,startIndex=0){
return values.reduce((result,curValue,i)=>{
result[curValue] = i+startIndex
return result
},{})
}
function diffArray(arr1,arr2){
let diffs = []
arr1.forEach((v,i)=>{
if(v!=arr2[i]) diffs.push([i,[v,arr2[i]]])
})
return diffs
}
const NOW = new Date("2022/08/12 10:12:36")
const zhDatetimes =[
"现在是{ value }",
"现在是{ value | date }",
"现在是{ value | date('local') }",
"现在是{ value | date('long') }",
"现在是{ value | date('short') }",
"现在是{ value | date('iso') }",
"现在是{ value | date('gmt') }",
"现在是{ value | date('utc') }",
"现在是{ value | date(0) }", // local
"现在是{ value | date(1) }", // long
"现在是{ value | date(2) }", // short
"现在是{ value | date(3) }", // iso
"现在是{ value | date(4) }", // gmt
"现在是{ value | date(5) }", // utc
"现在是{ value | date('YYYY-MM-DD HH:mm:ss')}",
"现在是{ value | date('YYYY-MM-DD')}",
"现在是{ value | date('HH:mm:ss')}",
"现在是{ value | month }",
"现在是{ value | month('long')}",
"现在是{ value | month('short')}",
"现在是{ value | month('number')}",
"现在是{ value | month(0)}",
"现在是{ value | month(1)}",
"现在是{ value | month(2)}",
"现在是{ value | weekday }",
"现在是{ value | weekday('long')}",
"现在是{ value | weekday('short')}",
"现在是{ value | weekday('number')}",
"现在是{ value | weekday(0)}",
"现在是{ value | weekday(1)}",
"现在是{ value | weekday(2)}",
// 时间
"现在时间 - { value | time }",
"现在时间 - { value | time('local') }",
"现在时间 - { value | time('long') }",
"现在时间 - { value | time('short') }",
"现在时间 - { value | time('timestamp') }",
"现在时间 - { value | time(0) }",
"现在时间 - { value | time(1) }",
"现在时间 - { value | time(2) }",
"现在时间 - { value | time(3) }",
"现在时间 - { value | time('HH:mm:ss') }",
"现在时间 - { value | time('mm:ss') }",
"现在时间 - { value | time('ss') }"
]
//
const expectZhDatetimes =[
"现在是2022/8/12 10:12:36", // { value }
"现在是2022/8/12 10:12:36", // { value | date }
`现在是${NOW.toLocaleString()}`, // { value | date('local') }
"现在是2022年08月12日 10点12分36秒", // { value | date('long') }
"现在是2022/08/12", // { value | date('short') }
`现在是${NOW.toISOString()}`, // { value | date('iso') }
`现在是${NOW.toGMTString()}`, // { value | date('gmt') }
`现在是${NOW.toUTCString()}`, // { value | date('utc') }
`现在是${NOW.toLocaleString()}`, // { value | date(0) } // local
"现在是2022年08月12日 10点12分36秒", // { value | date(1) } // long
"现在是2022/08/12", // { value | date(2) } // short
`现在是${NOW.toISOString()}`, // { value | date(3) } // iso
`现在是${NOW.toGMTString()}`, // { value | date(4) } // gmt
`现在是${NOW.toUTCString()}`, // { value | date(5) } // utc
"现在是2022-08-12 10:12:36", // { value | date('YYYY-MM-DD HH:mm:ss')}
"现在是2022-08-12", // { value | date('YYYY-MM-DD')}
"现在是10:12:36", // { value | date('HH:mm:ss')}
"现在是八月", // { value | month }
"现在是八月", // { value | month('long')}
"现在是八", // { value | month('short')}
"现在是8", // { value | month('number')}
"现在是八月", // { value | month(0)}
"现在是八", // { value | month(1)}
"现在是8", // { value | month(2)}
"现在是星期五", // { value | weekday }
"现在是星期五", // { value | weekday('long')}
"现在是五", // { value | weekday('short')}
"现在是5", // { value | weekday('number')}
"现在是星期五", // { value | weekday(0)}
"现在是五", // { value | weekday(1)}
"现在是5", // { value | weekday(2)}
// 时间
`现在时间 - ${NOW.toLocaleTimeString()}`, // { value | time }
`现在时间 - ${NOW.toLocaleTimeString()}`, // { value | time('local') }
"现在时间 - 10点12分36秒", // { value | time('long') }
"现在时间 - 10:12:36", // { value | time('short') }
"现在时间 - 1660270356000", // { value | time('timestamp') }
`现在时间 - ${NOW.toLocaleTimeString()}`, // { value | time(0) }
"现在时间 - 10点12分36秒", // { value | time(1) }
"现在时间 - 10:12:36", // { value | time(2) }
"现在时间 - 1660270356000", // { value | time(3) }
"现在时间 - 10:12:36", // { value | time('HH:mm:ss') }
"现在时间 - 12:36", // { value | time('mm:ss') }
"现在时间 - 36", // { value | time('ss') }"
]
const enDatetimes =[
"Now is { value }",
"Now is { value | date }",
"Now is { value | date('local') }",
"Now is { value | date('long') }",
"Now is { value | date('short') }",
"Now is { value | date('iso') }",
"Now is { value | date('gmt') }",
"Now is { value | date('utc') }",
"Now is { value | date(0) }", // local
"Now is { value | date(1) }", // long
"Now is { value | date(2) }", // short
"Now is { value | date(3) }", // iso
"Now is { value | date(4) }", // gmt
"Now is { value | date(5) }", // utc
"Now is { value | date('YYYY-MM-DD HH:mm:ss')}",
"Now is { value | date('YYYY-MM-DD')}",
"Now is { value | date('HH:mm:ss')}",
"Now is { value | month }",
"Now is { value | month('long')}",
"Now is { value | month('short')}",
"Now is { value | month('number')}",
"Now is { value | month(0)}",
"Now is { value | month(1)}",
"Now is { value | month(2)}",
"Now is { value | weekday }",
"Now is { value | weekday('long')}",
"Now is { value | weekday('short')}",
"Now is { value | weekday('number')}",
"Now is { value | weekday(0)}",
"Now is { value | weekday(1)}",
"Now is { value | weekday(2)}",
// 时间
"Now time: { value | time }",
"Now time: { value | time('local') }",
"Now time: { value | time('long') }",
"Now time: { value | time('short') }",
"Now time: { value | time('timestamp') }",
"Now time: { value | time(0) }",
"Now time: { value | time(1) }",
"Now time: { value | time(2) }",
"Now time: { value | time(3) }",
"Now time: { value | time('HH:mm:ss') }",
"Now time: { value | time('mm:ss') }",
"Now time: { value | time('ss') }"
]
const expectEnDatetimes =[
"Now is 2022/8/12 10:12:36", // { value }
"Now is 2022/8/12 10:12:36", // { value | date }
`Now is ${NOW.toLocaleString()}`, // { value | date('local') }
"Now is 2022/08/12 10:12:36", // { value | date('long') }
"Now is 2022/08/12", // { value | date('short') }
`Now is ${NOW.toISOString()}`, // { value | date('iso') }
`Now is ${NOW.toGMTString()}`, // { value | date('gmt') }
`Now is ${NOW.toUTCString()}`, // { value | date('utc') }
`Now is ${NOW.toLocaleString()}`, // { value | date(0) } // local
"Now is 2022/08/12 10:12:36", // { value | date(1) } // long
"Now is 2022/08/12", // { value | date(2) } // short
`Now is ${NOW.toISOString()}`, // { value | date(3) } // iso
`Now is ${NOW.toGMTString()}`, // { value | date(4) } // gmt
`Now is ${NOW.toUTCString()}`, // { value | date(5) } // utc
"Now is 2022-08-12 10:12:36", // { value | date('YYYY-MM-DD HH:mm:ss')}
"Now is 2022-08-12", // { value | date('YYYY-MM-DD')}
"Now is 10:12:36", // { value | date('HH:mm:ss')}
"Now is August", // { value | month }
"Now is August", // { value | month('long')}
"Now is Aug", // { value | month('short')}
"Now is 8", // { value | month('number')}
"Now is August", // { value | month(0)}
"Now is Aug", // { value | month(1)}
"Now is 8", // { value | month(2)}
"Now is Friday", // { value | weekday }
"Now is Friday", // { value | weekday('long')}
"Now is Fri", // { value | weekday('short')}
"Now is 5", // { value | weekday('number')}
"Now is Friday", // { value | weekday(0)}
"Now is Fri", // { value | weekday(1)}
"Now is 5", // { value | weekday(2)}
// 时间
`Now time: ${NOW.toLocaleTimeString()}`, // { value | time }
`Now time: ${NOW.toLocaleTimeString()}`, // { value | time('local') }
"Now time: 10:12:36", // { value | time('long') }
"Now time: 10:12:36", // { value | time('short') }
"Now time: 1660270356000", // { value | time('timestamp') }
`Now time: ${NOW.toLocaleTimeString()}`, // { value | time(0) }
"Now time: 10:12:36", // { value | time(1) }
"Now time: 10:12:36", // { value | time(2) }
"Now time: 1660270356000", // { value | time(3) }
"Now time: 10:12:36", // { value | time('HH:mm:ss') }
"Now time: 12:36", // { value | time('mm:ss') }
"Now time: 36", // { value | time('ss') }"
]
const MONEY = 123456789.8848
const zhMoneys = [
"商品价格: { value | currency }", // 默认格式,由语言配置指定,不同的语言不一样
"商品价格: { value | currency('long')}", // 长格式
"商品价格: { value | currency('short')}", // 短格式 == short(0)
"商品价格: { value | currency('long',1)}", // 长格式: 万元
"商品价格: { value | currency('long',2)}", // 长格式: 亿
"商品价格: { value | currency('long',3)}", // 长格式: 万亿
"商品价格: { value | currency('long',4)}", // 长格式: 万万亿
"商品价格: { value | currency('short')}", // 短格式
"商品价格: { value | currency('short',1)}", // 短格式
"商品价格: { value | currency('short',2)}", // 短格式
"商品价格: { value | currency('short',3)}", // 短格式
"商品价格: { value | currency('short',4)}", // 短格式
"商品价格: { value | currency('short',5)}", // 短格式
"商品价格: { value | currency({symbol,prefix ,suffix, division,precision,unit})}", // 自定义货币格式
"商品价格: { value | currency('¥')}", // 指定货币符号
"商品价格: { value | currency('¥','CNY')}", // 指定货币符号+前缀
"商品价格: { value | currency('¥','CNY','元')}", // 指定货币符号+前缀+后缀
"商品价格: { value | currency('¥','CNY','元',3)}", // 指定货币符号+前缀+后缀+分割位
"商品价格: { value | currency('¥','CNY','元',3,3)}", // 指定货币符号+前缀+后缀+分割位+精度
]
const expectZhMoneys =[
"商品价格: ¥1,2345,6789.88", // { value | currency }
"商品价格: ¥1,2345,6789.88元", // { value | currency('long')}
"商品价格: ¥1,2345,6789.88", // { value | currency('short')}
"商品价格: ¥1,2345,6789.88", // { value | currency('¥')}
"商品价格: CNY¥1,2345,6789.88", // { value | currency('¥','CNY')}
"商品价格: CNY¥1,2345,6789.88元", // { value | currency('¥','CNY','元')}
"商品价格: CNY¥123,456,789.885元", // { value | currency('¥','CNY','元',3)}
"商品价格: CNY¥123,456,789.885元", //{ value | currency('¥','CNY','元',3,3)}
]
const enMoneys = [
"Price: { value | currency }", // 默认格式,由语言配置指定,不同的语言不一样
"Price: { value | currency('long')}", // 长格式
"Price: { value | currency('short')}", // 短格式
"Price: { value | currency('$')}", // 指定货币符号
"Price: { value | currency('$','USD')}", // 指定货币符号+前缀
"Price: { value | currency('$','USD','')}", // 指定货币符号+前缀+后缀
"Price: { value | currency('$','USD','',3)}", // 指定货币符号+前缀+后缀+分割位
"Price: { value | currency('$','USD','',3,3)}", // 指定货币符号+前缀+后缀+分割位+精度
]
const expectEnMoneys =[
"Price: $123,456,789.88", // { value | currency }
"Price: $123,456,789.88", // { value | currency('long')}
"Price: $123,456,789.88", // { value | currency('short')}
"Price: $123,456,789.88", // { value | currency('$')}
"Price: USD$1,2345,6789.88", // { value | currency('$','USD')}
"Price: USD$1,2345,6789.88", // { value | currency('$','USD','元')}
"Price: USD$123,456,789.885", // { value | currency('$','USD','元',3)}
"Price: USD$123,456,789.885", //{ value | currency('$','USD','元',3,3)}
]
const loaders = {
zh:{
1:"你好",
2:"现在是{}",
2:"现在是{ value | }",
3:"我出生于{year}年,今年{age}岁",
4:"我有{}个朋友",
...toLanguageDict(zhDatetimes,5),
},
en :{
1:"hello",
2:"Now is {}",
3:"I was born in {year}, now is {age} years old",
4:["I have no friends","I have one friends","I have two friends","I have {} friends"]
4:["I have no friends","I have one friends","I have two friends","I have {} friends"],
...toLanguageDict(enDatetimes,5),
}
}
@ -32,9 +306,10 @@ const formatters = {
const idMap = {
"你好":1,
"现在是{}":2,
"现在是{ value | }":2,
"我出生于{year}年,今年{age}岁":3,
"我有{}个朋友":4
"我有{}个朋友":4,
...toLanguageIdMap(zhDatetimes,5)
}
const languages = [
{ name: "zh", title: "中文" },
@ -173,7 +448,7 @@ test("切换到其他语言时的自动匹配同名格式化器",async ()=>{
test("位置插值翻译文本内容",async ()=>{
const now = new Date()
expect(t("你好")).toBe("你好");
expect(t("现在是{}",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
// 经babel自动码换后文本内容会根据idMap自动转为id
expect(t("1")).toBe("你好");
@ -182,7 +457,7 @@ test("位置插值翻译文本内容",async ()=>{
await scope.change("en")
expect(t("你好")).toBe("hello");
expect(t("现在是{}",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`);
expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`);
expect(t("1")).toBe("hello");
expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`);
})
@ -190,11 +465,11 @@ test("位置插值翻译文本内容",async ()=>{
test("命名插值翻译文本内容",async ()=>{
const now = new Date()
expect(t("你好")).toBe("你好");
expect(t("现在是{}",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
expect(t("现在是{ value | }",now)).toBe(`现在是${dayjs(now).format('YYYY年MM月DD日 HH点mm分ss秒')}`);
await scope.change("en")
expect(t("你好")).toBe("hello");
expect(t("现在是{}",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`);
expect(t("现在是{ value | }",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`);
expect(t("1")).toBe("hello");
expect(t("2",now)).toBe(`Now is ${dayjs(now).format('YYYY/MM/DD HH:mm:ss')}`);
})
@ -222,4 +497,22 @@ test("翻译复数支持",async ()=>{
expect(t("我有{}个朋友",3)).toBe("I have 3 friends");
expect(t("我有{}个朋友",4)).toBe("I have 4 friends");
})
test("日期时间格式化器",async ()=>{
let zhTranslatedResults = zhDatetimes.map(v=>t(v,NOW))
expect(zhTranslatedResults).toStrictEqual(expectZhDatetimes)
await scope.change("en")
let enTranslatedResults = zhDatetimes.map(v=>t(v,NOW))
expect(enTranslatedResults).toStrictEqual(expectEnDatetimes)
})
test("货币格式化器",async ()=>{
let zhTranslatedResults = zhDatetimes.map(v=>t(v,NOW))
expect(zhTranslatedResults).toStrictEqual(expectZhDatetimes)
await scope.change("en")
let enTranslatedResults = zhDatetimes.map(v=>t(v,NOW))
expect(enTranslatedResults).toStrictEqual(expectEnDatetimes)
})