Skip to content

Commit 5d90f09

Browse files
authored
Merge pull request #1583 from The-Big-Danny/add-missing-translation-keys-audit
feat: add missing translation keys audit
2 parents 92a7f57 + 6922aaa commit 5d90f09

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"test:e2e:visual": "playwright test --project=visual-regression",
1717
"test:e2e:visual-update": "playwright test --project=visual-regression --update-snapshots",
1818
"storybook": "storybook dev -p 6006",
19-
"build-storybook": "storybook build"
19+
"build-storybook": "storybook build",
20+
"lint:i18n": "node scripts/audit-i18n.js",
21+
"lint": "npm run lint:i18n"
2022
},
2123
"dependencies": {
2224
"@sentry/react": "^9.14.0",

frontend/scripts/audit-i18n.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
8+
const SRC_DIR = path.resolve(__dirname, '../src');
9+
const LOCALES_DIR = path.resolve(__dirname, '../src/i18n/locales');
10+
11+
// Recursive function to get all files in a directory
12+
function getAllFiles(dirPath, arrayOfFiles = []) {
13+
const files = fs.readdirSync(dirPath);
14+
15+
files.forEach((file) => {
16+
const fullPath = path.join(dirPath, file);
17+
if (fs.statSync(fullPath).isDirectory()) {
18+
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
19+
} else {
20+
arrayOfFiles.push(fullPath);
21+
}
22+
});
23+
24+
return arrayOfFiles;
25+
}
26+
27+
// Function to set a nested value in an object given a dot-notated key
28+
function setNestedValue(obj, keyPath, value) {
29+
const keys = keyPath.split('.');
30+
let current = obj;
31+
for (let i = 0; i < keys.length - 1; i++) {
32+
const k = keys[i];
33+
if (!current[k]) {
34+
current[k] = {};
35+
}
36+
current = current[k];
37+
}
38+
current[keys[keys.length - 1]] = value;
39+
}
40+
41+
// Function to check if a nested key exists
42+
function hasNestedValue(obj, keyPath) {
43+
const keys = keyPath.split('.');
44+
let current = obj;
45+
for (let i = 0; i < keys.length; i++) {
46+
const k = keys[i];
47+
if (current[k] === undefined) {
48+
return false;
49+
}
50+
current = current[k];
51+
}
52+
return true;
53+
}
54+
55+
// Main audit function
56+
function auditI18n() {
57+
console.log('Auditing i18n translation keys...');
58+
59+
// 1. Find all translation keys used in the codebase
60+
const allFiles = getAllFiles(SRC_DIR);
61+
const codeFiles = allFiles.filter(file => file.endsWith('.ts') || file.endsWith('.tsx'));
62+
63+
const usedKeys = new Set();
64+
const tFunctionRegex = /\bt\(['"`]([\w.]+)['"`]\)/g;
65+
66+
codeFiles.forEach(file => {
67+
const content = fs.readFileSync(file, 'utf8');
68+
let match;
69+
while ((match = tFunctionRegex.exec(content)) !== null) {
70+
usedKeys.add(match[1]);
71+
}
72+
});
73+
74+
console.log(`Found ${usedKeys.size} unique translation keys referenced in code.`);
75+
76+
// 2. Read locale files and check for missing keys
77+
if (!fs.existsSync(LOCALES_DIR)) {
78+
console.error(`Locales directory not found at: ${LOCALES_DIR}`);
79+
process.exit(1);
80+
}
81+
82+
const localeFiles = fs.readdirSync(LOCALES_DIR).filter(file => file.endsWith('.json'));
83+
let missingKeysFound = false;
84+
85+
localeFiles.forEach(localeFile => {
86+
const localePath = path.join(LOCALES_DIR, localeFile);
87+
const localeContent = JSON.parse(fs.readFileSync(localePath, 'utf8'));
88+
89+
let updated = false;
90+
let missingCount = 0;
91+
92+
usedKeys.forEach(key => {
93+
if (!hasNestedValue(localeContent, key)) {
94+
console.log(`[Missing] ${localeFile}: '${key}'`);
95+
setNestedValue(localeContent, key, `Translation needed: ${key}`);
96+
updated = true;
97+
missingCount++;
98+
missingKeysFound = true;
99+
}
100+
});
101+
102+
if (updated) {
103+
fs.writeFileSync(localePath, JSON.stringify(localeContent, null, 2) + '\n', 'utf8');
104+
console.log(`Updated ${localeFile} with ${missingCount} missing key(s).`);
105+
} else {
106+
console.log(`${localeFile} is up-to-date.`);
107+
}
108+
});
109+
110+
// 3. Exit with error code if any gaps were found (so CI can catch it)
111+
if (missingKeysFound) {
112+
console.error('\nI18n Audit Failed: Missing translation keys were found and automatically added.');
113+
console.error('Please update the missing translations in the locale files.');
114+
process.exit(1);
115+
} else {
116+
console.log('\nI18n Audit Passed: All translation keys are present in all locales.');
117+
process.exit(0);
118+
}
119+
}
120+
121+
auditI18n();

0 commit comments

Comments
 (0)