主页 > 人工智能  > 

用Babel脚本实现多语言/国际化

用Babel脚本实现多语言/国际化
需求:

在目前代码基础上,找出所有翻译词,并达到在任意地方注册多语言全局生效

框架是nextjs + react + ts(脚本实现与框架无关)

基本思路(朴素):

基于key-value对,给所有翻译词套一个方法(t方法)来获取不同语言下的内容

t方法

现有的库

next自带的i18n

通过路由获取语言环境,同时cookie的优先级更高

支持拼变量(翻译词传入变量)

注意nextjs是app router还是pages router

React-intl

自己写

感觉没那么大需求,不用引入这么重的,直接写个方法然后引入翻译包就行,翻译包的路径可以参考nextjs的i18n

主要工作: 提取翻译词:

对所有需要翻译的文本进行某种形式的包裹。

鉴于已有项目,自动化完成这件事

A. 脚本正则化匹配,比较难匹配全

B. 使用工具如 Babel 插件,自动将匹配的字符串替换为国际化函数调用。包裹完之后是完全我们自己维护或者用现有的库都可以,不冲突。

参考: juejin /post/7112972686392836103(他是中文,不过大致思路应该是一样的)

Babel 插件的工作原理

解析为 AST:Babel 在处理 JavaScript 代码时,会首先将代码解析为抽象语法树(AST)。AST 是代码的结构化表示,允许以编程方式访问和修改代码。遍历和转换:Babel 提供了遍历 AST 的 API,允许开发者编写插件来访问和转换 AST 的特定节点。插件可以在遍历过程中修改节点,添加新的节点,或者提取信息。重新生成代码:经过插件处理后,Babel 将修改后的 AST 转换回 JavaScript 代码。

Babel 插件基本思路

