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
376 lines (330 loc) • 11.2 kB
JavaScript
// utils/i18n-helper.js
const path = require('path');
const fs = require('fs');
// Helper functions for OS-agnostic path handling
function toPosix(p) { return String(p).replace(/\\/g, '/'); }
function isBundledPath(p) {
const s = toPosix(p);
return s.includes('/node_modules/i18ntk/') || s.includes('/i18ntk/ui-locales/');
}
function safeRequireConfig() {
try { return require('./config-manager'); } catch { return null; }
}
function stripBOMAndComments(s) {
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
s = s.replace(/\/\*[\s\S]*?\*\//g, '');
s = s.replace(/^\s*\/\/.*$/mg, '');
return s;
}
function readJsonSafe(file) {
const raw = fs.readFileSync(file, 'utf8');
return JSON.parse(stripBOMAndComments(raw));
}
function pkgUiLocalesDirViaThisFile() {
return path.resolve(__dirname, '..', 'ui-locales');
}
function pkgUiLocalesDirViaResolve() {
try {
const enJson = require.resolve('i18ntk/ui-locales/en.json');
return path.dirname(enJson);
} catch { return null; }
}
function resolveLocalesDirs() {
const dirs = [];
const addDir = (dir) => {
if (typeof dir === 'string' && dir.trim()) {
try {
const normalized = path.normalize(path.resolve(dir.trim()));
if (fs.existsSync(normalized) && fs.statSync(normalized).isDirectory()) {
dirs.push(normalized);
}
} catch {
// Silently ignore invalid paths
}
}
};
const pkgA = pkgUiLocalesDirViaThisFile();
addDir(pkgA);
const pkgB = pkgUiLocalesDirViaResolve();
if (pkgB && pkgB !== pkgA) {
addDir(pkgB);
}
// Deduplicate while preserving order
const seen = new Set();
return dirs.filter(dir => {
if (seen.has(dir)) return false;
seen.add(dir);
return true;
});
}
function candidatesForLang(dir, lang) {
return [
path.join(dir, `${lang}.json`), // ui-locales/en.json
path.join(dir, lang, 'index.json') // ui-locales/en/index.json
];
}
function findLocaleFilesAllDirs(lang) {
const dirs = resolveLocalesDirs();
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
console.log('🔎 i18ntk locale search dirs:', dirs);
}
const files = [];
const errors = [];
for (const dir of dirs) {
for (const candidate of candidatesForLang(dir, lang)) {
try {
if (fs.existsSync(candidate)) {
const stats = fs.statSync(candidate);
if (stats.isFile() && stats.size > 0) {
// Validate file is readable and parseable
fs.accessSync(candidate, fs.constants.R_OK);
// Quick JSON validation
const content = fs.readFileSync(candidate, 'utf8');
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
files.push(candidate);
} else {
errors.push({ file: candidate, error: 'Invalid JSON format' });
}
}
}
} catch (error) {
errors.push({ file: candidate, error: error.message });
}
}
}
if (process.env.I18NTK_DEBUG_LOCALES === '1' && errors.length > 0) {
console.warn(`⚠️ Locale resolution errors for ${lang}:`, errors);
}
return files;
}
let translations = {};
let currentLanguage = 'en';
let isInitialized = false;
const missingWarned = new Set();
function loadTranslations(language) {
const cfg = safeRequireConfig();
const settings = cfg?.getConfig?.() || {};
const configuredLanguage = settings.uiLanguage || settings.language || 'en';
// Prioritize settings file language over environment variable
const requested = (configuredLanguage || language || 'en').toString();
const short = requested.split('-')[0].toLowerCase();
const tryOrder = [requested, short, 'en'];
const loadErrors = [];
for (const lang of tryOrder) {
const files = findLocaleFilesAllDirs(lang);
// Prioritize bundled locales over project ones
const prioritizedFiles = files.sort((a, b) => Number(isBundledPath(b)) - Number(isBundledPath(a)));
for (const file of prioritizedFiles) {
try {
translations = readJsonSafe(file);
currentLanguage = lang;
isInitialized = true;
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
console.log(`🗂 Loaded UI locale → ${file} (${lang})`);
}
// Validate translations object
if (typeof translations === 'object' && translations !== null) {
return translations;
} else {
loadErrors.push({ file, error: 'Invalid translation format' });
}
} catch (e) {
loadErrors.push({ file, error: e.message });
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
console.warn(`⚠️ Failed to parse ${file}: ${e.message}`);
}
}
}
}
// Log comprehensive error summary if debugging
if (process.env.I18NTK_DEBUG_LOCALES === '1' && loadErrors.length > 0) {
console.warn(`📊 Locale loading errors summary:`, {
requested: requested,
triedLanguages: tryOrder,
errors: loadErrors
});
}
// Fallback to built-in minimal translations
translations = {
menu: {
title: '🌍 i18ntk - I18N Management',
separator: '============================================================',
options: {
init: 'Initialize new languages',
analyze: 'Analyze translations',
validate: 'Validate translations',
usage: 'Check key usage',
complete: 'Complete translations',
sizing: 'Analyze sizing',
workflow: 'Run full workflow',
status: 'Show project status',
delete: 'Delete all reports',
settings: 'Settings',
help: 'Help',
debug: 'Debug Tools',
language: 'Change UI language',
exit: 'Exit'
},
selectOptionPrompt: 'Select an option:'
}
};
currentLanguage = 'en';
isInitialized = true;
if (loadErrors.length > 0) {
console.warn(`⚠️ No valid UI locale files found. Using built-in English strings.`);
}
return translations;
}
/**
* Get a translated string by key
* @param {string} key - Translation key (e.g., 'module.subkey')
* @param {object} params - Parameters to interpolate into the translation
* @returns {string} - Translated string or the key if translation not found
*/
function t(key, params = {}) {
// Auto-initialize translations if not already loaded
if (!isInitialized) {
loadTranslations();
isInitialized = true;
}
// Split the key into parts (e.g., 'module.subkey' -> ['module', 'subkey'])
const keyParts = key.split('.');
let value = translations;
// Try to find the key in the main translations object
for (let i = 0; i < keyParts.length; i++) {
const part = keyParts[i];
if (value && typeof value === 'object' && part in value) {
value = value[part];
} else {
value = undefined; // Key not found in main path
break;
}
}
// If not found, try to find it in hardcodedTexts
if (typeof value === 'undefined' && translations.hardcodedTexts) {
let hardcodedValue = translations.hardcodedTexts;
for (const part of keyParts) {
if (hardcodedValue && typeof hardcodedValue === 'object' && part in hardcodedValue) {
hardcodedValue = hardcodedValue[part];
} else {
hardcodedValue = undefined; // Key not found in hardcodedTexts path
break;
}
}
if (typeof hardcodedValue !== 'undefined') {
value = hardcodedValue;
}
}
if (typeof value === 'undefined') {
if (!missingWarned.has(key)) {
missingWarned.add(key);
console.warn(`Translation key not found: ${key}`);
}
return key;
}
// If we found a string, interpolate parameters
if (typeof value === 'string') {
return interpolateParams(value, params);
}
// Return the key if the final value is not a string
console.warn(`Translation key does not resolve to a string: ${key}`);
return key;
}
/**
* Interpolate parameters into a translation string
* @param {string} template - Template string with {{param or {param} placeholders
* @param {object} params - Parameters to interpolate
* @returns {string} - Interpolated string
*/
function interpolateParams(template, params) {
// Handle both {{param}} and {param} formats
return template
.replace(/\{\{(\w+)\}\}/g, (match, paramName) => {
if (paramName in params) {
return params[paramName];
}
return match;
})
.replace(/\{(\w+)\}/g, (match, paramName) => {
if (paramName in params) {
return params[paramName];
}
return match;
});
}
/**
* Get the current language
* @returns {string} - Current language code
*/
function getCurrentLanguage() {
return currentLanguage;
}
/**
* Get all available languages
* @returns {string[]} - Array of available language codes
*/
function getAvailableLanguages() {
const dirs = resolveLocalesDirs();
const langs = new Set();
for (const d of dirs) {
try {
if (!fs.existsSync(d)) continue;
for (const f of fs.readdirSync(d)) {
if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
}
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
if (f.isDirectory() && fs.existsSync(path.join(d, f.name, `${f.name}.json`))) {
langs.add(f.name);
}
}
} catch {}
}
return Array.from(langs.size ? langs : new Set(['en']));
}
/**
* Deep merges two objects.
* @param {object} target - The target object.
* @param {object} source - The source object.
* @returns {object} - The merged object.
*/
function deepMerge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key]) &&
typeof target[key] === 'object' && target[key] !== null && !Array.isArray(target[key])) {
target[key] = deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
/**
* Refresh language from settings manager
* This ensures translations stay in sync with settings changes
*/
function refreshLanguageFromSettings() {
const cfg = safeRequireConfig();
const settings = cfg?.getConfig?.() || {};
const configuredLanguage = settings.language || settings.uiLanguage || 'en';
if (configuredLanguage !== currentLanguage) {
isInitialized = false;
loadTranslations(configuredLanguage);
}
}
/**
* Refresh translations (alias for refreshLanguageFromSettings)
*/
function refreshTranslations() {
refreshLanguageFromSettings();
}
module.exports = {
loadTranslations,
t,
getCurrentLanguage,
getAvailableLanguages,
deepMerge,
refreshTranslations,
refreshLanguageFromSettings
};