diff --git a/config/routes.simple.ts b/config/routes.simple.ts index f9878635..ddb713d8 100644 --- a/config/routes.simple.ts +++ b/config/routes.simple.ts @@ -1,14 +1,10 @@ -/** - * @name 简单版路由配置 - * @description 此配置用于 npm run simple 命令执行后使用 - */ export default [ { path: '/user', layout: false, routes: [ { - name: 'login', + name: '登录', path: '/user/login', component: './user/login', }, @@ -16,13 +12,13 @@ export default [ }, { path: '/welcome', - name: 'welcome', + name: '欢迎', icon: 'smile', component: './Welcome', }, { path: '/admin', - name: 'admin', + name: '管理页', icon: 'crown', access: 'canAdmin', routes: [ @@ -32,13 +28,13 @@ export default [ }, { path: '/admin/sub-page', - name: 'sub-page', + name: '二级管理页', component: './Admin', }, ], }, { - name: 'list.table-list', + name: '查询表格', icon: 'table', path: '/list', component: './table-list', diff --git a/package.json b/package.json index f24e4971..aa6db4b4 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "max build", "deploy": "cross-env CI=true max build && gh-pages -d dist", "dev": "cross-env UMI_ENV=dev MOCK=none max dev", - "i18n-remove": "max i18n-remove --locale=zh-CN --write", + "i18n-remove": "node scripts/i18n-remove.js", "lint": "npm run biome:lint && npm run tsc", "lint-staged": "lint-staged", "biome:lint": "biome lint", diff --git a/scripts/i18n-remove.js b/scripts/i18n-remove.js new file mode 100644 index 00000000..47ce6b07 --- /dev/null +++ b/scripts/i18n-remove.js @@ -0,0 +1,588 @@ +/** + * i18n 移除脚本 - 将国际化的文本替换为中文,移除 i18n 相关代码 + * 执行 npm run i18n-remove 运行此脚本 + * + * 此操作不可逆,会执行以下变更: + * - 读取 src/locales/zh-CN 的翻译映射 + * - 替换 intl.formatMessage / useIntl().formatMessage 调用为中文硬编码 + * - 替换 组件为中文文本 + * - 移除 useIntl、FormattedMessage、SelectLang、getLocale 的 import 和使用 + * - 从 config/config.ts 中移除 locale 配置 + * - 将 routes.ts 中的 name 值替换为中文菜单名 + * - 删除 src/locales/ 目录 + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const I18N_SYMBOLS = [ + 'useIntl', + 'FormattedMessage', + 'SelectLang', + 'getLocale', + 'getAllLocales', + 'setLocale', +]; +const FORMAT_MESSAGE_PATTERNS = [ + 'intl.formatMessage(', + 'useIntl().formatMessage(', +]; +const QSTR = `'((?:[^'\\\\]|\\\\.)*)'|"((?:[^"\\\\]|\\\\.)*)"`; + +// ─── 工具函数 ─────────────────────────────────────────── + +function readDirRecursive(dir) { + const results = []; + if (!fs.existsSync(dir)) return results; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...readDirRecursive(full)); + } else { + results.push(full); + } + } + return results; +} + +function toStrLiteral(text) { + return `'${text + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029')}'`; +} + +function resolveText(id, defaultMsg, localeMap) { + return localeMap[id] || defaultMsg || id; +} + +/** + * 找到括号匹配的闭合位置 + * @param {string} str - 源字符串 + * @param {number} openIdx - 开括号 ( 的位置 + * @returns {number} 闭括号 ) 的位置,-1 表示未找到 + */ +function findClosingParen(str, openIdx) { + let depth = 0; + let i = openIdx; + while (i < str.length) { + const ch = str[i]; + if (ch === '(') depth++; + else if (ch === ')') { + depth--; + if (depth === 0) return i; + } else if (ch === "'" || ch === '"' || ch === '`') { + // skip string literal + const quote = ch; + i++; + while (i < str.length && str[i] !== quote) { + if (str[i] === '\\') i++; // skip escaped char + i++; + } + } else if (ch === '/' && str[i + 1] === '/') { + // skip single-line comment + while (i < str.length && str[i] !== '\n') i++; + } else if (ch === '/' && str[i + 1] === '*') { + // skip block comment + i += 2; + while (i < str.length - 1 && !(str[i] === '*' && str[i + 1] === '/')) i++; + i++; // skip past */ + } + i++; + } + return -1; +} + +// ─── Step 1: 构建 zh-CN 翻译映射 ──────────────────────── + +function buildLocaleMap() { + const localeDir = path.join('src', 'locales'); + const zhCNDir = path.join(localeDir, 'zh-CN'); + const files = readDirRecursive(zhCNDir).filter((f) => f.endsWith('.ts')); + const mainFile = path.join(localeDir, 'zh-CN.ts'); + const localeMap = {}; + + for (const file of files) { + const content = fs.readFileSync(file, 'utf-8'); + extractLocaleKeys(content, localeMap); + } + if (fs.existsSync(mainFile)) { + const content = fs.readFileSync(mainFile, 'utf-8'); + extractLocaleKeys(content, localeMap); + } + + return localeMap; +} + +function extractLocaleKeys(content, localeMap) { + // Match key: 'value' or key: "value", supporting escaped quotes inside values + const regex = + /['"]([a-zA-Z0-9_.-]+)['"]\s*:\s*'((?:[^'\\]|\\.)*)'|['"]([a-zA-Z0-9_.-]+)['"]\s*:\s*"((?:[^"\\]|\\.)*)"/g; + let match = regex.exec(content); + while (match !== null) { + const key = match[1] || match[3]; + const value = match[2] || match[4]; + localeMap[key] = value; + match = regex.exec(content); + } +} + +// ─── Step 2: 获取 menu 映射 ───────────────────────────── + +function buildMenuMap(localeMap) { + const menuMap = {}; + for (const [key, value] of Object.entries(localeMap)) { + if (key.startsWith('menu.')) { + menuMap[key.slice(5)] = value; + } + } + return menuMap; +} + +// ─── Step 3: 替换路由文件中的 name ────────────────────── + +function replaceRoutes(menuMap) { + const configDir = path.join('config'); + if (!fs.existsSync(configDir)) { + console.log('- config/ 目录不存在,跳过'); + return; + } + + const routeFiles = fs + .readdirSync(configDir) + .filter((f) => f.startsWith('routes') && f.endsWith('.ts')) + .map((f) => path.join(configDir, f)); + + for (const routesPath of routeFiles) { + let content = fs.readFileSync(routesPath, 'utf-8'); + let modified = false; + + content = content.replace(/name:\s*['"]([^'"]+)['"]/g, (match, name) => { + const zhValue = menuMap[name]; + if (zhValue) { + modified = true; + return `name: ${toStrLiteral(zhValue)}`; + } + return match; + }); + + if (modified) { + fs.writeFileSync(routesPath, content); + console.log( + `✓ 已替换 ${path.relative('.', routesPath)} 中的路由名称为中文`, + ); + } else { + console.log(`- ${path.relative('.', routesPath)} 无需替换`); + } + } +} + +// ─── Step 4: 从 config/config.ts 移除 locale 配置 ──────── + +function removeLocaleConfig() { + const configPath = path.join('config', 'config.ts'); + if (!fs.existsSync(configPath)) { + console.log('- config/config.ts 不存在,跳过'); + return; + } + + let content = fs.readFileSync(configPath, 'utf-8'); + const original = content; + + // 移除 locale 块及其紧邻的 JSDoc 注释(只匹配国际化相关的注释) + content = content.replace( + /\n\s*\/\*\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\n\s*locale:\s*\{[^}]*\},?\n/g, + '\n', + ); + + // 移除无注释的 locale: {...} 配置块 + content = content.replace(/\n\s*locale:\s*\{[^}]*\},?\n/g, '\n'); + + // 移除 layout 中的 locale: true + content = content.replace(/\n\s*locale:\s*true,?\n/g, '\n'); + + if (content !== original) { + fs.writeFileSync(configPath, content); + console.log('✓ 已从 config/config.ts 移除 locale 配置'); + } else { + console.log('- config/config.ts 无需修改'); + } +} + +// ─── Step 5: 替换源文件中的 i18n 调用 ────────────────── + +function processSourceFiles(localeMap) { + const srcDir = path.join('src'); + const files = readDirRecursive(srcDir).filter((f) => { + const ext = path.extname(f); + return ( + ['.tsx', '.ts', '.jsx', '.js'].includes(ext) && + !f.includes('locales') && + !f.includes('.umi') && + !f.includes('.umi-production') && + !f.includes('.umi-test') && + !f.endsWith('.d.ts') + ); + }); + + for (const file of files) { + processFile(file, localeMap); + } +} + +function processFile(filePath, localeMap) { + let content = fs.readFileSync(filePath, 'utf-8'); + const original = content; + + if ( + !content.includes('formatMessage') && + !content.includes('FormattedMessage') && + !content.includes('useIntl') && + !content.includes('SelectLang') && + !content.includes('getLocale') && + !content.includes('getAllLocales') && + !content.includes('setLocale') + ) { + return; + } + + // ── 1. 替换 formatMessage 调用(使用括号匹配精确处理) + // 只替换 id 为字符串字面量且无第二参数的简单调用 + content = replaceFormatMessageCalls(content, localeMap); + + // ── 2. 替换 (使用精确匹配) + // 只替换 id 为字符串字面量的调用 + content = replaceFormattedMessageComponents(content, localeMap); + + // ── 3. 移除 const intl = useIntl() 声明 + // 只在文件中不再有其他 intl. 引用时移除 + if (!content.match(/\bintl\./)) { + content = content.replace( + /\n\s*\/\*\*[\s\S]*?\*\/\n\s*const\s+intl\s*=\s*useIntl\(\)\s*;?\n/g, + '\n', + ); + content = content.replace( + /\n\s*const\s+intl\s*=\s*useIntl\(\)\s*;?\n/g, + '\n', + ); + } + + // ── 4. 移除 SelectLang 使用 + content = content.replace( + /\n?\s*\{SelectLang\s*&&\s*\s*\}/g, + '', + ); + + // ── 5. 处理 getLocale() 调用 + content = content.replace( + /const\s+(\w+)\s*=\s*getLocale\(\)\s*;?/g, + (_match, varName) => { + return `const ${varName} = 'zh-CN';`; + }, + ); + + // ── 5b. 处理 getAllLocales() 调用 + content = content.replace( + /useMemo\(\(\)\s*=>\s*getAllLocales\(\),\s*\[\]\)/g, + "['zh-CN']", + ); + content = content.replace( + /const\s+(\w+)\s*=\s*getAllLocales\(\)\s*;?/g, + (_match, varName) => { + return `const ${varName} = ['zh-CN'];`; + }, + ); + + // ── 5c. 移除 setLocale() 调用(使用括号匹配处理嵌套括号) + content = removeSetLocaleCalls(content); + + // ── 6. 移除 data-lang 属性 + content = content.replace( + /\s*data-lang(?:\s*=\s*(?:"[^"]*"|'[^']*'|\{[^}]*\}))?/g, + '', + ); + + // ── 7. 移除 Lang 组件调用和定义 + content = content.replace(/\n?\s*/g, ''); + content = content.replace( + /const Lang = \(\) => \{\s*\n\s*const \{ styles \} = useStyles\(\);\s*\n\s*return \(\s*\n\s*
\s*\n\s*<\/div>\s*\n\s*\);\s*\n\s*\};\n?/g, + '', + ); + + // ── 8. 清理 import 语句 + // 只移除文件中不再使用的 i18n 相关符号 + content = content.replace( + /import\s*\{([^}]*)\}\s*from\s*['"]@umijs\/max['"];?\n?/g, + (match, imports) => { + const codeWithoutImport = content.replace(match, ''); + const items = imports + .split(',') + .map((s) => s.trim()) + .filter((s) => { + if (!s || !I18N_SYMBOLS.includes(s)) return true; + const regex = new RegExp(`\\b${s}\\b`); + return regex.test(codeWithoutImport); + }); + + if (items.length === 0) { + return ''; + } + + return `import { ${items.join(', ')} } from '@umijs/max';\n`; + }, + ); + + // ── 9. 简化 JSX 文本子节点中的 {'中文'} → 中文 + content = content.replace(/\{'([^']*)'\}/g, (match, text, offset, str) => { + const before = str.slice(Math.max(0, offset - 30), offset); + const charBeforeBrace = before.trimEnd().slice(-1); + // 不简化: JSX 属性(=)、模板字符串($)、函数参数/数组/表达式((,[,,) + if ( + charBeforeBrace === '=' || + charBeforeBrace === '$' || + charBeforeBrace === '(' || + charBeforeBrace === ',' || + charBeforeBrace === '[' + ) { + return match; + } + return text; + }); + + // ── 10. 简化 JSX 文本子节点中 FormattedMessage 产生的 '中文' → 中文 + content = content.replace( + />(\s*)'([^'<]*?)'(\s*)<\//g, + (_match, ws1, text, ws2) => { + return `>${ws1}${text}${ws2} + * 只替换 id 为字符串字面量的组件 + */ +function replaceFormattedMessageComponents(content, localeMap) { + let searchFrom = 0; + const pattern = ' + + const fullTag = content.slice(idx, endIdx); + + const idMatch = fullTag.match(new RegExp(`id=${QSTR}`)); + if (!idMatch) { + searchFrom = endIdx; + continue; + } + + const id = idMatch[1] || idMatch[2]; + const dmMatch = fullTag.match(new RegExp(`defaultMessage=${QSTR}`)); + const defaultMsg = dmMatch ? (dmMatch[1] || dmMatch[2]) : ''; + + const zhText = resolveText(id, defaultMsg, localeMap); + const replacement = toStrLiteral(zhText); + content = content.slice(0, idx) + replacement + content.slice(endIdx); + searchFrom = idx + replacement.length; + } + + return content; +} + +/** + * 移除 setLocale() 调用,使用括号匹配处理嵌套括号 + */ +function removeSetLocaleCalls(content) { + const pattern = 'setLocale('; + let searchFrom = 0; + while (true) { + const idx = content.indexOf(pattern, searchFrom); + if (idx === -1) break; + + const openParenIdx = idx + pattern.length - 1; + const closeParenIdx = findClosingParen(content, openParenIdx); + if (closeParenIdx === -1) { + searchFrom = idx + 1; + continue; + } + + // Remove the entire call including trailing semicolon + let endIdx = closeParenIdx + 1; + if (content[endIdx] === ';') endIdx++; + + content = content.slice(0, idx) + content.slice(endIdx); + searchFrom = idx; + } + return content; +} + +// ─── Step 6: 删除 locales 目录 ───────────────────────── + +function deleteLocalesDir() { + const localeDir = path.join('src', 'locales'); + fs.rmSync(localeDir, { recursive: true, force: true }); + console.log('✓ 已删除 src/locales/ 目录'); +} + +// ─── 残留检查 ──────────────────────────────────────────── + +function checkResiduals() { + const residualSymbols = ['getLocale', 'getAllLocales', 'setLocale']; + const srcDir = path.join('src'); + const files = readDirRecursive(srcDir).filter((f) => { + const ext = path.extname(f); + return ( + ['.tsx', '.ts', '.jsx', '.js'].includes(ext) && + !f.includes('.umi') && + !f.includes('.umi-production') && + !f.includes('.umi-test') && + !f.endsWith('.d.ts') + ); + }); + + let found = false; + for (const file of files) { + const content = fs.readFileSync(file, 'utf-8'); + for (const sym of residualSymbols) { + const regex = new RegExp(`\\b${sym}\\b`); + const match = regex.exec(content); + if (match) { + const relPath = path.relative('.', file); + const line = content.slice(0, match.index).split('\n').length; + console.log(` ⚠ ${relPath}:${line} 残留 ${sym} 调用,请手动检查`); + found = true; + } + } + } + + if (!found) { + console.log('✓ 无残留 i18n 调用'); + } +} + +// ─── Step 7: 从 package.json 移除 i18n-remove 脚本 ────── + +function updatePackageJson() { + const pkgPath = 'package.json'; + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + + if (pkg.scripts?.['i18n-remove']) { + delete pkg.scripts['i18n-remove']; + fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); + console.log('✓ 已从 package.json 移除 i18n-remove 脚本'); + } +} + +// ─── 主函数 ───────────────────────────────────────────── + +function main() { + console.log('========================================'); + console.log(' 开始执行 i18n 移除脚本'); + console.log('========================================\n'); + + console.log('>>> 构建中文翻译映射...'); + const localeMap = buildLocaleMap(); + const menuMap = buildMenuMap(localeMap); + console.log(`✓ 已加载 ${Object.keys(localeMap).length} 条翻译映射`); + + console.log('\n>>> 替换路由名称为中文...'); + replaceRoutes(menuMap); + + console.log('\n>>> 移除 config/config.ts 中的 locale 配置...'); + removeLocaleConfig(); + + console.log('\n>>> 替换源文件中的 i18n 调用...'); + processSourceFiles(localeMap); + + console.log('\n>>> 删除 locales 目录...'); + deleteLocalesDir(); + + console.log('\n>>> 更新 package.json...'); + updatePackageJson(); + + console.log('\n>>> 检查残留 i18n 调用...'); + checkResiduals(); + + console.log('\n========================================'); + console.log(' i18n 移除完成!'); + console.log(' 请运行 npm install 并检查代码'); + console.log('========================================'); +} + +main();