From afa4f1a184be67ade49ffc301449f873bdf633ed Mon Sep 17 00:00:00 2001 From: wxzhang Date: Fri, 4 Mar 2022 22:17:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BD=BF=E7=94=A8=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/demo/package.json | 2 +- packages/tools/compile.js | 28 ++++++- pnpm-lock.yaml | 165 +++++++++++++++++++------------------ readme.md | 162 +++++++++++++++++++++++++++++++++++- 4 files changed, 272 insertions(+), 85 deletions(-) diff --git a/packages/demo/package.json b/packages/demo/package.json index 7b8a064..fd3d7b2 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -2,7 +2,7 @@ "name": "@voerkai18n/demo", "version": "1.0.0", "description": "", - "main": "babel.plugin.demo.js", + "main": "babel.plugin.demo.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/packages/tools/compile.js b/packages/tools/compile.js index bc9d99e..fa05b58 100644 --- a/packages/tools/compile.js +++ b/packages/tools/compile.js @@ -36,7 +36,7 @@ function normalizeCompileOptions(opts={}) { let options = Object.assign({ input:null, // 指定要编译的文件夹,即extract输出的语言文件夹 output:null, // 指定编译后的语言文件夹,如果没有指定,则使用input目录 - moduleType:"esm" // 指定编译后的语言文件的模块类型,取值common,cjs,esm,es + moduleType:"auto" // 指定编译后的语言文件的模块类型,取值common,cjs,esm,es }, opts) if(options.moduleType==="es") options.moduleType = "esm" if(options.moduleType==="cjs") options.moduleType = "commonjs" @@ -44,10 +44,34 @@ function normalizeCompileOptions(opts={}) { return options; } + +/** + * 从当前文件夹开始向上查找package.json文件,并解析出语言包的类型 + * @param {*} folder + */ +function findModuleType(folder){ + try{ + let pkgPath = path.join(folder, "package.json") + if(fs.existsSync(pkgPath)){ + let pkg = readJson.sync(pkgPath) + return pkg.type || "commonjs" + } + let parent = path.dirname(folder) + if(parent===folder) return null + return findModuleType(parent) + }catch{ + return "esm" + } +} + module.exports =async function compile(langFolder,opts={}){ const options = normalizeCompileOptions(opts); const { output,moduleType } = options; - + + if(moduleType==="auto"){ + moduleType = findModuleType(langFolder) + } + // 加载多语言配置文件 const settingsFile = path.join(langFolder,"settings.js") try{ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f5674b..b01bcc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,16 +47,6 @@ importers: gulp: 4.0.2 vinyl: 2.2.1 - packages/demo/apps/app/languages: - specifiers: - deepmerge: ^4.2.2 - gulp: ^4.0.2 - vinyl: ^2.2.1 - devDependencies: - deepmerge: 4.2.2 - gulp: 4.0.2 - vinyl: 2.2.1 - packages/formatters: specifiers: deepmerge: ^4.2.2 @@ -145,8 +135,8 @@ packages: slash: 2.0.0 source-map: 0.5.7 optionalDependencies: - '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 - chokidar: 3.5.3 + '@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 dev: false /@babel/code-frame/7.16.7: @@ -1522,12 +1512,6 @@ packages: '@jridgewell/resolve-uri': 3.0.5 '@jridgewell/sourcemap-codec': 1.4.11 - /@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3: - resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} - requiresBuild: true - dev: false - optional: true - /@rollup/plugin-babel/5.3.1_@babel+core@7.17.5+rollup@2.69.0: resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -2074,14 +2058,6 @@ packages: dev: false 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: true - optional: true - /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -2229,26 +2205,9 @@ packages: readdirp: 2.2.1 upath: 1.2.0 optionalDependencies: - fsevents: 1.2.13 + fsevents: registry.npmmirror.com/fsevents/1.2.13 dev: true - /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: false - optional: true - /ci-info/3.3.0: resolution: {integrity: sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==} dev: true @@ -2689,7 +2648,7 @@ packages: esutils: 2.0.3 optionator: 0.8.3 optionalDependencies: - source-map: 0.6.1 + source-map: registry.npmmirror.com/source-map/0.6.1 dev: false /escodegen/2.0.0: @@ -2702,7 +2661,7 @@ packages: esutils: 2.0.3 optionator: 0.8.3 optionalDependencies: - source-map: 0.6.1 + source-map: registry.npmmirror.com/source-map/0.6.1 dev: true /esprima/4.0.1: @@ -2848,12 +2807,6 @@ 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: true - optional: true - /fill-range/4.0.0: resolution: {integrity: sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=} engines: {node: '>=0.10.0'} @@ -2972,25 +2925,6 @@ 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: true - 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 - optional: true - /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true @@ -3792,7 +3726,7 @@ packages: micromatch: 4.0.4 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: registry.npmmirror.com/fsevents/2.3.2 dev: true /jest-jasmine2/27.5.1: @@ -4442,12 +4376,6 @@ packages: engines: {node: '>= 0.10'} dev: true - /nan/2.15.0: - resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} - requiresBuild: true - dev: true - optional: true - /nanomatch/1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -5122,7 +5050,7 @@ packages: engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: registry.npmmirror.com/fsevents/2.3.2 dev: true /safe-buffer/5.1.2: @@ -5639,7 +5567,7 @@ packages: hasBin: true dependencies: commander: 2.19.0 - source-map: 0.6.1 + source-map: registry.npmmirror.com/source-map/0.6.1 /unc-path-regex/0.1.2: resolution: {integrity: sha1-5z3T17DXxe2G+6xrCufYxqadUPo=} @@ -6010,12 +5938,49 @@ 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: false + 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: true + optional: true + + 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: false + 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 @@ -6030,6 +5995,45 @@ packages: requiresBuild: true 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: true + 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: true + 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 + optional: true + + 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: true + optional: true + registry.npmmirror.com/regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz} name: regenerator-runtime @@ -6041,4 +6045,3 @@ packages: name: source-map version: 0.6.1 engines: {node: '>=0.10.0'} - dev: false diff --git a/readme.md b/readme.md index 7f729fc..e391164 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,165 @@ +# 前言 + +基于`javascript`的国际化方案很多,比较有名的有`fbt`、`i18next`、`react-i18next`、`vue-i18n`、`react-intl`等等,每一种解决方案均有大量的用户。为什么还要再造一个轮子?好吧,再造轮子的理由不外乎不满足于现有方案,总想着现有方案的种种不足之处,然后就撸起袖子想造一个轮子,也不想想自己什么水平。 + +哪么到底是对现有解决方案有什么不满?最主要有三点: + +- 大部份均为要翻译的文本信息指定一个`key`,然后在源码文件中使用形如`$t("message.login")`之类的方式,然后在翻译时将之转换成最终的文本信息。此方式最大的问题是,在源码中必须人为地指定每一个`key`,在中文语境中,想为每一句中文均配套想一句符合语义的`英文key`是比较麻烦的,也很不直观。我希望在源文件中就直接使用中文,如`t("中华人民共和国万岁")`,然后国际化框架应该能自动处理后续的一系列麻烦。 + +- 要能够比较友好地支持多包`monorepo`场景下的国际化协作,当主程序切换语言时,其他包或库也可以自动切换,并且在开发上每个包或库均可以独立地进行开发,集成到主程序时能无缝集成。这点在现有方案上没有找到比较理想的解决方案。 + +- 大部份国际化框架均将中文视为二等公民,大部份情况下您应该采用英文作为第一语言,虽然这不是太大的问题,但是既然要再造一个轮子,为什么不将中文提升到一等公民呢。 + + + +当然,在使用方式上要尽可能简洁,便 + +基于此 + +# 安装 + +`VoerkaI18n`国际化框架是一个开源多包工程,主要由以下几个包组成: + +- **@voerkai18/runtime** + + 必须的运行时,安装到运行依赖`dependencies`中 + + ```javascript + npm install --save @voerkai18/runtime + yarn add @voerkai18/runtime + pnpm add @voerkai18/runtime + ``` + +- **@voerkai18/tools** + + 包含文本提取/编译等命行工具,应该安装到开发依赖`devDependencies`中 + + ```javascript + npm install --save-dev @voerkai18/tools + yarn add -D @voerkai18/tools + pnpm add -D @voerkai18/tools + ``` + +- **@voerkai18/formatters** + + 可选的,一些额外的格式化器,可以按需进行安装到`dependencies`中 + + + +# 快速入门 + +本节以标准的`Nodejs`应用程序为例,简要介绍`VoerkaI18n`国际化框架的基本使用。其他`vue`或`react`应用的使用也基本相同。 + +```shell +myapp + |--package.json + |--index.js +``` + +## 第一步:使用翻译函数 + +在源码文件中直接使用`t`翻译函数对要翻译文本信息进行封装,简单而粗暴。 + +```javascript +// index.js + +console.log(t("中华人民共和国万岁")) +console.log(t("中华人民共和国成立于{}",1949)) +``` + +`t`翻译函数是在哪里声明的?先卖个关子,后续揭晓。 + +## 第二步:提取要翻译的内容 + +接下来我们使用`voerkai18n extract`命令来自动扫描工程源码文件中的需要的翻译的文本信息。 + +```shell +myapp>voerkai18n extract --languages cn,en,de,jp --default cn --active cn +``` + +以上命令代表: + +- 扫描当前文件夹下所有源码文件 +- 计划支持`cn`、`en`、`de`、`jp`四种语言 +- 默认语言是中文。(指在源码文件中我们直接使用中文,好象其他方案大部份均要求采用英文) +- 激活语言是中文(即默认切换到中文) + +执行`voerkai18n extract`命令后,就会在`myapp/languages`通过生成`translates/default.json`文件,该文件就是需要进行翻译的文本信息,形式如下: + +```json +{ + "中华人民共和国万岁":{ + "en":"<在此编写对应的英文翻译内容>", + "de":"<在此编写对应的德文翻译内容>" + "jp":"<在此编写对应的日文翻译内容>", + "$files":["index.js"] // 记录了该信息是从哪几个文件中提取的 + }, + "中华人民共和国成立于{}":{ + "en":"<在此编写对应的英文翻译内容>", + "de":"<在此编写对应的德文翻译内容>" + "jp":"<在此编写对应的日文翻译内容>", + "$files":["index.js"] + } +} +``` + +最后文件结构如下: + +```shell +myapp + |-- languages + |-- settings.js // 语言配置文件 + |-- translates // 此文件夹是所有需要翻译的内容 + |-- default.json + |-- package.json + |-- index.js + +``` + +> **注:**当我们修改了源文件后,可以多次执行`voerkai18n extract`命令,该命令会自动同步合并已翻译的内容,不会导致进行了一半的翻译内容丢失,可以放心执行。 + +## 第三步:编码语言包 + +当我们完成`myapp/languages/translates`下的所有`JSON语言文件`的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续`名称空间`介绍),接下来需要对翻译后的文件进行编译。 + +```shell +myapp> voerkai18n compile +``` + +`compile`命令根据`myapp/languages/translates/*.json`和`myapp/languages/settings.js`文件编译生成以下文件: + +```javascript + |-- languages + |-- settings.js // 语言配置文件 + |-- idMap.js // 文本信息id映射表 + |-- index.js // 包含该应用作用域下的翻译函数等 + |-- cn.js // 语言包 + |-- en.js + |-- jp.js + |-- de.js + |-- translates // 此文件夹是所有需要翻译的内容 + |-- default.json + |-- package.json + |-- index.js + +``` + + + +## 第四步:导入翻译函数 + +第一步中我们在源文件中直接使用了`t`翻译函数包装要翻译的文本信息,该`t`翻译函数就是在编译环节自动生成并声明在`myapp/languages/index.js`中的。 + + + +import { t } from "./languages" + + + +是`myapp/languages/index.js`文件导出的翻译函数,但是现在`myapp/languages`还不存在,后续会使用工具自动生成。 + # 指南 ## 翻译函数 @@ -72,7 +232,7 @@ t("中华人民共和国成立于{date | year}年",{date:new Date('')}) 启用复数处理机制后,在`t`函数按如下方式进行处理。 - + - **不存在插值变量且t函数的第2个参数是数字**