diff --git a/demodata/index.js b/demodata/index.js index db615d8..f2ddd0b 100644 --- a/demodata/index.js +++ b/demodata/index.js @@ -1,14 +1,20 @@ -t("aaaaa") +// import {t } from "./languages" +// import { nanoid } from "./nanoid" +//const {t } = reuire("./languages") +function output(){ + t("aaaaa") -t('aaaaa1') -t("aaaaa2") -t('aaaaa 3') + t('aaaaa1') + t("aaaaa2") + t('aaaaa 3') + + t('bbbbb',a,b,c) + + t("cccc",()=>{},c) + + + + t("ddddd中国",()=>{},c) + t("eeeeee", ) +} -t('bbbbb',a,b,c) - -t("cccc",()=>{},c) - - - -t("ddddd中国",()=>{},c) -t("eeeeee", ) \ No newline at end of file diff --git a/package.json b/package.json index 7a0642f..4e1739e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "author": "", "license": "ISC", "dependencies": { + "art-template": "^4.13.2", "deepmerge": "^4.2.2", + "glob": "^7.2.0", "gulp": "^4.0.2", "logsets": "^1.0.6", "readjson": "^2.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d18287..2f4cf16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,14 +3,18 @@ lockfileVersion: 5.3 specifiers: '@babel/cli': ^7.17.6 '@babel/core': ^7.17.5 + art-template: ^4.13.2 deepmerge: ^4.2.2 + glob: ^7.2.0 gulp: ^4.0.2 logsets: ^1.0.6 readjson: ^2.2.2 through2: ^4.0.2 dependencies: + art-template: 4.13.2 deepmerge: 4.2.2 + glob: 7.2.0 gulp: 4.0.2 logsets: 1.0.6 readjson: registry.npmmirror.com/readjson/2.2.2 @@ -267,6 +271,12 @@ packages: dev: true optional: true + /acorn/5.7.4: + resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + /ansi-colors/1.1.0: resolution: {integrity: sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==} engines: {node: '>=0.10.0'} @@ -393,6 +403,20 @@ packages: engines: {node: '>=0.10.0'} dev: false + /art-template/4.13.2: + resolution: {integrity: sha512-04ws5k+ndA5DghfheY4c8F1304XJKeTcaXqZCLpxFkNMSkaR3ChW1pX2i9d3sEEOZuLy7de8lFriRaik1jEeOQ==} + engines: {node: '>= 1.0.0'} + dependencies: + acorn: 5.7.4 + escodegen: 1.14.3 + estraverse: 4.3.0 + html-minifier: 3.5.21 + is-keyword-js: 1.0.3 + js-tokens: 3.0.2 + merge-source-map: 1.1.0 + source-map: 0.5.7 + dev: false + /assign-symbols/1.0.0: resolution: {integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=} engines: {node: '>=0.10.0'} @@ -548,6 +572,13 @@ packages: get-intrinsic: 1.1.1 dev: false + /camel-case/3.0.0: + resolution: {integrity: sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + /camelcase/3.0.0: resolution: {integrity: sha1-MvxLn82vhF/N9+c7uXysImHwqwo=} engines: {node: '>=0.10.0'} @@ -612,6 +643,13 @@ packages: static-extend: 0.1.2 dev: false + /clean-css/4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + dependencies: + source-map: 0.6.1 + dev: false + /cliui/3.2.0: resolution: {integrity: sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=} dependencies: @@ -679,6 +717,14 @@ packages: hasBin: true dev: false + /commander/2.17.1: + resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==} + dev: false + + /commander/2.19.0: + resolution: {integrity: sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==} + dev: false + /commander/4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -757,6 +803,10 @@ packages: engines: {node: '>=0.10'} dev: false + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: false + /deepmerge/4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} @@ -882,6 +932,35 @@ packages: engines: {node: '>=0.8.0'} dev: true + /escodegen/1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /estraverse/4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: false + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: false + /expand-brackets/2.1.4: resolution: {integrity: sha1-t3c14xXOMPa27/D4OwQVGiJEliI=} engines: {node: '>=0.10.0'} @@ -955,6 +1034,10 @@ packages: resolution: {integrity: sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=} dev: false + /fast-levenshtein/2.0.6: + resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=} + dev: false + /file-uri-to-path/1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} requiresBuild: true @@ -1291,6 +1374,11 @@ packages: function-bind: 1.1.1 dev: false + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + /homedir-polyfill/1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -1302,6 +1390,20 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: false + /html-minifier/3.5.21: + resolution: {integrity: sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==} + engines: {node: '>=4'} + hasBin: true + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.17.1 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.4.10 + dev: false + /inflight/1.0.6: resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} dependencies: @@ -1444,6 +1546,11 @@ packages: dependencies: is-extglob: 2.1.1 + /is-keyword-js/1.0.3: + resolution: {integrity: sha1-rDDc81tnH0snsX9ctXI1EmAhEy0=} + engines: {node: '>=0.10.0'} + dev: false + /is-negated-glob/1.0.0: resolution: {integrity: sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=} engines: {node: '>=0.10.0'} @@ -1527,6 +1634,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /js-tokens/3.0.2: + resolution: {integrity: sha1-mGbfOVECEw449/mWvOtlRDIJwls=} + dev: false + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -1606,6 +1717,14 @@ packages: flush-write-stream: 1.1.1 dev: false + /levn/0.3.0: + resolution: {integrity: sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: false + /liftoff/3.1.0: resolution: {integrity: sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==} engines: {node: '>= 0.8'} @@ -1640,6 +1759,10 @@ packages: deepmerge: registry.npmmirror.com/deepmerge/4.2.2 dev: false + /lower-case/1.1.4: + resolution: {integrity: sha1-miyr0bno4K6ZOkv31YdcOcQujqw=} + dev: false + /make-dir/2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -1677,6 +1800,12 @@ packages: stack-trace: 0.0.10 dev: false + /merge-source-map/1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + dependencies: + source-map: 0.6.1 + dev: false + /micromatch/3.1.10: resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} engines: {node: '>=0.10.0'} @@ -1753,6 +1882,12 @@ packages: resolution: {integrity: sha1-yobR/ogoFpsBICCOPchCS524NCw=} dev: false + /no-case/2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + dependencies: + lower-case: 1.1.4 + dev: false + /node-releases/2.0.2: resolution: {integrity: sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==} dev: true @@ -1858,6 +1993,18 @@ packages: dependencies: wrappy: 1.0.2 + /optionator/0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: false + /ordered-read-streams/1.0.1: resolution: {integrity: sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=} dependencies: @@ -1871,6 +2018,12 @@ packages: lcid: 1.0.0 dev: false + /param-case/2.1.1: + resolution: {integrity: sha1-35T9jPZTHs915r75oIWPvHK+Ikc=} + dependencies: + no-case: 2.3.2 + dev: false + /parse-filepath/1.0.2: resolution: {integrity: sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=} engines: {node: '>=0.8'} @@ -1979,6 +2132,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /prelude-ls/1.1.2: + resolution: {integrity: sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=} + engines: {node: '>= 0.8.0'} + dev: false + /pretty-hrtime/1.0.3: resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=} engines: {node: '>= 0.8'} @@ -2073,6 +2231,11 @@ packages: safe-regex: 1.1.0 dev: false + /relateurl/0.2.7: + resolution: {integrity: sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=} + engines: {node: '>= 0.10'} + dev: false + /remove-bom-buffer/3.0.0: resolution: {integrity: sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==} engines: {node: '>=0.10.0'} @@ -2259,6 +2422,11 @@ packages: resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} engines: {node: '>=0.10.0'} + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + /sparkles/1.0.1: resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} engines: {node: '>= 0.10'} @@ -2439,6 +2607,13 @@ packages: through2: 2.0.5 dev: false + /type-check/0.3.2: + resolution: {integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: false + /type/1.2.0: resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} dev: false @@ -2451,6 +2626,15 @@ packages: resolution: {integrity: sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=} dev: false + /uglify-js/3.4.10: + resolution: {integrity: sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==} + engines: {node: '>=0.8.0'} + hasBin: true + dependencies: + commander: 2.19.0 + source-map: 0.6.1 + dev: false + /unc-path-regex/0.1.2: resolution: {integrity: sha1-5z3T17DXxe2G+6xrCufYxqadUPo=} engines: {node: '>=0.10.0'} @@ -2507,6 +2691,10 @@ packages: engines: {node: '>=4'} dev: false + /upper-case/1.1.3: + resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=} + dev: false + /urix/0.1.0: resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=} deprecated: Please see https://github.com/lydell/urix#deprecated @@ -2599,6 +2787,11 @@ packages: isexe: 2.0.0 dev: false + /word-wrap/1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: false + /wrap-ansi/2.1.0: resolution: {integrity: sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=} engines: {node: '>=0.10.0'} diff --git a/readme.md b/readme.md index 84527ae..a3fb368 100644 --- a/readme.md +++ b/readme.md @@ -170,14 +170,9 @@ t("今天是{date}",{date:new Date()}) ```javascript -import { injectLanguage } from "voerka-i18n" -import mylinLang from "mylib/languages" - -// 在当前工程注入第三方库的语言文件 -injectLanguage(mylinLang) - -import t from "./languages" - +import i18n from "voerka-i18n" +import { t } from "./languages" + t("xxxxx") VoerkaI18n实例 @@ -195,3 +190,6 @@ messages:{ ``` +## 一语多译 + +一语多译指同一句文本在不同的语景下,需要翻译成不同的内容。比如 \ No newline at end of file diff --git a/src/babel-plugin-voerkai18n.js b/src/babel-plugin-voerkai18n.js index 31daef9..6487dd1 100644 --- a/src/babel-plugin-voerkai18n.js +++ b/src/babel-plugin-voerkai18n.js @@ -2,6 +2,7 @@ * 转译源码中的t翻译函数的翻译内容转换为唯一的id值 * * - 将源文件中的t("xxxxx")转码成t(hash(xxxxx)) + * - 自动导入languages/index.js中的翻译函数t * * 使用方法: * @@ -18,12 +19,81 @@ const { getMessageId } = require('./utils'); const TRANSLATE_FUNCTION_NAME = "t" +const fs = require("fs"); +const DefaultI18nPluginOptions = { + translateFunctionName:"t", // 默认的翻译函数名称 + location:"./languages" // 默认的翻译文件存放的目录,即编译后的语言文件的文件夹 +} + +/** + * 判断当前是否是一个esmodule,判断依据是 + * - 包含import语句 + * - 包含export语句 + + * @param {*} path + */ +function isEsModule(path){ + for(let ele of path.node.body){ + if(ele.type==="ImportDeclaration" || ele.type==="ExportNamedDeclaration" || ele.type==="ExportDefaultDeclaration"){ + return true + } + } +} + +/** + * 判断是否导入了翻译函数 + * import { t } from "./i18n" + * const { t } form "./languages" + * @param {*} path + * @returns + */ +function hasImportTranslateFunction(path){ + for(let ele of path.node.body){ + if(ele.type==="ImportDeclaration"){ + if(ele.specifiers.findIndex(s => s.type === "ImportSpecifier" && s.imported.name ==TRANSLATE_FUNCTION_NAME && s.local.name===TRANSLATE_FUNCTION_NAME)>-1){ + return true + } + } + } +} +function hasRequireTranslateFunction(path){ + for(let ele of path.node.body){ + if(ele.type==="VariableDeclaration"){ + if(ele.specifiers.findIndex(s => s.type === "ImportSpecifier" && s.imported.name ==TRANSLATE_FUNCTION_NAME && s.local.name===TRANSLATE_FUNCTION_NAME)>-1){ + return true + } + } + } +} module.exports = function voerkai18nPlugin(babel) { const t = babel.types; + const pluginOptions = Object.assign({},DefaultI18nPluginOptions); return { visitor:{ + Program(path, state){ + Object.assign(pluginOptions,state.opts || {}); + const { location = "./languages", translateFunctionName } = pluginOptions + if(isEsModule(path)){ + // 如果没有定义t函数,则自动导入 + if(!hasImportTranslateFunction(path)){ + path.node.body.unshift(t.importDeclaration([ + t.ImportSpecifier(t.identifier(translateFunctionName),t.identifier(translateFunctionName) + )],t.stringLiteral(location))) + } + }else{ + if(!hasRequireTranslateFunction(path)){ + path.node.body.unshift(t.variableDeclaration("const",[ + t.variableDeclarator( + t.ObjectPattern([t.ObjectProperty(t.Identifier(translateFunctionName),t.Identifier(translateFunctionName),false,true)]), + t.CallExpression(t.Identifier("require"),[t.stringLiteral(location)]) + ) + ])) + } + } + }, CallExpression(path,state){ + let options = state.opts // 只对翻译函数进行转码 if(path.node.callee.name===TRANSLATE_FUNCTION_NAME){ if(path.node.arguments.length>0 && t.isStringLiteral(path.node.arguments[0])){ diff --git a/src/babel.plugin.test.js b/src/babel.plugin.test.js index 6695e4e..91ec6c2 100644 --- a/src/babel.plugin.test.js +++ b/src/babel.plugin.test.js @@ -9,7 +9,9 @@ babel.transform(code, { plugins: [ [ i18nPlugin, - {a:1,b:2} + { + location:"./languages" // 指定语言文件存放的目录,即保存编译后的语言文件的文件夹 + } ]] }, function(err, result) { console.log(result.code) diff --git a/src/compile.js b/src/compile.js index e39c99c..f4af80f 100644 --- a/src/compile.js +++ b/src/compile.js @@ -8,7 +8,7 @@ * * 编译输出: * - * hashId = hash([projectName]_[namespace]_[message]) + * hashId = getMessageId() * * - languages/index.js 主源码,用来引用语言文件 * { @@ -36,15 +36,73 @@ * @param {*} opts */ +const readJson = require("readjson") +const glob = require("glob") +const createLogger = require("logsets") +const path = require("path") +const { getMessageId } = require("./utils") +const fs = require("fs") +const logger = createLogger() +const artTemplate = require("art-template") + function normalizeCompileOptions(opts={}) { let options = Object.assign({ input:null, // 指定要编译的文件夹,即extract输出的语言文件夹 output:null, // 指定编译后的语言文件夹,如果没有指定,则使用input目录 - formatters:{}, // 对插值变量进行格式化的函数清单 + formatters:{}, // 对插值变量进行格式化的函数列表 }, opts) return opts; } -module.exports = function compile(opts={}){ +module.exports = function compile(langFolder,opts={}){ + let options = normalizeCompileOptions(opts); + let { output } = options; + + //1. 加载多语言配置文件 + import(`file:///${path.join(langFolder,"settings.js")}`).then(module=>{ + let { languages,defaultLanguage,activeLanguage,namespaces } = module.default; + + // 1. 合并生成最终的语言文件 + let messages = {} ,msgId =1 + glob.sync(path.join(langFolder,"translates/*.json")).forEach(file=>{ + try{ + let msg = readJson.sync(file) + Object.entries(msg).forEach(([msg,langs])=>{ + if(msg in messages){ + Object.assign(messages[msg],langs) + }else{ + messages[msg] = langs + } + }) + }catch(e){ + logger.log("读取语言文件{}失败:{}",file,e.message) + } + }) + // 2. 为每一个文本内容生成一个唯一的id + let messageIds = {} + Object.entries(messages).forEach(([msg,langs])=>{ + langs.$id = msgId++ + messageIds[msg] = langs.$id + }) + // 3. 为每一个语言生成对应的语言文件 + languages.forEach(lang=>{ + let langMessages = {} + Object.entries(messages).forEach(([message,translatedMsgs])=>{ + langMessages[translatedMsgs.$id] = lang.name in translatedMsgs ? translatedMsgs[lang.name] : message + }) + // 为每一种语言生成一个语言文件 + fs.writeFileSync(path.join(langFolder,`${lang.name}.js`),`export default ${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) + + }) + } \ No newline at end of file diff --git a/src/compile.test.js b/src/compile.test.js new file mode 100644 index 0000000..dbbe233 --- /dev/null +++ b/src/compile.test.js @@ -0,0 +1,7 @@ + + +const compile = require('./compile'); +const path = require("path") + + +compile(path.resolve(__dirname,"../demodata/languages")) \ No newline at end of file diff --git a/src/entry.template.js b/src/entry.template.js new file mode 100644 index 0000000..f9852fa --- /dev/null +++ b/src/entry.template.js @@ -0,0 +1,56 @@ +import messageIds from "./messageIds" +import { translate,i18n } from "voerka-i18n" +import defaultMessages from "./{{defaultLanguage}}.js" +import i18nSettings from "./settings.js" +import formatters from "voerka-i18n/formatters" + +// 自动创建全局VoerkaI18n实例 +if(!globalThis.VoerkaI18n){ + globalThis.VoerkaI18n = new i18n(i18nSettings) +} + +let scopeContext = { + messages : defaultMessages, // 当前语言的消息 + ids:messageIds, + formatters:{ + ...formatters, + ...i18nSettings.formatters || {} + }, + languageLoaders:{} +} + +let supportedlanguages = {} + +messages["{{defaultLanguage}}"]= defaultMessages +{{each languages}}{{if $value.name !== defaultLanguage}} +scopeContext.languageLoaders["{{$value.name}}"] = ()=>import("./{{$value.name}}.js") +{{/if}}{{/each}} + +const t = ()=> translate.bind(scopeContext)(...arguments) + + +// 侦听语言切换事件 +VoerkaI18n.on(async function(lang)=>{ + + if(lang === defaultLanguage){ + scopeContext.messages = defaultMessages + return + } + const loader = scopeContext.languageLoaders[lang] + + if(typeof(loader) === "function"){ + try{ + scopeContext.messages = await loader() + }catch(e){ + console.warn(`Error loading language ${lang} : ${e.message}`) + scopeContext.messages = defaultMessages + } + }else if(typeof(loader) === "object"){ + scopeContext.messages = loader + }else{ + scopeContext.messages = defaultMessages + } + +}) + +export t \ No newline at end of file diff --git a/src/extract.plugin.js b/src/extract.plugin.js index 57b681d..e948aa1 100644 --- a/src/extract.plugin.js +++ b/src/extract.plugin.js @@ -67,7 +67,7 @@ function extractTranslateTextUseRegexp(content,namespace,extractor,file,options) texts[ns][text] ={} languages.forEach(language=>{ if(language.name !== defaultLanguage){ - texts[ns][text][language.name] = "" + texts[ns][text][language.name] = text //"" } }) texts[ns][text]["$file"]=[file.relative] @@ -388,7 +388,7 @@ module.exports = function(options={}){ } } // 将元数据生成到 i18n.meta.json - const metaFile = path.join(outputPath,"i18n.settings.js") + const metaFile = path.join(outputPath,"settings.js") const meta = { languages : options.languages, defaultLanguage: options.defaultLanguage, diff --git a/src/index.js b/src/index.js index 41c9f16..3f67795 100644 --- a/src/index.js +++ b/src/index.js @@ -1,52 +1,223 @@ import deepMerge from "deepmerge" -let ParamRegExp=/\{\w*\}/g -//添加一个params参数,使字符串可以进行变量插值替换, -// "this is {a}+{b}".params({a:1,b:2}) --> this is 1+2 -// "this is {a}+{b}".params(1,2) --> this is 1+2 -// "this is {}+{}".params([1,2]) --> this is 1+2 -if(!String.prototype.hasOwnProperty("params")){ +// 用来提取字符里面的插值变量参数 +// let varRegexp = /\{\s*(?\w*\.?\w*)\s*\}/g +let varRegexp = /\{\s*((?\w+)?(\s*\|\s*(?\w*))?)?\s*\}/g +// 插值变量字符串替换正则 +//let varReplaceRegexp =String.raw`\{\s*(?{name}\.?\w*)\s*\}` - String.prototype.params=function (params) { - let result=this.valueOf() - if(typeof params === "object"){ - for(let name in params){ - result=result.replace("{"+ name +"}",params[name]) - } + +let varReplaceRegexp =String.raw`\{\s*{varname}\s*\}` + +/** + * 考虑到通过正则表达式进行插件的替换可能较慢,因此提供一个简单方法来过滤掉那些 + * 不需要进行插值处理的字符串 + * 原理很简单,就是判断一下是否同时具有{和}字符,如果有则认为可能有插值变量,如果没有则一定没有插件变量,则就不需要进行正则匹配 + * 从而可以减少不要的正则匹配 + * 注意:该方法只能快速判断一个字符串不包括插值变量 + * @param {*} str + * @returns {boolean} true=可能包含插值变量, + */ +function hasInterpolation(str){ + return str.includes("{") && str.includes("}") +} +/** + * 获取指定变量类型名称 + * getDataTypeName(1) == Number + * getDataTypeName("") == String + * getDataTypeName(null) == Null + * getDataTypeName(undefined) == Undefined + * getDataTypeName(new Date()) == Date + * getDataTypeName(new Error()) == Error + * + * @param {*} v + * @returns + */ +function getDataTypeName(v){ + if (v === null) return 'Null' + if (v === undefined) return 'Undefined' + if(typeof(v)==="function") return "Function" + return v.constructor && v.constructor.name; +}; + +/** + * 提取字符串中的插值变量 + * @param {*} str + * @returns {Array} 变量名称列表 + */ +function getInterpolatedVars(str){ + let result = [] + let match + while ((match = varRegexp.exec(str)) !== null) { + if (match.index === varRegexp.lastIndex) { + varRegexp.lastIndex++; + } + if(match.groups.varname) { + result.push(match.groups.formatter ? match.groups.varname+"|"+match.groups.formatter : match.groups.varname) + } + } + return result +} +/** + * 将要翻译内容提供了一个非文本内容时进行默认的转换 + * - 对函数则执行并取返回结果() + * - 对Array和Object使用JSON.stringify + * - 其他类型使用toString + * + * @param {*} value + * @returns + */ +function transformVarValue(value){ + let result = value + if(typeof(result)==="function") result = value() + if(!(typeof(result)==="string")){ + if(Array.isArray(result) || typeof(result)==="object"){ + result = JSON.stringify(result) }else{ - let i=0 - for(let match of result.match(ParamRegExp) || []){ - if(i",{变量名称:变量值,变量名称:变量值,...}) + * replaceInterpolateVars("<模板字符串>",[变量值,变量值,...]) + * replaceInterpolateVars("<模板字符串>",变量值,变量值,...]) + * + - 当只有两个参数并且第2个参数是{}时,将第2个参数视为命名变量的字典 + replaceInterpolateVars("this is {a}+{b},{a:1,b:2}) --> this is 1+2 + - 当只有两个参数并且第2个参数是[]时,将第2个参数视为位置参数 + replaceInterpolateVars"this is {}+{}",[1,2]) --> this is 1+2 + - 普通位置参数替换 + replaceInterpolateVars("this is {a}+{b}",1,2) --> this is 1+2 + - + + * @param {*} template + * @returns + */ +function replaceInterpolateVars(template,...args) { + let result=template + if(!hasInterpolation(template)) return + if(args.length===1 && typeof(args[0]) === "object" && !Array.isArray(args[0])){ // 变量插值 + for(let name in args[0]){ + // 如果变量中包括|管道符,则需要进行转换以适配更宽松的写法,比如data|time能匹配"data |time","data | time"等 + let nameRegexp = name.includes("|") ? name.split("|").join("\\s*\\|\\s*") : name + result=result.replaceAll(new RegExp(varReplaceRegexp.replaceAll("{varname}",nameRegexp),"g"),transformVarValue(args[0][name])) + } + }else{ // 位置插值 + const params=(args.length===1 && Array.isArray(args[0])) ? [...args[0]] : args + let i=0 + for(let match of result.match(varRegexp) || []){ + if(i= 2){ + options=[...arguments].splice(1) + } + // 默认语言是中文,不需要查询加载,只需要做插值变换即可 + if(this.language === this.defaultLanguage){ + return this._replaceVars(content,options) + }else{ + let result = this.messages[this.language][content] + if(content in this.messages[this.language]){ + // 复数形式,需要通过plurals来指定内容中包括的复数插值 + if(Array.isArray(result)){ + let plurals = options.plurals + if(typeof(plurals) == 'string' && (plurals in options)){ + return options[plurals]>1 ? result[1].params(options) : result[0].params(options) + }else{ + return this._replaceVars(result[0],options) + } + }else{ + return this._replaceVars(result,options); + } + }else{ + return this._replaceVars(result,options) + } + } + }catch(e){ + return content + } } -class i18n{ + +/** + * 多语言管理类 + * + * 当导入编译后的多语言文件时(import("./languages")),会自动生成全局实例VoerkaI18n + * + * VoerkaI18n.languages // 返回支持的语言列表 + * VoerkaI18n.defaultLanguage // 默认语言 + * VoerkaI18n.language // 当前语言 + * VoerkaI18n.change(language) // 切换到新的语言 + * + * + * VoerkaI18n.on("change",(language)=>{}) // 注册语言切换事件 + * VoerkaI18n.off("change",(language)=>{}) + * + * */ +export class I18n{ static instance = null; // 单例引用 - _language = "cn" // 当前语言 - defaultLanguage = "cn" - supportedLanguages = ["cn","en"] // 支持的语言 - builtInLanguages = ["cn","en"] // 内置语言 - messages = {} - callbacks = [] // 当切换语言时的回调事件 - constructor(){ + callbacks = [] // 当切换语言时的回调事件 + constructor(settings={}){ if(i18n.instance==null){ this.reset() i18n.instance = this; } + this._settings = deepMerge(defaultLanguageSettings,settings) + this._scopes=[] // [{cn:{...},en:Promise,de:Promise},{...},{...}] return i18n.instance; } - addListener(callback){ + // 当前激活语言 + get language(){return this._settings.activeLanguage} + // 默认语言 + get defaultLanguage(){return this.this._settings.defaultLanguage} + // 支持的语言列表 + get languages(){return this._settings.languages} + + on(callback){ this.callbacks.push(callback) } - removeListener(callback){ + off(callback){ for(let i=0;iscope[value]).filter(loader=>{ + loader!=null} + ) + // 加载所有 + if(asyncMsgLoaders.length>0){ + await Promise.all(asyncMsgLoaders) + } + + this._settings.activeLanguage = value this._triggerCallback() - } - } - /** - * 当配置更新时调用此方法 - */ - reset(){ - - let settings = { - current:"cn", - default:"cn", - supportedLanguages:["en","cn"] - } - if(VoerkaSettings!==undefined) Object.assign(settings,VoerkaSettings.get("i18n") ) - - this._language = settings.current - this.defaultLanguage = settings.default - this.supportedLanguages = settings.supportedLanguages - globalThis.t = this.translate.bind(this) - globalThis.i18n = this - } - merge(messages){ - this.messages = deepMerge(this.messages,messages) - } - - // 变量插值 - _replaceVars(source,params){ - if(Array.isArray(params)){ - return source.params(...params) }else{ - return source.params(params) + throw new Error("Not supported language:"+value) } } /** - * - * translate("要翻译的文本内容") 如果默认语言是中文,则不会进行翻译直接返回 - * translate("I am {} {}","man") == I am man 位置插值 - * translate("I am {p}",{p:"man"}) 字典插值 - * translate("I am {p}",{p:"man",ns:""}) 指定名称空间 - * translate("I am {p}",{p:"man",namespace:""}) - * translate("I am {p}",{p:"man",namespace:""}) - * translate("total {count} items", {count:1}) //复数形式 - * translate({count:1,plurals:'count'}) // 复数形式 - * translate("total {} {} {} items",a,b,c) // 位置变量插值 - * + * + * 注册一个新的作用域 + * + * 每一个库均对应一个作用域,每个作用域可以有多个语言包,且对应一个翻译函数 + * scope={ + * "<默认语言>":{ + * "":"", + * }, + * "<语言1>":()=>import(), + * "<语言2>":()=>import(), + * } + * + * 除了默认语言外,其他语言采用动态加载的方式 + * + * @param {*} scope */ - translate(){ - let content = arguments[0],options={} - try{ - if(arguments.length === 2 && typeof(arguments[1])=='object'){ - Object.assign(options,arguments[1]) - }else if(arguments.length >= 2){ - options=[...arguments].splice(1) - } - // 默认语言是中文,不需要查询加载,只需要做插值变换即可 - if(this.language === this.defaultLanguage){ - return this._replaceVars(content,options) - }else{ - let result = this.messages[this.language][content] - if(content in this.messages[this.language]){ - // 复数形式,需要通过plurals来指定内容中包括的复数插值 - if(Array.isArray(result)){ - let plurals = options.plurals - if(typeof(plurals) == 'string' && (plurals in options)){ - return options[plurals]>1 ? result[1].params(options) : result[0].params(options) - }else{ - return this._replaceVars(result[0],options) - } - }else{ - return this._replaceVars(result,options); - } - }else{ - return this._replaceVars(result,options) - } - } - }catch(e){ - return content - } + register(scope){ + this._scopes.push(scope) } } - - -const i18nInstance = new i18n() - -export default i18nInstance + \ No newline at end of file