能够批量找出项目所有汉字,并替换对应语言的语法格式。减轻开发人员痛苦查找汉字过程。
1.vue2,vue3国际化
// 解析vue国际化项目
const fs = require('fs');
const path = require('path')
const babel = require('@babel/core');
let traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default;
const compiler = require('@vue/compiler-sfc');
// 源代码目录
const startPath = '../agents-desktop/src'
const excludeFiles = ['.ts', '.vue', '.js']
// 所有文本数组
let textArr = []
// 预设
const presets = [
'@vue/babel-preset-jsx',
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22"
}
],
"@babel/preset-typescript"
]
function findFiles(dir) {
const files = fs.readdirSync(dir);
const result = [];
for (let i = 0; i < files.length; i++) {
const filePath = path.join(dir, files[i]);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 判断当前目录是否否存在,存在则创建
const dirPath = path.resolve(__dirname, filePath.replace('..\\agents-desktop\\', './').replace(/\\/, '/'))
!fs.existsSync(dirPath) && fs.mkdirSync(dirPath)
// 如果是目录,递归查找
result.push(...findFiles(filePath));
} else {
// 如果是文件,将文件路径添加到结果数组中
if (excludeFiles.includes(path.extname(filePath)))
result.push(filePath);
}
}
return result;
}
// 得到所有匹配到的文件
const files = findFiles(startPath);
files.forEach(a => {
readFile(a)
})
function readFile(filePath) {
let newCode = ''
const extname = path.extname(filePath)
const isVue = extname === '.vue';
const code = fs.readFileSync(filePath, 'utf-8')
if(!code){
return
}
if (isVue) {
// 将代码转化成ast
const result = compiler.parse(code);
const templateContent = result.descriptor.template?.ast;
let content = result.descriptor.template?.content;
// 遍历template中的节点
const templateTextArr = []
function transformAttributes(children = []) {
children.forEach(node => {
// 遍历属性
node.props && node.props.length && node.props.forEach(a => {
// 属性
if (a.type === 6) {
// 属性文本
// 文本节点
const regex = /[\u4e00-\u9fff]+/g;
const value = a.value?.content
if (!value || !regex.test(value)) {
return
}
const scoreCode = getSoucre(`const a = "${value}"`, isTemplate = true).replace('const a = ', '')
templateTextArr.push({
name: a.name,
matchText: a.loc.source,
value: `:${a.name}="${scoreCode.substring(0, scoreCode.length - 1)}"`
})
} else if (a.type === 7) {
// 遍历
const value = a.exp?.content
const regex = /[\u4e00-\u9fff]+/g;
// 匹配汉字
if (!value || !regex.test(value)) {
return
}
const scoreCode = getSoucre(`const a = ${value}`, isTemplate = true).replace('const a = ', '')
templateTextArr.push({
name: a.rawName,
matchText: a.loc.source,
len: a.loc.source.length,
value: `${a.rawName}="${scoreCode.substring(0, scoreCode.length - 1)}"`
})
}
})
if (node.type === 2) {
// 文本节点
const regex = /[\u4e00-\u9fff]+/g;
const value = node.content
// 匹配汉字
if (!regex.test(value)) {
return
}
templateTextArr.push({
matchText: node.loc.source,
length: value.length,
code: value
});
// 可以入库
textArr.push(value);
textArr = [...new Set(textArr)]
} else if (node.type === 5) {
const value = node.content.content
const regex = /[\u4e00-\u9fff]+/g;
// 匹配汉字
if (!regex.test(value)) {
return
}
const scoreCode = getSoucre(`${value}`, isTemplate = true)
templateTextArr.push({
matchText: node.loc.source,
length: value.length,
value: `{{${scoreCode.substring(0, scoreCode.length - 1)}}}`
});
}
node.children && node.children.length && transformAttributes(node.children)
})
}
// 应用修改
content && transformAttributes(templateContent.children)
let arr = templateTextArr.sort((a, b) => {
return a.value || a.length > b.length ? -1 : 1
})
arr.forEach(a => {
if (a.value) {
content = content.replace(a.matchText, a.value)
} else {
content = content.replace(a.matchText, `{{$t('${textArr.indexOf(a.code)}')}}`)
}
})
// 将修改后的AST转换回代码
// 针对vue文件中script部分
const scriptContent = result.descriptor.script?.content || result.descriptor.scriptSetup?.content || ''
// 拼接文档
function mergeContent(template, script, scriptConfig = {}, styles) {
const { attrs = {}, setup } = scriptConfig
delete attrs.setup
let styleContent = ''
styles.forEach(a => {
styleContent += `<style ${a.scoped ? 'scoped' : ''} lang=${a.lang}>${a.content}</style>\n`
})
return `<template>
${template}
</template>
<script ${setup ? 'setup' : ''} ${Object.entries(attrs).reduce((a, b) => (a + (`${b[0]}="${b[1]}" `)), '')}>
import locale from '@/i18n/index.ts';\n
${script}
</script>
${styleContent}
`
}
const newScriptCode = getSoucre(scriptContent, false, result.descriptor?.script?.setup || result.descriptor?.scriptSetup?.setup || false);
newCode = mergeContent(content, newScriptCode, result.descriptor.script || result.descriptor.scriptSetup || {}, result.descriptor.styles)
}else{
// 除了.vue文件之外的处理方式
const appendText = `import locale from '@/i18n/index.ts';\n`
newCode = `${appendText}${getSoucre(code,false)}`
}
function getSoucre(scriptContent, isTemplate = false , isSetup = false) {
const ast = babel.parse(scriptContent, {
filename: filePath,
ast: true,
presets,
plugins: [["transform-vue-jsx", {
"functional": false
}],"@babel/plugin-transform-typescript"],
})
traverse(ast, {
StringLiteral(path) {
const value = path.node.value
// 匹配汉字
const regex = /[\u4e00-\u9fff]+/g;
if (regex.test(value)) {
if(path.parent?.callee?.object?.name === 'console' && path.parent?.callee?.property?.name === 'log'){
return
}
textArr.push(value)
textArr = [...new Set(textArr)]
if(isVue){
path.node.value = `<tt>${isTemplate ? '$' : 'locale.global.'}t('${textArr.indexOf(value)}')<tt>`
}else{
path.node.value = `<tt>locale.global.t('${textArr.indexOf(value)}')<tt>`
}
}
},
TemplateElement(path){
const regex = /[\u4e00-\u9fff]+/g;
const value = path.node.value.raw
if(!value || !regex.test(value)){
return
}
textArr.push(value)
textArr = [...new Set(textArr)]
if(isVue){
path.node.value.raw = `\${${isTemplate ? '$' : 'locale.global.'}t('${textArr.indexOf(value)}')}`
}else{
path.node.value.raw = `\${${`locale.global.t('${textArr.indexOf(value)}')`}\}`
}
}
})
// script代码部分
const scriptCode = generate(ast, {}).code;
const regex1 = /("<tt>|'<tt>|<tt>"|<tt>')/g;
return scriptCode.replace(regex1, '')
}
fs.writeFileSync(`.${filePath.replace('..\\agents-desktop', '').replace(/\\/g, '/')}`, newCode)
// 针对.ts
}
// readFile('./AddTask.vue')
const obj = {}
textArr.forEach((a, b) => {
obj[b] = a
})
fs.writeFileSync('./agents-desktop.json', JSON.stringify(obj))
2.react项目国际化
// 解析react项目
const fs = require('fs');
const path = require('path')
const t = require('@babel/types')
const babel = require('@babel/core');
let traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
// 预设
const presets = [
"@babel/preset-react",
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22"
}
],
"@babel/preset-typescript"
]
// 文本内容数组
let textList = [];
// 示例:读取文件并输出处理后的AST
const dirPath = '../iflygpt-finetune-webapp-front-master/frontend/webfront/src/';
function findFiles(dir) {
const files = fs.readdirSync(dir);
let newDir = dir.replace('..\\iflygpt-finetune-webapp-front-master\\frontend\\webfront\\', '\\').replace(/\\/g, '/')
if (dirPath !== dir) {
const dirPath = path.join(__dirname, newDir)
!fs.existsSync(dirPath) && fs.mkdirSync(dirPath)
}
const result = [];
for (let i = 0; i < files.length; i++) {
const filePath = path.join(dir, files[i]);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 如果是目录,递归查找
result.push(...findFiles(filePath));
} else {
// 如果是文件,将文件路径添加到结果数组中
if (['.ts', '.tsx'].includes(path.extname(filePath)))
result.push(filePath);
}
}
return result;
}
function readFile(filePath) {
const len = textList.length;
let index = 0;
// 读取并解析TSX文件
const code = fs.readFileSync(filePath, 'utf-8')
const extname = path.extname(filePath)
const isTs = extname === '.ts'
// 每个文件前面先引入
const appendText = `
import { FormattedMessage, useIntl } from 'react-intl';\n
`
const appendText1 = `import locale from "@/locales/index.js";\n
`
const ast = babel.parseSync(code, {
ast: true,
filename: 'file.tsx',
presets: presets,
plugins: ["react-intl", "@babel/plugin-transform-react-jsx", "@babel/plugin-transform-typescript"],
})
traverse(ast, {
enter(path) {
const value = path.node.value;
// 匹配汉字
const regex = /[\u4e00-\u9fff]/g;
if (path.node.type === 'StringLiteral' && regex.test(value)) {
textList.push(path.node.value)
textList = [...new Set(textList)]
if(isTs){
path.node.value = `<tt>locale[${textList.indexOf(value)}]<tt>`
return
}
const parent = path.parent;
// 表达式
if (
t.isExpressionStatement(parent) ||
(t.isCallExpression(parent) && parent.arguments.includes(path.node))
) {
path.node.value = `<tt>intl.formatMessage({id:'${textList.indexOf(value)}'})<tt>`
}else{
path.node.value = `<tt>{intl.formatMessage({id:'${textList.indexOf(value)}'})}<tt>`
}
} else if (path.node.type === 'JSXText' && regex.test(value)) {
textList.push(path.node.value)
textList = [...new Set(textList)]
if(isTs){
path.node.value = `<tt>locale[${textList.indexOf(value)}]<tt>`
return
}
path.node.value = `<FormattedMessage id={'${textList.indexOf(value)}'} />`
}
},
CallExpression(path) {
if (
path.node.callee.name === 'useState' && index === 0
) {
index++
// 创建一个变量声明的节点
const variableDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('intl'), // 变量名
t.callExpression( // 函数调用
t.identifier('useIntl '), // 函数名
[] // 参数列表
)
)
]);
path.parentPath.insertAfter(variableDeclaration)
}
}
})
const result = generate(ast, {}).code;
const regex1 = /("<tt>|'<tt>|<tt>"|<tt>')/g
const newLength = textList.length
const newCode = `${isTs ? appendText1 : ''}${newLength === len && !isTs ? "" : appendText}${result.replace(regex1,"")}`
let a = filePath.replace('..\\iflygpt-finetune-webapp-front-master\\frontend\\webfront\\','\\').replace(/\\/g,'/')
fs.writeFileSync(`.${a}`,newCode)
// fs.writeFileSync(`./new.tsx`,newCode)
}
// readFile('./test.tsx')
const fileList = findFiles(dirPath)
fileList.forEach(a=>{
readFile(a)
})
const obj = {}
textList.forEach((a,b)=>{
obj[b] = a
})
fs.writeFileSync('./index.json',JSON.stringify(obj))
- 依赖
"devDependencies": {
"@babel/cli": "^7.24.5",
"@babel/core": "^7.24.5",
"@babel/plugin-transform-typescript": "^7.24.5",
"@babel/preset-env": "^7.24.5",
"@babel/types": "^7.24.5",
"@vue/babel-plugin-jsx": "^1.2.2",
"@vue/babel-preset-app": "^5.0.8",
"@vue/cli-plugin-babel": "^5.0.8",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^9.1.3",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"babel-preset-env": "^1.7.0",
"babel-preset-vue": "^2.0.2"
},
"dependencies": {
"@babel/generator": "^7.24.5",
"@babel/parser": "^7.24.5",
"@babel/plugin-transform-react-jsx": "^7.23.4",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
"@babel/traverse": "^7.24.5",
"@vue/compiler-core": "^3.4.27",
"@vue/compiler-dom": "^3.4.27",
"@vue/compiler-sfc": "^3.4.27",
"babel-plugin-react-intl": "^8.2.25",
"react-intl": "^6.6.6",
"vue-template-compiler": "^2.7.16"
}