UNPKG

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
'use strict'; 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 };