mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 03:26:51 +03:00
feat: implement locales scripts
This commit is contained in:
38
fe-app-podkop/distribute-locales.js
Normal file
38
fe-app-podkop/distribute-locales.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const sourceDir = path.resolve(__dirname, 'locales');
|
||||
const targetRoot = path.resolve(__dirname, '../luci-app-podkop/po');
|
||||
|
||||
async function main() {
|
||||
const files = await fs.readdir(sourceDir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(sourceDir, file);
|
||||
|
||||
if (file === 'podkop.pot') {
|
||||
const potTarget = path.join(targetRoot, 'templates', 'podkop.pot');
|
||||
await fs.mkdir(path.dirname(potTarget), { recursive: true });
|
||||
await fs.copyFile(filePath, potTarget);
|
||||
console.log(`✅ Copied POT: ${filePath} → ${potTarget}`);
|
||||
}
|
||||
|
||||
const match = file.match(/^podkop\.([a-zA-Z_]+)\.po$/);
|
||||
if (match) {
|
||||
const lang = match[1];
|
||||
const poTarget = path.join(targetRoot, lang, 'podkop.po');
|
||||
await fs.mkdir(path.dirname(poTarget), { recursive: true });
|
||||
await fs.copyFile(filePath, poTarget);
|
||||
console.log(`✅ Copied ${lang.toUpperCase()}: ${filePath} → ${poTarget}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('❌ Ошибка при распространении переводов:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
93
fe-app-podkop/extract-calls.js
Normal file
93
fe-app-podkop/extract-calls.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import fg from 'fast-glob';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const outputFile = 'locales/calls.json';
|
||||
|
||||
const tsSearchGlob = 'src/**/*.ts';
|
||||
const jsSearchGlob = '../luci-app-podkop/htdocs/luci-static/resources/view/podkop/**/*.js';
|
||||
|
||||
function extractAllUnderscoreCallsFromContent(content) {
|
||||
const results = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < content.length) {
|
||||
const start = content.indexOf('_(', index);
|
||||
if (start === -1) break;
|
||||
|
||||
let i = start + 2;
|
||||
let depth = 1;
|
||||
|
||||
while (i < content.length && depth > 0) {
|
||||
if (content[i] === '(') depth++;
|
||||
else if (content[i] === ')') depth--;
|
||||
i++;
|
||||
}
|
||||
|
||||
const raw = content.slice(start, i);
|
||||
results.push({ raw, index: start });
|
||||
index = i;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function getLineNumber(content, charIndex) {
|
||||
return content.slice(0, charIndex).split('\n').length;
|
||||
}
|
||||
|
||||
function extractKey(call) {
|
||||
const match = call.match(/^_\(\s*(['"`])((?:\\\1|.)*?)\1\s*\)$/);
|
||||
return match ? match[2].trim() : null;
|
||||
}
|
||||
|
||||
function normalizeCall(call) {
|
||||
return call
|
||||
.replace(/\s*\n\s*/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\(\s+/g, '(')
|
||||
.replace(/\s+\)/g, ')')
|
||||
.replace(/,\s*\)$/, ')')
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function extractAllUnderscoreCallsWithLocations() {
|
||||
const files = [
|
||||
...(await fg(tsSearchGlob, { ignore: ['**/*test.ts'], absolute: true })),
|
||||
...(await fg(jsSearchGlob, { ignore: ['**/main.js'], absolute: true })),
|
||||
];
|
||||
|
||||
const callMap = new Map();
|
||||
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file, 'utf8');
|
||||
const relativePath = path.relative(process.cwd(), file);
|
||||
const extracted = extractAllUnderscoreCallsFromContent(content);
|
||||
|
||||
for (const { raw, index } of extracted) {
|
||||
const line = getLineNumber(content, index);
|
||||
const location = `${relativePath}:${line}`;
|
||||
|
||||
const normalized = normalizeCall(raw);
|
||||
const key = extractKey(normalized);
|
||||
|
||||
if (!callMap.has(normalized)) {
|
||||
callMap.set(normalized, {
|
||||
call: normalized,
|
||||
key: key ?? '',
|
||||
places: [],
|
||||
});
|
||||
}
|
||||
|
||||
callMap.get(normalized).places.push(location);
|
||||
}
|
||||
}
|
||||
|
||||
const result = [...callMap.values()];
|
||||
await fs.mkdir(path.dirname(outputFile), { recursive: true });
|
||||
await fs.writeFile(outputFile, JSON.stringify(result, null, 2), 'utf8');
|
||||
|
||||
console.log(`✅ Найдено ${result.length} уникальных вызовов _(...). Сохранено в ${outputFile}`);
|
||||
}
|
||||
|
||||
extractAllUnderscoreCallsWithLocations().catch(console.error);
|
||||
113
fe-app-podkop/generate-po.js
Normal file
113
fe-app-podkop/generate-po.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import fs from 'fs/promises';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const lang = process.argv[2];
|
||||
if (!lang) {
|
||||
console.error('❌ Укажи язык, например: node generate-po.js ru');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const callsPath = 'locales/calls.json';
|
||||
const poPath = `locales/podkop.${lang}.po`;
|
||||
|
||||
function getGitUser() {
|
||||
try {
|
||||
return execSync('git config user.name').toString().trim();
|
||||
} catch {
|
||||
return 'Automatically generated';
|
||||
}
|
||||
}
|
||||
|
||||
function getHeader(lang) {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().split('T')[0];
|
||||
const time = now.toTimeString().split(' ')[0].slice(0, 5);
|
||||
const tzOffset = (() => {
|
||||
const offset = -now.getTimezoneOffset();
|
||||
const sign = offset >= 0 ? '+' : '-';
|
||||
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
||||
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
||||
return `${sign}${hours}${minutes}`;
|
||||
})();
|
||||
|
||||
const translator = getGitUser();
|
||||
const pluralForms = lang === 'ru'
|
||||
? 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);'
|
||||
: 'nplurals=2; plural=(n != 1);';
|
||||
|
||||
return [
|
||||
`# ${lang.toUpperCase()} translations for PODKOP package.`,
|
||||
`# Copyright (C) ${now.getFullYear()} THE PODKOP'S COPYRIGHT HOLDER`,
|
||||
`# This file is distributed under the same license as the PODKOP package.`,
|
||||
`# ${translator}, ${now.getFullYear()}.`,
|
||||
'#',
|
||||
'msgid ""',
|
||||
'msgstr ""',
|
||||
`"Project-Id-Version: PODKOP\\n"`,
|
||||
`"Report-Msgid-Bugs-To: \\n"`,
|
||||
`"POT-Creation-Date: ${date} ${time}${tzOffset}\\n"`,
|
||||
`"PO-Revision-Date: ${date} ${time}${tzOffset}\\n"`,
|
||||
`"Last-Translator: ${translator}\\n"`,
|
||||
`"Language-Team: none\\n"`,
|
||||
`"Language: ${lang}\\n"`,
|
||||
`"MIME-Version: 1.0\\n"`,
|
||||
`"Content-Type: text/plain; charset=UTF-8\\n"`,
|
||||
`"Content-Transfer-Encoding: 8bit\\n"`,
|
||||
`"Plural-Forms: ${pluralForms}\\n"`,
|
||||
'',
|
||||
];
|
||||
}
|
||||
|
||||
function parsePo(content) {
|
||||
const lines = content.split('\n');
|
||||
const translations = new Map();
|
||||
let msgid = null;
|
||||
let msgstr = null;
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('msgid ')) {
|
||||
msgid = JSON.parse(line.slice(6));
|
||||
} else if (line.startsWith('msgstr ') && msgid !== null) {
|
||||
msgstr = JSON.parse(line.slice(7));
|
||||
translations.set(msgid, msgstr);
|
||||
msgid = null;
|
||||
msgstr = null;
|
||||
}
|
||||
}
|
||||
return translations;
|
||||
}
|
||||
|
||||
function escapePoString(str) {
|
||||
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
async function generatePo() {
|
||||
const [callsRaw, oldPoRaw] = await Promise.all([
|
||||
fs.readFile(callsPath, 'utf8'),
|
||||
fs.readFile(poPath, 'utf8').catch(() => ''),
|
||||
]);
|
||||
|
||||
const calls = JSON.parse(callsRaw);
|
||||
const oldTranslations = parsePo(oldPoRaw);
|
||||
const header = getHeader(lang);
|
||||
|
||||
const body = calls
|
||||
.map(({ key }) => {
|
||||
const msgid = key;
|
||||
const msgstr = oldTranslations.get(msgid) || '';
|
||||
return [
|
||||
`msgid "${escapePoString(msgid)}"`,
|
||||
`msgstr "${escapePoString(msgstr)}"`,
|
||||
''
|
||||
].join('\n');
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const finalPo = header.join('\n') + '\n' + body;
|
||||
|
||||
await fs.writeFile(poPath, finalPo, 'utf8');
|
||||
console.log(`✅ Файл ${poPath} успешно сгенерирован. Переведено ${[...oldTranslations.keys()].length}/${calls.length}`);
|
||||
}
|
||||
|
||||
generatePo().catch((err) => {
|
||||
console.error('Ошибка генерации PO файла:', err);
|
||||
});
|
||||
73
fe-app-podkop/generate-pot.js
Normal file
73
fe-app-podkop/generate-pot.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import fs from 'fs/promises';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const inputFile = 'locales/calls.json';
|
||||
const outputFile = 'locales/podkop.pot';
|
||||
const projectId = 'PODKOP';
|
||||
|
||||
function getGitUser() {
|
||||
const name = execSync('git config user.name').toString().trim();
|
||||
const email = execSync('git config user.email').toString().trim();
|
||||
return { name, email };
|
||||
}
|
||||
|
||||
function getPotHeader({ name, email }) {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().replace('T', ' ').slice(0, 16);
|
||||
const offset = -now.getTimezoneOffset();
|
||||
const sign = offset >= 0 ? '+' : '-';
|
||||
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
||||
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
||||
const timezone = `${sign}${hours}${minutes}`;
|
||||
|
||||
return [
|
||||
'# SOME DESCRIPTIVE TITLE.',
|
||||
`# Copyright (C) ${now.getFullYear()} THE PACKAGE'S COPYRIGHT HOLDER`,
|
||||
`# This file is distributed under the same license as the ${projectId} package.`,
|
||||
`# ${name} <${email}>, ${now.getFullYear()}.`,
|
||||
'#, fuzzy',
|
||||
'msgid ""',
|
||||
'msgstr ""',
|
||||
`"Project-Id-Version: ${projectId}\\n"`,
|
||||
`"Report-Msgid-Bugs-To: \\n"`,
|
||||
`"POT-Creation-Date: ${date}${timezone}\\n"`,
|
||||
`"PO-Revision-Date: ${date}${timezone}\\n"`,
|
||||
`"Last-Translator: ${name} <${email}>\\n"`,
|
||||
`"Language-Team: LANGUAGE <LL@li.org>\\n"`,
|
||||
`"Language: \\n"`,
|
||||
`"MIME-Version: 1.0\\n"`,
|
||||
`"Content-Type: text/plain; charset=UTF-8\\n"`,
|
||||
`"Content-Transfer-Encoding: 8bit\\n"`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function escapePoString(str) {
|
||||
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function generateEntry(item) {
|
||||
const locations = item.places.map(loc => `#: ${loc}`).join('\n');
|
||||
const msgid = escapePoString(item.key);
|
||||
return [
|
||||
locations,
|
||||
`msgid "${msgid}"`,
|
||||
`msgstr ""`,
|
||||
''
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function generatePot() {
|
||||
const gitUser = getGitUser();
|
||||
const raw = await fs.readFile(inputFile, 'utf8');
|
||||
const entries = JSON.parse(raw);
|
||||
|
||||
const header = getPotHeader(gitUser);
|
||||
const body = entries.map(generateEntry).join('\n');
|
||||
|
||||
await fs.writeFile(outputFile, `${header}\n${body}`, 'utf8');
|
||||
|
||||
console.log(`✅ POT-файл успешно создан: ${outputFile}`);
|
||||
}
|
||||
|
||||
generatePot().catch(console.error);
|
||||
2570
fe-app-podkop/locales/calls.json
Normal file
2570
fe-app-podkop/locales/calls.json
Normal file
File diff suppressed because it is too large
Load Diff
1517
fe-app-podkop/locales/podkop.pot
Normal file
1517
fe-app-podkop/locales/podkop.pot
Normal file
File diff suppressed because it is too large
Load Diff
1086
fe-app-podkop/locales/podkop.ru.po
Normal file
1086
fe-app-podkop/locales/podkop.ru.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,12 @@
|
||||
"dev": "tsup src/main.ts --watch",
|
||||
"test": "vitest",
|
||||
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
|
||||
"watch:sftp": "node watch-upload.js"
|
||||
"watch:sftp": "node watch-upload.js",
|
||||
"locales:exctract-calls": "node extract-calls.js",
|
||||
"locales:generate-pot": "node generate-pot.js",
|
||||
"locales:generate-po:ru": "node generate-po.js ru",
|
||||
"locales:distribute": "node distribute-locales.js",
|
||||
"locales:actualize": "yarn locales:exctract-calls && yarn locales:generate-pot && yarn locales:generate-po:ru && yarn locales:distribute"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
@@ -21,6 +26,7 @@
|
||||
"dotenv": "17.2.3",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"fast-glob": "3.3.3",
|
||||
"glob": "11.0.3",
|
||||
"prettier": "3.6.2",
|
||||
"ssh2-sftp-client": "12.0.1",
|
||||
|
||||
@@ -996,6 +996,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fast-glob@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user