创建一个 Babel 插件: Babel 插件通常是一个 JavaScript 函数,接收 Babel 的 babel 对象作为参数。使用 babel.traverse API 来遍历 AST。 识别和提取文本节点: 在遍历过程中,识别出可能包含硬编码文本的节点类型,比如 StringLiteral 或 TemplateLiteral。过滤掉不需要翻译的文本(例如,变量名、非用户界面文本等)。( 添加忽略注释?注意编译时和运行时,(变量可能要占位符替换,因为只有在运行时才会有 自动替换为翻译函数并输出提取词: 将识别出的硬编码文本替换为国际化函数调用,比如 t('key')。为每个文本生成唯一的翻译键(key),并维护一个外部文件来存储这些键与文本的映射。

匹配Babel的AST树节点的记录:(记得排除node_modules)

JSXText: 都是

StringLiteral:

(有一部分是需要翻译的,类似写在文件最前面的key-value的const文本) 对于一些有键值的静态字面量,(通常是自己写的map),父节点是ObjectProperty,通过查看兄弟节点的Indentifier类型来获取key的名称,提取 text/ label/ description 的value值 但其实容易多提取导致报错,最后代码实现的时候这部分没有提取

nextjs用webpack打包比较慢,导致babel插件在开发的过程中体验不会特别好,加载新页面的时候会插入跑插件这一步(当然npm run build然后npm run start 页面不会卡顿,更新记得删.next 文件夹缓存)

考虑一样的思路改成用babel脚本(与框架无关),遍历节点的时候和babel插件的代码是一样的

目前脚本做的事情是,将jsx节点自动套t方法和将所有套了t方法的放到翻译包里,没有翻译的词放到整体的最前面,最后再把代码库里的代码用路径下的prettierrc文件定义的格式进行格式化

同时注意翻译词写入可能会有编码问题导致key匹配不上,再检查一下

参考代码:

// 存储待翻译的文本 const wordsToTranslate = new Set(); function isAlreadyWrappedWithT(node) { if ( node && node.type === 'CallExpression' && node.callee && node.callee.name === 't' ) { return true; } return false; } // 根据根目录的 prettierrc 格式化单个文件 async function formatFile(filePath, srcDir, outputDir) { try { console.log(`格式化文件: ${filePath}`); // 读取文件内容 const code = fs.readFileSync(filePath, 'utf-8'); // 读取 prettier 配置 const prettierConfig = await prettier.resolveConfig(process.cwd(), { editorconfig: true, config: path.join(process.cwd(), '.prettierrc'), }); // 使用 prettier 格式化代码 const formattedCode = await prettier.format(code, { ...prettierConfig, filepath: filePath, }); // 计算输出路径 const relativePath = path.relative(srcDir, filePath); const outputPath = path.join(outputDir, relativePath); // 确保输出目录存在 ensureDirectoryExists(path.dirname(outputPath)); // 写入格式化后的代码 fs.writeFileSync(outputPath, formattedCode, 'utf-8'); } catch (error) { console.error(`格式化文件 ${filePath} 时出错:`, error); } } // 处理单个文件(借助 Babel 的 AST 给代码添加 i18n) function processFile(filePath, srcDir, outputDir) { try { console.log(`处理文件: ${filePath}`); const code = fs.readFileSync(filePath, 'utf-8'); // 解析代码生成 AST,支持 TypeScript 和 JSX const ast = parse(code, { sourceType: 'module', plugins: ['typescript', 'jsx'], }); // 创建初始state对象 const state = { filename: filePath, }; // 遍历和修改 AST traverse( ast, { Program(path) { let hasI18nImport = false; path.traverse({ ImportDeclaration(childPath) { childPath.traverse({ StringLiteral(stringPath) { if (stringPath.node.value === '@/utils/i18') { hasI18nImport = true; } }, }); }, }); if ( !hasI18nImport && !filePath.includes('/node_modules/') ) { const importDeclaration = t.importDeclaration( [t.importSpecifier(t.identifier('t'), t.identifier('t'))], t.stringLiteral('@/utils/i18n'), ); path.unshiftContainer('body', importDeclaration); } }, JSXText(path) { const transValue = path.node.value .replace(/\s+/g, ' ') .replace(/\\n/g, ' ') .trim(); if ( /.*[a-zA-Z\p{P}].*/.test(transValue) && filePath.includes('/src/') && !filePath.includes('/node_modules/') && !isAlreadyWrappedWithT(path.parentPath.node) ) { path.replaceWith( t.jSXExpressionContainer( t.callExpression(t.identifier('t'), [ t.stringLiteral(transValue), ]), ), ); wordsToTranslate.add(transValue); } }, StringLiteral(path) { const transValue = path.node.value .replace(/\s+/g, ' ') .replace(/\\n/g, ' ') .trim(); if ( /.*[a-zA-Z\p{P}].*/.test(transValue) && filePath.includes('/src/') && !filePath.includes('/node_modules/') ) { if (path.parentPath.node.type === 'CallExpression') { if ( t.isCallExpression(path.parentPath.node) && path.parentPath.node.callee.name === 't' ) { wordsToTranslate.add(transValue); } } } } }, }, state, ); // 生成新代码 const output = generate(ast, {}, code); // 计算输出文件路径 const relativePath = path.relative(srcDir, filePath); const outputPath = path.join(outputDir, relativePath); // 确保输出目录存在 ensureDirectoryExists(path.dirname(outputPath)); // 写入修改后的代码 fs.writeFileSync(outputPath, output.code, 'utf-8'); } catch (error) { console.error(`处理文件 ${filePath} 时出错:`, error); } } function post() { if (wordsToTranslate.size > 0) { SUPPORTED_LOCALES.forEach((locale) => { // 设置文件输出路径 const outputDir = path.join(process.cwd(), 'public', 'locales', locale); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const outputPath = path.join(outputDir, 'common.json'); // 读取现有翻译 let existingWords = {}; if (fs.existsSync(outputPath)) { existingWords = JSON.parse(fs.readFileSync(outputPath, 'utf8')); } // 处理新增词 const newWords = Array.from(wordsToTranslate) .filter((word) => !Object.values(existingWords).includes(word)) .sort() .reduce((acc, word) => { const key = word; acc[key] = locale === 'en' ? word : ''; return acc; }, {}); // 合并所有词 const allWords = { ...newWords, ...existingWords }; // 分离空值和非空值 const emptyEntries = []; const nonEmptyEntries = []; Object.entries(allWords).forEach(([key, value]) => { if (!value || value.trim() === '') { emptyEntries.push([key, value]); } else { nonEmptyEntries.push([key, value]); } }); // 分别对空值和非空值按key排序 emptyEntries.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); nonEmptyEntries.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); // 合并排序后的结果,空值在前 const sortedWords = [...emptyEntries, ...nonEmptyEntries].reduce( (acc, [key, value]) => { acc[key] = value; return acc; }, {}, ); // 写入文件 fs.writeFileSync( outputPath, JSON.stringify(sortedWords, null, 2), 'utf8', ); }); } } // 主函数 function main() { const srcDir = path.join(__dirname, '../src'); const outputDir = path.join(__dirname, '../src'); // 确保输出目录存在 ensureDirectoryExists(outputDir); const files = getAllFiles(srcDir); files.forEach((file) => { processFile(file, srcDir, outputDir); formatFile(file, srcDir, outputDir); }); post(); } main(); 维护翻译词:

开发一个简易的翻译平台,所有人可以维护提取词和翻译包 key-value形式。具体形式还得再看看,key尽量参照国际化标准 达成两个目的:a.每个人把翻译提交 b.修改可以应用到项目里(导出json) 不确定能不能让llm写

有现成的平台

标签:

用Babel脚本实现多语言/国际化由讯客互联人工智能栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“用Babel脚本实现多语言/国际化