Files
bentopdf/scripts/extract-ui-strings.mjs
T

143 lines
4.4 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import ts from 'typescript';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '../src/js');
const OUT_FILE = path.resolve(__dirname, '../translations/ui-source.json');
const SKIP_DIRS = new Set(['editcore', 'types', 'i18n', 'config']);
const HELPER_ARGUMENTS = {
showAlert: 2,
showLoader: 1,
showAlertModal: 2,
showModal: 2,
};
const LETTERS = /\p{L}{2}/u;
export function extractUiStrings() {
const files = [];
(function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(full);
} else if (/\.ts$/.test(entry.name)) {
files.push(full);
}
}
})(ROOT);
const strings = new Map();
const formatControlLabel = (key) =>
key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (c) => c.toUpperCase())
.trim();
function add(text, file) {
if (!LETTERS.test(text)) return;
const entry = strings.get(text) || { files: [] };
const relative = path.relative(ROOT, file);
if (!entry.files.includes(relative)) entry.files.push(relative);
strings.set(text, entry);
}
function isStringLike(node) {
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
}
function collectLiterals(node, file) {
if (isStringLike(node)) {
add(node.text, file);
} else if (ts.isParenthesizedExpression(node)) {
collectLiterals(node.expression, file);
} else if (ts.isConditionalExpression(node)) {
collectLiterals(node.whenTrue, file);
collectLiterals(node.whenFalse, file);
} else if (
ts.isBinaryExpression(node) &&
(node.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)
) {
collectLiterals(node.left, file);
collectLiterals(node.right, file);
}
}
for (const file of files) {
const source = fs.readFileSync(file, 'utf8');
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
const visit = (node) => {
if (ts.isCallExpression(node)) {
const callee = ts.isIdentifier(node.expression)
? node.expression.text
: ts.isPropertyAccessExpression(node.expression)
? node.expression.name.text
: '';
if (
(callee === 'trText' || callee === 'trHtml') &&
node.arguments.length > 0
) {
collectLiterals(node.arguments[0], file);
} else if (callee in HELPER_ARGUMENTS) {
node.arguments
.slice(0, HELPER_ARGUMENTS[callee])
.forEach((argument) => collectLiterals(argument, file));
} else if (
callee === 'addControl' &&
node.arguments.length > 0 &&
isStringLike(node.arguments[0])
) {
add(formatControlLabel(node.arguments[0].text), file);
}
}
const relative = path.relative(ROOT, file);
if (
relative === path.join('workflow', 'nodes', 'registry.ts') &&
ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
['label', 'description'].includes(node.name.text) &&
isStringLike(node.initializer)
) {
add(node.initializer.text, file);
}
if (
relative === path.join('workflow', 'nodes', 'control-specs.ts') &&
ts.isArrayLiteralExpression(node) &&
node.elements.length === 2 &&
node.elements.every(isStringLike)
) {
add(node.elements[1].text, file);
}
ts.forEachChild(node, visit);
};
visit(sf);
}
const result = {};
for (const text of [...strings.keys()].sort()) {
const entry = strings.get(text);
result[text] = {
files: entry.files.slice(0, 3),
fileCount: entry.files.length,
};
}
return { result, fileCount: files.length };
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { result, fileCount } = extractUiStrings();
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
fs.writeFileSync(OUT_FILE, JSON.stringify(result, null, 2) + '\n');
const words = Object.keys(result).reduce(
(sum, text) => sum + text.split(/\s+/).length,
0
);
console.log(
`Extracted ${Object.keys(result).length} UI strings (${words} words) from ${fileCount} files.`
);
}