fes-locale-gen
Advanced tools
Comparing version 1.0.8 to 1.0.9-beta.1
{ | ||
"name": "fes-locale-gen", | ||
"version": "1.0.8", | ||
"version": "1.0.9-beta.1", | ||
"description": "", | ||
@@ -13,3 +13,6 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"dev": "node ./src/locale -d example/source/pages", | ||
"translate": "node ./src/locale translate", | ||
"list": "node ./src/locale config list" | ||
}, | ||
@@ -20,2 +23,3 @@ "dependencies": { | ||
"@babel/traverse": "^7.25.7", | ||
"@babel/types": "^7.25.9", | ||
"@vue/compiler-sfc": "^3.5.12", | ||
@@ -22,0 +26,0 @@ "dotenv": "^16.4.5", |
@@ -87,3 +87,14 @@ # FES 国际化自动脚本工具 | ||
- template中的表达式,如`<p>{{ row.compare === 1 ? '是' : '否' }}</p>` | ||
- 待补充... | ||
- 指令中的插值,例如 | ||
``` | ||
:label="`${variable}`" | ||
:rules="[ | ||
{ | ||
validator: (rule, value) => { | ||
return true | ||
}, | ||
trigger: ['blur', 'change'], | ||
message: `${test}工作流名称需以字母开头,允许字母、数字、下划线,不超过 128 字符` | ||
} | ||
]" | ||
``` |
@@ -5,3 +5,3 @@ #!/usr/bin/env node | ||
Object.keys(require.cache).forEach(function (key) { delete require.cache[key] }); | ||
// const { parse: parseVue } = require("@vue/compiler-sfc") | ||
const { parse: parseVue } = require("@vue/compiler-sfc") | ||
const fs = require('fs'); | ||
@@ -17,2 +17,3 @@ const { parse } = require('vue-eslint-parser'); | ||
const { OpenAI } = require('openai'); | ||
const t = require('@babel/types'); | ||
@@ -71,3 +72,3 @@ // 解析命令行参数 | ||
sourceType: isModule ? 'module' : 'script', | ||
plugins: ['jsx'], | ||
plugins: ['jsx', 'typescript'], | ||
}); | ||
@@ -93,3 +94,3 @@ | ||
StringLiteral(path) { | ||
if (/[\u4e00-\u9fa5]/.test(path.node.value) && | ||
if (path.parent.type !== 'JSXAttribute' && /[\u4e00-\u9fa5]/.test(path.node.value) && | ||
!path.findParent(p => p.isCallExpression() && (p.node.callee.name === '$t' || (p.node.callee.type === 'MemberExpression' && p.node.callee.object.name === 'locale' && p.node.callee.property.name === 't'))) && | ||
@@ -106,6 +107,32 @@ !path.node.value.startsWith('_.$t(') && | ||
end: path.node.end, | ||
text: isJSFile ? `locale.t('_.${escapedText}')` : `$t('_.${escapedText}')` | ||
text: `$t('_.${escapedText}')` | ||
}); | ||
} | ||
}, | ||
// 处理标签属性中的中文字符 | ||
JSXAttribute(path) { | ||
if (t.isJSXIdentifier(path.node.name) && t.isStringLiteral(path.node.value) && /[\u4e00-\u9fa5]/.test(path.node.value.value)) { | ||
const trimmedText = path.node.value.value.trim(); | ||
if (trimmedText && !trimmedText.startsWith('$t(\'_')) { | ||
const escapedText = escapeForTranslation(trimmedText); | ||
replacedTexts.add(escapedText); | ||
const wrappedText = `${path.node.name.name}={$t('_.${escapedText}')}`; | ||
replacements.push({ start: path.node.start, end: path.node.end, text: wrappedText }); | ||
} | ||
} | ||
}, | ||
// 处理标签内的中文字符 | ||
JSXText(path) { | ||
if (/[\u4e00-\u9fa5]/.test(path.node.value) && !path.node.value.startsWith('$t(\'_') ) { | ||
const text = path.node.value.trim(); | ||
const escapedText = escapeForTranslation(text); | ||
replacedTexts.add(escapedText); | ||
const wrappedText = '{`${' + `$t('_.${escapedText}')` + '}`}'; | ||
replacements.push({ | ||
start: path.node.start, | ||
end: path.node.end, | ||
text: wrappedText | ||
}); | ||
} | ||
}, | ||
TemplateLiteral(path) { | ||
@@ -125,3 +152,3 @@ if (!path.findParent((p) => p.isCallExpression() && p.node.callee.type === 'MemberExpression' && p.node.callee.object.name === 'console')) { | ||
end: quasi.end, | ||
text: isJSFile ? `\${locale.t('_.${escapedText}')}` : `\${$t('_.${escapedText}')}` | ||
text: `\${$t('_.${escapedText}')}` | ||
}); | ||
@@ -144,3 +171,3 @@ } | ||
sourceType: isModule ? 'module' : 'script', | ||
plugins: ['jsx'], | ||
plugins: ['jsx', 'typescript'], | ||
}); | ||
@@ -178,40 +205,54 @@ | ||
// 只有在非 JS 文件的情况下才添加 $t 声明 | ||
if (!isJSFile) { | ||
ast = parseJS(code, { | ||
sourceType: isModule ? 'module' : 'script', | ||
plugins: ['jsx'], | ||
}); | ||
ast = parseJS(code, { | ||
sourceType: isModule ? 'module' : 'script', | ||
plugins: ['jsx', 'typescript'], | ||
}); | ||
let hasDollarTDeclaration = false; | ||
let hasDollarTDeclaration = false; | ||
// 第三次遍历:检查是否有使用插件 | ||
traverse(ast, { | ||
ImportDeclaration(path) { | ||
lastImportIndex = Math.max(lastImportIndex, path.node.loc.end.line); | ||
}, | ||
VariableDeclaration(path) { | ||
path.node.declarations.forEach(declaration => { | ||
if (declaration.init && | ||
declaration.init.type === 'CallExpression' && | ||
declaration.init.callee.name === 'useI18n') { | ||
if (declaration.id.type === 'ObjectPattern') { | ||
const tProperty = declaration.id.properties.find(prop => | ||
prop.key.name === 't' && prop.value.name === '$t' | ||
); | ||
if (tProperty) { | ||
hasDollarTDeclaration = true; | ||
} | ||
// 第三次遍历:检查是否有使用插件 | ||
traverse(ast, { | ||
ImportDeclaration(path) { | ||
lastImportIndex = Math.max(lastImportIndex, path.node.loc.end.line); | ||
}, | ||
VariableDeclaration(path) { | ||
path.node.declarations.forEach(declaration => { | ||
if (declaration.init && | ||
declaration.init.type === 'CallExpression' && | ||
declaration.init.callee.name === 'useI18n') { | ||
if (declaration.id.type === 'ObjectPattern') { | ||
const tProperty = declaration.id.properties.find(prop => | ||
prop.key.name === 't' && prop.value.name === '$t' | ||
); | ||
if (tProperty) { | ||
hasDollarTDeclaration = true; | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
// 添加 $t 声明(如果需要) | ||
if (!hasDollarTDeclaration) { | ||
const lines = code.split('\n'); | ||
const insertIndex = lastImportIndex !== -1 ? lastImportIndex + 1 : 0; | ||
lines.splice(insertIndex, 0, `\nconst { t: $t } = useI18n();\n`); | ||
code = lines.join('\n'); | ||
} | ||
// 检查是否有 const $t = locale.t 的代码 | ||
if ( | ||
declaration.id.type === 'Identifier' && | ||
declaration.id.name === '$t' && | ||
declaration.init && | ||
declaration.init.type === 'MemberExpression' && | ||
declaration.init.object.name === 'locale' && | ||
declaration.init.property.name === 't' | ||
) { | ||
hasDollarTDeclaration = true; | ||
} | ||
}); | ||
}, | ||
}); | ||
// 添加 $t 声明(如果需要) | ||
if (!hasDollarTDeclaration) { | ||
const lines = code.split('\n'); | ||
const insertIndex = lastImportIndex !== -1 ? lastImportIndex + 1 : 0; | ||
const text = isJSFile? `\nconst $t = locale.t;\n`: `\nconst { t: $t } = useI18n();\n` | ||
lines.splice(insertIndex, 0, text); | ||
code = lines.join('\n'); | ||
} | ||
@@ -234,4 +275,9 @@ } | ||
handleAttributeNode(attr, replacements); | ||
}else if (attr.value && attr.value.type === 'VExpressionContainer' && t.isConditionalExpression(attr.value.expression)) { | ||
handleConditionExpression(attr.value.expression, replacements); | ||
} | ||
} | ||
} else if (node.type === 'VExpressionContainer' && t.isConditionalExpression(node.expression)) { | ||
handleConditionExpression(node.expression, replacements); | ||
} | ||
@@ -266,2 +312,30 @@ | ||
function handleConditionExpression(node, replacements) { | ||
// 检查字符串是否包含中文 | ||
const containsChinese = (str)=> { | ||
return /[\u4e00-\u9fa5]+/.test(str); | ||
} | ||
// 处理三元运算符 | ||
if (containsChinese(node.consequent.value)) { | ||
const trimmedText = node.consequent.value.trim(); | ||
if (trimmedText && !trimmedText.startsWith('$t(\'_')) { | ||
const escapedText = escapeForTranslation(trimmedText); | ||
replacedTexts.add(escapedText); | ||
const wrappedText = '`${' + `$t('_.${escapedText}')` + '}`'; | ||
replacements.push({ start: node.consequent.range[0], end: node.consequent.range[1], text: `${wrappedText}` }); | ||
} | ||
} | ||
if (containsChinese(node.alternate.value)) { | ||
const trimmedText = node.alternate.value.trim(); | ||
if (trimmedText && !trimmedText.startsWith('$t(\'_')) { | ||
const escapedText = escapeForTranslation(trimmedText); | ||
replacedTexts.add(escapedText); | ||
const wrappedText = '`${' + `$t('_.${escapedText}')` + '}`'; | ||
replacements.push({ start: node.alternate.range[0], end: node.alternate.range[1], text: `${wrappedText}` }); | ||
} | ||
} | ||
} | ||
function singleFileProcessor(filePath) { | ||
@@ -271,3 +345,3 @@ const fileContent = fs.readFileSync(filePath, 'utf-8'); | ||
if (fileExt === '.js' || fileExt === '.jsx') { | ||
if (fileExt === '.js' || fileExt === '.jsx' || fileExt === '.ts' || fileExt === '.tsx') { | ||
const processedContent = processJavaScript(fileContent, true, false, true); | ||
@@ -287,2 +361,11 @@ return { | ||
traverseTemplate(ast.templateBody, replacements); | ||
// 处理template中变量 | ||
const vueParseResult = parseVue(processedContent, { | ||
sourceType: 'module', | ||
}); | ||
if (vueParseResult.descriptor.template && vueParseResult.descriptor.template.ast) { | ||
const templateAst = vueParseResult.descriptor.template.ast; | ||
processTemplateAst(templateAst, replacements); | ||
} | ||
replacements.sort((a, b) => b.start - a.start); | ||
@@ -296,7 +379,2 @@ for (const { start, end, text } of replacements) { | ||
} | ||
// TODO: 处理template中变量 | ||
// const vueParseResult = parseVue(processedContent, { | ||
// sourceType: 'module', | ||
// }); | ||
// console.log('VUE:', vueParseResult.descriptor.template.ast.children[0].props); | ||
// 处理 script 和 script setup | ||
@@ -322,2 +400,45 @@ const scriptMatch = processedContent.match(/<script(\s+setup)?[^>]*>([\s\S]*?)<\/script>/i); | ||
function processTemplateAst(ast, replacements) { | ||
if (ast.props) { | ||
ast.props.forEach(prop => { | ||
if (prop.type === 7) { // 指令 | ||
processDirective(prop, replacements); | ||
} | ||
}); | ||
} | ||
if (ast.children) { | ||
ast.children.forEach(child => { | ||
content = processTemplateAst(child, replacements); | ||
}); | ||
} | ||
} | ||
function processDirective(prop, replacements) { | ||
if (prop.exp && prop.exp.content && !prop.exp.content.includes('$t(')) { | ||
const chineseRegex = /['"]([^'"]*[\u4e00-\u9fa5]+[^'"]*)['"]/g; | ||
let match; | ||
let processedContent = prop.exp.content; | ||
while ((match = chineseRegex.exec(prop.exp.content)) !== null) { | ||
const originalText = match[1]; | ||
const escapedText = escapeForTranslation(originalText); | ||
if (!replacedTexts.has(escapedText)) { | ||
replacedTexts.add(escapedText); | ||
} | ||
processedContent = processedContent.replace( | ||
`'${originalText}'`, | ||
`$t('_.${escapedText}')` | ||
); | ||
} | ||
if (processedContent !== prop.exp.content) { | ||
// 修改content(原文件)中对应内容 | ||
replacements.push({start: prop.exp.loc.start.offset, end: prop.exp.loc.end.offset, text: processedContent}) | ||
} | ||
} | ||
} | ||
async function generateLocaleFile() { | ||
@@ -405,3 +526,3 @@ // 获取用户执行命令时的当前工作目录 | ||
// console.log('dirPath', dirPath, excludedDirList); | ||
const files = glob.sync(path.join(dirPath, '**/*.{vue,js,jsx}').replace(/\\/g, '/'), { | ||
const files = glob.sync(path.join(dirPath, '**/*.{vue,js,jsx,tsx,ts}').replace(/\\/g, '/'), { | ||
ignore: excludedDirList, | ||
@@ -408,0 +529,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
54777
14
737
100
10
3
+ Added@babel/types@^7.25.9