Рефакторизация структуры кода для улучшения читаемости и удобства сопровождения.

This commit is contained in:
2026-04-30 07:41:41 +03:00
parent eec534760e
commit 89b17ea2b2
1406 changed files with 38483 additions and 3 deletions
+259
View File
@@ -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);
});
+260
View File
@@ -0,0 +1,260 @@
const fs = require('fs');
const path = require('path');
const workspaceRoot = path.resolve(__dirname, '..');
const targetFields = ['releaseNotes', 'tagline', 'description'];
const skipDirs = new Set(['.git', 'node_modules', '.venv']);
const translationCache = new Map();
const protectedPattern = /https?:\/\/\S+|www\.\S+|\b(?:OpenAI|TypeScript|Typescript|JavaScript|Docker|GitHub|OAuth2|OAuth|MCP|API|SDK|UI|CLI|AI|AWS|JSON|CSV|FFMPEG|VCF|PDF|BTC|S3|PostgreSQL|Redis|npmjs\.com|DFWebOS|dfwebOS|Copilot|Docker Compose|MySQL|SQLite|PostgreSQL|WebDAV|WebRTC|Nostr|Bitcoin|Lightning|Tor|TLS|SSH|REST|GraphQL|WebSocket|Prometheus|Grafana|InfluxDB|MongoDB|MariaDB|Node\.js|Nextcloud|OpenStreetMap|YouTube|Plex|Sonarr|Radarr|Lidarr|Readarr|Prowlarr|SABnzbd|qBittorrent|Transmission|Home Assistant|WordPress)\b/g;
function listManifestFiles(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(...listManifestFiles(path.join(dirPath, entry.name)));
continue;
}
if (entry.name === 'dfwebos-app.yml') {
files.push(path.join(dirPath, entry.name));
}
}
return files;
}
function hasCyrillic(text) {
return /[А-Яа-яЁё]/.test(text);
}
function parseTopLevelField(source, key) {
const lines = source.split('\n');
const startIndex = lines.findIndex((line) => line.startsWith(`${key}:`));
if (startIndex === -1) {
return null;
}
let endIndex = lines.length;
for (let index = startIndex + 1; index < lines.length; index += 1) {
if (/^[^\s#][^:]*:/.test(lines[index])) {
endIndex = index;
break;
}
}
const firstLine = lines[startIndex];
const inlineValue = firstLine.slice(`${key}:`.length).trim();
let style = 'plain';
let value = '';
if (inlineValue === '>' || inlineValue === '>-' || inlineValue === '|' || inlineValue === '|-') {
style = inlineValue;
value = lines
.slice(startIndex + 1, endIndex)
.map((line) => (line.startsWith(' ') ? line.slice(2) : line))
.join('\n');
} else if (inlineValue.startsWith('"') && inlineValue.endsWith('"')) {
style = 'double-quoted';
value = JSON.parse(inlineValue);
} else if (inlineValue.startsWith("'") && inlineValue.endsWith("'")) {
style = 'single-quoted';
value = inlineValue.slice(1, -1).replace(/''/g, "'");
} else {
style = 'plain';
value = inlineValue;
}
return { startIndex, endIndex, style, value };
}
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 postProcessTranslation(text) {
return text
.replace(/\bUI\b/g, 'интерфейс')
.replace(/\bAI\b/g, 'ИИ')
.replace(/AI-/g, 'ИИ-')
.replace(/\bself-hosted\b/gi, 'самостоятельно размещаемый')
.replace(/\bopen source\b/gi, 'с открытым исходным кодом')
.replace(/\bbug fixes\b/gi, 'исправления ошибок')
.replace(/\brelease notes\b/gi, 'примечания к выпуску')
.replace(/\bkey features\b/gi, 'ключевые возможности')
.replace(/\benterprise-ready\b/gi, 'готовность к корпоративному использованию')
.replace(/\bAI provider\b/gi, 'провайдер ИИ')
.replace(/\bhot reloading\b/gi, 'горячая перезагрузка');
}
async function translateText(text) {
if (!text.trim()) {
return text;
}
if (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)}`;
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0',
},
});
if (!response.ok) {
throw new Error(`Translation request failed with ${response.status}`);
}
const payload = await response.json();
const translated = payload[0].map((item) => item[0]).join('');
const restored = restoreTerms(translated, tokens);
const processed = postProcessTranslation(restored);
translationCache.set(text, processed);
return processed;
}
function splitIntoChunks(value) {
const parts = value.split(/(\n{2,})/);
const chunks = [];
for (const part of parts) {
if (!part) {
continue;
}
if (/^\n+$/.test(part)) {
chunks.push({ type: 'separator', text: part });
continue;
}
if (part.length <= 1500) {
chunks.push({ type: 'text', text: part });
continue;
}
const lines = part.split('\n');
let current = [];
let currentLength = 0;
for (const line of lines) {
const nextLength = currentLength + line.length + (current.length ? 1 : 0);
if (current.length && nextLength > 1500) {
chunks.push({ type: 'text', text: current.join('\n') });
current = [line];
currentLength = line.length;
} else {
current.push(line);
currentLength = nextLength;
}
}
if (current.length) {
chunks.push({ type: 'text', text: current.join('\n') });
}
}
return chunks;
}
async function translateMultiline(value) {
const chunks = splitIntoChunks(value);
const translatedChunks = [];
for (const chunk of chunks) {
if (chunk.type === 'separator' || !chunk.text.trim()) {
translatedChunks.push(chunk.text);
continue;
}
translatedChunks.push(await translateText(chunk.text));
}
return translatedChunks.join('');
}
function quoteSingle(text) {
return `'${text.replace(/'/g, "''")}'`;
}
function formatField(key, style, value) {
if (style === '>' || style === '>-' || style === '|' || style === '|-') {
return [`${key}: ${style}`, ...value.split('\n').map((line) => ` ${line}`)].join('\n');
}
if (style === 'double-quoted') {
return `${key}: ${JSON.stringify(value)}`;
}
if (style === 'single-quoted') {
return `${key}: ${quoteSingle(value)}`;
}
if (/^[\p{L}\p{N} .,!?:;()\-\/]+$/u.test(value)) {
return `${key}: ${value}`;
}
return `${key}: ${JSON.stringify(value)}`;
}
async function processManifest(filePath) {
const source = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
const lines = source.split('\n');
const replacements = [];
for (const field of targetFields) {
const parsed = parseTopLevelField(source, field);
if (!parsed || hasCyrillic(parsed.value)) {
continue;
}
const translated = await translateMultiline(parsed.value);
replacements.push({ ...parsed, field, translated });
}
if (!replacements.length) {
return false;
}
replacements.sort((left, right) => right.startIndex - left.startIndex);
for (const replacement of replacements) {
const rendered = formatField(replacement.field, replacement.style, replacement.translated).split('\n');
lines.splice(replacement.startIndex, replacement.endIndex - replacement.startIndex, ...rendered);
}
fs.writeFileSync(filePath, lines.join('\n'));
return true;
}
async function main() {
const files = listManifestFiles(workspaceRoot);
let changedCount = 0;
for (const filePath of files) {
const changed = await processManifest(filePath);
if (changed) {
changedCount += 1;
console.log(`translated ${path.relative(workspaceRoot, filePath)}`);
}
}
console.log(`updated manifests: ${changedCount}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});