UNPKG

create-ui18n-app

Version:

⚡ Create a fully internationalized app in 30 seconds with React, Vue, Angular, or Svelte - zero config required

646 lines (565 loc) 23.6 kB
#!/usr/bin/env node /** * create-ui18n-app * 创建国际化应用的项目脚手架工具 * Usage: npx create-ui18n-app my-app --template react-vite */ import { program } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import fs from 'fs-extra'; import path from 'path'; import { execSync } from 'child_process'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); console.log(chalk.cyan.bold(` ╔═══════════════════════════════════════════════════════╗ ║ 🌍 Create UI18N App - Project Scaffolding ║ ║ ║ ║ ⚡ Generate a full i18n project in 30 seconds ║ ╚═══════════════════════════════════════════════════════╝ `)); // CLI程序配置 program .name('create-ui18n-app') .description('⚡ Create a fully internationalized app in 30 seconds') .version('0.1.0-rc.3') .argument('[project-name]', 'project directory name') .option('-t, --template <template>', 'specify template (react-vite, react-cra, vue-nuxt, node-api)', 'react-vite') .option('--skip-install', 'skip npm install') .option('--skip-git', 'skip git init') .action(async (projectName, options) => { try { await createProject(projectName, options); } catch (error) { console.error(chalk.red('Error creating project:'), error.message); process.exit(1); } }); /** * 智能获取作者信息 * 尝试从git config获取,失败则返回合理默认值 * 实现"意外惊喜"用户体验 - 用户无需手动输入,脚手架智能填充 */ async function getAuthorInfo() { try { // 尝试从git config获取用户信息 const name = execSync('git config --global user.name', { stdio: 'pipe', encoding: 'utf8', timeout: 3000 // 3秒超时,避免阻塞 }).trim(); const email = execSync('git config --global user.email', { stdio: 'pipe', encoding: 'utf8', timeout: 3000 }).trim(); // 优先返回 "Name <email>" 格式 if (name && email) { return `${name} <${email}>`; } else if (name) { return name; } } catch (error) { // Git不可用或未配置,静默失败 // 这是精益设计:不因为git问题影响脚手架主功能 } // 返回友好的默认值 return 'Your Name'; } async function createProject(projectName, options) { // 1. 获取项目名称 if (!projectName) { const { name } = await inquirer.prompt([ { type: 'input', name: 'name', message: '📁 Project name:', default: 'my-ui18n-app', validate: (input) => { if (!input.trim()) return 'Please enter a project name'; if (!/^[a-zA-Z0-9-_]+$/.test(input)) return 'Project name should only contain letters, numbers, hyphens and underscores'; return true; } } ]); projectName = name; } // 2. 项目配置询问 const config = await inquirer.prompt([ // 模板选择(如果未通过命令行指定) ...((!options.template || !['react-vite', 'react-cra', 'vue-nuxt', 'node-api'].includes(options.template)) ? [{ type: 'list', name: 'template', message: '🛠️ Choose a template:', choices: [ { name: '⚛️ React + Vite + TypeScript (Recommended)', value: 'react-vite' }, { name: '📦 React + CRA + JavaScript (Traditional)', value: 'react-cra' }, { name: '🟢 Vue 3 + Nuxt + TypeScript', value: 'vue-nuxt' }, { name: '🚀 Node.js API + Express + TypeScript', value: 'node-api' } ], default: 'react-vite' }] : []), // 项目描述 { type: 'input', name: 'description', message: '📝 Project description (optional):', default: (answers) => { const templateName = answers.template || options.template || 'react-vite'; const frameworkName = templateName === 'react-vite' ? 'React' : templateName === 'react-cra' ? 'React' : templateName === 'vue-nuxt' ? 'Vue' : templateName === 'node-api' ? 'Node.js API' : 'React'; return templateName === 'node-api' ? `A modern Node.js REST API with built-in internationalization powered by UI18n` : `A fully internationalized ${frameworkName} application powered by UI18n`; } }, // 默认语言选择 { type: 'list', name: 'defaultLanguage', message: '🌐 Default language:', choices: [ { name: '🇺🇸 English', value: 'en' }, { name: '🇨🇳 中文 (Chinese)', value: 'zh' }, { name: '🇪🇸 Español (Spanish)', value: 'es' }, { name: '🇫🇷 Français (French)', value: 'fr' } ], default: 'en' }, // 额外语言支持 { type: 'checkbox', name: 'additionalLanguages', message: '🗣️ Additional languages to include:', choices: (answers) => { const allLanguages = [ { name: '🇺🇸 English', value: 'en' }, { name: '🇨🇳 中文 (Chinese)', value: 'zh' }, { name: '🇪🇸 Español (Spanish)', value: 'es' }, { name: '🇫🇷 Français (French)', value: 'fr' }, { name: '🇩🇪 Deutsch (German)', value: 'de' }, { name: '🇯🇵 日本語 (Japanese)', value: 'ja' }, { name: '🇰🇷 한국어 (Korean)', value: 'ko' } ]; // 排除已选择的默认语言 return allLanguages.filter(lang => lang.value !== answers.defaultLanguage); }, default: (answers) => { // 根据默认语言推荐其他语言 const recommendations = { 'en': ['zh'], // 英语项目推荐中文 'zh': ['en'], // 中文项目推荐英语 'es': ['en'], // 其他语言推荐英语 'fr': ['en'], 'de': ['en'], 'ja': ['en'], 'ko': ['en'] }; return recommendations[answers.defaultLanguage] || ['en']; } }, // UI18n 功能配置 { type: 'confirm', name: 'enableCaching', message: '⚡ Enable translation caching for better performance?', default: true }, { type: 'confirm', name: 'enableAutoDetection', message: '🔍 Enable automatic language detection (browser/system)?', default: true }, { type: 'confirm', name: 'enablePersistence', message: '💾 Enable language preference persistence (localStorage)?', default: true }, // 项目设置 { type: 'confirm', name: 'includeExamples', message: '📚 Include additional translation examples and demos?', default: true } ]); // 智能获取作者信息(无用户感知,提升体验) const authorInfo = await getAuthorInfo(); // 合并配置 const template = config.template || options.template || 'react-vite'; const projectConfig = { ...config, template, author: authorInfo, supportedLanguages: [config.defaultLanguage, ...config.additionalLanguages].filter(Boolean) }; // 3. 验证目标目录 const targetDir = path.resolve(process.cwd(), projectName); if (fs.existsSync(targetDir)) { const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: `📂 Directory "${projectName}" already exists. What should we do?`, choices: [ { name: '❌ Cancel', value: 'cancel' }, { name: '🗑️ Remove existing directory', value: 'overwrite' }, { name: '📂 Use different name', value: 'rename' } ] } ]); if (action === 'cancel') { console.log(chalk.yellow('\n✋ Project creation cancelled.')); return; } if (action === 'rename') { const { newName } = await inquirer.prompt([ { type: 'input', name: 'newName', message: '📁 New project name:', validate: (input) => { if (!input.trim()) return 'Please enter a project name'; if (fs.existsSync(path.resolve(process.cwd(), input))) return 'This name also exists'; return true; } } ]); projectName = newName; targetDir = path.resolve(process.cwd(), projectName); } if (action === 'overwrite') { await fs.remove(targetDir); } } console.log(chalk.green(`\n🚀 Creating project "${projectName}" with template "${template}"...\n`)); // 4. 复制模板文件 const spinner = ora('📁 Creating project structure...').start(); try { const templateDir = path.resolve(__dirname, 'templates', template); if (!fs.existsSync(templateDir)) { throw new Error(`Template "${template}" not found`); } await fs.copy(templateDir, targetDir); // 替换模板变量 await replaceTemplateVariables(targetDir, projectName, projectConfig); // 生成额外的语言文件 await generateLanguageFiles(targetDir, { ...projectConfig, projectName }); spinner.succeed('📁 Project structure created!'); } catch (error) { spinner.fail('Failed to create project structure'); throw error; } // 5. 安装依赖 if (!options.skipInstall) { const installSpinner = ora('📦 Installing dependencies...').start(); try { const packageManager = detectPackageManager(); const installCmd = getInstallCommand(packageManager); execSync(installCmd, { cwd: targetDir, stdio: 'ignore' }); installSpinner.succeed(`📦 Dependencies installed with ${packageManager}!`); } catch (error) { installSpinner.fail('Failed to install dependencies'); console.log(chalk.yellow('\n💡 You can install manually with:')); console.log(chalk.cyan(` cd ${projectName}`)); console.log(chalk.cyan(' npm install')); } } // 6. Git初始化 if (!options.skipGit) { const gitSpinner = ora('🔧 Initializing git repository...').start(); try { execSync('git init', { cwd: targetDir, stdio: 'ignore' }); execSync('git add .', { cwd: targetDir, stdio: 'ignore' }); execSync('git commit -m "feat: initial commit with UI18n setup"', { cwd: targetDir, stdio: 'ignore' }); gitSpinner.succeed('🔧 Git repository initialized!'); } catch (error) { gitSpinner.warn('Git initialization skipped (git not available)'); } } // 7. 显示成功信息和下一步指引 showSuccessMessage(projectName, template, targetDir, projectConfig); } async function replaceTemplateVariables(targetDir, projectName, projectConfig) { const { template, description, defaultLanguage, supportedLanguages, enableCaching, enableAutoDetection, enablePersistence, author } = projectConfig; const replacements = { '{{PROJECT_NAME}}': projectName, '{{PROJECT_NAME_PASCAL}}': toPascalCase(projectName), '{{PROJECT_NAME_KEBAB}}': toKebabCase(projectName), '{{DESCRIPTION}}': description, '{{TEMPLATE}}': template, '{{AUTHOR}}': author || 'Your Name', // 智能获取的作者信息,提供后备默认值 '{{YEAR}}': new Date().getFullYear().toString(), '{{DEFAULT_LANGUAGE}}': defaultLanguage, '{{SUPPORTED_LANGUAGES}}': JSON.stringify(supportedLanguages), // 重要:Boolean值需要替换为JavaScript字面量,而不是字符串 '{{ENABLE_CACHING}}': enableCaching ? 'true' : 'false', '{{ENABLE_AUTO_DETECTION}}': enableAutoDetection ? 'true' : 'false', '{{ENABLE_PERSISTENCE}}': enablePersistence ? 'true' : 'false' }; // 需要处理的文件类型 const fileExtensions = ['.json', '.js', '.ts', '.tsx', '.vue', '.md', '.html']; const processFile = async (filePath) => { const ext = path.extname(filePath); if (!fileExtensions.includes(ext)) return; try { let content = await fs.readFile(filePath, 'utf8'); let hasChanges = false; // 执行变量替换 - 使用全局替换确保所有占位符被替换 for (const [placeholder, replacement] of Object.entries(replacements)) { const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); if (content.includes(placeholder)) { content = content.replace(regex, replacement); hasChanges = true; } } // 只在有变更时写入文件 if (hasChanges) { await fs.writeFile(filePath, content, 'utf8'); } } catch (error) { console.warn(`Warning: Could not process file ${filePath}:`, error.message); } }; // 递归处理目录中的所有文件 const processDirectory = async (dir) => { const items = await fs.readdir(dir); for (const item of items) { const itemPath = path.join(dir, item); const stat = await fs.stat(itemPath); if (stat.isDirectory()) { // 跳过 node_modules 和其他不需要处理的目录 if (['node_modules', '.git', '.DS_Store'].includes(item)) continue; await processDirectory(itemPath); } else { await processFile(itemPath); } } }; await processDirectory(targetDir); } async function generateLanguageFiles(targetDir, projectConfig) { const { template, supportedLanguages, includeExamples, projectName } = projectConfig; // 基础翻译内容模板 - 使用实际项目名称而非占位符 const baseTranslations = { es: { app: { title: projectName || 'Mi Aplicación' }, welcome: { title: template === 'node-api' ? '¡Bienvenido a tu API internacional! 🚀' : '¡Bienvenido a tu aplicación internacional! 🌍', description: template === 'node-api' ? 'Esta es una API REST de Node.js completamente funcional con internacionalización impulsada por UI18n.' : `Esta es una aplicación ${template === 'react-vite' ? 'React' : 'Vue'} completamente funcional con internacionalización impulsada por UI18n.` }, languageSwitcher: { selectLanguage: 'Idioma', switchTo: 'Cambiar a' }, footer: { poweredBy: 'Impulsado por UI18n - La solución moderna de internacionalización' } }, fr: { app: { title: projectName || 'Mon Application' }, welcome: { title: template === 'node-api' ? 'Bienvenue dans votre API internationale ! 🚀' : 'Bienvenue dans votre application internationale ! 🌍', description: template === 'node-api' ? 'Il s\'agit d\'une API REST Node.js entièrement fonctionnelle avec l\'internationalisation alimentée par UI18n.' : `Il s'agit d'une application ${template === 'react-vite' ? 'React' : 'Vue'} entièrement fonctionnelle avec l'internationalisation alimentée par UI18n.` }, languageSwitcher: { selectLanguage: 'Langue', switchTo: 'Passer à' }, footer: { poweredBy: 'Alimenté par UI18n - La solution d\'internationalisation moderne' } }, de: { app: { title: projectName || 'Meine Anwendung' }, welcome: { title: 'Willkommen in Ihrer internationalen Anwendung! 🌍', description: `Dies ist eine voll funktionsfähige ${template === 'react-vite' ? 'React' : 'Vue'}-Anwendung mit Internationalisierung powered by UI18n.` }, languageSwitcher: { selectLanguage: 'Sprache', switchTo: 'Wechseln zu' }, footer: { poweredBy: 'Angetrieben von UI18n - Die moderne Internationalisierungslösung' } }, ja: { app: { title: projectName || 'マイアプリ' }, welcome: { title: 'あなたの国際アプリケーションへようこそ! 🌍', description: `これは UI18n による国際化機能を持つ完全な機能の${template === 'react-vite' ? 'React' : 'Vue'}アプリケーションです。` }, languageSwitcher: { selectLanguage: '言語', switchTo: '切り替え' }, footer: { poweredBy: 'UI18n による提供 - 現代の国際化ソリューション' } }, ko: { app: { title: projectName || '내 앱' }, welcome: { title: '국제 애플리케이션에 오신 것을 환영합니다! 🌍', description: `이것은 UI18n으로 구동되는 국제화 기능을 갖춘 완전한 기능의 ${template === 'react-vite' ? 'React' : 'Vue'} 애플리케이션입니다.` }, languageSwitcher: { selectLanguage: '언어', switchTo: '전환' }, footer: { poweredBy: 'UI18n 제공 - 현대적인 국제화 솔루션' } } }; // 为每个额外语言生成翻译文件 const localesDir = path.join(targetDir, 'locales'); for (const lang of supportedLanguages) { if (['en', 'zh'].includes(lang)) continue; // 这些已经在模板中存在 const translations = baseTranslations[lang]; if (translations) { const filePath = path.join(localesDir, `${lang}.json`); await fs.writeFile(filePath, JSON.stringify(translations, null, 2), 'utf8'); } } } function detectPackageManager() { try { execSync('yarn --version', { stdio: 'ignore' }); return 'yarn'; } catch {} try { execSync('pnpm --version', { stdio: 'ignore' }); return 'pnpm'; } catch {} return 'npm'; } function getInstallCommand(packageManager) { const commands = { npm: 'npm install', yarn: 'yarn install', pnpm: 'pnpm install' }; return commands[packageManager]; } function toPascalCase(str) { return str .replace(/[^a-zA-Z0-9]/g, ' ') .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase()) .replace(/\s+/g, ''); } function toKebabCase(str) { return str .replace(/[^a-zA-Z0-9]/g, '-') .replace(/--+/g, '-') .replace(/^-+|-+$/g, '') .toLowerCase(); } function showSuccessMessage(projectName, template, targetDir, projectConfig) { const { supportedLanguages, defaultLanguage, enableCaching, enableAutoDetection, enablePersistence, description } = projectConfig; console.log(chalk.green.bold('\n✨ Project created successfully!')); const languageFlags = { 'en': '🇺🇸', 'zh': '🇨🇳', 'es': '🇪🇸', 'fr': '🇫🇷', 'de': '🇩🇪', 'ja': '🇯🇵', 'ko': '🇰🇷' }; const languagesDisplay = supportedLanguages.map(lang => `${languageFlags[lang] || '🌍'} ${lang.toUpperCase()}` ).join(', '); console.log(` ╔═══════════════════════════════════════════════════════╗ ║ 🎉 Setup Complete! ║ ╠═══════════════════════════════════════════════════════╣ ║ 📁 Project: ${projectName.padEnd(37)} ║ ║ 🛠️ Template: ${template.padEnd(36)} ║ ║ 📍 Location: ${path.relative(process.cwd(), targetDir).padEnd(35)} ║ ║ 🌐 Languages: ${languagesDisplay.padEnd(34)} ║ ║ 🎯 Default: ${`${languageFlags[defaultLanguage] || '🌍'} ${defaultLanguage.toUpperCase()}`.padEnd(36)} ║ ╚═══════════════════════════════════════════════════════╝ `); console.log(chalk.cyan.bold('🎯 Next Steps:')); console.log(chalk.gray(' 1. Navigate to project:') + chalk.cyan(` cd ${projectName}`)); const commands = { 'react-vite': { dev: 'npm run dev', build: 'npm run build', preview: 'npm run preview' }, 'vue-nuxt': { dev: 'npm run dev', build: 'npm run build', preview: 'npm run preview' }, 'node-api': { dev: 'npm run dev', build: 'npm run build', preview: 'npm start' } }; const templateCommands = commands[template]; console.log(chalk.gray(' 2. Start development:') + chalk.cyan(` ${templateCommands.dev}`)); console.log(chalk.gray(' 3. Build for production:') + chalk.cyan(` ${templateCommands.build}`)); console.log(chalk.yellow.bold('\n🌍 UI18n Configuration:')); console.log(chalk.gray(' ✅ Languages: ') + chalk.cyan(languagesDisplay)); console.log(chalk.gray(' ✅ Default language: ') + chalk.cyan(`${languageFlags[defaultLanguage] || '🌍'} ${defaultLanguage.toUpperCase()}`)); console.log(chalk.gray(' ' + (enableCaching ? '✅' : '❌') + ' Translation caching')); console.log(chalk.gray(' ' + (enableAutoDetection ? '✅' : '❌') + ' Auto language detection')); console.log(chalk.gray(' ' + (enablePersistence ? '✅' : '❌') + ' Language persistence')); console.log(chalk.yellow.bold('\n🚀 Features Ready:')); if (template === 'node-api') { console.log(chalk.gray(' ✅ RESTful API with internationalization')); console.log(chalk.gray(' ✅ Translation endpoints (/api/translate)')); console.log(chalk.gray(' ✅ Language management (/api/language)')); console.log(chalk.gray(' ✅ Auto language detection')); console.log(chalk.gray(' ✅ Comprehensive error handling')); console.log(chalk.gray(' ✅ Security middleware (CORS, Helmet)')); console.log(chalk.gray(' ✅ Rate limiting and input validation')); console.log(chalk.gray(' ✅ Health check and monitoring')); } else { console.log(chalk.gray(' ✅ Language switcher component')); console.log(chalk.gray(' ✅ Translation examples with interpolation')); console.log(chalk.gray(' ✅ Hot reload for translations')); console.log(chalk.gray(' ✅ Responsive design with CSS variables')); console.log(chalk.gray(' ✅ Dark mode support')); } console.log(chalk.gray(' ✅ TypeScript support')); console.log(chalk.gray(' ✅ UI18n powered internationalization')); if (supportedLanguages.length > 2) { console.log(chalk.magenta.bold('\n🎨 Additional Languages Generated:')); const additionalLangs = supportedLanguages.filter(lang => !['en', 'zh'].includes(lang)); additionalLangs.forEach(lang => { console.log(chalk.gray(` ✅ ${languageFlags[lang] || '🌍'} ${lang.toUpperCase()} translations created`)); }); } console.log(chalk.blue.bold('\n📚 Learn More:')); console.log(chalk.gray(' 🔗 Documentation: ') + chalk.cyan('https://github.com/iron-wayne/UI18N-OSS')); console.log(chalk.gray(' 🔗 Translations: ') + chalk.cyan(`${projectName}/locales/`)); if (template === 'node-api') { console.log(chalk.gray(' 🔗 API Routes: ') + chalk.cyan(`${projectName}/src/routes/`)); console.log(chalk.gray(' 🔗 API Docs: ') + chalk.cyan(`http://localhost:3000/api`)); console.log(chalk.gray(' 🔗 Health Check: ') + chalk.cyan(`http://localhost:3000/health`)); } else { console.log(chalk.gray(' 🔗 Components: ') + chalk.cyan(`${projectName}/src/components/`)); } console.log(chalk.green('\n🎉 Ready to build amazing international experiences with UI18n!')); console.log(chalk.gray(' 💡 Tip: Try switching languages to see UI18n in action!\n')); } // 解析命令行参数并执行 program.parse();