feat: added scanner for missed translations

This commit is contained in:
Botzy 2025-05-23 16:34:41 +03:00
parent bed2a58060
commit 657f9cd29e
4 changed files with 150 additions and 2 deletions

3
.gitignore vendored
View file

@ -3,4 +3,5 @@
/yarn.lock
/npm-debug.log
.DS_Store
.prettierignore
.prettierignore
i18n-report.csv

38
package-lock.json generated
View file

@ -66,6 +66,7 @@
"mini-css-extract-plugin": "2.9.2",
"postcss-loader": "8.1.1",
"readdirp": "4.0.2",
"recast": "0.23.11",
"terser-webpack-plugin": "5.3.10",
"thread-loader": "^4.0.4",
"ts-loader": "^9.5.1",
@ -4786,6 +4787,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ast-types": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz",
"integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -12497,6 +12511,23 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/recast": {
"version": "0.23.11",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
"integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ast-types": "^0.16.1",
"esprima": "~4.0.0",
"source-map": "~0.6.1",
"tiny-invariant": "^1.3.3",
"tslib": "^2.0.1"
},
"engines": {
"node": ">= 4"
}
},
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@ -13976,6 +14007,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"dev": true,

View file

@ -10,7 +10,8 @@
"start-prod": "webpack serve --mode production",
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint src"
"lint": "eslint src",
"scan-translations": "node scan-i18n-report.js"
},
"dependencies": {
"@babel/runtime": "7.26.0",
@ -70,6 +71,7 @@
"mini-css-extract-plugin": "2.9.2",
"postcss-loader": "8.1.1",
"readdirp": "4.0.2",
"recast": "0.23.11",
"terser-webpack-plugin": "5.3.10",
"thread-loader": "^4.0.4",
"ts-loader": "^9.5.1",

107
scan-i18n-report.js Normal file
View file

@ -0,0 +1,107 @@
const fs = require('fs');
const path = require('path');
const recast = require('recast');
const babelParser = require('@babel/parser');
const directoryToScan = './src';
const report = [];
function toKey(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '_')
.slice(0, 40);
}
function scanFile(filePath) {
try {
const code = fs.readFileSync(filePath, 'utf8');
const ast = babelParser.parse(code, {
sourceType: 'module',
plugins: [
'jsx',
'typescript',
'classProperties',
'objectRestSpread',
'optionalChaining',
'nullishCoalescingOperator',
],
errorRecovery: true,
});
recast.types.visit(ast, {
// Text directly inside JSX
visitJSXText(path) {
const text = path.node.value.trim();
if (text.length > 1 && /\w/.test(text)) {
const loc = path.node.loc?.start || { line: 0 };
report.push({
file: filePath,
line: loc.line,
string: text,
key: toKey(text),
});
}
this.traverse(path);
},
// { "hello" } style
visitJSXExpressionContainer(path) {
const expr = path.node.expression;
// Skip expressions that call t()
if (
expr.type === 'CallExpression' &&
expr.callee.type === 'Identifier' &&
expr.callee.name === 't'
) {
return false;
}
// Find only { "text" } expressions
if (expr.type === 'StringLiteral') {
const parent = path.parentPath.node;
if (parent.type === 'JSXElement') {
const loc = path.node.loc?.start || { line: 0 };
report.push({
file: filePath,
line: loc.line,
string: expr.value,
key: toKey(expr.value),
});
}
}
this.traverse(path);
}
});
} catch (err) {
console.warn(`❌ Skipping ${filePath}: ${err.message}`);
}
}
function walk(dir) {
fs.readdirSync(dir).forEach((file) => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walk(fullPath);
} else if (/\.(js|jsx|ts|tsx)$/.test(file)) {
console.log('Scanning file:', fullPath);
scanFile(fullPath);
}
});
}
function writeReport() {
const header = `File,Line,Hardcoded String,Suggested Key\n`;
const rows = report.map((row) =>
`"${row.file}",${row.line},"${row.string.replace(/"/g, '""')}","${row.key}"`
);
fs.writeFileSync('i18n-report.csv', header + rows.join('\n'));
console.log('✅ Report written to i18n-report.csv');
}
walk(directoryToScan);
writeReport();