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
JavaScript
/**
* 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();