From 25f548535b3d7b5023ea6754651abe672bb6ce75 Mon Sep 17 00:00:00 2001 From: wxzhang Date: Wed, 2 Mar 2022 18:01:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8F=92=E5=80=BC=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demoapps/app/languages/cn.js | 2 +- demoapps/app/languages/de.js | 2 +- demoapps/app/languages/en.js | 2 +- .../app/languages/{messageIds.js => idMap.js} | 2 +- demoapps/app/languages/index.js | 29 +- demoapps/app/languages/jp.js | 2 +- pnpm-lock.yaml | 222 +++++------ readme.md | 3 + src/compile.js | 44 ++- src/formatters.js | 11 +- src/index.js | 368 ++++++++++++++---- src/{entry.template.js => templates/entry.js} | 28 +- src/templates/formatters.js | 83 ++++ test/compile.test.js | 104 ++++- 14 files changed, 653 insertions(+), 249 deletions(-) rename demoapps/app/languages/{messageIds.js => idMap.js} (95%) rename src/{entry.template.js => templates/entry.js} (56%) create mode 100644 src/templates/formatters.js diff --git a/demoapps/app/languages/cn.js b/demoapps/app/languages/cn.js index da50c6f..8d413eb 100644 --- a/demoapps/app/languages/cn.js +++ b/demoapps/app/languages/cn.js @@ -1,4 +1,4 @@ -export default { +module.exports = { "1": "a1:aaaaa", "2": "no aaaaa", "3": "bbbbb", diff --git a/demoapps/app/languages/de.js b/demoapps/app/languages/de.js index fa78e24..f4ec482 100644 --- a/demoapps/app/languages/de.js +++ b/demoapps/app/languages/de.js @@ -1,4 +1,4 @@ -export default { +module.exports = { "1": "德文:aaaaa", "2": "no aaaaa", "3": "bbbbb", diff --git a/demoapps/app/languages/en.js b/demoapps/app/languages/en.js index da50c6f..8d413eb 100644 --- a/demoapps/app/languages/en.js +++ b/demoapps/app/languages/en.js @@ -1,4 +1,4 @@ -export default { +module.exports = { "1": "a1:aaaaa", "2": "no aaaaa", "3": "bbbbb", diff --git a/demoapps/app/languages/messageIds.js b/demoapps/app/languages/idMap.js similarity index 95% rename from demoapps/app/languages/messageIds.js rename to demoapps/app/languages/idMap.js index d139cfc..1ab9b16 100644 --- a/demoapps/app/languages/messageIds.js +++ b/demoapps/app/languages/idMap.js @@ -1,4 +1,4 @@ -export default { +module.exports = { "a1:aaaaa": 1, "no aaaaa": 2, "bbbbb": 3, diff --git a/demoapps/app/languages/index.js b/demoapps/app/languages/index.js index 487d306..11bc444 100644 --- a/demoapps/app/languages/index.js +++ b/demoapps/app/languages/index.js @@ -1,8 +1,10 @@ -import messageIds from "./messageIds" -import { translate,i18n } from "voerka-i18n" -import defaultMessages from "./cn.js" -import i18nSettings from "./settings.js" -import formatters from "voerka-i18n/formatters" + +const messageIds = require("./idMap") +const { translate,i18n } = require("voerka-i18n") +const defaultMessages = require("./cn.js") +const i18nSettings = require("./settings.js") +const formatters = require("../formatters") + // 自动创建全局VoerkaI18n实例 if(!globalThis.VoerkaI18n){ @@ -13,13 +15,10 @@ let scope = { defaultLanguage: "cn", // 默认语言名称 default: defaultMessages, // 默认语言包 messages : defaultMessages, // 当前语言包 - ids:messageIds, // 消息id映射列表 - formatters:{ - ...formatters, - ...i18nSettings.formatters || {} - }, - loaders:{}, // 异步加载语言文件的函数列表 - settings:{} // 引用全局VoerkaI18n实例的配置 + idMap:messageIds, // 消息id映射列表 + formatters, // 当前作用域的格式化函数列表 + loaders:{}, // 异步加载语言文件的函数列表 + global:{} // 引用全局VoerkaI18n配置 } let supportedlanguages = {} @@ -38,5 +37,7 @@ const t = ()=> translate.bind(scope)(...arguments) // 注册当前作用域到全局VoerkaI18n实例 VoerkaI18n.register(scope) -export scope -export t \ No newline at end of file + +module.exports.scope = scope +module.exports.t = t + diff --git a/demoapps/app/languages/jp.js b/demoapps/app/languages/jp.js index da50c6f..8d413eb 100644 --- a/demoapps/app/languages/jp.js +++ b/demoapps/app/languages/jp.js @@ -1,4 +1,4 @@ -export default { +module.exports = { "1": "a1:aaaaa", "2": "no aaaaa", "3": "bbbbb", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e39328a..81d10d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,8 +56,8 @@ packages: slash: 2.0.0 source-map: 0.5.7 optionalDependencies: - '@nicolo-ribaudo/chokidar-2': registry.npmmirror.com/@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3 - chokidar: registry.npmmirror.com/chokidar/3.5.3 + '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 + chokidar: 3.5.3 dev: true /@babel/code-frame/7.16.7: @@ -531,7 +531,7 @@ packages: dependencies: callsites: 3.1.0 graceful-fs: 4.2.9 - source-map: registry.npmmirror.com/source-map/0.6.1 + source-map: 0.6.1 dev: true /@jest/test-result/27.5.1: @@ -606,6 +606,12 @@ packages: '@jridgewell/sourcemap-codec': 1.4.11 dev: true + /@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3: + resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} + requiresBuild: true + dev: true + optional: true + /@sinonjs/commons/1.8.3: resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==} dependencies: @@ -1059,6 +1065,14 @@ packages: dev: true optional: true + /bindings/1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true + dependencies: + file-uri-to-path: 1.0.0 + dev: false + optional: true + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1193,6 +1207,42 @@ packages: engines: {node: '>=10'} dev: true + /chokidar/2.1.8: + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} + deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies + dependencies: + anymatch: 2.0.0 + async-each: 1.0.3 + braces: 2.3.2 + glob-parent: 3.1.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 4.0.3 + normalize-path: 3.0.0 + path-is-absolute: 1.0.1 + readdirp: 2.2.1 + upath: 1.2.0 + optionalDependencies: + fsevents: 1.2.13 + dev: false + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + requiresBuild: true + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + optional: true + /ci-info/3.3.0: resolution: {integrity: sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==} dev: true @@ -1614,7 +1664,7 @@ packages: esutils: 2.0.3 optionator: 0.8.3 optionalDependencies: - source-map: registry.npmmirror.com/source-map/0.6.1 + source-map: 0.6.1 dev: false /escodegen/2.0.0: @@ -1627,7 +1677,7 @@ packages: esutils: 2.0.3 optionator: 0.8.3 optionalDependencies: - source-map: registry.npmmirror.com/source-map/0.6.1 + source-map: 0.6.1 dev: true /esprima/4.0.1: @@ -1765,6 +1815,12 @@ packages: bser: 2.1.1 dev: true + /file-uri-to-path/1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true + dev: false + optional: true + /fill-range/4.0.0: resolution: {integrity: sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=} engines: {node: '>=0.10.0'} @@ -1884,6 +1940,26 @@ packages: /fs.realpath/1.0.0: resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} + /fsevents/1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2. + requiresBuild: true + dependencies: + bindings: 1.5.0 + nan: 2.15.0 + dev: false + optional: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} @@ -1961,7 +2037,7 @@ packages: dependencies: anymatch: 2.0.0 async-done: 1.3.2 - chokidar: registry.npmmirror.com/chokidar/2.1.8 + chokidar: 2.1.8 is-negated-glob: 1.0.0 just-debounce: 1.1.0 normalize-path: 3.0.0 @@ -2477,7 +2553,7 @@ packages: dependencies: debug: 4.3.3 istanbul-lib-coverage: 3.2.0 - source-map: registry.npmmirror.com/source-map/0.6.1 + source-map: 0.6.1 transitivePeerDependencies: - supports-color dev: true @@ -2676,7 +2752,7 @@ packages: micromatch: 4.0.4 walker: 1.0.8 optionalDependencies: - fsevents: registry.npmmirror.com/fsevents/2.3.2 + fsevents: 2.3.2 dev: true /jest-jasmine2/27.5.1: @@ -3304,6 +3380,12 @@ packages: engines: {node: '>= 0.10'} dev: false + /nan/2.15.0: + resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} + requiresBuild: true + dev: false + optional: true + /nanomatch/1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -4009,7 +4091,7 @@ packages: define-property: 0.2.5 extend-shallow: 2.0.1 map-cache: 0.2.2 - source-map: registry.npmmirror.com/source-map/0.5.7 + source-map: 0.5.7 source-map-resolve: 0.5.3 use: 3.1.1 dev: false @@ -4029,7 +4111,7 @@ packages: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: buffer-from: 1.1.2 - source-map: registry.npmmirror.com/source-map/0.6.1 + source-map: 0.6.1 dev: true /source-map-url/0.4.1: @@ -4045,6 +4127,11 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + /source-map/0.7.3: + resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} + engines: {node: '>= 8'} + dev: true + /sparkles/1.0.1: resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} engines: {node: '>= 0.10'} @@ -4466,7 +4553,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.8.0 - source-map: registry.npmmirror.com/source-map/0.7.3 + source-map: 0.7.3 dev: true /v8flags/3.2.0: @@ -4727,70 +4814,12 @@ packages: regenerator-runtime: registry.npmmirror.com/regenerator-runtime/0.13.9 dev: false - registry.npmmirror.com/@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3: - resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz} - name: '@nicolo-ribaudo/chokidar-2' - version: 2.1.8-no-fsevents.3 - requiresBuild: true - dev: true - optional: true - registry.npmmirror.com/ansicolor/1.1.100: resolution: {integrity: sha512-Jl0pxRfa9WaQVUX57AB8/V2my6FJxrOR1Pp2qqFbig20QB4HzUoQ48THTKAgHlUCJeQm/s2WoOPcoIDhyCL/kw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ansicolor/-/ansicolor-1.1.100.tgz} name: ansicolor version: 1.1.100 dev: false - registry.npmmirror.com/bindings/1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz} - name: bindings - version: 1.5.0 - requiresBuild: true - dependencies: - file-uri-to-path: registry.npmmirror.com/file-uri-to-path/1.0.0 - dev: false - optional: true - - registry.npmmirror.com/chokidar/2.1.8: - resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/chokidar/-/chokidar-2.1.8.tgz} - name: chokidar - version: 2.1.8 - deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies - dependencies: - anymatch: 2.0.0 - async-each: 1.0.3 - braces: 2.3.2 - glob-parent: 3.1.0 - inherits: 2.0.4 - is-binary-path: 1.0.1 - is-glob: 4.0.3 - normalize-path: 3.0.0 - path-is-absolute: 1.0.1 - readdirp: 2.2.1 - upath: 1.2.0 - optionalDependencies: - fsevents: registry.npmmirror.com/fsevents/1.2.13 - dev: false - - registry.npmmirror.com/chokidar/3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz} - name: chokidar - version: 3.5.3 - engines: {node: '>= 8.10.0'} - requiresBuild: true - dependencies: - anymatch: 3.1.2 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: registry.npmmirror.com/fsevents/2.3.2 - dev: true - optional: true - registry.npmmirror.com/core-js-pure/3.21.1: resolution: {integrity: sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.21.1.tgz} name: core-js-pure @@ -4818,52 +4847,12 @@ packages: engines: {node: '>=0.10.0'} dev: false - registry.npmmirror.com/file-uri-to-path/1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz} - name: file-uri-to-path - version: 1.0.0 - requiresBuild: true - dev: false - optional: true - - registry.npmmirror.com/fsevents/1.2.13: - resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-1.2.13.tgz} - name: fsevents - version: 1.2.13 - engines: {node: '>= 4.0'} - os: [darwin] - deprecated: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2. - requiresBuild: true - dependencies: - bindings: registry.npmmirror.com/bindings/1.5.0 - nan: registry.npmmirror.com/nan/2.15.0 - dev: false - optional: true - - registry.npmmirror.com/fsevents/2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz} - name: fsevents - version: 2.3.2 - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - registry.npmmirror.com/jju/1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jju/-/jju-1.4.0.tgz} name: jju version: 1.4.0 dev: false - registry.npmmirror.com/nan/2.15.0: - resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nan/-/nan-2.15.0.tgz} - name: nan - version: 2.15.0 - requiresBuild: true - dev: false - optional: true - registry.npmmirror.com/readjson/2.2.2: resolution: {integrity: sha512-PdeC9tsmLWBiL8vMhJvocq+OezQ3HhsH2HrN7YkhfYcTjQSa/iraB15A7Qvt7Xpr0Yd2rDNt6GbFwVQDg3HcAw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readjson/-/readjson-2.2.2.tgz} name: readjson @@ -4880,25 +4869,12 @@ packages: version: 0.13.9 dev: false - registry.npmmirror.com/source-map/0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz} - name: source-map - version: 0.5.7 - engines: {node: '>=0.10.0'} - dev: false - registry.npmmirror.com/source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz} name: source-map version: 0.6.1 engines: {node: '>=0.10.0'} - - registry.npmmirror.com/source-map/0.7.3: - resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/source-map/-/source-map-0.7.3.tgz} - name: source-map - version: 0.7.3 - engines: {node: '>= 8'} - dev: true + dev: false registry.npmmirror.com/try-catch/3.0.0: resolution: {integrity: sha512-3uAqUnoemzca1ENvZ72EVimR+E8lqBbzwZ9v4CEbLjkaV3Q+FtdmPUt7jRtoSoTiYjyIMxEkf6YgUpe/voJ1ng==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/try-catch/-/try-catch-3.0.0.tgz} diff --git a/readme.md b/readme.md index bf79b9a..a97eb98 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,10 @@ t("中华人民共和国") // 位置插值变量 t("中华人民共和国{}","万岁") t("中华人民共和国成立于{}年,首都{}",1949,"北京") +// 当仅有两个参数且第2个参数是[]类型时,自动展开第一个参数进行位置插值 +t("中华人民共和国成立于{year}年,首都{capital}",[1949,"北京"]) +、 // 当仅有两个参数且第2个参数是{}类型时,启用字典插值变量 t("中华人民共和国成立于{year}年,首都{capital}",{year:1949,capital:"北京"}) diff --git a/src/compile.js b/src/compile.js index f4af80f..d3d2f29 100644 --- a/src/compile.js +++ b/src/compile.js @@ -49,18 +49,22 @@ function normalizeCompileOptions(opts={}) { let options = Object.assign({ input:null, // 指定要编译的文件夹,即extract输出的语言文件夹 output:null, // 指定编译后的语言文件夹,如果没有指定,则使用input目录 - formatters:{}, // 对插值变量进行格式化的函数列表 + moduleType:"esm" // 指定编译后的语言文件的模块类型,取值common,cjs,esm,es }, opts) + if(options.moduleType==="es") options.moduleType = "esm" + if(options.moduleType==="cjs") options.moduleType = "common" + if(["common","cjs","esm","es"].includes(options.moduleType)) options.moduleType = "esm" return opts; } module.exports = function compile(langFolder,opts={}){ - let options = normalizeCompileOptions(opts); - let { output } = options; + const options = normalizeCompileOptions(opts); + const { output,moduleType } = options; //1. 加载多语言配置文件 import(`file:///${path.join(langFolder,"settings.js")}`).then(module=>{ - let { languages,defaultLanguage,activeLanguage,namespaces } = module.default; + const langSettings = module.default; + let { languages,defaultLanguage,activeLanguage,namespaces } = langSettings // 1. 合并生成最终的语言文件 let messages = {} ,msgId =1 @@ -90,19 +94,33 @@ module.exports = function compile(langFolder,opts={}){ Object.entries(messages).forEach(([message,translatedMsgs])=>{ langMessages[translatedMsgs.$id] = lang.name in translatedMsgs ? translatedMsgs[lang.name] : message }) + const langFile = path.join(langFolder,`${lang.name}.js`) // 为每一种语言生成一个语言文件 - fs.writeFileSync(path.join(langFolder,`${lang.name}.js`),`export default ${JSON.stringify(langMessages,null,4)}`) + if(moduleType==="esm"){ + fs.writeFileSync(langFile,`export default ${JSON.stringify(langMessages,null,4)}`) + }else{ + fs.writeFileSync(langFile,`module.exports = ${JSON.stringify(langMessages,null,4)}`) + } }) // 4. 生成id映射文件 - fs.writeFileSync(path.join(langFolder,"messageIds.js"),`export default ${JSON.stringify(messageIds,null,4)}`) - - const hasFormatters = fs.existsSync(path.join(langFolder,"formatters.js")) - // 生成编译后的访问入口文件 - const entryContent = artTemplate(path.join(__dirname,"entry.template.js"), {languages,defaultLanguage,activeLanguage,namespaces } ) - fs.writeFileSync(path.join(langFolder,"index.js"),entryContent) + const idMapFile = path.join(langFolder,"idMap.js") + if(moduleType==="esm"){ + fs.writeFileSync(idMapFile,`export default ${JSON.stringify(messageIds,null,4)}`) + }else{ + fs.writeFileSync(idMapFile,`module.exports = ${JSON.stringify(messageIds,null,4)}`) + } + // 5. 生成编译后的访问入口文件 + const entryFile = path.join(langFolder,"index.js") + const entryContent = artTemplate(path.join(__dirname,"templates","entry.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } ) + fs.writeFileSync(entryFile,entryContent) + + // 6 . 生成编译后的格式化函数文件 + const formattersFile = path.join(langFolder,"formatters.js") + if(!fs.existsSync(formattersFile)){ + const formattersContent = artTemplate(path.join(__dirname,"templates","formatters.js"), {languages,defaultLanguage,activeLanguage,namespaces,moduleType } ) + fs.writeFileSync(formattersFile,formattersContent) + } }) - - } \ No newline at end of file diff --git a/src/formatters.js b/src/formatters.js index 7c66104..192bdb0 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -3,13 +3,13 @@ * * 使用方法: * - * 在translates/xxx.json文件中进行翻译时,可以对插值变量进行格式化, - * * { - * "Now is {date}":{ - * "zh-CN":"现在是{date|time}" + * "My birthday is {date}":{ + * "zh-CN":"我的生日是{date|time}" * } * } + * + * t("My birthday is {date}",new Date(1975,11,25)) * * */ @@ -17,6 +17,9 @@ const dayjs = require("dayjs"); module.exports = { cn:{ + "*":{ // 适用于所有类型的格式化器 + default:null // 默认格式化器 + }, Date:{ default:(value)=>dayjs(value).format("YYYY年MM年DD日"), // 默认的格式化器 time:(value)=>dayjs(value).format("HH:mm:ss"), diff --git a/src/index.js b/src/index.js index 95271d2..feb8b12 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,15 @@ const formatters = require("./formatters") // 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter } -// let varRegexp = /\{\s*(?\w*\.?\w*)\s*\}/g -let varWidthPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w+\s*)*)\s*\}/g +// 不支持参数: let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*\s*)*)\s*\}/g + +// 支持参数: { var | formatter(x,x,..) | formatter } +let varWithPipeRegexp = /\{\s*(?\w+)?(?(\s*\|\s*\w*(\(.*\)){0,1}\s*)*)\s*\}/g + + + // 插值变量字符串替换正则 + //let varReplaceRegexp =String.raw`\{\s*(?{name}\.?\w*)\s*\}` @@ -42,27 +48,87 @@ function getDataTypeName(v){ return v.constructor && v.constructor.name; }; + +/** + 通过正则表达式对原始文本内容进行解析匹配后得到的 + formatters="| aaa(1,1) | bbb " + + 需要统一解析为 + + [ + [aaa,[1,1]], // [formatter'name,[args,...]] + [bbb,[]], + ] + + formatters="| aaa(1,1,"dddd") | bbb " + + 目前对参数采用简单的split(",")来解析,因为无法正确解析aaa(1,1,"dd,,dd")形式的参数 + 在此场景下基本够用了,如果需要支持更复杂的参数解析,可以后续考虑使用正则表达式来解析 + + @returns [,[,[,,...]]] + */ +function parseFormatters(formatters){ + if(!formatters) return [] + // 1. 先解析为 ["aaa()","bbb"]形式 + let result = formatters.trim().substr(1).trim().split("|").map(r=>r.trim()) + + // 2. 解析格式化器参数 + return result.map(formatter=>{ + let firstIndex = formatter.indexOf("(") + let lastIndex = formatter.lastIndexOf(")") + if(firstIndex!==-1 && lastIndex!==-1){ // 带参数的格式化器 + const argsContent = formatter.substr(firstIndex+1,lastIndex-firstIndex-1).trim() + let args = argsContent=="" ? [] : argsContent.split(",").map(arg=>{ + arg = arg.trim() + if(!isNaN(parseInt(arg))){ + return parseInt(arg) // 数字 + }else if((arg.startsWith('\"') && arg.endsWith('\"')) || (arg.startsWith('\'') && arg.endsWith('\'')) ){ + return arg.substr(1,arg.length-2) // 字符串 + }else if(arg.toLowerCase()==="true" || arg.toLowerCase()==="false"){ + return arg.toLowerCase()==="true" // 布尔值 + }else if((arg.startsWith('{') && arg.endsWith('}')) || (arg.startsWith('[') && arg.endsWith(']'))){ + try{ + return JSON.parse(arg) + }catch(e){ + return String(arg) + } + }else{ + return String(arg) + } + }) + return [formatter.substr(0,firstIndex),args] + }else{// 不带参数的格式化器 + return [formatter,[]] + } + }) +} + /** * 提取字符串中的插值变量 * @param {*} str + * @param {*} isFull =true 保留所有插值变量 =false 进行去重 * @returns {Array} [[变量名称,[]],[变量名称,[formatter,formatter,...]],...] */ -module.exports.getInterpolatedVars = function(str){ - let results = [] - let match - while ((match = varWidthPipeRegexp.exec(str)) !== null) { - if (match.index === varWidthPipeRegexp.lastIndex) { - varWidthPipeRegexp.lastIndex++; +function getInterpolatedVars(str,isFull=false){ + let results = [], match + while ((match = varWithPipeRegexp.exec(str)) !== null) { + if (match.index === varWithPipeRegexp.lastIndex) { + varWithPipeRegexp.lastIndex++; } - const varname = match.groups.varname - const formatters = match.groups.formatters ? match.groups.formatters.trim().substr(1).trim().split("|").map(r=>r.trim()) : [] - if(varname) { - const varDefine = formatters ? [varname,formatters] : [varname,[]] - if(results.findIndex(item=>item[0]===varDefine[0] && item[1].join()===varDefine[1].join()) === -1) results.push(varDefine) + const varname = match.groups.varname || "" + // 解析格式化器和参数 = [,[,[,,...]]] + const formatters = parseFormatters(match.groups.formatters) + const varInfo = [varname,formatters] + if(isFull) { + results.push([varname,formatters] ) + }else{ + if(results.findIndex(item=>item[0]===varInfo[0] && item[1].join()===varInfo[1].join()) === -1) results.push(varInfo) } } return results } + + /** * 将要翻译内容提供了一个非文本内容时进行默认的转换 * - 对函数则执行并取返回结果() @@ -72,7 +138,7 @@ module.exports.getInterpolatedVars = function(str){ * @param {*} value * @returns */ -function transformVarValue(value){ +function transformToString(value){ let result = value if(typeof(result)==="function") result = value() if(!(typeof(result)==="string")){ @@ -84,65 +150,217 @@ function transformVarValue(value){ } return result } + + +// 缓存数据类型的格式化器,避免每次都调用getDataTypeDefaultFormatter +let datatypeFormattersCache ={ + $activeLanguage:null, +} +/** + * 取得指定数据类型的默认格式化器 + * + * 可以为每一个数据类型指定一个格式化器,当传入插值变量时,会自动调用该格式化器来对值进行格式化转换 + + const formatters = { + "*":{ + $types:{...} // 在所有语言下只作用于特定数据类型的格式化器 + }, // 在所有语言下生效的格式化器 + cn:{ + $types:{ + [数据类型]:(value)=>{...}, + }, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + }, + } + * @param {*} scope + * @param {*} activeLanguage + * @param {*} dataType 数字类型 + * @returns {Function} 格式化函数 + */ +function getDataTypeDefaultFormatter(scope,activeLanguage,dataType){ + if(datatypeFormattersCache.$activeLanguage === activeLanguage) { + if(dataType in datatypeFormattersCache) return datatypeFormattersCache[dataType] + }else{// 清空缓存 + datatypeFormattersCache = { $activeLanguage:activeLanguage } + } + + // 先在当前作用域中查找,再在全局查找 + const targets = [scope.formatters,scope.global.formatters] + for(const target of targets){ + if(activeLanguage in target){ + // 在当前语言的$types中查找 + let formatters = target[activeLanguage].$types || {} + for(let [name,formatter] of Object.entries(formatters)){ + if(name === dataType && typeof(formatter)==="function") { + datatypeFormattersCache[dataType] = formatter + return formatter + } + } + } + // 在所有语言的$types中查找 + let formatters = target["*"].$types || {} + for(let [name,formatter] of Object.entries(formatters)){ + if(name === dataType && typeof(formatter)==="function") { + datatypeFormattersCache[dataType] = formatter + return formatter + } + } + } +} + +/** + * 获取指定名称的格式化器函数 + * @param {*} scope + * @param {*} activeLanguage + * @param {*} name 格式化器名称 + * @returns {Function} 格式化函数 + */ +let formattersCache = { $activeLanguage:null} +function getFormatter(scope,activeLanguage,name){ + if(formattersCache.$activeLanguage === activeLanguage) { + if(name in formattersCache) return formattersCache[dataType] + }else{ // 当切换语言时需要清空缓存 + formattersCache = { $activeLanguage:activeLanguage } + } + // 先在当前作用域中查找,再在全局查找 + const targets = [scope.formatters,scope.global.formatters] + for(const target of targets){ + // 优先在当前语言查找 + if(activeLanguage in target){ + let formatters = target[activeLanguage] || {} + if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name] + } + // 在所有语言的$types中查找 + let formatters = target["*"] || {} + if((name in formatters) && typeof(formatters[name])==="function") return formattersCache[name] = formatters[name] + } +} + +/** + * 执行格式化器并返回结果 + * @param {*} value + * @param {*} formatters + */ +function executeFormatter(value,formatters){ + if(formatters.length===0) return value + let result = value + try{ + for(let formatter of formatters){ + if(typeof(formatter) === "function") { + result = formatter(result) + }else{ // 碰到无效的格式化器时,直接返回 + return result + } + } + }catch(e){ + console.error(`Error while execute i18n formatter for ${value}: ${e.message} ` ) + } + return result +} +/** + * 将 [[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]]格式化器转化为 + * + * + * + * @param {*} scope + * @param {*} activeLanguage + * @param {*} formatters + */ +function buildFormatters(scope,activeLanguage,formatters){ + let results = [] + for(let formatter of formatters){ + if(formatter[0]){ + results.push((v)=>{ + return getFormatter(scope,activeLanguage,formatter[0])(v,...formatter[1]) + }) + } + } + return results +} + /** * 字符串可以进行变量插值替换, - * replaceInterpolateVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...}) - * replaceInterpolateVars("<模板字符串>",[变量值,变量值,...]) - * replaceInterpolateVars("<模板字符串>",变量值,变量值,...]) + * replaceInterpolatedVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...}) + * replaceInterpolatedVars("<模板字符串>",[变量值,变量值,...]) + * replaceInterpolatedVars("<模板字符串>",变量值,变量值,...]) * - 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典 - replaceInterpolateVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2 + replaceInterpolatedVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2 - 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数 - replaceInterpolateVars"this is {}+{}",[1,2]) --> this is 1+2 + replaceInterpolatedVars"this is {}+{}",[1,2]) --> this is 1+2 - 普通位置参数替换 - replaceInterpolateVars("this is {a}+{b}",1,2) --> this is 1+2 + replaceInterpolatedVars("this is {a}+{b}",1,2) --> this is 1+2 - this == scope == { formatters: {}, ... } * @param {*} template * @returns */ -module.exports.replaceInterpolateVars = function(template,...args) { +function replaceInterpolatedVars(template,...args) { const scope = this - const activeLanguage = scope.activeLanguage + // 当前激活语言 + const activeLanguage = scope.global.activeLanguage let result=template if(!hasInterpolation(template)) return template - // 变量插值 + // ****************************变量插值**************************** if(args.length===1 && typeof(args[0]) === "object" && !Array.isArray(args[0])){ - // 格式化器只能用在字典变量中 - // {var1:[formatter,formatter,...],var2:[formatter,formatter,...],...} - let varNames = getInterpolatedVars(template) + // 读取模板字符串中的插值变量列表 + // [[var1:[formatter,formatter,...]],[var2:[formatter,formatter,...]],...} + let interpVars = getInterpolatedVars(template) let varValues = args[0] - if(varNames.length===0) return template - for(let [name,formatters] of varNames){ - // 计算出变量值 + if(interpVars.length===0) return template // 没有变量插值则的返回原字符串 + // 开始处理插值变量 + for(let [name,formatters] of interpVars){ + // 1. 取得格式化器函数列表 formatters=[[格式化器名称,[参数,参数,...]],[格式化器名称,[参数,参数,...]]] + const formatterFuncs = buildFormatters(scope,activeLanguage,formatters) + // 2. 取变量值 let value = (name in varValues) ? varValues[name] : '' - // 针对每种数据类型的默认格式化器 - let dataType= getDataTypeName(value) - if(dataType in scope.formatters[activeLanguage]){ - const formatter = scope.formatters[activeLanguage][dataType] - formatters.splice(0,0,formatter) - } - if(formatters.length > 0 ){ - formatters.reduce((v,formatter)=>{ - if(formatter in scope.formatters){ - return scope.formatters[formatter](v) - }else{ - return v - } - },value) - } + // 3. 查找每种数据类型默认格式化器,并添加到formatters最前面,默认数据类型格式化器优先级最高 + const defaultFormatter = getDataTypeDefaultFormatter(scope,activeLanguage,getDataTypeName(value)) + if(defaultFormatter){ + formatterFuncs.splice(0,0,defaultFormatter) + } + + // 4. 执行格式化器 + value = executeFormatter(value,formatterFuncs) + + // 5. 进行值替换 // 如果变量中包括|管道符,则需要进行转换以适配更宽松的写法,比如data|time能匹配"data |time","data | time"等 - let nameRegexp =new RegExp(`${name}\\s*\\|\\s*${formatters.join("\\s*\\|\\s*")}`,"g") - result=result.replaceAll(nameRegexp,"gm"),transformVarValue(value)) + let nameRegexp = + if(formatters.length===0){ + nameRegexp = new RegExp(String.raw`\{\s*${name}\s*\}`,"gm") + }else{ + nameRegexp = new RegExp(`${name}\\s*\\|\\s*${formatters.join("\\s*\\|\\s*")}`,"gm") + } + + result= result.replaceAll(nameRegexp,transformToString(value)) } - }else{ // 位置插值 - const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args + + }else{ + // ****************************位置插值**************************** + // 如果只有一个Array参数,则认为是位置变量列表,进行展开 + const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args + + // 取得模板字符串中的插值变量列表 , 包含命令变量和位置变量 + let interpVars = getInterpolatedVars(template,true) + if(interpVars.length===0) return template // 没有变量插值则的返回原字符串 + let i=0 - for(let match of result.match(varWidthPipeRegexp) || []){ + for(let match of result.match(varWithPipeRegexp) || []){ if(i{ + Object.entries(vars).forEach(([name,value])=>{ if(typeof(value)==="function"){ try{ - vars[key] = value() + vars[name] = value() }catch(e){ - vars[key] = value + vars[name] = value } } - if(key.startsWith("$")) pluralVars.push(key) // 复数变量 + // 复数变量 + if(name.startsWith("$")) pluralVars.push(name) }) }else if(arguments.length >= 2){ vars = [...arguments].splice(1).map(arg=>typeof(arg)==="function" ? arg() : arg) @@ -206,7 +426,7 @@ module.exports.translate = function(message) { if(isMessageId(message)){ message = scope.default[message] || message } - return replaceInterpolateVars.call(scope,message,vars) + return replaceInterpolatedVars.call(scope,message,vars) }else{ // 1. 获取翻译后的文本内容 // 如果没有启用babel插件时,需要先将文本内容转换为msgId @@ -243,24 +463,26 @@ module.exports.translate = function(message) { * VoerkaI18n.off("change",(language)=>{}) * * */ - module.exports.i18n = class I18n{ - static instance = null; // 单例引用 - callbacks = [] // 当切换语言时的回调事件 + class I18n{ + static instance = null; // 单例引用 + callbacks = [] // 当切换语言时的回调事件 constructor(settings={}){ if(i18n.instance==null){ this.reset() i18n.instance = this; } this._settings = deepMerge(defaultLanguageSettings,settings) - this._scopes=[] // [{cn:{...},en:Promise,de:Promise},{...},{...}] + this._scopes=[] return i18n.instance; } + get settings(){ return this._settings } + get scopes(){ return this._scopes } // 当前激活语言 - get language(){return this._settings.activeLanguage} + get activeLanguage(){ return this._settings.activeLanguage} // 默认语言 - get defaultLanguage(){return this.this._settings.defaultLanguage} + get defaultLanguage(){ return this.this._settings.defaultLanguage} // 支持的语言列表 - get languages(){return this._settings.languages} + get languages(){ return this._settings.languages} // 订阅语言切换事件 on(callback){ this.callbacks.push(callback) @@ -353,8 +575,16 @@ module.exports.translate = function(message) { * @param {*} scope */ register(scope){ - scope.i18nSettings = this.settings + scope.global = this._settings this._scopes.push(scope) } } - \ No newline at end of file + + +module.exports ={ + getInterpolatedVars, + replaceInterpolatedVars, + I18n, + translate, + defaultLanguageSettings +} \ No newline at end of file diff --git a/src/entry.template.js b/src/templates/entry.js similarity index 56% rename from src/entry.template.js rename to src/templates/entry.js index 42b732b..8ab8a1e 100644 --- a/src/entry.template.js +++ b/src/templates/entry.js @@ -1,8 +1,16 @@ -import messageIds from "./messageIds" +{{if moduleTye === "esm"}} +import messageIds from "./idMap.js" import { translate,i18n } from "voerka-i18n" import defaultMessages from "./{{defaultLanguage}}.js" import i18nSettings from "./settings.js" -import formatters from "voerka-i18n/formatters" +import formatters from "../formatters" +{{else}} +const messageIds = require("./idMap") +const { translate,i18n } = require("voerka-i18n") +const defaultMessages = require("./{{defaultLanguage}}.js") +const i18nSettings = require("./settings.js") +const formatters = require("../formatters") +{{/if}} // 自动创建全局VoerkaI18n实例 if(!globalThis.VoerkaI18n){ @@ -13,13 +21,10 @@ let scope = { defaultLanguage: "{{defaultLanguage}}", // 默认语言名称 default: defaultMessages, // 默认语言包 messages : defaultMessages, // 当前语言包 - idMap:messageIds, // 消息id映射列表 - formatters:{ - ...formatters, - ...i18nSettings.formatters || {} - }, + idMap:messageIds, // 消息id映射列表 + formatters, // 当前作用域的格式化函数列表 loaders:{}, // 异步加载语言文件的函数列表 - i18nSettings:{} // 引用全局VoerkaI18n实例 + global:{} // 引用全局VoerkaI18n配置,注册后自动引用 } let supportedlanguages = {} @@ -34,5 +39,10 @@ const t = ()=> translate.bind(scope)(...arguments) // 注册当前作用域到全局VoerkaI18n实例 VoerkaI18n.register(scope) +{{if moduleTye === "esm"}} export scope -export t \ No newline at end of file +export t +{{else}} +module.exports.scope = scope +module.exports.t = t +{{/if}} diff --git a/src/templates/formatters.js b/src/templates/formatters.js new file mode 100644 index 0000000..99c8995 --- /dev/null +++ b/src/templates/formatters.js @@ -0,0 +1,83 @@ +/** + + 格式化器用来对翻译文本内容中的插值变量进行格式化, + 比如将一个数字格式化为货币格式,或者将一个日期格式化为友好的日期格式。 + + - 以下定义了一些格式化器,在中文场景下,会启用这些格式化器。 + import dayjs from "dayjs"; + const formatters = { + "*":{ // 在所有语言下生效的格式化器 + $types:{...} // 只作用于特定数据类型的默认格式化器 + }, + cn:{ + // 只作用于特定数据类型的格式化器 + $types:{ + Date:(value)=>dayjs(value).format("YYYY年MM月DD日 HH:mm:ss"), + }, + date:(value)=>dayjs(value).format("YYYY年MM月DD日") + bjTime:(value)=>"北京时间"+ value, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + }, + en:{ + $types:{ + Date:(value)=>dayjs(value).format("YYYY/MM/DD HH:mm:ss"), // 默认的格式化器 + }, + date:(value)=>dayjs(value).format("YYYY/MM/DD") + bjTime:(value)=>"BeiJing "+ value, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + [格式化器名称]:(value)=>{...}, + } + } + - 在翻译函数中使用格式化器的方法,示例如下: + + t("Now is { value | date | bjTime }",{value: new Date()}) + 其等效于: + t(`Now is ${bjTime(date(value))",{value: new Date()}) + 由于value分别经过两个管道符转换,上一个管道符的输出作为下一个管道符的输入,可以多次使用管道符。 + + 最终的输出结果: + 中文: "现在是北京时间2022年3月1日" + 英文: "Now is BeiJing 2022/03/01" + + + * + */ + +const formatters = { + "*":{ }, // 在所有语言下生效的格式化器 + $types:{ } // 在所有语言下只作用于特定数据类型的格式化器 +{{each languages}} + {{$value.name}}:{ + $types:{ + "*":{ + + }, + Date:{ + + }, + Number:{ + + }, + String:{ + + }, + Array:{ + + }, + Object:{ + + } + } + } +{{/each}} +} + +{{if moduleTye === "esm"}} +export default formatters +{{else}} +module.exports = formatters +{{/if}} + diff --git a/test/compile.test.js b/test/compile.test.js index f185170..73977ec 100644 --- a/test/compile.test.js +++ b/test/compile.test.js @@ -1,7 +1,74 @@ -const { getInterpolatedVars, replaceInterpolateVars} = require('../src/index.js') +const dayjs = require('dayjs'); +const { getInterpolatedVars, replaceInterpolatedVars} = require('../src/index.js') - -test("获取表达式中的插值变量",done=>{ +const scope1 ={ + defaultLanguage: "cn", // 默认语言名称 + default: { // 默认语言包 + + }, + messages : { // 当前语言包 + + }, + idMap:{ // 消息id映射列表 + + }, + formatters:{ // 当前作用域的格式化函数列表 + "*":{ + $types:{ + Date:(v)=>dayjs(v).format('YYYY/MM/DD'), + Boolean:(v)=>v?"True":"False", + }, + upper:(v)=>v.toUpperCase(), + lower:(v)=>v.toLowerCase(), + padStart:(v,len,pad)=>v.padStart(len,pad), + padStart:(v,len,pad)=>v.padStart(len,pad), + }, + cn:{ + $types:{ + Date:(v)=>dayjs(v).format('YYYY年MM月DD日'), + Boolean:(v)=>v?"是":"否", + } + }, + en:{ + $types:{ + + } + }, + }, + loaders:{}, // 异步加载语言文件的函数列表 + global:{// 引用全局VoerkaI18n配置 + defaultLanguage: "cn", + activeLanguage: "cn", + languages:[ + {name:"cn",title:"中文",default:true}, + {name:"en",title:"英文"}, + {name:"de",title:"德语"}, + {name:"jp",title:"日语"} + ], + formatters:{ // 当前作用域的格式化函数列表 + "*":{ + $types:{ + + } + }, + cn:{ + $types:{ + + } + }, + en:{ + $types:{ + + } + }, + } + } +} + +const replaceVars = replaceInterpolatedVars.bind(scope1) + + +test("获取翻译内容中的插值变量",done=>{ const results = getInterpolatedVars("中华人民共和国成立于{date | year | time }年,首都是{city}市"); expect(results.map(r=>r[0]).join(",")).toBe("date,city"); expect(results[0][0]).toEqual("date"); @@ -10,7 +77,8 @@ test("获取表达式中的插值变量",done=>{ expect(results[1][1]).toEqual([]); done() }) -test("表达式中定义了重复的插值变量",done=>{ + +test("获取翻译内容中定义了重复的插值变量",done=>{ const results = getInterpolatedVars("{a}{a}{a|x}{a|x}{a|x|y}{a|x|y}"); expect(results.length).toEqual(3); expect(results[0][0]).toEqual("a"); @@ -21,11 +89,23 @@ test("表达式中定义了重复的插值变量",done=>{ expect(results[2][1]).toEqual(["x","y"]); done() }) -// test("替代表达式中的插值变量",done=>{ -// const results = replaceInterpolateVars("中华人民共和国成立于{date}年,首都是{city}市",{ -// date:1949, -// city:"北京" -// }); -// expect(results).toBe("中华人民共和国成立于1949年,首都是北京市"); -// done() -// } \ No newline at end of file + +test("替换翻译内容的位置插值变量",done=>{ + + expect(replaceVars("{}{}{}",1,2,3)).toBe("123"); + expect(replaceVars("{a}{b}{c}",1,2,3)).toBe("123"); + // 定义了一些无效的格式化器,直接忽略 + expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3)).toBe("123"); + expect(replaceVars("{a|xxx}{b|dd}{c|}",1,2,3,4,5,6)).toBe("123"); + expect(replaceVars("{ a|}{b|dd}{c|}{}",1,2,3)).toBe("123{}"); + // 数据值进行 + expect(replaceVars("{}{}{}",1,"2",true)).toBe("12true"); + + done() +}) + +test("替换翻译内容的命名插值变量",done=>{ + expect(replaceVars("{a}{b}{c}",{a:1,b:2,c:3})).toBe("123"); + expect(replaceVars("{a}{b}{c}{a}{b}{c}",{a:1,b:"2",c:3})).toBe("123123"); + done() +}) \ No newline at end of file