mirror of
https://github.com/mue/mue.git
synced 2026-06-05 23:45:53 +02:00
225 lines
6.8 KiB
JavaScript
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();
|