i18ntk
Version:
i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.
65 lines (56 loc) • 2.88 kB
JavaScript
;
const path = require('path');
const SecurityUtils = require('./security');
const DEFAULT_EXCLUDES = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', 'i18ntk-reports']);
function normalizeLocale(locale) {
return String(locale || '').trim().toLowerCase().replace(/_/g, '-');
}
function isLocaleName(name) {
const value = String(name || '');
if (!/^[a-z]{2,3}(?:[-_][a-z0-9]{2,8})*$/i.test(value)) return false;
try {
const baseLanguage = value.split(/[-_]/)[0].toLowerCase();
const displayNames = new Intl.DisplayNames(['en'], { type: 'language', fallback: 'none' });
return Boolean(displayNames.of(baseLanguage));
} catch (_) {
// Older/small-ICU Node builds may not expose DisplayNames. Retain the
// structural validation in that environment.
return true;
}
}
/** Discover monolith (en.json) and directory (en/*.json) layouts. */
function discoverLocaleFiles(baseDir, options = {}) {
const root = path.resolve(baseDir);
const sourceLocale = normalizeLocale(options.sourceLocale || '');
const excludes = new Set([...DEFAULT_EXCLUDES, ...(options.excludeDirs || [])].map(String));
const found = [];
if (!SecurityUtils.safeExistsSync(root, path.dirname(root))) return found;
const rootStat = SecurityUtils.safeStatSync(root, path.dirname(root));
if (!rootStat || !rootStat.isDirectory()) return found;
const walk = (dir, localeHint = '', displayLocale = '') => {
const entries = SecurityUtils.safeReaddirSync(dir, path.dirname(dir), { withFileTypes: true }) || [];
for (const entry of entries) {
if (excludes.has(entry.name)) continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const entryIsLocale = isLocaleName(entry.name);
const nextLocale = entryIsLocale ? normalizeLocale(entry.name) : localeHint;
walk(fullPath, nextLocale, entryIsLocale ? entry.name : displayLocale);
} else if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.json') {
const fileLocale = localeHint || normalizeLocale(path.basename(entry.name, '.json'));
if (!isLocaleName(fileLocale)) continue;
if (sourceLocale && fileLocale !== sourceLocale && options.sourceOnly) continue;
found.push({ filePath: fullPath, locale: fileLocale,
displayLocale: displayLocale || path.basename(entry.name, '.json'),
namespace: localeHint ? path.basename(entry.name, '.json') : 'default',
type: localeHint ? 'namespaced' : 'direct' });
}
}
};
walk(root);
return found.sort((a, b) => a.filePath.localeCompare(b.filePath));
}
function discoverLocales(baseDir, options = {}) {
return [...new Set(discoverLocaleFiles(baseDir, options).map(item => item.locale))].sort();
}
module.exports = { discoverLocaleFiles, discoverLocales, isLocaleName, normalizeLocale };