- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации - Services CRUD: полное управление услугами с возможностью активации/деактивации - Banner system: новая модель Banner с CRUD операциями и аналитикой кликов - Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений - Media management: улучшенная загрузка файлов с оптимизацией изображений и превью - UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой - Database: добавлена таблица banners с полями для баннеров и аналитики
116 lines
3.0 KiB
JavaScript
116 lines
3.0 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const localesDir = path.join(__dirname, '..', 'locales');
|
|
const files = fs.readdirSync(localesDir).filter(f => f.endsWith('.json'));
|
|
|
|
// priority order for falling back when filling missing translations
|
|
const priority = ['en.json', 'ko.json', 'ru.json', 'kk.json'];
|
|
|
|
function readJSON(file) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(path.join(localesDir, file), 'utf8'));
|
|
} catch (e) {
|
|
console.error('Failed to read', file, e.message);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function flatten(obj, prefix = '') {
|
|
const res = {};
|
|
for (const k of Object.keys(obj)) {
|
|
const val = obj[k];
|
|
const key = prefix ? `${prefix}.${k}` : k;
|
|
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
Object.assign(res, flatten(val, key));
|
|
} else {
|
|
res[key] = val;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function unflatten(flat) {
|
|
const res = {};
|
|
for (const flatKey of Object.keys(flat)) {
|
|
const parts = flatKey.split('.');
|
|
let cur = res;
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const p = parts[i];
|
|
if (i === parts.length - 1) {
|
|
cur[p] = flat[flatKey];
|
|
} else {
|
|
cur[p] = cur[p] || {};
|
|
cur = cur[p];
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// load all
|
|
const data = {};
|
|
for (const f of files) {
|
|
data[f] = readJSON(f);
|
|
}
|
|
|
|
// use en.json as canonical key set if exists, else merge all keys
|
|
let canonical = {};
|
|
if (files.includes('en.json')) {
|
|
canonical = flatten(data['en.json']);
|
|
} else {
|
|
for (const f of files) {
|
|
canonical = Object.assign(canonical, flatten(data[f]));
|
|
}
|
|
}
|
|
|
|
function getFallback(key, currentFile) {
|
|
for (const p of priority) {
|
|
if (p === currentFile) continue;
|
|
if (!files.includes(p)) continue;
|
|
const flat = flatten(data[p]);
|
|
if (flat[key] && typeof flat[key] === 'string' && flat[key].trim()) return flat[key];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
let report = {};
|
|
for (const f of files) {
|
|
const flat = flatten(data[f]);
|
|
report[f] = { added: 0, marked: 0 };
|
|
const out = {};
|
|
for (const key of Object.keys(canonical)) {
|
|
if (flat.hasOwnProperty(key)) {
|
|
out[key] = flat[key];
|
|
} else {
|
|
const fb = getFallback(key, f);
|
|
if (fb) {
|
|
out[key] = fb;
|
|
} else {
|
|
out[key] = '[TRANSLATE] ' + canonical[key];
|
|
report[f].marked++;
|
|
}
|
|
report[f].added++;
|
|
}
|
|
}
|
|
// also keep any extra keys present in this file but not in canonical
|
|
for (const key of Object.keys(flat)) {
|
|
if (!out.hasOwnProperty(key)) {
|
|
out[key] = flat[key];
|
|
}
|
|
}
|
|
// write back
|
|
const nested = unflatten(out);
|
|
try {
|
|
fs.writeFileSync(path.join(localesDir, f), JSON.stringify(nested, null, 2), 'utf8');
|
|
} catch (e) {
|
|
console.error('Failed to write', f, e.message);
|
|
}
|
|
}
|
|
|
|
console.log('Locale sync report:');
|
|
for (const f of files) {
|
|
console.log(`- ${f}: added ${report[f].added} keys, ${report[f].marked} marked for translation`);
|
|
}
|
|
console.log('Done. Review files in locales/ and replace [TRANSLATE] placeholders with correct translations.');
|