Files
mue/scripts/findUnusedTranslations.cjs
2026-02-04 14:00:16 +00:00

225 lines
6.8 KiB
JavaScript

/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const LOCALE_FILE = path.join(__dirname, '../src/i18n/locales/en_GB.json');
const ACHIEVEMENTS_FILE = path.join(__dirname, '../src/i18n/locales/achievements/en_GB.json');
const SEARCH_DIR = path.join(__dirname, '../src');
const FILE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.json'];
/**
* Flatten nested JSON object into dot-notation keys
* @param {Object} obj - The object to flatten
* @param {String} prefix - The prefix for nested keys
* @returns {Array} Array of flattened keys
*/
function flattenKeys(obj, prefix = '') {
const keys = [];
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
keys.push(...flattenKeys(obj[key], fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
function getAllFiles(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (!file.startsWith('.') && file !== 'node_modules' && file !== 'dist' && file !== 'build') {
getAllFiles(filePath, fileList);
}
} else {
const ext = path.extname(file);
if (FILE_EXTENSIONS.includes(ext)) {
fileList.push(filePath);
}
}
});
return fileList;
}
/**
* Search files for usage of a translation key
* Handles both direct usage and dynamic template literal construction
* @param {String} key - The translation key to search for
* @param {Array} files - Array of file paths to search
* @param {Map} fileContentsCache - Cache of file contents
* @returns {Boolean} True if the key is found in any file
*/
function isKeyUsed(key, files, fileContentsCache) {
const keySegments = key.split('.');
const patterns = [];
const escapedFullKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
patterns.push(new RegExp(`['"\`]${escapedFullKey}['"\`]`, 'g'));
if (keySegments.length >= 2) {
const lastTwo = keySegments.slice(-2).map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\.');
patterns.push(new RegExp(`['"\`]${lastTwo}['"\`]`, 'g'));
}
if (keySegments.length >= 3) {
const lastThree = keySegments.slice(-3).map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\.');
patterns.push(new RegExp(`['"\`]${lastThree}['"\`]`, 'g'));
}
if (keySegments.length >= 3) {
const finalSegment = keySegments[keySegments.length - 1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
patterns.push(new RegExp(`\\.${finalSegment}['"\`]`, 'g'));
}
for (const file of files) {
let content = fileContentsCache.get(file);
if (content === undefined) {
try {
content = fs.readFileSync(file, 'utf-8');
fileContentsCache.set(file, content);
} catch {
fileContentsCache.set(file, '');
continue;
}
}
for (const pattern of patterns) {
if (pattern.test(content)) {
return true;
}
}
}
return false;
}
function main() {
console.log('Finding unused translation keys...\n');
let translationKeys = [];
try {
const localeContent = fs.readFileSync(LOCALE_FILE, 'utf-8');
const localeData = JSON.parse(localeContent);
translationKeys = flattenKeys(localeData);
console.log(`Found ${translationKeys.length} translation keys in en_GB.json`);
} catch (error) {
console.error(`Error reading locale file: ${error.message}`);
process.exit(1);
}
try {
if (fs.existsSync(ACHIEVEMENTS_FILE)) {
const achievementsContent = fs.readFileSync(ACHIEVEMENTS_FILE, 'utf-8');
const achievementsData = JSON.parse(achievementsContent);
const achievementKeys = flattenKeys(achievementsData, 'achievements');
translationKeys.push(...achievementKeys);
console.log(`Found ${achievementKeys.length} achievement keys in achievements/en_GB.json`);
}
} catch (error) {
console.warn(`Warning: Could not read achievements file: ${error.message}`);
}
console.log(`\nTotal keys to check: ${translationKeys.length}`);
console.log('Scanning source files...');
const files = getAllFiles(SEARCH_DIR);
console.log(`Found ${files.length} files to search\n`);
const fileContentsCache = new Map();
const unusedKeys = [];
const usedKeys = [];
console.log('Searching for key usage (including template literals)...');
let processed = 0;
const totalKeys = translationKeys.length;
const startTime = Date.now();
for (const key of translationKeys) {
processed++;
if (processed % 10 === 0 || processed === totalKeys) {
const percent = Math.round(processed / totalKeys * 100);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
process.stdout.write(`\r Progress: ${processed}/${totalKeys} (${percent}%) - ${elapsed}s elapsed`);
}
if (isKeyUsed(key, files, fileContentsCache)) {
usedKeys.push(key);
} else {
unusedKeys.push(key);
}
}
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\r Progress: ${totalKeys}/${totalKeys} (100%) - ${totalTime}s total \n`);
console.log('\n' + '='.repeat(70));
console.log('RESULTS');
console.log('='.repeat(70) + '\n');
console.log(`Used keys: ${usedKeys.length}`);
console.log(`Unused keys: ${unusedKeys.length}`);
console.log(`Usage rate: ${((usedKeys.length / totalKeys) * 100).toFixed(2)}%\n`);
if (unusedKeys.length > 0) {
console.log('Unused translation keys:\n');
const grouped = {};
unusedKeys.forEach(key => {
const topLevel = key.split('.')[0];
if (!grouped[topLevel]) {
grouped[topLevel] = [];
}
grouped[topLevel].push(key);
});
Object.keys(grouped).sort().forEach(category => {
console.log(`\n ${category}:`);
grouped[category].sort().forEach(key => {
console.log(` - ${key}`);
});
});
const outputFile = path.join(__dirname, '../unused-translations.txt');
const outputContent = [
'# Unused Translation Keys',
`# Generated: ${new Date().toISOString()}`,
`# Total unused: ${unusedKeys.length}`,
`# Note: This script checks for full keys and partial keys (last 2-3 segments)`,
`# to catch dynamic template literal usage like \`\${PREFIX}.key\``,
'',
...unusedKeys.sort()
].join('\n');
fs.writeFileSync(outputFile, outputContent, 'utf-8');
console.log(`\nFull list saved to: ${path.relative(process.cwd(), outputFile)}`);
} else {
console.log('No unused translation keys found!');
}
console.log('\n' + '='.repeat(70) + '\n');
if (unusedKeys.length > 0) {
console.log('Tip: You can safely remove these keys from your translation files to reduce bundle size.');
console.log('Note: Some keys might be used dynamically - review before removing!');
}
}
main();