Removing Unused Translation Keys with GitHub Copilot
When managing multiple languages (i18n) in a project, unused translation keys are left behind every time a feature is changed or removed.
Here is how I cleaned up the unneeded translation keys that had been piling up in the WiniLogis project.
❗️ The problem
WiniLogis manages its translations in a Google Cloud Sheet. In the development environment, a script converts the translation data written in the Sheet and saves it as public/locales/{language}/translation.json.
public/
└─ locales/
├─ en/translation.json
├─ es/translation.json
└─ ru/translation.jsonBut as the project grew and features were added and removed, unused translation keys gradually started to accumulate. As a result, translations that were not actually used kept remaining in the Google Sheet.
🔍 Analysis
The i18n system itself has no feature that automatically tells you “whether this key is actually in use.” That means a developer would have to search through all the project code by hand, which seemed very hard to do in practice.
So I decided to solve the problem with this approach:
- Extract all translation keys from the translation.json file
- Go through every code file in the src folder and check whether each key is actually used
- Print only the unused keys separately and clean them up
✅ Solution
To optimize this process, I wrote a script that finds unused i18n keys with help from GitHub Copilot.
// Script: find unused i18n keys
import fs from 'fs';
import path from 'path';
const LOCALES_DIR = './public/locales/en/translation.json';
// Read the JSON file
const translationData = JSON.parse(fs.readFileSync(LOCALES_DIR, 'utf8'));
// Extract all keys (nested keys are joined with a dot (.))
const getAllKeys = (obj, prefix = '') => {
let keys = [];
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
keys = keys.concat(getAllKeys(obj[key], fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
};
// Recursively find all files with the given extensions in the src folder (excluding node_modules)
const getFilesRecursive = (dir, extensions) => {
let results = [];
const list = fs.readdirSync(dir, { withFileTypes: true });
for (const file of list) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
if (file.name === 'node_modules') continue;
results = results.concat(getFilesRecursive(fullPath, extensions));
} else {
if (extensions.includes(path.extname(file.name))) {
results.push(fullPath);
}
}
}
return results;
};
// Check whether a given key is used in the project files
const checkKeyUsage = (keyToCheck) => {
const files = getFilesRecursive('src', ['.ts', '.tsx', '.js', '.jsx']);
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
// Check key usage with several patterns
const patterns = [
`'${keyToCheck}'`,
`"${keyToCheck}"`,
`\`${keyToCheck}\``,
`t('${keyToCheck}')`,
`t("${keyToCheck}")`,
];
if (patterns.some((pattern) => content.includes(pattern))) {
return true; // In use
}
} catch (error) {
console.error(`Error reading file ${file}:`, error.message);
}
}
return false; // Not used
};
// Main function: print unused keys
const findUnusedKeys = () => {
const allKeys = getAllKeys(translationData);
const unusedKeys = [];
for (const key of allKeys) {
const isUsed = checkKeyUsage(key);
if (!isUsed) {
unusedKeys.push(key);
}
}
console.log('Unused i18n Keys:', unusedKeys.length);
unusedKeys.forEach((key) => console.log(key));
};
// Run
findUnusedKeys();💡 Looking back
Running the script found 69 unused translation keys in total.
I removed the same keys from the Google Cloud Sheet as well, then ran the automatic sync script again so the translation.json files stay cleaned up.
Thanks to GitHub Copilot, I could write the repetitive search logic quickly. From now on, I plan to run this script regularly to keep the translation data clean at all times.
- #GitHub Copilot
- #i18n
- #Translation Key Cleanup