以 locals 文件夹下面存放国际化文件为例,我们将 zh 存放中文为例,下面将演示遍历所有中文文件,并进行翻译,翻译后生成到对应的文件夹下,保持和中文目录下的一致的目录结构。
目录结构如下
├── index.js
├── translate-core
│ ├── google-translate.js
│ ├── index.js
│ └── string.js
└── translate-helper.js
需要的 npm 包如下
"jsonic": "^0.3.1",
"klaw": "^3.0.0",
"recast": "^0.19.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"rimraf": "^3.0.2",
"through2": "^3.0.1"
在使用 Node 脚本对前端国际化初探已经做了翻译 API,的封装,本次将上次内容放到 translate-core 下。
translate-helper 存放了一些工具函数,内容如下
const through2 = require("through2")
const path = require("path")
const fs = require("fs")
const filterFolder = through2.obj(function(item, enc, next) {
// 添加文件夹标识
item.isFolder = false
if (item.stats.isDirectory()) {
item.isFolder = true
}
this.push(item)
next()
})
const filterJsFile = (item) => {
// 这种方式过滤会把文件夹下的js文件过滤掉
const extname = path.extname(item)
return extname === ".js"
}
const filterJs = through2.obj(function(item, enc, next) {
// 过滤所有js文件
if (path.extname(item.path) === ".js") {
this.push(item)
}
next()
})
/**
* 读取路径信息
* @param {string} path 路径
*/
function getStat(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if (err) {
resolve(false)
} else {
resolve(stats)
}
})
})
}
/**
* 创建路径
* @param {string} dir 路径
*/
function mkdir(dir) {
return new Promise((resolve, reject) => {
fs.mkdir(dir, (err) => {
if (err) {
resolve(false)
} else {
resolve(true)
}
})
})
}
/**
* 路径是否存在,不存在则创建
* @param {string} dir 路径
*/
async function dirExists(dir) {
let isExists = await getStat(dir)
//如果该路径且不是文件,返回true
if (isExists && isExists.isDirectory()) {
return true
} else if (isExists) {
//如果该路径存在但是文件,返回false
return false
}
//如果该路径不存在
let tempDir = path.parse(dir).dir //拿到上级路径
//递归判断,如果上级目录也不存在,则会代码会在此处继续循环执行,直到目录存在
let status = await dirExists(tempDir)
let mkdirStatus
if (status) {
mkdirStatus = await mkdir(dir)
}
return mkdirStatus
}
module.exports = {
filterFolder,
filterJsFile,
filterJs,
dirExists,
getStat,
}
index.js 封装了翻译的主体,一些配置项也在这里
更新:添加对 index.js 这中入口文件添加兼容,不做翻译处理
const klaw = require("klaw")
const path = require("path")
const fs = require("fs")
const recast = require("recast")
const { parse, print } = recast
const jsonic = require("jsonic")
const { filterJs, dirExists, getStat } = require("./translate-helper")
const { translate } = require("./translate-core/index")
const rimraf = require("rimraf")
const through2 = require("through2")
// 需要翻译的语言以及翻译后生成的文件夹名
const pathMap = {
en: "en-Us",
ru: "ru-Ru",
vi: "vi-Vi",
}
const baseDir = "zh"
// 需要翻译的文件夹路径
const rootDir = path.resolve(__dirname, "../locals")
let sourceMap = []
// 每次一处旧文件,重新创建新的文件
deleteOldDir()
.then(() => {
// 文件删除完成,开始翻译重建
init()
})
.catch((e) => {
console.error(e)
})
/**
* 读取指定文件夹下的所有文件并过滤出js文件,保存路径和ast到集合中
* 读取是个异步过程
*/
function init() {
klaw(path.join(rootDir))
.pipe(filterJs)
.pipe(filterEntryIndex)
.on("data", function(item) {
const ast = parse(fs.readFileSync(item.path, "utf8"))
sourceMap.push({
path: item.path,
ast,
isEntry: path.basename(item.path, ".js") === "index",
})
})
.on("end", () => {
handleParseObject()
console.log("translate is runing,dont close window......")
sourceMap.forEach((item) => {
Object.keys(pathMap).forEach((key) => {
translateSource(item.path, replaceKeyByZero(item.code), key)
})
})
})
}
/**
* 将字符串astcode转换为对象
*/
function astCodeToObject(content) {
return jsonic(content)
}
/**
* 将ast获取的对象表达式字符串转换为js对象
*/
function getSourceObject(ast) {
let codeObj
recast.visit(ast, {
visitObjectExpression(path) {
const astCode = print(path.node).code
codeObj = astCodeToObject(astCode)
return false
},
})
return codeObj
}
/**
* 批量处理读取的js文件
*/
function handleParseObject() {
sourceMap = sourceMap.map((item) => {
item.code = getSourceObject(item.ast)
return item
})
return sourceMap
}
/**
* 将翻译后的文件写回对应的文件夹下
* @param {*} dir 目录名称
* @param {*} lang 语言选型
* @param {*} result 翻译结果(没有翻译的文件传空字符)
* @param {*} ast ast结果
*/
function rewriteFolder(dir, lang, result, ast) {
let rewritPath = dir.replace(baseDir, pathMap[lang])
let resultPath = path.parse(rewritPath).dir
let sourceCode
if (result) {
sourceCode = `module.exports = ${JSON.stringify(result, null, 2)}`
} else {
sourceCode = ast
}
dirExists(resultPath).then(() => {
fs.writeFileSync(rewritPath, sourceCode)
})
}
/**
* 处理翻译
*/
function translateSource(dir, source, lang) {
translate({ source, target: lang, log: true })
.then((result) => {
rewriteFolder(dir, lang, replaceZeroByKey(result))
})
.catch((e) => {
throw e
})
}
/**
* 删除存在的其他语言文件夹
*/
function deleteOldDir() {
return Object.keys(pathMap).reduce((acc, cur) => {
return acc.then(() => {
const dir = path.join(rootDir, pathMap[cur])
return new Promise((resolve, reject) => {
getStat(dir).then((status) => {
if (status && status.isDirectory()) {
rimraf(dir, (err) => {
if (err) {
console.log(err)
reject()
}
// 文件删除完成
resolve()
})
} else {
resolve()
}
})
})
// })
})
}, Promise.resolve())
}
/**
* 过滤文件夹下index入口文件
*/
const filterEntryIndex = through2.obj(function(item, enc, callback) {
if (path.basename(item.path, ".js") === "index") {
Object.keys(pathMap).forEach((key) => {
const ast = fs.readFileSync(item.path, "utf8")
rewriteFolder(item.path, key, false, ast)
})
} else {
this.push(item)
}
// this.push(chunk);
callback()
})
/**
* 去除文本中的{key},避免翻译格式错误
* @param {*} source
*/
function replaceKeyByZero(source) {
const nokeySource = {}
Object.keys(source).forEach((key) => {
nokeySource[key] = source[key].replace(/{key}/g, "{0}")
})
return nokeySource
}
/**
* 还原文本中的{key}
* @param {*} source
*/
function replaceZeroByKey(source) {
const noZeroSource = {}
Object.keys(source).forEach((key) => {
noZeroSource[key] = source[key].replace(/{[0]*}/g, "{key}")
})
return noZeroSource
}