diff --git a/.umirc.ts b/.umirc.ts index 856bf1a..1197aa0 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -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" ], diff --git a/docs/src/guide/advanced/remoteLoad.md b/docs/src/guide/advanced/dynamic-add.md similarity index 82% rename from docs/src/guide/advanced/remoteLoad.md rename to docs/src/guide/advanced/dynamic-add.md index 8a5e036..0927392 100644 --- a/docs/src/guide/advanced/remoteLoad.md +++ b/docs/src/guide/advanced/dynamic-add.md @@ -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 +}) + +``` + + diff --git a/docs/src/guide/advanced/lngpatch.md b/docs/src/guide/advanced/lngpatch.md index 959a3b6..a4252fa 100644 --- a/docs/src/guide/advanced/lngpatch.md +++ b/docs/src/guide/advanced/lngpatch.md @@ -5,4 +5,9 @@ **基本思路是,应用上线后发现翻译错误时,可以在服务器上约定位置放置语言包补丁,应用会自动进行更新修复,很实用的一个特性。** +### 关于语言包补丁 +语言包补丁仅对在`settings.json`配置的语言起作用,而动态增加的语种因为其语言包本身就保存在服务器,因此就不存在补丁的问题。 +语言包补丁会在加载时自动合并到源码中的语言包,并且会自动在本地`localStorage`中缓存。 + + 使用方法详见`动态加载语言包`介绍。 diff --git a/docs/src/guide/intro/get-started.md b/docs/src/guide/intro/get-started.md index 7aa991b..7571308 100644 --- a/docs/src/guide/intro/get-started.md +++ b/docs/src/guide/intro/get-started.md @@ -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 "/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)功能介绍。 diff --git a/docs/src/index.md b/docs/src/index.md index 20d9687..e312e2f 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -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 diff --git a/packages/runtime/formatters/en.js b/packages/runtime/formatters/en.js index 1832e4a..6a76993 100644 --- a/packages/runtime/formatters/en.js +++ b/packages/runtime/formatters/en.js @@ -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, diff --git a/packages/runtime/formatters/zh.js b/packages/runtime/formatters/zh.js index 377129e..ef2aa2f 100644 --- a/packages/runtime/formatters/zh.js +++ b/packages/runtime/formatters/zh.js @@ -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, diff --git a/packages/runtime/index.js b/packages/runtime/index.js index b4633c0..6d8059b 100644 --- a/packages/runtime/index.js +++ b/packages/runtime/index.js @@ -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 } diff --git a/packages/runtime/utils.js b/packages/runtime/utils.js index 3cfdfda..9388609 100644 --- a/packages/runtime/utils.js +++ b/packages/runtime/utils.js @@ -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=args.length-1) break // 最后一参数是配置 + for(let i =0; 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"); }) - \ No newline at end of file +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) + })