Skip to content

Commit a08c3ae

Browse files
committed
Script to find missing & unused translations added
Signed-off-by: Sanjay Babu <sanjaytkbabu@gmail.com>
1 parent 262515c commit a08c3ae

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"scripts": {
99
"build": "vite build",
1010
"build:dts": "vue-tsc --declaration --emitDeclarationOnly",
11+
"check:translations": "npx tsx scripts/check-translations.ts",
1112
"clean": "rimraf coverage dist",
1213
"debug": "vite --mode debug",
1314
"format": "prettier ./src --write",
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
// Get directory paths
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
const projectRoot = path.resolve(__dirname, '..');
9+
const srcDir = path.join(projectRoot, 'src');
10+
const localeFile = path.join(projectRoot, 'src', 'locales', 'en-CA.json');
11+
12+
interface MissingTranslation {
13+
file: string;
14+
line: number;
15+
key: string;
16+
context: string;
17+
}
18+
19+
// Read locale file
20+
function loadLocaleKeys(): Set<string> {
21+
const localeContent = fs.readFileSync(localeFile, 'utf-8');
22+
const localeData = JSON.parse(localeContent);
23+
24+
const keys = new Set<string>();
25+
26+
function extractKeys(obj: any, prefix = '') {
27+
for (const key in obj) {
28+
const fullKey = prefix ? `${prefix}.${key}` : key;
29+
if (typeof obj[key] === 'object' && obj[key] !== null) {
30+
extractKeys(obj[key], fullKey);
31+
} else {
32+
keys.add(fullKey);
33+
}
34+
}
35+
}
36+
37+
extractKeys(localeData);
38+
return keys;
39+
}
40+
41+
// Find all .vue and .ts files recursively
42+
function findFiles(dir: string, extensions: string[]): string[] {
43+
const files: string[] = [];
44+
45+
function traverse(currentDir: string) {
46+
const items = fs.readdirSync(currentDir);
47+
48+
for (const item of items) {
49+
const fullPath = path.join(currentDir, item);
50+
const stat = fs.statSync(fullPath);
51+
52+
if (stat.isDirectory()) {
53+
// Skip node_modules, dist, coverage, etc.
54+
if (!['node_modules', 'dist', 'coverage', '.git', 'sbin'].includes(item)) {
55+
traverse(fullPath);
56+
}
57+
} else if (stat.isFile()) {
58+
const ext = path.extname(item);
59+
if (extensions.includes(ext)) {
60+
files.push(fullPath);
61+
}
62+
}
63+
}
64+
}
65+
66+
traverse(dir);
67+
return files;
68+
}
69+
70+
// Extract translation keys from file content
71+
function extractTranslationKeys(
72+
content: string,
73+
filePath: string
74+
): Array<{ key: string; line: number; context: string }> {
75+
const results: Array<{ key: string; line: number; context: string }> = [];
76+
77+
// Regex patterns for t('...') or $t('...')
78+
const patterns = [/\bt\(['"]([^'"]+)['"]\)/g, /\$t\(['"]([^'"]+)['"]\)/g, /this\.\$t\(['"]([^'"]+)['"]\)/g];
79+
80+
const lines = content.split('\n');
81+
82+
for (let i = 0; i < lines.length; i++) {
83+
const line = lines[i];
84+
if (!line) continue;
85+
86+
for (const pattern of patterns) {
87+
let match;
88+
const regex = new RegExp(pattern);
89+
90+
while ((match = regex.exec(line)) !== null) {
91+
const key = match[1];
92+
if (!key) continue;
93+
94+
results.push({
95+
key,
96+
line: i + 1,
97+
context: line.trim()
98+
});
99+
}
100+
}
101+
}
102+
103+
return results;
104+
}
105+
106+
// Main function
107+
function checkMissingTranslations() {
108+
console.log('🔍 Checking for missing translation keys...\n');
109+
110+
// Load all translation keys from en-CA.json
111+
const localeKeys = loadLocaleKeys();
112+
console.log(`✅ Loaded ${localeKeys.size} translation keys from en-CA.json\n`);
113+
114+
// Find all Vue and TypeScript files
115+
const files = findFiles(srcDir, ['.vue', '.ts']);
116+
console.log(`📁 Found ${files.length} files to check\n`);
117+
118+
// Track missing translations
119+
const missingTranslations: MissingTranslation[] = [];
120+
const usedKeys = new Set<string>();
121+
122+
// Check each file
123+
for (const file of files) {
124+
const content = fs.readFileSync(file, 'utf-8');
125+
const keys = extractTranslationKeys(content, file);
126+
127+
for (const { key, line, context } of keys) {
128+
usedKeys.add(key);
129+
130+
if (!localeKeys.has(key)) {
131+
missingTranslations.push({
132+
file: path.relative(projectRoot, file),
133+
line,
134+
key,
135+
context
136+
});
137+
}
138+
}
139+
}
140+
141+
// Find unused translations
142+
const unusedTranslations: string[] = [];
143+
for (const localeKey of localeKeys) {
144+
if (!usedKeys.has(localeKey)) {
145+
unusedTranslations.push(localeKey);
146+
}
147+
}
148+
149+
// Display results
150+
console.log('═══════════════════════════════════════════════════════════════\n');
151+
console.log(`📊 Summary:`);
152+
console.log(` Total translation keys defined: ${localeKeys.size}`);
153+
console.log(` Total translation keys used: ${usedKeys.size}`);
154+
console.log(` Missing translation keys: ${missingTranslations.length}`);
155+
console.log(` Unused translation keys: ${unusedTranslations.length}`);
156+
console.log('\n═══════════════════════════════════════════════════════════════\n');
157+
158+
let hasIssues = false;
159+
160+
if (missingTranslations.length > 0) {
161+
hasIssues = true;
162+
console.log('❌ Missing Translation Keys:\n');
163+
164+
// Group by file
165+
const byFile = new Map<string, MissingTranslation[]>();
166+
for (const missing of missingTranslations) {
167+
if (!byFile.has(missing.file)) {
168+
byFile.set(missing.file, []);
169+
}
170+
byFile.get(missing.file)!.push(missing);
171+
}
172+
173+
// Display grouped by file
174+
for (const [file, items] of byFile) {
175+
console.log(`\n📄 ${file}`);
176+
for (const item of items) {
177+
console.log(` Line ${item.line}: ${item.key}`);
178+
console.log(` Context: ${item.context}`);
179+
console.log('');
180+
}
181+
}
182+
183+
// Display unique missing keys
184+
const uniqueMissing = [...new Set(missingTranslations.map((m) => m.key))];
185+
console.log('\n═══════════════════════════════════════════════════════════════\n');
186+
console.log('📋 Unique Missing Keys:\n');
187+
uniqueMissing.sort().forEach((key) => console.log(` - ${key}`));
188+
console.log('\n═══════════════════════════════════════════════════════════════\n');
189+
}
190+
191+
if (unusedTranslations.length > 0) {
192+
console.log('⚠️ Unused Translation Keys:\n');
193+
console.log('The following keys are defined in en-CA.json but not used anywhere:\n');
194+
unusedTranslations.sort().forEach((key) => console.log(` - ${key}`));
195+
console.log('\n═══════════════════════════════════════════════════════════════\n');
196+
}
197+
198+
if (!hasIssues && unusedTranslations.length === 0) {
199+
console.log('✅ All translation keys are defined and used!\n');
200+
process.exit(0);
201+
} else if (hasIssues) {
202+
process.exit(1);
203+
} else {
204+
console.log('✅ All used translation keys are defined!\n');
205+
console.log('💡 Tip: Consider removing unused translation keys to keep the codebase clean.\n');
206+
process.exit(0);
207+
}
208+
}
209+
210+
// Run the check
211+
checkMissingTranslations();

0 commit comments

Comments
 (0)