Files
NeoTachyon/fe-app-tachyon/extract-calls.js
T
Dushnilin 603c0df91b feat(security,ci,ui): harden CI/CD, LuCI ACL, low-space updates and mobile layout
- ci: make frontend CI read-only with strict diff check and deterministic calls.json
- ci: pin external GitHub Actions in workflows to immutable commit SHAs
- ci: add openwrt-e2e blocking gate job running docker_e2e_test.sh
- fix(diagnostic): fix contstants.ts typo and update all 13 import sites
- fix(acl): remove unused /bin/busybox exec from rpcd ACL and add regression test
- feat(updates): add low-space update fallback when creating sing-box.old is impossible
- fix(routes): preserve dedicated Discord gateway subnets (162.159.128.0/21)
- feat(ui): implement responsive auto-fit grid and mobile modal viewport bounds
- test: add viewport regression test suite in vitest
2026-09-14 23:00:18 +03:00

88 lines
2.8 KiB
JavaScript

import fs from 'fs/promises';
import path from 'path';
import glob from 'fast-glob';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function stripIllegalReturn(code) {
return code.replace(/^\s*return\s+[^;]+;\s*$/gm, (match, offset, input) => {
const after = input.slice(offset + match.length).trim();
return after === '' ? '' : match;
});
}
const rawFiles = await glob([
'src/**/*.ts',
'src/**/*.tsx',
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/**/*.js',
], {
cwd: __dirname,
ignore: [
'**/*.test.ts',
'**/main.js',
],
absolute: true,
});
const files = rawFiles.sort();
console.log('Found files:', files.length);
const results = {};
for (const file of files) {
const contentRaw = await fs.readFile(file, 'utf8');
const content = stripIllegalReturn(contentRaw);
const relativePath = path.relative(__dirname, file).replaceAll('\\', '/');
let ast;
try {
ast = parse(content, {
sourceType: 'module',
allowReturnOutsideFunction: true,
plugins: file.endsWith('.ts') ? ['typescript'] : [],
});
} catch (e) {
console.warn(`⚠️ Parse error in ${relativePath}, skipping:`, e.message);
continue;
}
traverse.default(ast, {
CallExpression(path) {
if (
t.isIdentifier(path.node.callee, { name: '_' }) ||
t.isIdentifier(path.node.callee, { name: 'translate' })
) {
const arg = path.node.arguments[0];
if (t.isStringLiteral(arg)) {
const key = arg.value.trim();
if (!key) return; // ❌ пропустить пустые ключи
const location = `${relativePath}:${path.node.loc?.start.line ?? '?'}`;
if (!results[key]) {
results[key] = { call: key, key, places: [] };
}
results[key].places.push(location);
}
}
},
});
}
const outFile = path.resolve(__dirname, 'locales/calls.json');
const sorted = Object.values(results)
.map((item) => ({
...item,
places: Array.from(new Set(item.places)).sort(),
}))
.sort((a, b) => a.key.localeCompare(b.key)); // 🔤 сортировка по ключу
await fs.mkdir(path.dirname(outFile), { recursive: true });
await fs.writeFile(outFile, JSON.stringify(sorted, null, 2), 'utf8');
console.log(`✅ Extracted ${sorted.length} translations to ${outFile}`);