i18ntk
Version:
🚀 The fastest i18n toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive transla
544 lines (500 loc) • 16.9 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
const { envManager } = require('./env-manager');
// Determine package directory and user project root
const packageDir = path.resolve(__dirname, '..');
const userProjectRoot = process.cwd();
// Always use package's internal settings directory to avoid polluting user projects
// The settings directory should be within the package, not in user space
const PROJECT_SETTINGS_DIR = path.join(packageDir, 'settings');
const PROJECT_CONFIG_PATH = path.join(PROJECT_SETTINGS_DIR, 'i18ntk-config.json');
// Setup tracking file
const SETUP_COMPLETED_FILE = path.join(PROJECT_SETTINGS_DIR, 'setup.json');
// Legacy home directory config (for migration only)
const LEGACY_CONFIG_DIR = path.join(os.homedir(), '.i18ntk');
const LEGACY_CONFIG_PATH = path.join(LEGACY_CONFIG_DIR, 'i18ntk-config.json');
// Package defaults fallback (read-only) - always points to package internals
const PACKAGE_SETTINGS_DIR = path.join(packageDir, 'settings');
const PACKAGE_CONFIG_PATH = path.join(PACKAGE_SETTINGS_DIR, 'i18ntk-config.json');
// Keep projectRoot for path resolution functions
const projectRoot = userProjectRoot;
// Back-compat: expose CONFIG_DIR/CONFIG_PATH but point to project settings
const CONFIG_DIR = PROJECT_SETTINGS_DIR;
const CONFIG_PATH = PROJECT_CONFIG_PATH;
// Default configuration values - comprehensive configuration
const DEFAULT_CONFIG = {
"language": "en",
"uiLanguage": "en",
"theme": "system",
"projectRoot": ".",
"sourceDir": "./locales",
"i18nDir": "./locales",
"outputDir": "./i18ntk-reports",
"setup": {
"completed": false,
"completedAt": null,
"version": null,
"setupId": null
},
"framework": {
"preference": "auto", // one of: auto | vanilla | react | vue | angular | svelte | i18next | nuxt | next
"fallback": "vanilla", // when auto detects nothing, use this value
"detect": true,
"supported": ["react", "vue", "angular", "svelte", "i18next", "nuxt", "next", "vanilla"]
},
"scriptDirectories": {
"init": null,
"analyze": null,
"validate": null,
"usage": null,
"sizing": null,
"summary": null,
"complete": null,
"manage": null
},
"processing": {
"batchSize": 2000,
"concurrency": 32,
"maxFileSize": 524288,
"timeout": 3000,
"retryAttempts": 0,
"retryDelay": 0,
"cacheEnabled": true,
"cacheTTL": 180000,
"validateOnSave": false,
"autoBackup": false,
"validateOnLoad": false,
"fileFilter": "**/*.json",
"notTranslatedMarker": "NOT_TRANSLATED",
"excludeFiles": [
".DS_Store",
"Thumbs.db",
"*.tmp",
"*.bak",
"*.log",
"~*",
"*.swp"
],
"performanceMode": "ultra-extreme",
"memoryLimit": "256MB",
"gcInterval": 250,
"streaming": true,
"compression": "brotli",
"parallelProcessing": true,
"minimalLogging": true
},
"reports": {
"format": "json",
"includeStats": true,
"includeMissingKeys": true,
"includeUnusedKeys": true,
"includeUsageStats": true,
"includeValidationErrors": true,
"outputFormat": "both",
"generateSummary": true,
"generateDetailed": true,
"saveToFile": true,
"filenameTemplate": "i18n-report-{timestamp}"
},
"ui": {
"showProgress": true,
"showDetailedOutput": false,
"colorOutput": true,
"interactive": true,
"confirmActions": true,
"autoSave": true,
"autoLoad": true
},
"behavior": {
"autoDetectLanguage": true,
"strictMode": false,
"caseSensitive": true,
"ignoreComments": false,
"ignoreWhitespace": true,
"normalizeKeys": true,
"validateOnStartup": false
},
"notifications": {
"enabled": true,
"types": {
"success": true,
"warning": true,
"error": true,
"info": true,
"warnings": true,
"errors": true,
"progress": true
},
"sound": false,
"desktop": false,
"webhook": null
},
"dateTime": {
"format": "YYYY-MM-DD HH:mm:ss",
"timezone": "local",
"locale": "en-US"
},
"advanced": {
"backupBeforeChanges": true,
"validateSettings": true,
"logLevel": "error",
"performanceTracking": true,
"memoryLimit": "256MB",
"enableExperimental": true,
"batchSize": 2000,
"maxConcurrentFiles": 32,
"enableProgressBars": false,
"enableColorOutput": false,
"strictMode": false,
"enableAuditLog": false,
"validateOnSave": false,
"sizingThreshold": 50,
"sizingFormat": "table",
"timeout": 3000,
"performanceMode": "ultra-extreme",
"gcInterval": 250,
"streaming": true,
"compression": "brotli",
"parallelProcessing": true,
"aggressiveGC": true,
"memoryPooling": true,
"stringInterning": true
},
"backup": {
"enabled": false,
"singleFileMode": false,
"singleBackupFile": "i18ntk-central-backup.json",
"retentionDays": 30,
"maxBackups": 100
},
"security": {
"adminPinEnabled": false,
"adminPinPromptOnInit": true,
"keepAuthenticatedUntilExit": true,
"sessionTimeout": 30,
"maxFailedAttempts": 3,
"lockoutDuration": 15,
"enablePathValidation": true,
"maxFileSize": 10485760,
"allowedExtensions": [
".json",
".js",
".jsx",
".ts",
".tsx",
".vue",
".svelte"
],
"notTranslatedMarker": "[NOT TRANSLATED]",
"excludeFiles": [
"node_modules",
".git",
"dist",
"build"
],
"strictMode": false,
"pinProtection": {
"enabled": true,
"protectedScripts": {
"debugMenu": true,
"deleteReports": true,
"summaryReports": true,
"settingsMenu": true,
"init": false,
"analyze": false,
"validate": false,
"complete": false,
"manage": false,
"sizing": false,
"usage": false
}
}
},
"debug": {
"enabled": false,
"showSecurityLogs": false,
"verboseLogging": false,
"logLevel": "info",
"saveDebugLogs": false,
"debugLogPath": "./debug.log"
},
"sourceLanguage": "en",
"defaultLanguages": [
"de",
"es",
"fr",
"ru"
],
"reportLanguage": "auto",
"autoSave": true,
"dateFormat": "DD/MM/YYYY",
"timeFormat": "24h",
"timezone": "auto"
};
// Mapping of supported environment variables to config keys
const ENV_VAR_MAP = {
I18NTK_PROJECT_ROOT: 'projectRoot',
I18NTK_SOURCE_DIR: 'sourceDir',
I18NTK_I18N_DIR: 'i18nDir',
I18NTK_OUTPUT_DIR: 'outputDir',
I18NTK_FRAMEWORK_PREFERENCE: 'framework.preference',
I18NTK_FRAMEWORK_FALLBACK: 'framework.fallback',
I18NTK_FRAMEWORK_DETECT: 'framework.detect',
I18NTK_LOG_LEVEL: 'advanced.logLevel',
I18NTK_LANG: 'uiLanguage',
I18NTK_SILENT: 'processing.minimalLogging',
};
let currentConfig = null;
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function ensureProjectSettingsDir() {
// Only create settings directory within the package, not in user projects
// Since PROJECT_SETTINGS_DIR is now set to package internals, no action needed
// if directory doesn't exist, we'll use package defaults
}
function normalizePathValue(keyPath, value) {
if (typeof value !== 'string') return value;
const last = keyPath.split('.').pop();
if (/dir|directory|root|path$/i.test(last)) {
// Always resolve paths relative to user's project root, not package directory
const userProjectRoot = process.cwd();
const abs = path.resolve(userProjectRoot, value);
return toRelative(abs);
}
return value;
}
// Deep merge that also normalizes path-like leaves based on their key path
function deepMerge(target, source, basePath = '') {
if (!target || typeof target !== 'object') target = {};
for (const [key, val] of Object.entries(source || {})) {
const pathKey = basePath ? `${basePath}.${key}` : key;
if (val && typeof val === 'object' && !Array.isArray(val)) {
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
target[key] = {};
}
deepMerge(target[key], val, pathKey);
} else {
target[key] = normalizePathValue(pathKey, val);
}
}
return target;
}
function applyEnvOverrides(cfg) {
for (const [envVar, keyPath] of Object.entries(ENV_VAR_MAP)) {
const value = envManager.get(envVar);
if (value === null || value === undefined) continue;
const keys = keyPath.split('.');
let current = cfg;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!current[k] || typeof current[k] !== 'object') current[k] = {};
current = current[k];
}
const leaf = keys[keys.length - 1];
if (keyPath === 'framework.detect' || envVar === 'I18NTK_FRAMEWORK_DETECT') {
current[leaf] = String(value).toLowerCase() !== 'false' && value !== '0';
} else if (envVar === 'I18NTK_SILENT') {
current[leaf] = String(value).toLowerCase() === 'true' || value === '1';
} else {
current[leaf] = normalizePathValue(keyPath, value);
}
}
return cfg;
}
function tryReadJson(filePath) {
try {
if (!fs.existsSync(filePath)) {
return null;
}
const data = fs.readFileSync(filePath, 'utf8');
if (!data || data.trim() === '') {
console.warn(`[i18ntk] Warning: Empty or invalid JSON file at ${filePath}`);
return null;
}
try {
return JSON.parse(data);
} catch (parseError) {
console.error(`[i18ntk] Error parsing JSON from ${filePath}: ${parseError.message}`);
// Create a backup of the corrupted file
const backupPath = `${filePath}.corrupted-${Date.now()}.bak`;
try {
fs.writeFileSync(backupPath, data, 'utf8');
console.warn(`[i18ntk] Created backup of corrupted config at ${backupPath}`);
} catch (backupError) {
console.error(`[i18ntk] Failed to create backup of corrupted config: ${backupError.message}`);
}
return null;
}
} catch (error) {
console.error(`[i18ntk] Error reading config file at ${filePath}: ${error.message}`);
return null;
}
}
async function migrateLegacyIfNeeded(baseCfg) {
// If project config does not exist but legacy exists, migrate once
if (!fs.existsSync(PROJECT_CONFIG_PATH) && fs.existsSync(LEGACY_CONFIG_PATH)) {
const legacy = tryReadJson(LEGACY_CONFIG_PATH);
if (legacy && typeof legacy === 'object') {
const merged = deepMerge(clone(baseCfg), legacy);
// Mark migration completion in the merged config (QoL telemetry)
try { merged.migrationComplete = true; } catch (_) {}
ensureProjectSettingsDir();
try {
await fs.promises.writeFile(PROJECT_CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf8');
// Best-effort removal of legacy file to prevent future use
try { fs.unlinkSync(LEGACY_CONFIG_PATH); } catch (_) {}
// Deprecation notice
console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Your config has been migrated to settings/i18ntk-config.json. Please commit the settings/ directory to your project.');
return merged;
} catch (_) {
// If write fails, fall back to in-memory config without deleting legacy
console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Using migrated settings in memory; failed to persist to settings/. Ensure the project has write permissions.');
return merged;
}
}
}
return null;
}
function loadConfig() {
if (currentConfig) return currentConfig;
let cfg = clone(DEFAULT_CONFIG);
// 1) Project config (primary)
const projectCfg = tryReadJson(PROJECT_CONFIG_PATH);
if (projectCfg) {
cfg = deepMerge(clone(DEFAULT_CONFIG), projectCfg);
} else {
// 2) Package default (read-only)
const pkgCfg = tryReadJson(PACKAGE_CONFIG_PATH);
if (pkgCfg) {
cfg = deepMerge(clone(DEFAULT_CONFIG), pkgCfg);
}
// 3) Legacy migration (read-only source)
if (!projectCfg) {
const fromLegacy = tryReadJson(LEGACY_CONFIG_PATH);
if (fromLegacy) {
cfg = deepMerge(clone(DEFAULT_CONFIG), fromLegacy);
// Attempt to migrate to project settings
// Ignore migration errors; we still return merged cfg in memory
// eslint-disable-next-line no-unused-vars
console.warn('[i18ntk] Detected legacy config at ~/.i18ntk. Migrating to project settings directory...');
const _ = (async () => { await migrateLegacyIfNeeded(DEFAULT_CONFIG); })();
}
}
}
applyEnvOverrides(cfg);
currentConfig = cfg;
return currentConfig;
}
async function saveConfig(cfg = currentConfig) {
if (!cfg) return;
try {
// Ensure settings directory exists
if (!fs.existsSync(PROJECT_SETTINGS_DIR)) {
fs.mkdirSync(PROJECT_SETTINGS_DIR, { recursive: true });
}
// Save configuration to the project settings directory
await fs.promises.writeFile(PROJECT_CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8');
currentConfig = cfg;
} catch (error) {
console.error('[i18ntk] Error saving configuration:', error.message);
throw error;
}
}
function getConfig() {
if (currentConfig) {
return resolvePaths(currentConfig);
}
try {
// Ensure settings directory exists
if (!fs.existsSync(PROJECT_SETTINGS_DIR)) {
fs.mkdirSync(PROJECT_SETTINGS_DIR, { recursive: true });
}
// Setup is now handled automatically by the unified config system
// No need to check here - handled by getUnifiedConfig
// Check if config file exists
if (fs.existsSync(PROJECT_CONFIG_PATH)) {
const config = JSON.parse(fs.readFileSync(PROJECT_CONFIG_PATH, 'utf8'));
currentConfig = config;
return resolvePaths(config);
}
// Check for legacy config for migration
if (fs.existsSync(LEGACY_CONFIG_PATH)) {
console.log('📦 Migrating legacy configuration...');
const legacyConfig = JSON.parse(fs.readFileSync(LEGACY_CONFIG_PATH, 'utf8'));
const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
saveConfig(migratedConfig);
currentConfig = migratedConfig;
// Clean up legacy config
try {
fs.unlinkSync(LEGACY_CONFIG_PATH);
if (fs.readdirSync(LEGACY_CONFIG_DIR).length === 0) {
fs.rmdirSync(LEGACY_CONFIG_DIR);
}
} catch (cleanupError) {
// Ignore cleanup errors
}
return resolvePaths(migratedConfig);
}
// Use package defaults for new installation
console.log('📦 Initializing with default configuration...');
saveConfig(DEFAULT_CONFIG);
currentConfig = DEFAULT_CONFIG;
return resolvePaths(DEFAULT_CONFIG);
} catch (error) {
console.warn('⚠️ Error loading configuration, using defaults:', error.message);
currentConfig = DEFAULT_CONFIG;
return resolvePaths(DEFAULT_CONFIG);
}
}
async function setConfig(cfg) {
currentConfig = deepMerge(clone(DEFAULT_CONFIG), cfg || {});
// Don't save to disk - use in-memory config only
return currentConfig;
}
async function updateConfig(patch) {
const cfg = loadConfig();
deepMerge(cfg, patch);
// Don't save to disk - use in-memory config only
return cfg;
}
async function resetToDefaults() {
currentConfig = clone(DEFAULT_CONFIG);
// Don't save to disk - use in-memory config only
return currentConfig;
}
function resolvePaths(cfg = getConfig()) {
const root = path.resolve(projectRoot, cfg.projectRoot || '.');
const resolved = clone(cfg);
resolved.projectRoot = root;
['sourceDir', 'i18nDir', 'outputDir'].forEach(key => {
if (resolved[key]) resolved[key] = path.resolve(root, resolved[key]);
});
if (resolved.scriptDirectories) {
resolved.scriptDirectories = { ...resolved.scriptDirectories };
for (const [k, v] of Object.entries(resolved.scriptDirectories)) {
if (v) resolved.scriptDirectories[k] = path.resolve(root, v);
}
}
return resolved;
}
function toRelative(absPath) {
if (!absPath) return absPath;
const rel = path.relative(projectRoot, absPath);
const normalized = rel ? `./${rel.replace(/\\/g, '/')}` : '.';
return normalized;
}
module.exports = {
CONFIG_PATH,
DEFAULT_CONFIG,
loadConfig,
saveConfig,
getConfig,
updateConfig,
setConfig,
resetToDefaults,
resolvePaths,
toRelative,
normalizePathValue,
}