This commit is contained in:
wxzhang 2022-02-28 18:00:41 +08:00
parent 5d348a23d1
commit c8c30b39bf
11 changed files with 655 additions and 141 deletions

View File

@ -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", )

View File

@ -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",

193
pnpm-lock.yaml generated
View File

@ -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'}

View File

@ -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:{
```
## 一语多译
一语多译指同一句文本在不同的语景下,需要翻译成不同的内容。比如

View File

@ -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])){

View File

@ -9,7 +9,9 @@ babel.transform(code, {
plugins: [
[
i18nPlugin,
{a:1,b:2}
{
location:"./languages" // 指定语言文件存放的目录,即保存编译后的语言文件的文件夹
}
]]
}, function(err, result) {
console.log(result.code)

View File

@ -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)
})
}

7
src/compile.test.js Normal file
View File

@ -0,0 +1,7 @@
const compile = require('./compile');
const path = require("path")
compile(path.resolve(__dirname,"../demodata/languages"))

56
src/entry.template.js Normal file
View File

@ -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

View File

@ -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,

View File

@ -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*(?<var>\w*\.?\w*)\s*\}/g
let varRegexp = /\{\s*((?<varname>\w+)?(\s*\|\s*(?<formatter>\w*))?)?\s*\}/g
// 插值变量字符串替换正则
//let varReplaceRegexp =String.raw`\{\s*(?<var>{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<arguments.length){
result=result.replace(match,arguments[i])
i+=1
}
result = result.toString()
}
}
return result
}
/**
* 字符串可以进行变量插值替换
* replaceInterpolateVars("<模板字符串>",{变量名称:变量值,变量名称:变量值,...})
* 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<params.length){
let param = transformVarValue(params[i])
result=result.replace(match,param)
i+=1
}
}
return result
}
}
return result
}
// 默认语言配置
export const defaultLanguageSettings = {
defaultLanguage: "cn",
activeLanguage: "cn",
languages:{
cn:{name:"cn",title:"中文",default:true},
en:{name:"en",title:"英文"},
}
}
/**
* 翻译函数
*
* 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) // 位置变量插值
*
*/
export function 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
}
}
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;i<this.callbacks.length;i++){
if(this.callbacks[i]===callback ){
this.callbacks.splice(i,1)
@ -54,7 +225,7 @@ class i18n{
}
}
}
removeAllListeners(){
offAll(){
this.callbacks=[]
}
_triggerCallback(){
@ -63,95 +234,46 @@ class i18n{
callback.call(this,this.language)
}
})
}
get language(){
return this._language
}
set language(value){
if(value in this.supportedLanguages){
this._language = value
}
/**
* 切换语言
*/
async change(value){
if(value in this.languages){
//
let asyncMsgLoaders = this._scopes.map(scope=>scope[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={
* "<默认语言>":{
* "<id>":"<text>",
* },
* "<语言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