i18ntk
Version:
i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.
381 lines (316 loc) • 12.5 kB
JavaScript
/**
* i18ntk-java.js - Java Language I18n Management Command
*
* Supports:
* - Spring Boot i18n
* - Java ResourceBundle
* - Android string resources
* - Java internationalization patterns
* - Properties files (.properties)
* - XML resource files
*/
const fs = require('fs');
const path = require('path');
const SecurityUtils = require('../utils/security');
const { getConfig, saveConfig } = require('../utils/config-helper');
const I18nHelper = require('../utils/i18n-helper');
const SetupEnforcer = require('../utils/setup-enforcer');
const { program } = require('../utils/mini-commander');
(async () => {
try {
await SetupEnforcer.checkSetupCompleteAsync();
} catch (error) {
console.error('Setup check failed:', error.message);
process.exit(1);
}
})();
class JavaI18nManager {
constructor() {
this.supportedPatterns = [
'getString(R.string.',
'getResources().getString(',
'messages.getString(',
'bundle.getString(',
'MessageSource.getMessage(',
'@Value("${',
'#{messages.',
'localeMessage.get(',
'i18n.get(',
'translate('
];
this.fileExtensions = ['.java', '.kt', '.xml', '.properties'];
this.resourceFormats = ['.properties', '.xml', '.json'];
}
async detectFramework(sourceDir) {
// Check for Android
const androidManifest = path.join(sourceDir, 'AndroidManifest.xml');
if (SecurityUtils.safeExistsSync(androidManifest)) {
return 'android';
}
// Check for Spring Boot
const pomXml = path.join(sourceDir, 'pom.xml');
const gradleFile = path.join(sourceDir, 'build.gradle');
if (SecurityUtils.safeExistsSync(pomXml)) {
const content = SecurityUtils.safeReadFileSync(pomXml, path.dirname(pomXml), 'utf8');
if (content.includes('spring-boot')) {
return 'spring-boot';
}
}
if (SecurityUtils.safeExistsSync(gradleFile)) {
const content = SecurityUtils.safeReadFileSync(gradleFile, path.dirname(gradleFile), 'utf8');
if (content.includes('spring-boot')) {
return 'spring-boot';
}
}
// Check for standard Java
const javaFiles = this.findFiles(sourceDir, '.java');
if (javaFiles.length > 0) {
return 'standard-java';
}
// Check for Kotlin
const kotlinFiles = this.findFiles(sourceDir, '.kt');
if (kotlinFiles.length > 0) {
return 'kotlin';
}
return 'generic';
}
async extractTranslations(sourceDir, options = {}) {
const framework = await this.detectFramework(sourceDir);
const translations = new Set();
// Process Java/Kotlin files
const javaFiles = [...this.findFiles(sourceDir, '.java'), ...this.findFiles(sourceDir, '.kt')];
for (const file of javaFiles) {
const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
// Extract Android string references
const androidPatterns = [
/R\.string\.([a-zA-Z0-9_]+)/g,
/getString\(R\.string\.([a-zA-Z0-9_]+)\)/g
];
// Extract Spring/JVM patterns
const springPatterns = [
/getResources\(\)\.getString\(R\.string\.([a-zA-Z0-9_]+)\)/g,
/messages\.getString\("([^"]+)"\)/g,
/bundle\.getString\("([^"]+)"\)/g,
/MessageSource\.getMessage\("([^"]+)"/g,
/@Value\("\$\{([^}]+)\}"\)/g,
/localeMessage\.get\("([^"]+)"\)/g,
/i18n\.get\("([^"]+)"\)/g,
/translate\("([^"]+)"\)/g
];
const patterns = framework === 'android' ? androidPatterns : [...androidPatterns, ...springPatterns];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
translations.add(match[1]);
}
}
}
// Process XML files (Android)
const xmlFiles = this.findFiles(sourceDir, '.xml');
for (const file of xmlFiles) {
if (file.includes('strings.xml')) {
const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8') || '';
const stringPatterns = [
/<string name="([^"]+)"/g,
/<string-array name="([^"]+)"/g,
/<plurals name="([^"]+)"/g
];
for (const pattern of stringPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
translations.add(match[1]);
}
}
}
}
// Process properties files
const propertiesFiles = this.findFiles(sourceDir, '.properties');
for (const file of propertiesFiles) {
if (file.includes('messages') || file.includes('i18n')) {
const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8') || '';
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) {
const key = trimmed.split('=')[0].trim();
translations.add(key);
}
}
}
}
return {
framework,
translations: Array.from(translations),
files: javaFiles.length + xmlFiles.length + propertiesFiles.length,
patterns: this.supportedPatterns
};
}
async createLocaleStructure(outputDir, languages = ['en'], framework = 'standard-java') {
const localesDir = path.join(outputDir, 'locales');
for (const lang of languages) {
const langDir = path.join(localesDir, lang);
fs.mkdirSync(langDir, { recursive: true });
if (framework === 'android') {
// Android string resources
SecurityUtils.safeWriteFileSync(
path.join(langDir, 'strings.xml'),
`<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">My App</string>
<string name="hello">Hello, World!</string>
<string name="items_count">%d items</string>
<plurals name="items">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>`,
path.dirname(path.join(langDir, 'strings.xml'))
);
// JSON format
SecurityUtils.safeWriteFileSync(path.join(langDir, 'messages.json'), JSON.stringify({
app: { name: "My Application" },
hello: { message: "Hello, World!" },
items: { count: "{0} items" }
}, null, 2));
}
}
return localesDir;
}
findFiles(dir, extension) {
const files = [];
function traverse(currentDir) {
if (!SecurityUtils.safeExistsSync(currentDir)) return;
const items = fs.readdirSync(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
try {
const stat = fs.statSync(fullPath);
if (stat.isDirectory() && !item.startsWith('.') &&
!['node_modules', 'build', 'target'].includes(item)) {
traverse(fullPath);
} else if (stat.isFile() && path.extname(item) === extension) {
files.push(fullPath);
}
} catch (error) {
// Skip files we can't access
}
}
}
traverse(dir);
return files;
}
async generateReport(results, outputDir) {
const report = {
timestamp: new Date().toISOString(),
framework: results.framework,
totalTranslations: results.translations.length,
translations: results.translations,
filesProcessed: results.files,
patterns: results.patterns,
recommendations: this.getRecommendations(results)
};
const reportPath = path.join(outputDir, 'i18ntk-java-report.json');
SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2));
return reportPath;
}
getRecommendations(results) {
const recommendations = [];
if (results.translations.length === 0) {
recommendations.push('No translations found. Check your Java i18n patterns');
}
if (results.files === 0) {
recommendations.push('No Java/Kotlin files found in source directory');
}
if (results.framework === 'generic') {
recommendations.push('Consider using Spring Boot for better i18n support');
}
if (results.framework === 'android') {
recommendations.push('Use Android string resources for best compatibility');
}
return recommendations;
}
}
// CLI Implementation
program
.name('i18ntk-java')
.description('Java language i18n management tool')
.version('1.10.1');
program
.command('init')
.description('Initialize Java i18n structure')
.option('-s, --source-dir <dir>', 'Source directory', './')
.option('-o, --output-dir <dir>', 'Output directory', './i18ntk-reports')
.option('-l, --languages <langs>', 'Languages (comma-separated)', 'en')
.option('-f, --framework <framework>', 'Framework (spring-boot|android|standard)', 'standard-java')
.action(async (options) => {
try {
const manager = new JavaI18nManager();
const languages = options.languages.split(',');
const localesDir = await manager.createLocaleStructure(options.outputDir, languages, options.framework);
console.log(`✅ Java i18n structure initialized in: ${localesDir}`);
console.log(`📊 Languages: ${languages.join(', ')}`);
console.log(`🎯 Framework: ${options.framework}`);
} catch (error) {
console.error('❌ Error initializing Java i18n:', error.message);
process.exit(1);
}
});
program
.command('analyze')
.description('Analyze Java i18n usage')
.option('-s, --source-dir <dir>', 'Source directory', './')
.option('-o, --output-dir <dir>', 'Output directory', './i18ntk-reports')
.option('--dry-run', 'Preview without making changes')
.action(async (options) => {
try {
SecurityUtils.validatePath(options.sourceDir);
const manager = new JavaI18nManager();
const results = await manager.extractTranslations(options.sourceDir);
console.log(`🔍 Framework detected: ${results.framework}`);
console.log(`📊 Files processed: ${results.files}`);
console.log(`📝 Translations found: ${results.translations.length}`);
if (!options.dryRun) {
const reportPath = await manager.generateReport(results, options.outputDir);
console.log(`📄 Report saved: ${reportPath}`);
}
} catch (error) {
console.error('❌ Error analyzing Java i18n:', error.message);
process.exit(1);
}
});
program
.command('extract')
.description('Extract Java translations')
.option('-s, --source-dir <dir>', 'Source directory', './')
.option('-o, --output-dir <dir>', 'Output directory', './locales')
.option('-l, --languages <langs>', 'Languages (comma-separated)', 'en')
.option('-f, --framework <framework>', 'Framework (spring-boot|android|standard)', 'standard-java')
.action(async (options) => {
try {
SecurityUtils.validatePath(options.sourceDir);
const manager = new JavaI18nManager();
const results = await manager.extractTranslations(options.sourceDir);
await manager.createLocaleStructure(options.outputDir, options.languages.split(','), options.framework);
console.log(`✅ Extracted ${results.translations.length} translations`);
console.log(`📁 Locale structure created in: ${options.outputDir}`);
console.log(`🎯 Framework: ${options.framework}`);
} catch (error) {
console.error('❌ Error extracting Java translations:', error.message);
process.exit(1);
}
});
// Handle uncaught errors
process.on('uncaughtException', (error) => {
console.error('❌ Uncaught exception:', error.message);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('❌ Unhandled rejection:', reason);
process.exit(1);
});
// Export for programmatic use
module.exports = { JavaI18nManager };
if (require.main === module) {
program.parse();
}