mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 03:14:56 +08:00
405 lines
12 KiB
JavaScript
405 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { readdirSync, readFileSync } from 'node:fs'
|
||
import { basename, dirname, extname, join, relative } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
const __filename = fileURLToPath(import.meta.url)
|
||
const __dirname = dirname(__filename)
|
||
|
||
const LOCALE_DIR = join(__dirname, '../src/i18n/locales')
|
||
const SRC_DIR = join(__dirname, '../src')
|
||
|
||
console.log(`\n🔍 Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}\n`)
|
||
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
bright: '\x1b[1m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
blue: '\x1b[34m',
|
||
magenta: '\x1b[35m',
|
||
cyan: '\x1b[36m'
|
||
}
|
||
|
||
function colorize(text, color) {
|
||
return `${colors[color]}${text}${colors.reset}`
|
||
}
|
||
|
||
function flattenKeys(obj, prefix = '') {
|
||
const keys = []
|
||
|
||
for (const [key, value] of Object.entries(obj)) {
|
||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||
|
||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||
keys.push(...flattenKeys(value, fullKey))
|
||
} else {
|
||
keys.push(fullKey)
|
||
}
|
||
}
|
||
|
||
return keys
|
||
}
|
||
|
||
function readLocaleFile(filePath) {
|
||
try {
|
||
const content = readFileSync(filePath, 'utf8')
|
||
return JSON.parse(content)
|
||
} catch (error) {
|
||
console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red'))
|
||
return {}
|
||
}
|
||
}
|
||
|
||
function getAllI18nKeys() {
|
||
const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.json'))
|
||
const allKeys = new Set()
|
||
const localeData = {}
|
||
|
||
console.log(colorize('\n📁 Found locale files:', 'blue'))
|
||
|
||
for (const file of localeFiles) {
|
||
const filePath = join(LOCALE_DIR, file)
|
||
const locale = basename(file, '.json')
|
||
const data = readLocaleFile(filePath)
|
||
const keys = flattenKeys(data)
|
||
|
||
localeData[locale] = {
|
||
file: filePath,
|
||
keys,
|
||
data
|
||
}
|
||
|
||
keys.forEach(key => allKeys.add(key))
|
||
|
||
console.log(` ${colorize('✓', 'green')} ${file} (${keys.length} keys)`)
|
||
}
|
||
|
||
return {
|
||
allKeys: Array.from(allKeys).sort(),
|
||
localeData
|
||
}
|
||
}
|
||
|
||
function findFiles(dir, extensions = ['.vue', '.ts', '.js']) {
|
||
const files = []
|
||
|
||
function walk(currentDir) {
|
||
const entries = readdirSync(currentDir, { withFileTypes: true })
|
||
|
||
for (const entry of entries) {
|
||
const fullPath = join(currentDir, entry.name)
|
||
|
||
if (entry.isDirectory()) {
|
||
if (!['node_modules', '.git', 'dist', 'build', 'target'].includes(entry.name)) {
|
||
walk(fullPath)
|
||
}
|
||
} else if (entry.isFile()) {
|
||
const ext = extname(entry.name)
|
||
if (extensions.includes(ext)) {
|
||
files.push(fullPath)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
walk(dir)
|
||
return files
|
||
}
|
||
|
||
function findKeyUsage(keys) {
|
||
const usage = {}
|
||
const dynamicPatterns = []
|
||
|
||
keys.forEach(key => {
|
||
usage[key] = {
|
||
used: false,
|
||
files: [],
|
||
patterns: [],
|
||
dynamicMatch: false
|
||
}
|
||
})
|
||
|
||
console.log(colorize('\n🔍 Searching for key usage in source files...', 'blue'))
|
||
|
||
const sourceFiles = findFiles(SRC_DIR)
|
||
|
||
console.log(` Found ${sourceFiles.length} source files to analyze`)
|
||
|
||
const searchPatterns = [
|
||
/\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
||
/(?:^|[^a-zA-Z])t\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
||
/\{\{\s*\$?t\s*\(\s*['"`]([^'"`]+)['"`]/g
|
||
]
|
||
|
||
const dynamicPattern = /\$?t\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*)`/g
|
||
|
||
sourceFiles.forEach(filePath => {
|
||
try {
|
||
const content = readFileSync(filePath, 'utf8')
|
||
const relativePath = relative(join(__dirname, '..'), filePath)
|
||
|
||
searchPatterns.forEach((pattern, patternIndex) => {
|
||
let match
|
||
while ((match = pattern.exec(content)) !== null) {
|
||
const key = match[1]
|
||
if (usage[key]) {
|
||
usage[key].used = true
|
||
if (!usage[key].files.includes(relativePath)) {
|
||
usage[key].files.push(relativePath)
|
||
}
|
||
if (!usage[key].patterns.includes(patternIndex)) {
|
||
usage[key].patterns.push(patternIndex)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
let dynamicMatch
|
||
while ((dynamicMatch = dynamicPattern.exec(content)) !== null) {
|
||
const templateString = dynamicMatch[1]
|
||
|
||
const staticParts = templateString.split(/\$\{[^}]+\}/)
|
||
|
||
const patternInfo = {
|
||
template: templateString,
|
||
file: relativePath,
|
||
staticParts
|
||
}
|
||
|
||
if (!dynamicPatterns.some(p => p.template === templateString && p.file === relativePath)) {
|
||
dynamicPatterns.push(patternInfo)
|
||
}
|
||
|
||
keys.forEach(key => {
|
||
if (matchesDynamicPattern(key, staticParts)) {
|
||
if (usage[key]) {
|
||
usage[key].used = true
|
||
usage[key].dynamicMatch = true
|
||
if (!usage[key].files.includes(relativePath)) {
|
||
usage[key].files.push(relativePath)
|
||
}
|
||
if (!usage[key].patterns.includes('dynamic')) {
|
||
usage[key].patterns.push('dynamic')
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red'))
|
||
}
|
||
})
|
||
|
||
usage._dynamicPatterns = dynamicPatterns
|
||
|
||
return usage
|
||
}
|
||
|
||
function matchesDynamicPattern(key, staticParts) {
|
||
if (staticParts.length === 0) return false
|
||
|
||
let keyIndex = 0
|
||
|
||
for (let i = 0; i < staticParts.length; i++) {
|
||
const part = staticParts[i]
|
||
|
||
if (part === '') {
|
||
if (i < staticParts.length - 1) {
|
||
const nextPart = staticParts[i + 1]
|
||
if (nextPart) {
|
||
const nextIndex = key.indexOf(nextPart, keyIndex)
|
||
if (nextIndex === -1) return false
|
||
keyIndex = nextIndex
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
if (i === 0) {
|
||
if (!key.startsWith(part)) return false
|
||
keyIndex = part.length
|
||
} else if (i === staticParts.length - 1) {
|
||
if (part && !key.endsWith(part)) return false
|
||
} else {
|
||
const index = key.indexOf(part, keyIndex)
|
||
if (index === -1) return false
|
||
keyIndex = index + part.length
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
function findLocaleInconsistencies(localeData) {
|
||
const locales = Object.keys(localeData)
|
||
const inconsistencies = {}
|
||
|
||
if (locales.length < 2) {
|
||
return inconsistencies
|
||
}
|
||
|
||
locales.forEach(locale => {
|
||
const currentKeys = new Set(localeData[locale].keys)
|
||
inconsistencies[locale] = {
|
||
missing: [],
|
||
extra: []
|
||
}
|
||
|
||
locales.forEach(otherLocale => {
|
||
if (locale !== otherLocale) {
|
||
localeData[otherLocale].keys.forEach(key => {
|
||
if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) {
|
||
inconsistencies[locale].missing.push(key)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
|
||
localeData[locale].keys.forEach(key => {
|
||
const existsInOthers = locales.some(
|
||
otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key)
|
||
)
|
||
if (!existsInOthers) {
|
||
inconsistencies[locale].extra.push(key)
|
||
}
|
||
})
|
||
})
|
||
|
||
return inconsistencies
|
||
}
|
||
|
||
function main() {
|
||
console.log(colorize('🌐 OpenList Desktop - I18n Usage Analyzer', 'cyan'))
|
||
console.log(colorize('==========================================', 'cyan'))
|
||
|
||
const { allKeys, localeData } = getAllI18nKeys()
|
||
|
||
console.log(colorize(`\n📊 Total unique keys found: ${allKeys.length}`, 'yellow'))
|
||
const usage = findKeyUsage(allKeys)
|
||
const dynamicPatterns = usage._dynamicPatterns || []
|
||
delete usage._dynamicPatterns
|
||
|
||
const usedKeys = allKeys.filter(key => usage[key].used)
|
||
const unusedKeys = allKeys.filter(key => !usage[key].used)
|
||
const dynamicallyUsedKeys = usedKeys.filter(key => usage[key].dynamicMatch)
|
||
const staticUsedKeys = usedKeys.filter(key => !usage[key].dynamicMatch)
|
||
|
||
const inconsistencies = findLocaleInconsistencies(localeData)
|
||
|
||
console.log(colorize('\n📈 Usage Summary:', 'blue'))
|
||
console.log(` ${colorize('✓', 'green')} Used keys: ${usedKeys.length}`)
|
||
console.log(` ${colorize('→', 'cyan')} Static usage: ${staticUsedKeys.length}`)
|
||
console.log(` ${colorize('→', 'magenta')} Dynamic usage: ${dynamicallyUsedKeys.length}`)
|
||
console.log(` ${colorize('✗', 'red')} Unused keys: ${unusedKeys.length}`)
|
||
console.log(` ${colorize('📊', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`)
|
||
|
||
if (dynamicPatterns.length > 0) {
|
||
console.log(colorize('\n🔮 Dynamic I18n Patterns Detected:', 'magenta'))
|
||
console.log(colorize('===================================', 'magenta'))
|
||
|
||
dynamicPatterns.forEach((pattern, index) => {
|
||
console.log(colorize(`\n${index + 1}. Template: \`${pattern.template}\``, 'cyan'))
|
||
console.log(` File: ${pattern.file}`)
|
||
console.log(` Static parts: [${pattern.staticParts.map(p => `"${p}"`).join(', ')}]`)
|
||
|
||
const matchingKeys = allKeys.filter(key => matchesDynamicPattern(key, pattern.staticParts))
|
||
if (matchingKeys.length > 0) {
|
||
console.log(
|
||
` ${colorize('Matches', 'green')} (${matchingKeys.length}): ${matchingKeys.slice(0, 5).join(', ')}${
|
||
matchingKeys.length > 5 ? '...' : ''
|
||
}`
|
||
)
|
||
}
|
||
})
|
||
}
|
||
|
||
if (unusedKeys.length > 0) {
|
||
console.log(colorize('\n🗑️ Unused I18n Keys:', 'red'))
|
||
console.log(colorize('====================', 'red'))
|
||
|
||
const groupedUnused = {}
|
||
unusedKeys.forEach(key => {
|
||
const namespace = key.split('.')[0]
|
||
if (!groupedUnused[namespace]) {
|
||
groupedUnused[namespace] = []
|
||
}
|
||
groupedUnused[namespace].push(key)
|
||
})
|
||
|
||
Object.entries(groupedUnused).forEach(([namespace, keys]) => {
|
||
console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow'))
|
||
keys.forEach(key => {
|
||
console.log(` ${colorize('✗', 'red')} ${key}`)
|
||
})
|
||
})
|
||
} else {
|
||
console.log(colorize('\n🎉 No unused keys found! All i18n keys are being used.', 'green'))
|
||
}
|
||
|
||
const hasInconsistencies = Object.values(inconsistencies).some(inc => inc.missing.length > 0 || inc.extra.length > 0)
|
||
|
||
if (hasInconsistencies) {
|
||
console.log(colorize('\n⚠️ Locale Inconsistencies:', 'yellow'))
|
||
console.log(colorize('=========================', 'yellow'))
|
||
|
||
Object.entries(inconsistencies).forEach(([locale, data]) => {
|
||
if (data.missing.length > 0 || data.extra.length > 0) {
|
||
console.log(colorize(`\n[${locale}.json]:`, 'cyan'))
|
||
|
||
if (data.missing.length > 0) {
|
||
console.log(colorize(` Missing ${data.missing.length} keys:`, 'red'))
|
||
data.missing.forEach(key => {
|
||
console.log(` ${colorize('✗', 'red')} ${key}`)
|
||
})
|
||
}
|
||
|
||
if (data.extra.length > 0) {
|
||
console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue'))
|
||
data.extra.forEach(key => {
|
||
console.log(` ${colorize('!', 'blue')} ${key}`)
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
|
||
console.log(colorize('\n📋 Sample Used Keys (first 10):', 'blue'))
|
||
console.log(colorize('=================================', 'blue'))
|
||
|
||
usedKeys.slice(0, 10).forEach(key => {
|
||
const files = usage[key].files.slice(0, 3) // Show first 3 files
|
||
const moreFiles = usage[key].files.length > 3 ? ` (+${usage[key].files.length - 3} more)` : ''
|
||
const usageType = usage[key].dynamicMatch ? colorize('(dynamic)', 'magenta') : colorize('(static)', 'cyan')
|
||
console.log(` ${colorize('✓', 'green')} ${key} ${usageType}`)
|
||
console.log(` Used in: ${files.join(', ')}${moreFiles}`)
|
||
})
|
||
|
||
if (dynamicallyUsedKeys.length > 0) {
|
||
console.log(colorize('\n🔮 Dynamic Key Usage Details:', 'magenta'))
|
||
console.log(colorize('=============================', 'magenta'))
|
||
|
||
dynamicallyUsedKeys.slice(0, 5).forEach(key => {
|
||
const files = usage[key].files.slice(0, 2)
|
||
console.log(` ${colorize('✨', 'magenta')} ${key}`)
|
||
console.log(` Files: ${files.join(', ')}`)
|
||
})
|
||
|
||
if (dynamicallyUsedKeys.length > 5) {
|
||
console.log(` ... and ${dynamicallyUsedKeys.length - 5} more dynamic keys`)
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(colorize('\n✨ Analysis complete!', 'cyan'))
|
||
|
||
if (unusedKeys.length > 0) {
|
||
console.log(colorize('\n💡 Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue'))
|
||
}
|
||
}
|
||
|
||
main()
|