Next.js lingui.js 多语言自动提取翻译键 - ats-node
组件依赖
{
"scripts": {
"i18n:extract": "lingui extract"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
],
"**/*.{weapp,jpg,png}": "node scripts/compress-images.mjs"
},
"dependencies": {
"@lingui/core": "^4.0.0-next.3",
"@lingui/macro": "^4.0.0-next.3",
"@lingui/react": "^4.0.0-next.3",
},
"devDependencies": {
"@lingui/cli": "^4.0.0-next.3",
"@lingui/loader": "^4.0.0-next.3",
"@lingui/swc-plugin": "4.0.3",
"@babel/parser": "^7.26.3",
"@babel/traverse": "^7.26.4",
"@babel/generator": "^7.26.3",
"@babel/types": "^7.26.3",
}
}
脚本代码
scripts/transform-lingui-ast.js
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')
const fs = require('fs')
const path = require('path')
// 判断是否为中文字符
function containsChinese(str) {
return /[\u4e00-\u9fa5]/.test(str)
}
// 创建Trans元素
function createTransElement(text) {
return t.jsxElement(
t.jsxOpeningElement(t.jsxIdentifier('Trans'), [], false),
t.jsxClosingElement(t.jsxIdentifier('Trans')),
[t.jsxText(text)],
false
)
}
// 创建t(i18n)表达式
function createTExpression(text) {
return t.taggedTemplateExpression(
t.callExpression(t.identifier('t'), [t.identifier('i18n')]),
t.templateLiteral(
[t.templateElement({ raw: text, cooked: text }, true)],
[]
)
)
}
// 创建msg表达式
function createMsgExpression(text) {
return t.taggedTemplateExpression(
t.identifier('msg'),
t.templateLiteral(
[t.templateElement({ raw: text, cooked: text }, true)],
[]
)
)
}
// 检查是否是React组件声明
function isReactComponentDeclaration(path) {
// 函数声明组件
if (
path.type === 'FunctionDeclaration' &&
path.node.id &&
/^[A-Z]/.test(path.node.id.name)
) {
return true
}
// 箭头函数组件
if (
path.type === 'VariableDeclarator' &&
path.node.id &&
/^[A-Z]/.test(path.node.id.name)
) {
return true
}
// forwardRef组件
if (
path.type === 'CallExpression' &&
path.node.callee.name === 'forwardRef' &&
path.parent.type === 'VariableDeclarator' &&
/^[A-Z]/.test(path.parent.id.name)
) {
return true
}
// 类组件
if (
path.type === 'ClassDeclaration' &&
path.node.superClass &&
((path.node.superClass.type === 'MemberExpression' &&
path.node.superClass.object.name === 'React' &&
path.node.superClass.property.name === 'Component') ||
(path.node.superClass.type === 'Identifier' &&
path.node.superClass.name === 'Component'))
) {
return true
}
return false
}
// 检查是否在React组件内部
function isInsideReactComponent(path) {
let currentPath = path
while (currentPath) {
// 检查函数声明
if (
currentPath.node.type === 'FunctionDeclaration' &&
currentPath.node.id &&
/^[A-Z]/.test(currentPath.node.id.name)
) {
return true
}
// 检查箭头函数组件
if (
currentPath.node.type === 'VariableDeclarator' &&
currentPath.node.id &&
/^[A-Z]/.test(currentPath.node.id.name)
) {
return true
}
// 检查forwardRef组件
if (
currentPath.node.type === 'ArrowFunctionExpression' &&
currentPath.parent?.type === 'CallExpression' &&
currentPath.parent.callee?.name === 'forwardRef'
) {
return true
}
// 检查类组件
if (
currentPath.node.type === 'ClassDeclaration' &&
currentPath.node.superClass &&
((currentPath.node.superClass.type === 'MemberExpression' &&
currentPath.node.superClass.object.name === 'React' &&
currentPath.node.superClass.property.name === 'Component') ||
(currentPath.node.superClass.type === 'Identifier' &&
currentPath.node.superClass.name === 'Component'))
) {
return true
}
currentPath = currentPath.parentPath
}
return false
}
// 创建导入声明
function createImportDeclaration(specifiers, source) {
return t.importDeclaration(
specifiers.map((name) =>
t.importSpecifier(t.identifier(name), t.identifier(name))
),
t.stringLiteral(source)
)
}
// 创建useLingui hook声明
function createUseLinguiDeclaration() {
return t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern([
t.objectProperty(
t.identifier('i18n'),
t.identifier('i18n'),
false,
true
)
]),
t.callExpression(t.identifier('useLingui'), [])
)
])
}
// 处理单个文件
function transformFile(filePath) {
try {
// 检查文件是否为 TypeScript/React 文件
if (!/\.(tsx?|jsx?)$/.test(filePath)) {
return
}
console.log('处理文件:', filePath)
const sourceCode = fs.readFileSync(filePath, 'utf-8')
// 解析代码生成 AST
const ast = parser.parse(sourceCode, {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'decorators-legacy'],
tokens: true,
attachComment: true
})
// 用于跟踪需要添加的导入和hooks
let needsLinguiMacro = false
let needsLinguiReact = false
let componentsNeedingUseLingui = new Set()
let hasExistingLinguiImports = false
// 第一次遍历:检查现有的导入
traverse(ast, {
ImportDeclaration(path) {
const source = path.node.source.value
if (source === '@lingui/macro' || source === '@lingui/react') {
hasExistingLinguiImports = true
// 移除现有的导入,稍后会重新添加
path.remove()
}
}
})
// 转换JSX中的中文内容
traverse(ast, {
// 处理组件声明
'FunctionDeclaration|VariableDeclarator|CallExpression'(path) {
if (!isReactComponentDeclaration(path)) return
let componentName
if (path.type === 'FunctionDeclaration') {
componentName = path.node.id.name
} else if (path.type === 'VariableDeclarator') {
componentName = path.node.id.name
} else if (
path.type === 'CallExpression' &&
path.node.callee.name === 'forwardRef'
) {
// 获取forwardRef组件的名称
if (path.parent?.type === 'VariableDeclarator') {
componentName = path.parent.id.name
}
}
if (componentName) {
componentsNeedingUseLingui.add(componentName)
needsLinguiReact = true
}
},
// 处理JSX属性中的中文
JSXAttribute: {
exit(path) {
if (!isInsideReactComponent(path)) return
const value = path.node.value
if (
value &&
value.type === 'StringLiteral' &&
containsChinese(value.value)
) {
path.node.value = t.jsxExpressionContainer(
createTExpression(value.value)
)
needsLinguiMacro = true
needsLinguiReact = true
}
}
},
// 处理JSX文本中的中文
JSXText: {
exit(path) {
if (!isInsideReactComponent(path)) return
const text = path.node.value.trim()
if (containsChinese(text) && text.length > 0) {
const parent = path.parent
if (
parent.type === 'JSXElement' &&
parent.openingElement.name.name === 'Trans'
) {
needsLinguiMacro = true // 即使已经是Trans标签,也需要确保导入
return
}
path.replaceWith(createTransElement(path.node.value))
needsLinguiMacro = true
}
}
},
// 检查是否使用了Trans组件
JSXElement(path) {
if (path.node.openingElement.name.name === 'Trans') {
needsLinguiMacro = true
}
},
// 处理字符串字面量
StringLiteral: {
exit(path) {
if (
!path.node.value ||
!containsChinese(path.node.value) ||
path.findParent((p) => p.isImportDeclaration()) ||
path.findParent(
(p) =>
p.isJSXElement() && p.node.openingElement.name.name === 'Trans'
)
) {
if (path.findParent(
(p) =>
p.isJSXElement() && p.node.openingElement.name.name === 'Trans'
)) {
needsLinguiMacro = true // 如果在Trans标签内,也需要确保导入
}
return
}
const isInComponent = isInsideReactComponent(path)
if (isInComponent && path.parent.type === 'JSXAttribute') {
return
}
if (isInComponent) {
path.replaceWith(createTExpression(path.node.value))
needsLinguiMacro = true
needsLinguiReact = true
} else {
path.replaceWith(createMsgExpression(path.node.value))
needsLinguiMacro = true
}
}
}
})
// 第三次遍历:添加useLingui hook到需要的组件
traverse(ast, {
'FunctionDeclaration|ArrowFunctionExpression'(path) {
let componentName
let isForwardRef = false
// 处理普通函数组件
if (path.node.type === 'FunctionDeclaration') {
componentName = path.node.id?.name
}
// 处理箭头函数组件
else if (path.parent?.type === 'VariableDeclarator') {
componentName = path.parent.id?.name
}
// 处理forwardRef组件
else if (
path.parent?.type === 'CallExpression' &&
path.parent.callee?.name === 'forwardRef' &&
path.parent.parent?.type === 'VariableDeclarator'
) {
componentName = path.parent.parent.id?.name
isForwardRef = true
}
if (!componentName || !componentsNeedingUseLingui.has(componentName))
return
const body = path.node.body
if (t.isBlockStatement(body)) {
// 检查是否已经有useLingui声明
const hasUseLingui = body.body.some(
(node) =>
t.isVariableDeclaration(node) &&
node.declarations.some(
(dec) =>
dec.init?.type === 'CallExpression' &&
dec.init.callee.name === 'useLingui'
)
)
if (!hasUseLingui) {
body.body.unshift(createUseLinguiDeclaration())
}
} else if (t.isJSXElement(body)) {
// 如果直接返回JSX,需要包装在代码块中
path.node.body = t.blockStatement([
createUseLinguiDeclaration(),
t.returnStatement(body)
])
}
},
// 专门处理forwardRef的箭头函数组件
CallExpression(path) {
if (
path.node.callee.name === 'forwardRef' &&
path.node.arguments.length > 0 &&
t.isArrowFunctionExpression(path.node.arguments[0])
) {
const arrowFunction = path.node.arguments[0]
const componentName = path.parent?.id?.name
if (!componentName || !componentsNeedingUseLingui.has(componentName))
return
const body = arrowFunction.body
if (t.isBlockStatement(body)) {
// 检查是否已经有useLingui声明
const hasUseLingui = body.body.some(
(node) =>
t.isVariableDeclaration(node) &&
node.declarations.some(
(dec) =>
dec.init?.type === 'CallExpression' &&
dec.init.callee.name === 'useLingui'
)
)
if (!hasUseLingui) {
body.body.unshift(createUseLinguiDeclaration())
}
} else if (t.isJSXElement(body)) {
// 如果直接返回JSX,需要包装在代码块中
arrowFunction.body = t.blockStatement([
createUseLinguiDeclaration(),
t.returnStatement(body)
])
}
}
}
})
// 添加必要的导入语句
const imports = []
if (needsLinguiMacro) {
imports.push(
createImportDeclaration(
['Trans', 'msg', 't', 'Plural'],
'@lingui/macro'
)
)
}
if (needsLinguiReact) {
imports.push(createImportDeclaration(['useLingui'], '@lingui/react'))
}
// 在文件开头添加导入语句
if (imports.length > 0) {
ast.program.body.unshift(...imports)
}
// 生成转换后的代码
const output = generate(ast, {
retainLines: true,
compact: 'auto',
concise: false,
jsescOption: {
minimal: true
},
sourceMaps: false,
comments: true
})
// 直接覆盖源文件
fs.writeFileSync(filePath, output.code)
console.log('已更新文件:', filePath)
} catch (error) {
console.error(`处理文件 ${filePath} 时出错:`, error)
}
}
// 处理文件夹
function transformDirectory(dirPath) {
try {
const files = fs.readdirSync(dirPath)
files.forEach((file) => {
const fullPath = path.join(dirPath, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
// 递归处理子文件夹
transformDirectory(fullPath)
} else {
// 处理文件
transformFile(fullPath)
}
})
} catch (error) {
console.error(`处理文件夹 ${dirPath} 时出错:`, error)
}
}
// 获取命令行参数
const targetPath = process.argv[2]
if (!targetPath) {
console.error('请提供文件或文件夹路径!')
console.log('使用方法: node transform-lingui-ast.js <文件或文件夹路径>')
process.exit(1)
}
// 检查路径是否存在
if (!fs.existsSync(targetPath)) {
console.error('指定的路径不存在!')
process.exit(1)
}
// 判断是文件还是文件夹
const stat = fs.statSync(targetPath)
if (stat.isDirectory()) {
transformDirectory(targetPath)
} else {
transformFile(targetPath)
}
如何使用
执行下面命令,脚本会自动对项目中的中文进行特定标签的包裹
node scripts/transform-lingui-ast.js
愿你走出半生,归来仍是少年

浙公网安备 33010602011771号