i18ntk
Version:
i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.
622 lines (562 loc) ⢠23.7 kB
JavaScript
/**
* Unified Configuration Helper
* Provides consistent directory configuration across all i18n toolkit scripts
* Configuration is managed through settings files only
*/
const fs = require('fs');
const path = require('path');
const configManager = require('./config-manager');
const SecurityUtils = require('./security');
const {loadTranslations} = require('./i18n-helper');
const SettingsManager = require('../settings/settings-manager');
const { envManager } = require('./env-manager');
const { checkInitialized } = require('./init-helper');
const { closeGlobalReadline } = require('./cli');
const { detectFramework } = require('./framework-detector');
const { getConfigUpgradeStatus, upgradeConfig } = require('./framework-config-templates');
const settingsManager = new SettingsManager();
const { ask } = require('./cli');
/**
* Normalize path to ensure consistent format
* @param {string} dirPath - Directory path to normalize
* @returns {string} Normalized absolute path
*/
function normalizePath(dirPath) {
if (!dirPath) return path.resolve('./locales');
return path.resolve(dirPath);
}
/**
* Get unified configuration for any script
* @param {string} scriptName - Name of the script (e.g., 'complete', 'analyze', 'validate')
* @param {object} cliArgs - Command line arguments parsed from the script
* @returns {object} Unified configuration object
*/
async function getUnifiedConfig(scriptName, cliArgs = {}) {
try {
// Setup is now handled automatically by config-manager, no need to check here
let cfg;
let projectRoot;
let settingsDir;
const toStr = v => (typeof v === 'string' ? v : null);
const configDirArg = toStr(cliArgs.configDir);
if (configDirArg) {
const safeConfigDir = SecurityUtils.validatePath(configDirArg, process.cwd());
if (!safeConfigDir) {
throw new Error('Invalid config directory');
}
settingsDir = safeConfigDir;
const configFile = path.join(settingsDir, 'i18ntk-config.json');
const rawConfig = SecurityUtils.safeReadFileSync(configFile, settingsDir, 'utf8');
cfg = rawConfig ? SecurityUtils.safeParseJSON(rawConfig) : {};
if (!cfg || typeof cfg !== 'object') cfg = {};
projectRoot = settingsDir;
cfg.projectRoot = projectRoot;
cfg.sourceDir = path.resolve(projectRoot, toStr(cfg.sourceDir) || './locales');
cfg.i18nDir = path.resolve(projectRoot, toStr(cfg.i18nDir) || cfg.sourceDir);
cfg.outputDir = path.resolve(projectRoot, toStr(cfg.outputDir) || './i18ntk-reports');
} else {
cfg = configManager.getConfig();
// Use current working directory instead of hardcoded path
const isSuspiciousPath = cfg.projectRoot && (
cfg.projectRoot.includes('i18n-management-toolkit-main') ||
cfg.projectRoot.includes('i18ntk-') ||
!SecurityUtils.safeExistsSync(cfg.projectRoot, path.dirname(cfg.projectRoot || '.'))
);
projectRoot = isSuspiciousPath ? process.cwd() : path.resolve(cfg.projectRoot || '.');
// Update config with dynamic project root
if (isSuspiciousPath) {
cfg.projectRoot = '.';
}
// Older project configs may predate one or more path fields. Keep those
// setups usable while the interactive v5 upgrade prompt is shown.
cfg.sourceDir = cfg.sourceDir || './locales';
cfg.i18nDir = cfg.i18nDir || cfg.sourceDir;
cfg.outputDir = cfg.outputDir || './i18ntk-reports';
const detectedFramework = detectFramework(projectRoot);
if (detectedFramework?.id) {
const upgrade = getConfigUpgradeStatus(cfg, detectedFramework.id);
if (cfg.setup?.completed !== false && upgrade.needsUpgrade) {
const upgraded = upgradeConfig(cfg, detectedFramework.id);
await configManager.saveConfig(upgraded.config);
cfg = upgraded.config;
if (!cliArgs.json) {
console.warn(`[i18ntk] Applied the one-time v${upgraded.requiredVersion} configuration update for ${upgraded.template.label}; existing settings were kept.`);
}
}
}
const updates = {};
const sourceDirArg = toStr(cliArgs.sourceDir);
if (sourceDirArg) {
const safe = SecurityUtils.validatePath(sourceDirArg, projectRoot);
if (!safe) {
throw new Error('Invalid source directory');
}
const abs = safe;
updates.sourceDir = configManager.toRelative(abs);
}
const i18nDirArg = toStr(cliArgs.i18nDir);
if (i18nDirArg) {
const safe = SecurityUtils.validatePath(i18nDirArg, projectRoot);
if (!safe) {
throw new Error('Invalid i18n directory');
}
const abs = safe;
updates.i18nDir = configManager.toRelative(abs);
}
const outputDirArg = toStr(cliArgs.outputDir);
if (outputDirArg) {
const safe = SecurityUtils.validatePath(outputDirArg, projectRoot);
if (!safe) {
throw new Error('Invalid output directory');
}
const abs = safe;
updates.outputDir = configManager.toRelative(abs);
}
if (Object.keys(updates).length > 0) {
await configManager.updateConfig(updates);
cfg = configManager.getConfig();
}
// Resolve all paths to absolute
cfg = configManager.resolvePaths(cfg);
// Script-specific override for sourceDir
if (cfg.scriptDirectories?.[scriptName]) {
cfg.sourceDir = path.resolve(cfg.projectRoot, cfg.scriptDirectories[scriptName]);
}
// Auto-fix configured i18nDir only when the user did not pass an explicit locale path.
if (!cliArgs.i18nDir && !SecurityUtils.safeExistsSync(cfg.i18nDir, projectRoot) && SecurityUtils.safeExistsSync(cfg.sourceDir, projectRoot)) {
await configManager.updateConfig({ i18nDir: configManager.toRelative(cfg.sourceDir) });
cfg.i18nDir = cfg.sourceDir;
}
settingsDir = settingsManager.configDir || projectRoot;
}
const hasExplicitSourceDir = Boolean(cliArgs.sourceDir);
const hasExplicitI18nDir = Boolean(cliArgs.i18nDir);
if (hasExplicitSourceDir && hasExplicitI18nDir) {
cfg.sourceDir = SecurityUtils.validatePath(normalizePath(cliArgs.sourceDir), projectRoot) || normalizePath(cliArgs.sourceDir);
cfg.i18nDir = SecurityUtils.validatePath(normalizePath(cliArgs.i18nDir), projectRoot) || normalizePath(cliArgs.i18nDir);
} else if (hasExplicitSourceDir && !hasExplicitI18nDir) {
const explicitSrc = SecurityUtils.validatePath(normalizePath(cliArgs.sourceDir), projectRoot) || normalizePath(cliArgs.sourceDir);
cfg.sourceDir = explicitSrc;
cfg.i18nDir = cfg.i18nDir || cfg.sourceDir;
} else if (!hasExplicitSourceDir && hasExplicitI18nDir) {
cfg.i18nDir = SecurityUtils.validatePath(normalizePath(cliArgs.i18nDir), projectRoot) || normalizePath(cliArgs.i18nDir);
cfg.sourceDir = cfg.sourceDir || cfg.i18nDir;
} else {
cfg.sourceDir = normalizePath(cfg.sourceDir || './locales');
cfg.i18nDir = normalizePath(cfg.i18nDir || cfg.sourceDir);
}
const displayPaths = {
projectRoot: '.',
sourceDir: path.relative(projectRoot, cfg.sourceDir) || '.',
i18nDir: path.relative(projectRoot, cfg.i18nDir) || '.',
outputDir: path.relative(projectRoot, cfg.outputDir) || '.',
};
const rawMarkers =
cfg.notTranslatedMarkers ||
cfg.processing?.notTranslatedMarkers ||
cfg.notTranslatedMarker ||
cfg.processing?.notTranslatedMarker ||
'NOT_TRANSLATED';
const markerList = Array.isArray(rawMarkers) ? rawMarkers : [rawMarkers];
const configuredBackupKeep = parseInt(cfg.backup?.maxBackups, 10);
const normalizedBackupMaxBackups = Number.isInteger(configuredBackupKeep)
? Math.min(Math.max(configuredBackupKeep, 1), 3)
: 1;
const normalizedBackupLocation = cfg.backup?.location || './i18ntk-backups';
const config = {
...cfg,
sourceLanguage: cliArgs.sourceLanguage || cfg.sourceLanguage || 'en',
uiLanguage: cliArgs.uiLanguage || cfg.uiLanguage || 'en',
notTranslatedMarker: markerList[0],
notTranslatedMarkers: markerList,
supportedExtensions: cfg.supportedExtensions || cfg.processing?.supportedExtensions || ['.json', '.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts', '.vue', '.svelte', '.astro', '.mdx', '.rs'],
excludeFiles: cfg.excludeFiles || cfg.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
excludeDirs: cfg.excludeDirs || cfg.processing?.excludeDirs || ['node_modules', '.next', '.nuxt', '.output', '.astro', '.svelte-kit', '.cache', '__generated__', '.git', 'dist', 'build', 'target'],
strictMode: cliArgs.strictMode || cfg.strictMode || false,
backupDir: path.resolve(projectRoot, normalizedBackupLocation),
tempDir: path.join(settingsDir, 'temp'),
cacheDir: path.join(settingsDir, '.cache'),
configDir: settingsDir,
backup: {
...(cfg.backup || {}),
enabled: cfg.backup?.enabled === true,
location: normalizedBackupLocation,
maxBackups: normalizedBackupMaxBackups
},
settings: {
defaultLanguages: cfg.defaultLanguages || ['en', 'de', 'es', 'fr', 'ru'],
processing: { ...cfg.processing },
security: { ...cfg.security },
advanced: cfg.advanced || {},
},
debug: cfg.debug || {},
displayPaths,
};
config.codeDir = config.sourceDir;
config.localesDir = config.i18nDir;
SecurityUtils.validateConfig(config);
return config;
} catch (error) {
throw new Error(`Configuration error for ${scriptName}: ${error.message}`);
}
}
/**
* Get environment-specific configuration
* @returns {object} Environment configuration
*/
function getEnvironmentConfig() {
const settings = settingsManager.getAllSettings();
return {
nodeEnv: settings.nodeEnv || 'production',
isProduction: (settings.nodeEnv || 'production') === 'production',
isDevelopment: (settings.nodeEnv || 'production') !== 'production',
isTest: (settings.nodeEnv || 'production') === 'test',
port: parseInt(settings.apiPort || '3000', 10),
host: settings.apiHost || 'localhost',
apiEnabled: settings.apiEnabled === true,
hotReload: settings.hotReload === true,
mockData: settings.mockData === true,
testMode: settings.testMode === true
};
}
/**
* Display current basic configuration
*/
function displayBasicConfig() {
const basicConfig = getBasicConfig();
console.log('\nš§ Basic Configuration:');
console.log(` Node Environment: ${basicConfig.nodeEnv}`);
console.log(` API Enabled: ${basicConfig.apiEnabled}`);
console.log(` Port: ${basicConfig.port}`);
console.log(` Host: ${basicConfig.host}`);
console.log(` Hot Reload: ${basicConfig.hotReload}`);
console.log(` Test Mode: ${basicConfig.testMode}`);
console.log(` Mock Data: ${basicConfig.mockData}`);
}
/**
* Parse common CLI arguments for all scripts
* @param {Array} args - process.argv.slice(2)
* @returns {object} Parsed arguments
*/
function parseCommonArgs(args) {
const parsed = {};
const availableLangCodes = settingsManager.getAvailableLanguages().map(l => l.code);
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
let [key, value] = arg.substring(2).split('=');
if (value === undefined && i + 1 < args.length && !args[i + 1].startsWith('--')) {
value = args[i + 1];
i++;
}
const sanitizedKey = key?.trim();
const sanitizedValue = value !== undefined ? value.trim() : true;
switch (sanitizedKey) {
case 'code-dir':
case 'source-code-dir':
case 'source-dir':
parsed.sourceDir = sanitizedValue;
break;
case 'locales-dir':
case 'i18n-dir':
parsed.i18nDir = sanitizedValue;
break;
case 'output-dir':
parsed.outputDir = sanitizedValue;
break;
case 'config-dir':
parsed.configDir = sanitizedValue;
break;
case 'source-locale':
case 'source-language':
parsed.sourceLanguage = sanitizedValue;
break;
case 'ui-language':
parsed.uiLanguage = sanitizedValue;
break;
case 'log-level':
parsed.logLevel = sanitizedValue;
break;
case 'framework':
parsed.frameworkPreference = sanitizedValue;
break;
case 'silent':
parsed.silent = sanitizedValue === 'true' || sanitizedValue === true;
break;
case 'debug-locales':
parsed.debugLocales = sanitizedValue === 'true' || sanitizedValue === true;
break;
case 'strict':
parsed.strictMode = true;
break;
case 'no-prompt':
parsed.noPrompt = true;
break;
case 'help':
case 'h':
parsed.help = true;
break;
case 'dry-run':
parsed.dryRun = true;
break;
case 'auto-translate':
parsed.autoTranslate = true;
break;
case 'watch':
parsed.watch = true;
break;
default:
// Handle language shorthand flags like --de, --fr
if (availableLangCodes.includes(sanitizedKey)) {
parsed.uiLanguage = sanitizedKey;
}
break;
}
}
}
// Apply environment variable defaults if CLI args not provided
if (!parsed.logLevel) {
const envLogLevel = envManager.get('I18NTK_LOG_LEVEL');
if (envLogLevel && envLogLevel !== 'error') {
parsed.logLevel = envLogLevel;
}
}
if (!parsed.uiLanguage) {
const envLang = envManager.get('I18NTK_UI_LANGUAGE');
if (envLang && envLang !== 'en') {
parsed.uiLanguage = envLang;
}
}
if (!parsed.outputDir) {
const envOutDir = envManager.get('I18NTK_OUTDIR');
if (envOutDir && envOutDir !== './i18ntk-reports') {
parsed.outputDir = envOutDir;
}
}
if (!parsed.sourceDir) {
const envSourceDir = envManager.get('I18NTK_SOURCE_DIR');
if (envSourceDir && envSourceDir !== './locales') {
parsed.sourceDir = envSourceDir;
}
}
if (!parsed.i18nDir) {
const envI18nDir = envManager.get('I18NTK_I18N_DIR');
if (envI18nDir && envI18nDir !== './locales') {
parsed.i18nDir = envI18nDir;
}
}
if (!parsed.frameworkPreference) {
const envFramework = envManager.get('I18NTK_FRAMEWORK_PREFERENCE');
if (envFramework && envFramework !== 'auto') {
parsed.frameworkPreference = envFramework;
}
}
return parsed;
}
/**
* Display help for script usage
* @param {string} scriptName - Name of the script
* @param {object} additionalOptions - Additional script-specific options
*/
function displayHelp(scriptName, additionalOptions = {}) {
const commonOptions = {
'code-dir': 'Application source code directory to scan',
'source-code-dir': 'Alias for --code-dir',
'source-dir': 'Legacy alias; source code for scanner commands, locale root for locale-only commands',
'locales-dir': 'Directory containing locale/i18n files',
'i18n-dir': 'Alias for --locales-dir',
'output-dir': 'Output directory for reports',
'source-locale': 'Source locale code (e.g., en, de)',
'source-language': 'Legacy alias for --source-locale',
'ui-language': 'UI language for messages',
'log-level': 'Logging level (error, warn, info, debug, silent)',
'framework': 'Preferred framework (auto, react, vue, etc.)',
'silent': 'Run in silent mode (true/false)',
'debug-locales': 'Enable debug logging for locale loading (true/false)',
'strict': 'Enable strict validation mode',
'no-prompt': 'Skip interactive prompts',
'help': 'Show this help message',
'watch': 'Watch for changes in source files',
'dry-run': 'Run validation without modifying files',
'auto-translate': 'Automatically translate missing keys',
'json': 'Output results in JSON format',
'sort-keys': 'Sort JSON keys alphabetically',
'indent': 'JSON indentation level (default: 2)',
'newline': 'Newline format: lf|cr|crlf (default: lf)',
'yes': 'Skip confirmation prompts for mutating operations',
};
const allOptions = { ...commonOptions, ...additionalOptions };
console.log(`\n${scriptName} - i18n Toolkit Script`);
console.log('='.repeat(30));
console.log('\nUsage:');
console.log(` node ${scriptName}.js [options]`);
console.log(` npx i18ntk ${scriptName.replace('i18ntk-', '')} [options]`);
console.log('\nOptions:');
Object.entries(allOptions).forEach(([flag, description]) => {
console.log(` --${flag.padEnd(15)} ${description}`);
});
console.log(`\nEnvironment Variables:`);
console.log(` I18NTK_LOG_LEVEL Logging level (error, warn, info, debug, silent)`);
console.log(` I18NTK_OUTDIR Output directory for reports`);
console.log(` I18NTK_UI_LANGUAGE UI language (en, de, es, fr, ru, ja, zh)`);
console.log(` I18NTK_SILENT Run in silent mode (true/false)`);
console.log(` I18NTK_DEBUG_LOCALES Enable debug logging for locale loading`);
console.log(` I18NTK_SOURCE_DIR Source directory for scanning`);
console.log(` I18NTK_I18N_DIR Directory containing i18n files`);
console.log(` I18NTK_PROJECT_ROOT Project root directory`);
console.log(` I18NTK_FRAMEWORK_PREFERENCE Preferred framework (auto, react, vue, etc.)`);
console.log(`\nExamples:`);
console.log(` node ${scriptName}.js --locales-dir=./locales --source-locale=en`);
console.log(` node ${scriptName}.js --code-dir=./app --locales-dir=./locales`);
console.log(` node ${scriptName}.js --output-dir=./i18ntk-reports`);
console.log(` I18NTK_LOG_LEVEL=debug node ${scriptName}.js --locales-dir=./locales`);
console.log(` npx i18ntk ${scriptName.replace('i18ntk-', '')} --help`);
console.log('\nConfiguration:');
console.log(` Settings are loaded from ${configManager.CONFIG_PATH}`);
console.log(' Use --code-dir, --locales-dir, --source-locale, and --output-dir to override');
console.log(' Environment variables can also be used for configuration');
}
/**
* Ensure directory exists, create if necessary
* @param {string} dirPath - Directory path
*/
function ensureDirectory(dirPath) {
if (!dirPath || typeof dirPath !== 'string') {
// Silently handle undefined or invalid paths to prevent security errors
return;
}
if (!SecurityUtils.safeExistsSync(dirPath, process.cwd())) {
SecurityUtils.safeMkdirSync(dirPath, process.cwd(), { recursive: true });
}
}
/**
* Validate that source directory exists
* @param {string} sourceDir - Source directory path
* @param {string} scriptName - Script name for error messages
*/
function validateSourceDir(sourceDir, scriptName) {
ensureDirectory(sourceDir);
}
// Display commonly used directories
function displayPaths(cfg = {}) {
if (cfg.sourceDir) console.log(`š Source directory: ${cfg.sourceDir}`);
if (cfg.i18nDir) console.log(`š I18n directory: ${cfg.i18nDir}`);
if (cfg.outputDir) console.log(`š¤ Output directory: ${cfg.outputDir}`);
}
// Ensure project has been initialized with source language files
async function ensureInitialized(cfg) {
try {
const sourceDir = cfg?.sourceDir || './locales';
const sourceLanguage = cfg?.sourceLanguage || 'en';
const { initialized } = await checkInitialized({ sourceDir, sourceLanguage });
if (initialized) {
return true;
}
const nonInteractive = !process.stdin.isTTY;
if (nonInteractive) {
const langDir = path.join(sourceDir, sourceLanguage);
console.warn(`Missing source language files in ${langDir}. Running initialization...`);
await initializeSourceFiles(sourceDir, sourceLanguage);
return true;
}
const langDir = path.join(sourceDir, sourceLanguage);
const answer = await ask(`Source language files not found in ${langDir}. Run initialization now? (y/N) `);
closeGlobalReadline();
if (answer.trim().toLowerCase().startsWith('y')) {
await initializeSourceFiles(sourceDir, sourceLanguage);
return true;
}
return false;
} catch (err) {
console.error(`Initialization check failed: ${err.message}`);
return false;
}
}
/**
* Initialize source language files directly (safe alternative to spawnSync)
*/
async function initializeSourceFiles(sourceDir, sourceLang) {
const sourceFile = path.join(sourceDir, `${sourceLang}.json`);
// Create default source language file with basic structure
const defaultContent = {
app: {
title: "Application",
description: "Application description"
},
common: {
yes: "Yes",
no: "No",
cancel: "Cancel",
save: "Save"
},
navigation: {
home: "Home",
about: "About",
contact: "Contact"
}
};
// Ensure source directory exists
ensureDirectory(sourceDir);
// Write the default source language file
SecurityUtils.safeWriteFileSync(sourceFile, JSON.stringify(defaultContent, null, 2), sourceDir, 'utf8');
// Create directories for supported languages
const supportedLanguages = ['es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt', 'it', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ko', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'];
supportedLanguages.forEach(lang => {
const langFile = path.join(sourceDir, `${lang}.json`);
if (!SecurityUtils.safeExistsSync(langFile, sourceDir)) {
// Create empty object structure for each language
const emptyStructure = {
app: {},
common: {},
navigation: {}
};
SecurityUtils.safeWriteFileSync(langFile, JSON.stringify(emptyStructure, null, 2), sourceDir, 'utf8');
}
});
// Create v2 project config if it doesn't exist
const configFile = '.i18ntk-config';
if (!SecurityUtils.safeExistsSync(configFile, process.cwd())) {
const version = (() => {
try {
return require('../package.json').version;
} catch {
return '2.0.0';
}
})();
const defaultConfig = {
version,
sourceDir: sourceDir,
outputDir: "./i18ntk-reports",
defaultLanguage: sourceLang,
supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt', 'it', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ko', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'],
setup: {
completed: true,
completedAt: new Date().toISOString(),
version,
setupId: `setup_${Date.now()}`
},
security: {
adminPinEnabled: true,
sessionTimeout: 1800000,
maxFailedAttempts: 3
},
performance: {
mode: "extreme",
cacheEnabled: true,
batchSize: 1000
}
};
SecurityUtils.safeWriteFileSync(configFile, JSON.stringify(defaultConfig, null, 2), process.cwd(), 'utf8');
}
}
module.exports = {
getUnifiedConfig,
parseCommonArgs,
displayHelp,
getEnvironmentConfig,
displayBasicConfig,
ensureDirectory,
validateSourceDir,
displayPaths,
ensureInitialized,
};