PasteBarApp/scripts/translation-audit.ts

133 lines
4.7 KiB
TypeScript

// translation-audit.ts
// Script to audit missing translation keys in non-English YAML files
// Usage: Run with `ts-node` or compile to JS and run with `node`
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
// Recursively scan a directory for files with given extensions
function scanFiles(dir: string, exts: string[], fileList: string[] = []): string[] {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
scanFiles(fullPath, exts, fileList)
} else if (exts.some(ext => entry.name.endsWith(ext))) {
fileList.push(fullPath)
}
}
return fileList
}
// Check if a translation key is used in any of the code files
function isKeyUsedInCode(key: string, codeFiles: string[]): boolean {
// Accept both dot and bracket notation, e.g. t('common.key') or t("common.key")
const keyPattern = new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
for (const file of codeFiles) {
const content = fs.readFileSync(file, 'utf-8')
if (keyPattern.test(content)) {
return true
}
}
return false
}
// Helper to flatten nested objects into dot notation keys
type AnyObject = Record<string, any>
function flattenKeys(obj: AnyObject, prefix = ''): string[] {
let keys: string[] = []
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
const value = obj[key]
const newKey = prefix ? `${prefix}.${key}` : key
if (typeof value === 'object' && value !== null) {
keys = keys.concat(flattenKeys(value, newKey))
} else {
keys.push(newKey)
}
}
return keys
}
// Load supported languages from languges.ts (extract code from LANGUAGES array)
function loadSupportedLanguages(filePath: string): string[] {
const content = fs.readFileSync(filePath, 'utf-8')
// Match LANGUAGES array of objects and extract code values
const codeRegex = /code:\s*['"]([\w]+)['"]/g
const codes: string[] = []
let match
while ((match = codeRegex.exec(content)) !== null) {
codes.push(match[1])
}
if (codes.length === 0) throw new Error('No language codes found in LANGUAGES')
return codes
}
// Main audit function
async function auditTranslations() {
const baseDir = path.resolve(__dirname, '../packages/pastebar-app-ui/src/locales/lang')
const enDir = path.join(baseDir, 'en')
const langFile = path.join(baseDir, '../languges.ts')
const report: string[] = []
// Scan all code files in src (ts, tsx, js, jsx, vue, svelte)
const codeDir = path.resolve(__dirname, '../packages/pastebar-app-ui/src')
const codeFiles = scanFiles(codeDir, ['.ts', '.tsx', '.js', '.jsx', '.vue', '.svelte'])
// 1. Load supported languages
let languages: string[] = []
try {
languages = loadSupportedLanguages(langFile).filter(l => l !== 'en')
report.push(`Loaded supported languages: ${languages.join(', ')}`)
} catch (e) {
report.push(`Error loading supported languages: ${e}`)
return fs.writeFileSync('translation-audit-report.txt', report.join('\n'))
}
// 2. List English YAML files
const enFiles = fs.readdirSync(enDir).filter(f => f.endsWith('.yaml'))
for (const enFile of enFiles) {
const enPath = path.join(enDir, enFile)
let enKeys: string[] = []
let enParseError = ''
try {
const enData = yaml.load(fs.readFileSync(enPath, 'utf-8')) as AnyObject
enKeys = flattenKeys(enData)
} catch (e) {
enParseError = String(e)
report.push(`\n[${enFile}] English file parse error: ${enParseError}`)
continue
}
report.push(`\n[${enFile}]`)
for (const lang of languages) {
const langPath = path.join(baseDir, lang, enFile)
if (!fs.existsSync(langPath)) {
report.push(` [${lang}] MISSING FILE: All ${enKeys.length} keys missing.`)
continue
}
try {
const langData = yaml.load(fs.readFileSync(langPath, 'utf-8')) as AnyObject
const langKeys = flattenKeys(langData)
const missing = enKeys.filter(k => !langKeys.includes(k))
if (missing.length > 0) {
report.push(` [${lang}] Missing keys (${missing.length}):`)
for (const key of missing) {
const used = isKeyUsedInCode(key, codeFiles)
report.push(` ${key} ${used ? '[USED in code]' : '[NOT USED in code]'}`)
}
} else {
report.push(` [${lang}] All keys present.`)
}
} catch (e) {
report.push(
` [${lang}] PARSE ERROR: ${e} (All ${enKeys.length} keys considered missing)`
)
}
}
}
fs.writeFileSync('translation-audit-report.txt', report.join('\n'))
console.log('Translation audit complete. See translation-audit-report.txt')
}
auditTranslations()