Рефакторизация структуры кода для улучшения читаемости и удобства сопровождения.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const workspaceRoot = path.resolve(__dirname, '..');
|
||||
const skipDirs = new Set(['.git', 'node_modules', '.venv']);
|
||||
const skipExtensions = new Set(['.md', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg', '.pdf', '.zip', '.tar', '.gz', '.7z']);
|
||||
const nonHashCommentExtensions = new Set(['.html', '.htm', '.css', '.scss', '.sass', '.less', '.js', '.jsx', '.ts', '.tsx']);
|
||||
const translationCache = new Map();
|
||||
|
||||
function sleep(delayMs) {
|
||||
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
const protectedPattern = /https?:\/\/\S+|www\.\S+|\$\{?[A-Z0-9_]+\}?|\/[A-Za-z0-9._\-/]+|\b[a-z0-9._-]+\/[a-z0-9._-]+\b|\b[a-z0-9._-]+\.[a-z]{2,}\b|\*[A-Za-z]+|`[^`]+`|"[^"]+"|'[^']+'/g;
|
||||
|
||||
function shouldSkipFile(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return skipExtensions.has(ext);
|
||||
}
|
||||
|
||||
function supportsHashComments(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return !nonHashCommentExtensions.has(ext);
|
||||
}
|
||||
|
||||
function listFiles(dirPath) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (skipDirs.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
files.push(...listFiles(path.join(dirPath, entry.name)));
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(dirPath, entry.name);
|
||||
if (!shouldSkipFile(filePath)) {
|
||||
files.push(filePath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function isProbablyTextFile(filePath) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const length = Math.min(buffer.length, 1024);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (buffer[index] === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasCyrillic(text) {
|
||||
return /[А-Яа-яЁё]/.test(text);
|
||||
}
|
||||
|
||||
function isCodeLikeComment(text) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
return true;
|
||||
}
|
||||
if (/^#[A-Za-z_-][A-Za-z0-9_-]*\s*[>{.{:\[]/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[!\/]/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[\[\]{}()<>:=,$"'`*_./\\-]+$/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[A-Za-z0-9_.-]+\s*:\s*[^\s].*$/.test(trimmed) && !/\s{2,}|\b(is|are|if|when|for|with|without|must|can|will|should|here|example)\b/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[A-Za-z0-9_.-]+\s*=\s*[^\s].*$/.test(trimmed) && !/\b(is|are|if|when|for|with|without|must|can|will|should|here|example)\b/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^-\s*[A-Z0-9_]+\s*=/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[A-Za-z0-9_.-]+\/$/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function protectTerms(text) {
|
||||
const tokens = [];
|
||||
const protectedText = text.replace(protectedPattern, (match) => {
|
||||
const placeholder = `ZXQQ${tokens.length}QQXZ`;
|
||||
tokens.push(match);
|
||||
return placeholder;
|
||||
});
|
||||
return { protectedText, tokens };
|
||||
}
|
||||
|
||||
function restoreTerms(text, tokens) {
|
||||
let restored = text;
|
||||
tokens.forEach((token, index) => {
|
||||
restored = restored.replace(new RegExp(`ZXQQ${index}QQXZ`, 'g'), token);
|
||||
});
|
||||
return restored;
|
||||
}
|
||||
|
||||
function postProcess(text) {
|
||||
return text
|
||||
.replace(/\bUI\b/g, 'интерфейс')
|
||||
.replace(/\bAPI\b/g, 'API')
|
||||
.replace(/\bHTTP\b/g, 'HTTP')
|
||||
.replace(/\bHTTPS\b/g, 'HTTPS')
|
||||
.replace(/\bSSH\b/g, 'SSH')
|
||||
.replace(/\bDNS\b/g, 'DNS')
|
||||
.replace(/\bTor\b/g, 'Tor')
|
||||
.replace(/\bDocker Compose\b/g, 'Docker Compose')
|
||||
.replace(/\bDFWebOS\b/g, 'DFWebOS');
|
||||
}
|
||||
|
||||
async function translateText(text) {
|
||||
if (!text.trim() || hasCyrillic(text)) {
|
||||
return text;
|
||||
}
|
||||
if (translationCache.has(text)) {
|
||||
return translationCache.get(text);
|
||||
}
|
||||
|
||||
const { protectedText, tokens } = protectTerms(text);
|
||||
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=ru&dt=t&q=${encodeURIComponent(protectedText)}`;
|
||||
let response;
|
||||
for (let attempt = 0; attempt < 4; attempt += 1) {
|
||||
response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
||||
if (response.ok) {
|
||||
break;
|
||||
}
|
||||
if (response.status < 500 || attempt === 3) {
|
||||
throw new Error(`Translation failed with status ${response.status}`);
|
||||
}
|
||||
await sleep(500 * (attempt + 1));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const translated = payload[0].map((item) => item[0]).join('');
|
||||
const restored = restoreTerms(translated, tokens);
|
||||
const processed = postProcess(restored);
|
||||
translationCache.set(text, processed);
|
||||
return processed;
|
||||
}
|
||||
|
||||
function splitInlineComment(text) {
|
||||
const marker = text.indexOf(' # ');
|
||||
if (marker === -1) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
head: text.slice(0, marker + 1),
|
||||
tail: text.slice(marker + 3),
|
||||
};
|
||||
}
|
||||
|
||||
async function translateCommentLine(line) {
|
||||
const match = line.match(/^(\s*#+)(\s*)(.*)$/);
|
||||
if (!match) {
|
||||
return { changed: false, line };
|
||||
}
|
||||
|
||||
const prefix = match[1];
|
||||
const spacing = match[2];
|
||||
const body = match[3];
|
||||
|
||||
if (prefix.startsWith('#!')) {
|
||||
return { changed: false, line };
|
||||
}
|
||||
if (!body.trim()) {
|
||||
return { changed: false, line };
|
||||
}
|
||||
|
||||
const inlineComment = splitInlineComment(body);
|
||||
if (inlineComment && isCodeLikeComment(inlineComment.head.trim())) {
|
||||
const translatedTail = await translateText(inlineComment.tail);
|
||||
const updated = `${prefix}${spacing}${inlineComment.head} # ${translatedTail}`;
|
||||
return { changed: updated !== line, line: updated };
|
||||
}
|
||||
|
||||
if (isCodeLikeComment(body)) {
|
||||
return { changed: false, line };
|
||||
}
|
||||
|
||||
const translated = await translateText(body);
|
||||
const updated = `${prefix}${spacing}${translated}`;
|
||||
return { changed: updated !== line, line: updated };
|
||||
}
|
||||
|
||||
async function processFile(filePath) {
|
||||
if (!supportsHashComments(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isProbablyTextFile(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
if (!/^\s*#/m.test(source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = source.split('\n');
|
||||
let changed = false;
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const result = await translateCommentLine(lines[index]);
|
||||
if (result.changed) {
|
||||
lines[index] = result.line;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(filePath, lines.join('\n'));
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const filePaths = args.length
|
||||
? args.map((filePath) => path.resolve(workspaceRoot, filePath))
|
||||
: listFiles(workspaceRoot);
|
||||
|
||||
let updated = 0;
|
||||
const failedFiles = [];
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
const changed = await processFile(filePath);
|
||||
if (changed) {
|
||||
updated += 1;
|
||||
console.log(`translated ${path.relative(workspaceRoot, filePath)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
failedFiles.push(path.relative(workspaceRoot, filePath));
|
||||
console.warn(`failed ${path.relative(workspaceRoot, filePath)}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
console.log(`updated files: ${updated}`);
|
||||
if (failedFiles.length > 0) {
|
||||
console.log(`failed files: ${failedFiles.length}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user