mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Add unused i18n keys detection to pre-commit hook (#4328)
This commit is contained in:
@@ -3,3 +3,6 @@ if [[ "$OS" == "Windows_NT" ]]; then
|
||||
else
|
||||
npx lint-staged
|
||||
fi
|
||||
|
||||
# Check for unused i18n keys in staged files
|
||||
npx tsx scripts/check-unused-i18n-keys.ts
|
||||
|
||||
179
scripts/check-unused-i18n-keys.ts
Executable file
179
scripts/check-unused-i18n-keys.ts
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { execSync } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import { globSync } from 'glob'
|
||||
|
||||
interface LocaleData {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// Configuration
|
||||
const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*']
|
||||
const IGNORE_PATTERNS = [
|
||||
// Keys that might be dynamically constructed
|
||||
/^commands\./, // Command definitions are loaded dynamically
|
||||
/^settings\..*\.options\./, // Setting options are rendered dynamically
|
||||
/^nodeDefs\./, // Node definitions are loaded from backend
|
||||
/^templateWorkflows\./, // Template workflows are loaded dynamically
|
||||
/^dataTypes\./, // Data types might be referenced dynamically
|
||||
/^contextMenu\./, // Context menu items might be dynamic
|
||||
/^color\./ // Color names might be used dynamically
|
||||
]
|
||||
|
||||
// Get list of staged locale files
|
||||
function getStagedLocaleFiles(): string[] {
|
||||
try {
|
||||
const output = execSync('git diff --cached --name-only --diff-filter=AM', {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
return output
|
||||
.split('\n')
|
||||
.filter(
|
||||
(file) => file.startsWith('src/locales/') && file.endsWith('.json')
|
||||
)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all keys from a nested object
|
||||
function extractKeys(obj: any, prefix = ''): string[] {
|
||||
const keys: string[] = []
|
||||
|
||||
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(...extractKeys(value, fullKey))
|
||||
} else {
|
||||
keys.push(fullKey)
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
// Get new keys added in staged files
|
||||
function getNewKeysFromStagedFiles(stagedFiles: string[]): Set<string> {
|
||||
const newKeys = new Set<string>()
|
||||
|
||||
for (const file of stagedFiles) {
|
||||
try {
|
||||
// Get the staged content
|
||||
const stagedContent = execSync(`git show :${file}`, { encoding: 'utf-8' })
|
||||
const stagedData: LocaleData = JSON.parse(stagedContent)
|
||||
const stagedKeys = new Set(extractKeys(stagedData))
|
||||
|
||||
// Get the current HEAD content (if file exists)
|
||||
let headKeys = new Set<string>()
|
||||
try {
|
||||
const headContent = execSync(`git show HEAD:${file}`, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
const headData: LocaleData = JSON.parse(headContent)
|
||||
headKeys = new Set(extractKeys(headData))
|
||||
} catch {
|
||||
// File is new, all keys are new
|
||||
}
|
||||
|
||||
// Find keys that are in staged but not in HEAD
|
||||
stagedKeys.forEach((key) => {
|
||||
if (!headKeys.has(key)) {
|
||||
newKeys.add(key)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${file}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return newKeys
|
||||
}
|
||||
|
||||
// Check if a key should be ignored
|
||||
function shouldIgnoreKey(key: string): boolean {
|
||||
return IGNORE_PATTERNS.some((pattern) => pattern.test(key))
|
||||
}
|
||||
|
||||
// Search for key usage in source files
|
||||
function isKeyUsed(key: string, sourceFiles: string[]): boolean {
|
||||
// Common patterns for i18n key usage
|
||||
const patterns = [
|
||||
// Direct usage: $t('key'), t('key'), i18n.t('key')
|
||||
new RegExp(`[t$]\\s*\\(\\s*['"\`]${key}['"\`]`, 'g'),
|
||||
// With namespace: $t('g.key'), t('namespace.key')
|
||||
new RegExp(
|
||||
`[t$]\\s*\\(\\s*['"\`][^'"]+\\.${key.split('.').pop()}['"\`]`,
|
||||
'g'
|
||||
),
|
||||
// Dynamic keys might reference parts of the key
|
||||
new RegExp(`['"\`]${key}['"\`]`, 'g')
|
||||
]
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8')
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.test(content)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function checkNewUnusedKeys() {
|
||||
const stagedLocaleFiles = getStagedLocaleFiles()
|
||||
|
||||
if (stagedLocaleFiles.length === 0) {
|
||||
// No locale files staged, nothing to check
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Get all new keys from staged files
|
||||
const newKeys = getNewKeysFromStagedFiles(stagedLocaleFiles)
|
||||
|
||||
if (newKeys.size === 0) {
|
||||
// Silent success - no output needed
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Get all source files
|
||||
const sourceFiles = globSync(SOURCE_PATTERNS)
|
||||
|
||||
// Check each new key
|
||||
const unusedNewKeys: string[] = []
|
||||
|
||||
newKeys.forEach((key) => {
|
||||
if (!shouldIgnoreKey(key) && !isKeyUsed(key, sourceFiles)) {
|
||||
unusedNewKeys.push(key)
|
||||
}
|
||||
})
|
||||
|
||||
// Report results
|
||||
if (unusedNewKeys.length > 0) {
|
||||
console.log('\n❌ Found unused NEW i18n keys:\n')
|
||||
|
||||
for (const key of unusedNewKeys.sort()) {
|
||||
console.log(` - ${key}`)
|
||||
}
|
||||
|
||||
console.log(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
|
||||
console.log(
|
||||
'\nThese keys were added but are not used anywhere in the codebase.'
|
||||
)
|
||||
console.log('Please either use them or remove them before committing.')
|
||||
|
||||
process.exit(1)
|
||||
} else {
|
||||
// Silent success - no output needed
|
||||
}
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkNewUnusedKeys().catch((err) => {
|
||||
console.error('Error checking unused keys:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user