update
This commit is contained in:
parent
fd778cff39
commit
ad970ef539
@ -6,15 +6,16 @@ t('no aaaaa')
|
||||
t("no aaaaa")
|
||||
t('no aaaaa')
|
||||
|
||||
t('a1:bbbbb',a,b,c)
|
||||
t('bbbbb',a,b,c)
|
||||
|
||||
t("cccc Arrow Function",()=>{},c)
|
||||
|
||||
|
||||
|
||||
t("dddd中国",c)
|
||||
t("eeeeee", )
|
||||
|
||||
|
||||
|
||||
t("x:from a")
|
||||
|
||||
|
||||
t("中华人民共和国")
|
@ -11,10 +11,8 @@ t('fdgfdgfdg',a,b,c)
|
||||
t("cccc Arrow Function",()=>{},c)
|
||||
|
||||
|
||||
|
||||
t("ddddd中国",c)
|
||||
t("eeeeee", )
|
||||
|
||||
|
||||
|
||||
t("x:from a")
|
||||
t("x::from a")
|
@ -13,4 +13,4 @@ t("ddddd中国",()=>{},c)
|
||||
t("eeeeee")
|
||||
|
||||
t("ddddd中国",()=>{},c)
|
||||
t("x:from b")
|
||||
t("x::from b")
|
@ -6,4 +6,4 @@ t('cccccdc')
|
||||
|
||||
|
||||
|
||||
t("x:from c",ddd)
|
||||
t("x::from c1",ddd)
|
@ -17,4 +17,4 @@ t("eeeeee", )
|
||||
|
||||
|
||||
|
||||
t("x:from a")
|
||||
t("x::from c2")
|
5
demodata/c/h1.html
Normal file
5
demodata/c/h1.html
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
<a data-i18n="html-a" src="sdds" ></a>
|
||||
<img src="sdds" data-i18n="html-b" />
|
||||
<img data-i18n="html-c" src="sdds" />
|
@ -1,3 +0,0 @@
|
||||
export default {
|
||||
"确定":"OK" // a/b/b1.js
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"gulp": "^4.0.2",
|
||||
"logsets": "^1.0.2",
|
||||
"logsets": "^1.0.6",
|
||||
"readjson": "^2.2.2",
|
||||
"through2": "^4.0.2"
|
||||
}
|
||||
|
105
pnpm-lock.yaml
generated
105
pnpm-lock.yaml
generated
@ -3,14 +3,14 @@ lockfileVersion: 5.3
|
||||
specifiers:
|
||||
deepmerge: ^4.2.2
|
||||
gulp: ^4.0.2
|
||||
logsets: ^1.0.2
|
||||
logsets: ^1.0.6
|
||||
readjson: ^2.2.2
|
||||
through2: ^4.0.2
|
||||
|
||||
dependencies:
|
||||
deepmerge: 4.2.2
|
||||
gulp: 4.0.2
|
||||
logsets: registry.npmmirror.com/logsets/1.0.2
|
||||
logsets: 1.0.6
|
||||
readjson: registry.npmmirror.com/readjson/2.2.2
|
||||
through2: 4.0.2
|
||||
|
||||
@ -195,6 +195,14 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/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:
|
||||
@ -270,7 +278,7 @@ packages:
|
||||
readdirp: 2.2.1
|
||||
upath: 1.2.0
|
||||
optionalDependencies:
|
||||
fsevents: registry.npmmirror.com/fsevents/1.2.13
|
||||
fsevents: 1.2.13
|
||||
dev: false
|
||||
|
||||
/class-utils/0.3.6:
|
||||
@ -587,6 +595,12 @@ packages:
|
||||
resolution: {integrity: sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=}
|
||||
dev: false
|
||||
|
||||
/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'}
|
||||
@ -679,6 +693,18 @@ packages:
|
||||
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
||||
dev: false
|
||||
|
||||
/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
|
||||
|
||||
/function-bind/1.1.1:
|
||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||
dev: false
|
||||
@ -1176,6 +1202,15 @@ packages:
|
||||
strip-bom: 2.0.0
|
||||
dev: false
|
||||
|
||||
/logsets/1.0.6:
|
||||
resolution: {integrity: sha512-tTtHpJ2pEVC3goIfqswDIaY5wwImPpfkfdKAcuQ6F6KdaDW3HvftL3X5WyFYAn02Ig/MPnLzgd/6DPm+ewFr3g==}
|
||||
dependencies:
|
||||
'@babel/runtime-corejs3': registry.npmmirror.com/@babel/runtime-corejs3/7.17.2
|
||||
ansicolor: registry.npmmirror.com/ansicolor/1.1.100
|
||||
core-js: registry.npmmirror.com/core-js/3.21.1
|
||||
deepmerge: registry.npmmirror.com/deepmerge/4.2.2
|
||||
dev: false
|
||||
|
||||
/make-iterator/1.0.1:
|
||||
resolution: {integrity: sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -1247,6 +1282,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'}
|
||||
@ -2125,16 +2166,6 @@ packages:
|
||||
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/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
|
||||
@ -2156,60 +2187,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/get-own-enumerable-property-symbols/3.0.2:
|
||||
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz}
|
||||
name: get-own-enumerable-property-symbols
|
||||
version: 3.0.2
|
||||
dev: false
|
||||
|
||||
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/logsets/1.0.2:
|
||||
resolution: {integrity: sha512-iA1FVnA89QeicyT3g3YtrWcoNIhwVIOalSAj507VJ2OXKvAMKrfsK/OFv5pCz8NOhWV1Vlo5jZUlPV9dUsrOJw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/logsets/-/logsets-1.0.2.tgz}
|
||||
name: logsets
|
||||
version: 1.0.2
|
||||
dependencies:
|
||||
'@babel/runtime-corejs3': registry.npmmirror.com/@babel/runtime-corejs3/7.17.2
|
||||
ansicolor: registry.npmmirror.com/ansicolor/1.1.100
|
||||
core-js: registry.npmmirror.com/core-js/3.21.1
|
||||
deepmerge: registry.npmmirror.com/deepmerge/4.2.2
|
||||
get-own-enumerable-property-symbols: registry.npmmirror.com/get-own-enumerable-property-symbols/3.0.2
|
||||
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
|
||||
|
2
src/compile.js
Normal file
2
src/compile.js
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
@ -1,32 +1,39 @@
|
||||
/**
|
||||
* 从源文件中提取要翻译的文本
|
||||
*
|
||||
* Gulp插件,用来提取指定文件夹中的翻译文本并输出到指定目录
|
||||
*
|
||||
* 本插件需要配合gulp.src(...)使用
|
||||
*
|
||||
*/
|
||||
|
||||
const through2 = require('through2')
|
||||
const deepmerge = require("deepmerge")
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const readJson = require("readjson")
|
||||
const { deepStrictEqual } = require('assert')
|
||||
const createLogger = require("logsets")
|
||||
|
||||
// 捕获翻译函数的表达式
|
||||
const DefaultTranslateMatcher = /\bt\(\s*("|'){1}(?:((?<namespace>\w+):))?(?<text>.*?)(((\1\s*\)){1})|((\1){1}\s*(,(\w|\d|(?:\{.*\})|(?:\[.*\])|([\"\'\(].*[\"\'\)]))*)*\s*\)))/gm
|
||||
const logger = createLogger()
|
||||
|
||||
// 捕获翻译文本的默认正则表达式
|
||||
const DefaultTranslateExtractor = String.raw`\b{funcName}\(\s*("|'){1}(?:((?<namespace>\w+)::))?(?<text>.*?)(((\1\s*\)){1})|((\1){1}\s*(,(\w|\d|(?:\{.*\})|(?:\[.*\])|([\"\'\(].*[\"\'\)]))*)*\s*\)))`
|
||||
|
||||
// 从html文件标签中提取翻译文本
|
||||
const DefaultHtmlAttrExtractor = String.raw`\<(?<tagName>\w+)(.*?)(?<i18nKey>{attrName}\s*\=\s*)([\"\'']{1})(?<text>.*?)(\4){1}\s*(.*?)(\>|\/\>)`
|
||||
|
||||
// 获取指定文件的名称空间
|
||||
/**
|
||||
*
|
||||
* @param {*} file
|
||||
* @param {*} namespaces 名称空间配置 {<name>:[path,...,path],<name>:path,<name>:(file)=>{}}
|
||||
* @param {*} options.namespaces 名称空间配置 {<name>:[path,...,path],<name>:path,<name>:(file)=>{}}
|
||||
*/
|
||||
function getFileNamespace(file,options){
|
||||
const {output, namespaces } = options
|
||||
const refPath = file.relative.toLowerCase()
|
||||
const refPath = file.relative.toLowerCase() // 当前文件相对源文件夹的路径
|
||||
for(let [name,paths] of Object.entries(options.namespaces)){
|
||||
if(typeof paths === "string"){
|
||||
paths = [paths]
|
||||
}
|
||||
for(let path of paths){
|
||||
if(refPath.startsWith(path.toLowerCase())){
|
||||
if(typeof(path) === "string" && refPath.startsWith(path.toLowerCase())){
|
||||
return name
|
||||
}else if(typeof path === "function" && path(file)===true){
|
||||
return name
|
||||
}
|
||||
}
|
||||
@ -34,28 +41,22 @@ const DefaultTranslateMatcher = /\bt\(\s*("|'){1}(?:((?<namespace>\w+):))?(?<te
|
||||
return "default"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 找出要翻译的文本列表 {namespace:[text,text],...}
|
||||
* {namespace:{text:{cn:"",en:"",$source:""},...}
|
||||
* 使用正则表达式提取翻译文本
|
||||
* @param {*} content
|
||||
* @param {*} matcher
|
||||
* @returns
|
||||
* @param {*} file
|
||||
* @param {*} options
|
||||
* @returns {namespace:{text:{cn:"",en:"",...,$file:""},text:{cn:"",en:"",...,$file:""}}
|
||||
*/
|
||||
function getTranslateTexts(content,file,options){
|
||||
function extractTranslateTextUseRegexp(content,namespace,extractor,file,options){
|
||||
|
||||
let { matcher,languages,defaultLanguage } = options
|
||||
let { languages,defaultLanguage } = options
|
||||
|
||||
if(!matcher) return
|
||||
|
||||
// 获取当前文件的名称空间
|
||||
const namespace = getFileNamespace(file,options)
|
||||
|
||||
let texts = {default:{}}
|
||||
while ((result = matcher.exec(content)) !== null) {
|
||||
let texts = {}
|
||||
while ((result = extractor.exec(content)) !== null) {
|
||||
// 这对于避免零宽度匹配的无限循环是必要的
|
||||
if (result.index === matcher.lastIndex) {
|
||||
matcher.lastIndex++;
|
||||
if (result.index === extractor.lastIndex) {
|
||||
extractor.lastIndex++;
|
||||
}
|
||||
const text = result.groups.text
|
||||
if(text){
|
||||
@ -65,8 +66,8 @@ function getTranslateTexts(content,file,options){
|
||||
}
|
||||
texts[ns][text] ={}
|
||||
languages.forEach(language=>{
|
||||
if(language !== defaultLanguage){
|
||||
texts[ns][text][language] = ""
|
||||
if(language.name !== defaultLanguage){
|
||||
texts[ns][text][language.name] = ""
|
||||
}
|
||||
})
|
||||
texts[ns][text]["$file"]=[file.relative]
|
||||
@ -75,29 +76,277 @@ function getTranslateTexts(content,file,options){
|
||||
return texts
|
||||
}
|
||||
|
||||
|
||||
function mergeLanguageFile(langFile,texts,options){
|
||||
|
||||
|
||||
/**
|
||||
* 使用函数提取器
|
||||
* @param {*} content
|
||||
* @param {*} namespace
|
||||
* @param {*} extractor 函数提取器(content,file,options)
|
||||
* @param {*} file
|
||||
* @param {*} options
|
||||
* @returns {}
|
||||
*/
|
||||
function extractTranslateTextUseFunction(content,namespace,extractor,file,options){
|
||||
let texts = extractor(content,file,options)
|
||||
if(typeof(texts)==="object"){
|
||||
return texts
|
||||
}else{
|
||||
return {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = function(options={}){
|
||||
/**
|
||||
*
|
||||
* 返回指定文件类型的提取器
|
||||
* @param {*} filetype 文件扩展名
|
||||
* @param {*} extractor 提取器配置={default:[],js:[],html:[],"sass,css":[],json:[],"*":[]}"}
|
||||
*/
|
||||
function getFileTypeExtractors(filetype,extractor){
|
||||
if(!typeof(extractor)==="object") return null
|
||||
let matchers=[]
|
||||
for(let [key,value] of Object.entries(extractor)){
|
||||
if(filetype.toLowerCase()===key.toLowerCase()){
|
||||
matchers = value
|
||||
break
|
||||
}else if(key.split(",").includes(filetype)){
|
||||
matchers = value
|
||||
break
|
||||
}
|
||||
}
|
||||
// * 适用于所有文件类型
|
||||
if("*" in extractor){
|
||||
matchers = matchers.concat(extractor["*"])
|
||||
}
|
||||
// 如果没有指定提取器,则使用默认提取器
|
||||
if(matchers.length===0){
|
||||
matchers = extractor["default"]
|
||||
}
|
||||
return matchers
|
||||
}
|
||||
/**
|
||||
* 找出要翻译的文本列表 {namespace:[text,text],...}
|
||||
* {namespace:{text:{cn:"",en:"",$source:""},...}
|
||||
* @param {*} content
|
||||
* @param {*} extractor
|
||||
* @returns
|
||||
*/
|
||||
function getTranslateTexts(content,file,options){
|
||||
|
||||
let { extractor: extractorOptions,languages,defaultLanguage,activeLanguage,debug } = options
|
||||
|
||||
if(!options || Object.keys(extractorOptions).length===0) return
|
||||
|
||||
// 获取当前文件的名称空间
|
||||
const namespace = getFileNamespace(file,options)
|
||||
const fileExtName = file.extname.substr(1).toLowerCase() // 文件扩展名
|
||||
|
||||
let texts = {}
|
||||
// 提取器
|
||||
let useExtractors = getFileTypeExtractors(fileExtName,extractorOptions)
|
||||
|
||||
// 分别执行所有提取器并合并提取结果
|
||||
return useExtractors.reduce((preTexts,extractor)=>{
|
||||
let matchedTexts = {} , extractFunc = ()=>{}
|
||||
if(extractor instanceof RegExp){
|
||||
extractFunc = extractTranslateTextUseRegexp
|
||||
}else if(typeof(extractor) === "function"){
|
||||
extractFunc = extractTranslateTextUseFunction
|
||||
}else{
|
||||
return preTexts
|
||||
}
|
||||
try{
|
||||
matchedTexts = extractFunc(content,namespace,extractor,file,options)
|
||||
}catch(e){
|
||||
console.error(`Extract translate text has occur error from ${file.relative}:${extractor.toString()}.`,e)
|
||||
}
|
||||
return deepmerge(preTexts,matchedTexts)
|
||||
},texts)
|
||||
}
|
||||
|
||||
|
||||
function normalizeLanguageOptions(options){
|
||||
options = Object.assign({
|
||||
debug : true, // 输出调试信息
|
||||
format : "js", // 目标文件格式,取值JSON,JS
|
||||
languages : ["en","cn"], // 目标语言列表
|
||||
defaultLanguage: "cn", // 默认语言
|
||||
matcher : DefaultTranslateMatcher, // 匹配翻译函数并提取内容的正则表达式
|
||||
debug : true, // 输出调试信息,控制台输出相关的信息
|
||||
languages : [ // 支持的语言列表
|
||||
{name:"en",title:"英文"},
|
||||
{name:"cn",title:"中文",active:true,default:true} // 通过default指定默认语言
|
||||
],
|
||||
defaultLanguage: "cn", // 默认语言:指的是在源代码中的原始文本语言
|
||||
activeLanguage : "cn", // 当前激活语言:指的是当前启用的语言,比如在源码中使用中文,在默认激活的是英文
|
||||
extractor : { // 匹配翻译函数并提取内容的正则表达式
|
||||
//default : DefaultTranslateExtractor,
|
||||
"*" : DefaultTranslateExtractor,
|
||||
"html,vue,jsx" : DefaultHtmlAttrExtractor
|
||||
},
|
||||
namespaces : {}, // 命名空间, {[name]: [path,...,path]}
|
||||
output : null, // 输出目录,如果没有指定,则转让
|
||||
merge : true, // 输出文本时默认采用合并更新方式
|
||||
output : {
|
||||
path : null, // 输出目录,如果没有指定则输出到原目录/languages
|
||||
// 输出文本时默认采用合并更新方式,当重新扫描时输出时可以用来保留已翻译的内容
|
||||
// 0 - overwrite 覆盖模式,可能导致翻译了一半的原始内容丢失(不推荐),
|
||||
// 1 - merge 合并,尽可能保留原来已翻译的内容
|
||||
// 2 - sync 同步, 在合并基础上,如果文本已经被删除,则同步移除原来的内容
|
||||
updateMode : 'sync',
|
||||
},
|
||||
// 以下变量会被用来传递给提取器正则表达式
|
||||
translation : {
|
||||
funcName : "t", // 翻译函数名称
|
||||
attrName :"data-i18n", // 用在html组件上的翻译属性名称
|
||||
}
|
||||
},options)
|
||||
let {debug,output:outputPath,languages} = options
|
||||
// 输出语言文件 {cn:{default:<文件>,namespace:<文件>},en:{default:{}}}
|
||||
let outputFiles = languages.map(language=>{})
|
||||
// 输出配置
|
||||
if(typeof(options.output)==="string"){
|
||||
options.output = {path:options.output,updateMode: 'sync'}
|
||||
}else{
|
||||
options.output = Object.assign({},{updateMode: 'sync',path:null},options.output)
|
||||
}
|
||||
// 语言配置 languages = [{name:"en",title:"英文"},{name:"cn",title:"中文",active:true,default:true}]
|
||||
if(!Array.isArray(options.languages)){
|
||||
throw new TypeError("options.languages must be an array")
|
||||
}else{
|
||||
if(options.languages.length === 0) throw new TypeError("options.languages'length must be greater than 0")
|
||||
let defaultLanguage = options.defaultLanguage
|
||||
let activeLanguage = options.activeLanguage
|
||||
options.languages = options.languages.map(item=>{
|
||||
let language = item
|
||||
if(typeof item === "string"){
|
||||
return {name:item,title:item}
|
||||
}else if(typeof item === "object"){
|
||||
return Object.assign({name:"",title:""},item)
|
||||
}
|
||||
if(typeof(item.title)==="string" && item.title.trim().length===0){
|
||||
item.title = item.name
|
||||
}
|
||||
// 默认语言
|
||||
if(item.default===true && item.name){
|
||||
defaultLanguage = item.name
|
||||
}
|
||||
// 激活语言
|
||||
if(item.active ===true && item.name){
|
||||
activeLanguage = item.name
|
||||
}
|
||||
return item
|
||||
})
|
||||
if(!defaultLanguage) defaultLanguage = options.languages[0].name
|
||||
options.defaultLanguage = defaultLanguage
|
||||
options.activeLanguage = activeLanguage
|
||||
}
|
||||
// 提取正则表达式匹配
|
||||
if(typeof(options.extractor)==="string") options.extractor = new RegExp(options.extractor,"gm")
|
||||
if(options.extractor instanceof RegExp){
|
||||
options.extractor = {default: [options.extractor] } // 默认文件类型的匹配器
|
||||
}
|
||||
// extractor = {default:[regexp,regexp,...],js:[regexp,regexp,...],json:[regexp,regexp,...],"jsx,ts":[regexp,regexp,...],"*":[regexp,regexp,...],...}"}
|
||||
// 提取器可以是:正则表达式字符串、正则表达式或者是函数
|
||||
if(typeof(options.extractor)==="object"){
|
||||
if(Object.keys(options.extractor).length === 0){
|
||||
throw new TypeError("options.extractor must be an object with at least one key")
|
||||
}
|
||||
Object.entries(options.extractor).forEach(([filetype,value])=>{
|
||||
if(!Array.isArray(value)) value = [value]
|
||||
value= value.map(item=>{
|
||||
if(typeof(item)==="string"){ // 如果是字符串,则支持插值变量后,转换为正则表达式
|
||||
return new RegExp(item.params(options.translation),"gm")
|
||||
}else if(item instanceof RegExp){
|
||||
return item
|
||||
}else if(typeof(item)==="function"){
|
||||
return item
|
||||
}
|
||||
})
|
||||
options.extractor[filetype] = value
|
||||
})
|
||||
if(("*" in options.extractor) && options.extractor["*"].length===0) options.extractor["*"] = []
|
||||
if(("default" in options.extractor) && options.extractor["default"].length===0) options.extractor["default"] = [DefaultTranslateExtractor]
|
||||
}else{
|
||||
options.extractor= {default:[ DefaultTranslateExtractor ]}
|
||||
}
|
||||
// 名称空间
|
||||
if(typeof(options.namespaces)!=="object"){
|
||||
throw new TypeError("options.namespaces must be an object")
|
||||
}else{
|
||||
Object.entries(options.namespaces).forEach(([name,paths])=>{
|
||||
if(!Array.isArray(paths)) paths = [paths]
|
||||
if(typeof(name)==="string" && name.trim().length>0){
|
||||
options.namespaces[name] = paths
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
logger.log("Supported languages\t: {}",options.languages.map(item=>`${item.title}(${item.name})`))
|
||||
logger.log("Default language\t: {}",options.defaultLanguage)
|
||||
logger.log("Active language\t\t: {}",options.activeLanguage)
|
||||
logger.log("Language namespaces\t: {}",Object.keys(options.namespaces).join(","))
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
合并更新语言文件
|
||||
|
||||
当使用extract提取到待翻译内容并保存到languages目标文件夹后
|
||||
翻译人员就可以对该文件夹内容进行翻译
|
||||
接下来,如果源码更新后,重新进行扫描extract并重新生成语言文件
|
||||
此时,需要将重新扫描后的文件合并到已经翻译了一半的内容,以保证翻译的内容不会丢失
|
||||
|
||||
|
||||
*/
|
||||
function updateLanguageFile(fromTexts,toLangFile,options){
|
||||
const { output:{ updateMode } } = options
|
||||
|
||||
// 默认的overwrite
|
||||
if(!["merge","sync"].includes(updateMode)){
|
||||
fs.writeFileSync(toLangFile,JSON.stringify(targetTexts,null,4))
|
||||
return
|
||||
}
|
||||
let targetTexts = {}
|
||||
// 读取原始翻译文件
|
||||
try{
|
||||
targetTexts = readJson.sync(toLangFile)
|
||||
}catch(e){
|
||||
logger.log("Error while read language file <{}>: {}",toLangFile,e.message)
|
||||
// 如果读取出错,可能是语言文件不是有效的json文件,则备份一下
|
||||
}
|
||||
// 同步模式下,如果原始文本在新扫描的内容中,则需要删除
|
||||
if(updateMode==="sync"){
|
||||
Object.keys(targetTexts).forEach((text)=>{
|
||||
if(!(text in fromTexts)){
|
||||
delete targetTexts[text]
|
||||
}
|
||||
})
|
||||
}
|
||||
Object.entries(fromTexts).forEach(([text,sourceLangs])=>{
|
||||
if(text in targetTexts){ // 合并
|
||||
let targetLangs = targetTexts[text] //{cn:'',en:''}
|
||||
Object.entries(sourceLangs).forEach(([langName,sourceText])=>{
|
||||
if(langName.startsWith("$")) return //
|
||||
const langExists = langName in targetLangs
|
||||
const targetText = targetLangs[langName]
|
||||
// 如果目标语言已经存在并且内容不为空,则不需要更新
|
||||
if(!langExists){
|
||||
targetLangs[langName] = sourceText
|
||||
}else if(typeof(targetText) === "string" && targetText.trim().length==0){
|
||||
targetLangs[langName] = sourceText
|
||||
}else if(Array.isArray(targetText)){// 当文本内容支持复数时,可以用[单数文本,复数文本]的形式
|
||||
if(targetText.length>0){
|
||||
targetLangs[langName] = sourceText
|
||||
}else{
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}else{
|
||||
targetTexts[text] = sourceLangs
|
||||
}
|
||||
})
|
||||
fs.writeFileSync(toLangFile,JSON.stringify(targetTexts,null,4))
|
||||
}
|
||||
module.exports = function(options={}){
|
||||
options = normalizeLanguageOptions(options)
|
||||
let {debug,output:{ path:outputPath, updateMode },languages} = options
|
||||
|
||||
// 保存提交提取的文本 = {}
|
||||
let results = {}
|
||||
let fileCount=0 // 文件总数
|
||||
// file == vinyl实例
|
||||
return through2.obj(function(file, encoding, callback){
|
||||
// 如果没有指定输出路径,则默认输出到<原文件夹/languages>
|
||||
@ -114,26 +363,49 @@ module.exports = function(options={}){
|
||||
}
|
||||
|
||||
// 提取翻译文本
|
||||
try{
|
||||
const texts = getTranslateTexts(file.contents.toString(),file,options)
|
||||
results = deepmerge(results,texts)
|
||||
fileCount++
|
||||
if(debug){
|
||||
const textCount = Object.values(texts).reduce((sum,item)=>sum+Object.keys(item).length,0)
|
||||
logger.log("Extract <{}>, found [{}] namespaces and {} texts.",file.relative,Object.keys(texts).join(),textCount)
|
||||
}
|
||||
}catch(err){
|
||||
logger.log("Error while extract text from <{}> : {}",file.relative,err.message)
|
||||
}
|
||||
|
||||
callback()
|
||||
},function(callback){
|
||||
|
||||
console.log("输出路径:",outputPath)
|
||||
logger.log("")
|
||||
logger.log("Extracting finished.")
|
||||
logger.log(" - Total of files\t: {}",fileCount)
|
||||
logger.log(" - Output location\t: {}",outputPath)
|
||||
const translatesPath = path.join(outputPath,"translates")
|
||||
if(!fs.existsSync(translatesPath)) fs.mkdirSync(translatesPath)
|
||||
|
||||
// 每个名称空间对应一个文件
|
||||
for(let [namespace,texts] of Object.entries(results)){
|
||||
const langFile = path.join(outputPath,"translates",`${namespace}.json`)
|
||||
const isExists = fs.existsSync(langFile)
|
||||
const langTexts = isExists ? readJson.sync(langFile) : {}
|
||||
if(isExists && options.merge){
|
||||
mergeLanguageFile(langFile,langTexts,options)
|
||||
const langTexts = {}
|
||||
if(isExists){
|
||||
updateLanguageFile(texts,langFile,options)
|
||||
logger.log(" Update language file : {}",path.relative(outputPath,langFile))
|
||||
}else{
|
||||
fs.writeFileSync(langFile,JSON.stringify(texts,null,4))
|
||||
logger.log(" Save language file : {}",path.relative(outputPath,langFile))
|
||||
}
|
||||
}
|
||||
// 将元数据生成到 i18n.meta.json
|
||||
const metaFile = path.join(outputPath,"i18n.meta.json")
|
||||
const meta = {
|
||||
languages : options.languages,
|
||||
defaultLanguage: options.defaultLanguage,
|
||||
activeLanguage : options.activeLanguage,
|
||||
namespaces : options.namespaces
|
||||
}
|
||||
fs.writeFileSync(metaFile,JSON.stringify(meta,null,4))
|
||||
logger.log(" - Generate language metadata : {}",metaFile)
|
||||
callback()
|
||||
});
|
||||
}
|
@ -5,12 +5,22 @@ const path = require('path');
|
||||
|
||||
const soucePath = path.join(__dirname,'../demodata')
|
||||
|
||||
|
||||
|
||||
gulp.src([
|
||||
soucePath+ '/**',
|
||||
"!"+ soucePath+ '/languages/**'
|
||||
]).pipe(extract({
|
||||
debug:true,
|
||||
// output: path.join(soucePath , 'languages'),
|
||||
languages: ['en','cn','de','jp'],
|
||||
languages: [{name:'en',title:"英文"},{name:'cn',title:"中文",default:true},{name:'de',title:"德语"},{name:'jp',title:"日本語"}],
|
||||
// extractor:{
|
||||
// default:[new RegExp()], // 默认匹配器,当文件类型没有对应的提取器时使用
|
||||
// "*" : [new RegExp()], // 所有类型均会执行的提取器
|
||||
// js:new RegExp(), // 只有一个正则表达式,js文件提取正则表达式
|
||||
// html:[new RegExp(),new RegExp()] // 多个表达式可以用数组
|
||||
// "js,jsx":[new RegExp(),(content,file)=>{...})] // 提取器也可以是一个函数,传入文件和文件内容,返回提取结果
|
||||
// },
|
||||
namespaces:{
|
||||
"a":"a",
|
||||
"b":"b",
|
||||
|
157
src/index.js
157
src/index.js
@ -0,0 +1,157 @@
|
||||
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")){
|
||||
|
||||
String.prototype.params=function (params) {
|
||||
let result=this.valueOf()
|
||||
if(typeof params === "object"){
|
||||
for(let name in params){
|
||||
result=result.replace("{"+ name +"}",params[name])
|
||||
}
|
||||
}else{
|
||||
let i=0
|
||||
for(let match of result.match(ParamRegExp) || []){
|
||||
if(i<arguments.length){
|
||||
result=result.replace(match,arguments[i])
|
||||
i+=1
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class i18n{
|
||||
static instance = null; // 单例引用
|
||||
_language = "cn" // 当前语言
|
||||
defaultLanguage = "cn"
|
||||
supportedLanguages = ["cn","en"] // 支持的语言
|
||||
builtInLanguages = ["cn","en"] // 内置语言
|
||||
messages = {}
|
||||
callbacks = [] // 当切换语言时的回调事件
|
||||
constructor(){
|
||||
if(i18n.instance==null){
|
||||
this.reset()
|
||||
i18n.instance = this;
|
||||
}
|
||||
return i18n.instance;
|
||||
}
|
||||
addListener(callback){
|
||||
this.callbacks.push(callback)
|
||||
}
|
||||
removeListener(callback){
|
||||
for(let i=0;i<this.callbacks.length;i++){
|
||||
if(this.callbacks[i]===callback ){
|
||||
this.callbacks.splice(i,1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
removeAllListeners(){
|
||||
this.callbacks=[]
|
||||
}
|
||||
_triggerCallback(){
|
||||
this.callbacks.forEach(callback=>{
|
||||
if(typeof(callback)=="function"){
|
||||
callback.call(this,this.language)
|
||||
}
|
||||
})
|
||||
}
|
||||
get language(){
|
||||
return this._language
|
||||
}
|
||||
set language(value){
|
||||
if(value in this.supportedLanguages){
|
||||
this._language = 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)
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 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) // 位置变量插值
|
||||
*
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const i18nInstance = new i18n()
|
||||
|
||||
export default i18nInstance
|
Loading…
x
Reference in New Issue
Block a user