i18n-adapt
Version:
Automatically internationalize and adapt web application UI for multiple languages
373 lines (318 loc) • 13.2 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
// Initialize React project with i18n structure
async function initialize(projectRoot) {
console.log(chalk.blue('🚀 Initializing React project for internationalization...'));
const i18nFile = path.join(projectRoot, 'src/i18n.js');
// Create i18n.js file if it doesn't exist
if (!await fs.pathExists(i18nFile)) {
const i18nContent = `
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// English translations
const enTranslations = {
common: {
loading: 'Loading',
error: 'Error',
retry: 'Retry',
},
navigation: {
home: 'Home',
about: 'About',
contact: 'Contact',
},
};
// Initialize i18next
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: {
translation: enTranslations
}
},
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false
}
});
export default i18n;
`;
await fs.outputFile(i18nFile, i18nContent);
console.log(chalk.green(`✓ Created i18n.js file at ${i18nFile}`));
}
// Check for react-i18next in package.json and install if needed
try {
const packagePath = path.join(projectRoot, 'package.json');
const pkg = await fs.readJson(packagePath);
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
if (!deps['react-i18next'] || !deps['i18next'] || !deps['i18next-browser-languagedetector']) {
console.log(chalk.yellow('⚠️ i18n dependencies not found in package.json.'));
console.log(chalk.yellow('Please run: npm install --save react-i18next i18next i18next-browser-languagedetector'));
}
} catch (err) {
console.error(chalk.red(`Error checking dependencies: ${err.message}`));
}
// Create an example component with i18n usage
const exampleComponent = path.join(projectRoot, 'src/components/I18nExample.js');
if (!await fs.pathExists(exampleComponent)) {
const componentContent = `
import React from 'react';
import { useTranslation } from 'react-i18next';
import '../ResponsiveLanguage.css';
const I18nExample = () => {
const { t, i18n } = useTranslation();
const changeLanguage = (lang) => {
i18n.changeLanguage(lang);
document.documentElement.lang = lang;
};
return (
<div className="i18n-example">
<h2 className="lang-wrap">{t('navigation.home')}</h2>
<p className="lang-wrap">{t('common.loading')}</p>
<div className="language-buttons">
<button className="lang-btn" onClick={() => changeLanguage('en')}>English</button>
<button className="lang-btn" onClick={() => changeLanguage('es')}>Español</button>
<button className="lang-btn" onClick={() => changeLanguage('zh')}>中文</button>
<button className="lang-btn" onClick={() => changeLanguage('hi')}>हिंदी</button>
</div>
</div>
);
};
export default I18nExample;
`;
// Create components directory if it doesn't exist
await fs.ensureDir(path.join(projectRoot, 'src/components'));
await fs.outputFile(exampleComponent, componentContent);
console.log(chalk.green(`✓ Created example component at ${exampleComponent}`));
}
return true;
}
// Analyze React project for strings and UI files
async function analyze(projectRoot, structure) {
console.log(chalk.blue('🔍 Analyzing React components...'));
const strings = new Set();
const { uiFiles, i18nFile } = structure;
// First check if i18n file exists and extract current translations
let currentTranslations = {};
if (await fs.pathExists(i18nFile)) {
const content = await fs.readFile(i18nFile, 'utf8');
// Extract existing English translations
const enMatch = content.match(/const\s+enTranslations\s*=\s*\{([\s\S]*?)\};/);
if (enMatch) {
try {
// Create a safe evaluation context
const evalFn = new Function('return {' + enMatch[1] + '};');
currentTranslations = evalFn();
} catch (err) {
console.warn(chalk.yellow(`⚠️ Could not parse existing translations: ${err.message}`));
// Fallback to regex extraction
const namespaceRegex = /(\w+):\s*{([^{}]*(?:{[^{}]*(?:{[^{}]*}[^{}]*)*}[^{}]*)*)}/g;
let namespaceMatch;
while ((namespaceMatch = namespaceRegex.exec(enMatch[1])) !== null) {
const namespace = namespaceMatch[1];
currentTranslations[namespace] = {};
const keyValueRegex = /(\w+):\s*(['"])((?:\\\\|\\\2|.)*?)\2/g;
let keyValueMatch;
while ((keyValueMatch = keyValueRegex.exec(namespaceMatch[2])) !== null) {
const key = keyValueMatch[1];
const value = keyValueMatch[3].replace(/\\(['"])/g, '$1');
currentTranslations[namespace][key] = value;
strings.add(value);
}
}
}
}
}
// Now scan UI files for potential strings to translate
for (const file of uiFiles) {
try {
const content = await fs.readFile(file, 'utf8');
// Look for hardcoded text in JSX
const jsxTextRegex = />([^<>{}\n]+)</g;
let match;
while ((match = jsxTextRegex.exec(content)) !== null) {
const text = match[1].trim();
if (text && text.length > 1 && !/^\d+$/.test(text) && !text.includes('${')) {
strings.add(text);
}
}
// Look for hardcoded text in JSX attributes (e.g., placeholder, title, alt)
const jsxAttributeRegex = /(?:placeholder|title|alt|aria-label)=['"](.*?)['"]|(?:placeholder|title|alt|aria-label)=\{['"](.*?)['"]\}/g;
while ((match = jsxAttributeRegex.exec(content)) !== null) {
const text = (match[1] || match[2])?.trim();
if (text && text.length > 1 && !/^\d+$/.test(text) && !text.includes('${')) {
strings.add(text);
}
}
} catch (err) {
console.warn(chalk.yellow(`⚠️ Could not analyze file ${file}: ${err.message}`));
}
}
return {
strings: Array.from(strings),
uiFiles,
currentTranslations
};
}
// Update translations in React project
async function updateTranslations(structure, translations, language, forceAll) {
console.log(chalk.blue(`📝 Updating React translations for ${language}...`));
const { i18nFile } = structure;
if (!await fs.pathExists(i18nFile)) {
throw new Error(`i18n file not found: ${i18nFile}`);
}
const content = await fs.readFile(i18nFile, 'utf8');
// Check if the target language translation object already exists
const langRegex = new RegExp(`const\\s+${language}Translations\\s*=\\s*\\{[\\s\\S]*?\\};`);
let updatedContent;
if (langRegex.test(content)) {
// Update existing translations
if (forceAll) {
// Replace entire translation object
updatedContent = content.replace(
langRegex,
`const ${language}Translations = ${JSON.stringify(translations, null, 2)};`
);
} else {
// Merge with existing translations
const existingMatch = content.match(langRegex);
if (existingMatch) {
try {
const evalFn = new Function(`return ${existingMatch[0].replace(`const ${language}Translations =`, '')}`);
const existingTranslations = evalFn();
// Deep merge
const mergedTranslations = { ...existingTranslations };
Object.keys(translations).forEach(namespace => {
mergedTranslations[namespace] = {
...(mergedTranslations[namespace] || {}),
...translations[namespace]
};
});
updatedContent = content.replace(
langRegex,
`const ${language}Translations = ${JSON.stringify(mergedTranslations, null, 2)};`
);
} catch (err) {
console.warn(chalk.yellow(`⚠️ Error merging translations: ${err.message}`));
// Fallback to replacement
updatedContent = content.replace(
langRegex,
`const ${language}Translations = ${JSON.stringify(translations, null, 2)};`
);
}
}
}
} else {
// Add new language translations
updatedContent = content.replace(
/(const \w+Translations = {[\s\S]*?};)([\s\S]*?)(i18n)/,
`$1\n\n// ${language === 'es' ? 'Spanish' : language === 'zh' ? 'Chinese' : language === 'hi' ? 'Hindi' : language} translations\nconst ${language}Translations = ${JSON.stringify(translations, null, 2)};$2$3`
);
// Also add to resources section
updatedContent = updatedContent.replace(
/(resources: {\s*.*?\s*en: {[\s\S]*?})(,?\s*)(})/,
`$1,\n ${language}: {\n translation: ${language}Translations\n }$3`
);
}
// Create backup
const backupPath = `${i18nFile}.backup-${Date.now()}`;
await fs.copy(i18nFile, backupPath);
// Write updated file
await fs.writeFile(i18nFile, updatedContent);
console.log(chalk.green(`✓ Updated translations in ${i18nFile}`));
return true;
}
// Update UI for language responsiveness
async function updateUI(structure, uiFiles) {
console.log(chalk.blue('🎨 Updating React UI components for language responsiveness...'));
const { responsiveCssFile } = structure;
// Ensure responsive CSS exists
if (!await fs.pathExists(responsiveCssFile)) {
const cssContent = await fs.readFile(path.join(__dirname, '../styles/responsive-language.css'), 'utf8');
await fs.outputFile(responsiveCssFile, cssContent);
console.log(chalk.green(`✓ Created ResponsiveLanguage.css at ${responsiveCssFile}`));
}
// Find App.js or equivalent to make sure it imports the CSS
const appFile = uiFiles.find(file =>
path.basename(file) === 'App.js' ||
path.basename(file) === 'App.jsx' ||
path.basename(file) === 'app.js'
);
if (appFile) {
const content = await fs.readFile(appFile, 'utf8');
if (!content.includes('ResponsiveLanguage.css')) {
const updatedContent = content.replace(
/(import\s+.*\s+from\s+['"][^'"]+['"];)(\s*)/,
`$1\nimport './ResponsiveLanguage.css'; // Added for language-responsive UI$2`
);
await fs.writeFile(appFile, updatedContent);
console.log(chalk.green(`✓ Added CSS import to ${appFile}`));
}
}
// Process each UI file
for (const file of uiFiles) {
try {
const content = await fs.readFile(file, 'utf8');
let updatedContent = content;
let changesMade = false;
// Find components that need to be language-responsive
// 1. Buttons
const buttonPattern = /<button([^>]*)>([^<]+)<\/button>/g;
updatedContent = updatedContent.replace(buttonPattern, (match, attrs, text) => {
if (!attrs.includes('className="lang-btn"') &&
!attrs.includes('className={') &&
text.trim().length > 0) {
changesMade = true;
return `<button${attrs} className="lang-btn">${text}</button>`;
} else if (attrs.includes('className="') &&
!attrs.includes('lang-btn') &&
text.trim().length > 0) {
changesMade = true;
return `<button${attrs.replace('className="', 'className="lang-btn ')}>${text}</button>`;
}
return match;
});
// 2. Text elements
const textElementPattern = /(<(?:p|span|div|h[1-6])[^>]*>)([^<>{}\n]+)(<\/(?:p|span|div|h[1-6])>)/g;
updatedContent = updatedContent.replace(textElementPattern, (match, opening, content, closing) => {
const trimmedContent = content.trim();
if (trimmedContent.length === 0 || opening.includes('lang-wrap')) {
return match;
}
changesMade = true;
if (!opening.includes('className=')) {
return `${opening.replace('>', ' className="lang-wrap">')}${content}${closing}`;
} else if (opening.includes('className="')) {
return opening.replace('className="', 'className="lang-wrap ') + content + closing;
}
return match;
});
// 3. Language switcher code - add HTML lang attribute
if (content.includes('changeLanguage') && !content.includes('document.documentElement.lang')) {
const langSwitcherPattern = /(const changeLanguage\s*=\s*\([^)]*\)\s*=>\s*\{\s*)(.*?i18n\.changeLanguage\([^)]*\);)/;
updatedContent = updatedContent.replace(langSwitcherPattern, (match, prefix, changeFn) => {
changesMade = true;
return `${prefix}${changeFn}\n document.documentElement.lang = langCode;`;
});
}
if (changesMade) {
await fs.writeFile(file, updatedContent);
console.log(chalk.green(`✓ Updated ${path.basename(file)} for language responsiveness`));
}
} catch (err) {
console.warn(chalk.yellow(`⚠️ Could not update file ${file}: ${err.message}`));
}
}
return true;
}
module.exports = {
initialize,
analyze,
updateTranslations,
updateUI
};