CentralBackend/controller/admin/I18nPreferencesController.js
DeathKaioken 0bb230cf66 crashout
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 02:00:44 +02:00

309 lines
11 KiB
JavaScript

const I18nPreferencesRepository = require('../../repositories/settings/I18nPreferencesRepository');
const { logger } = require('../../middleware/logger');
const repo = new I18nPreferencesRepository();
class I18nPreferencesController {
static _normalizeLanguageCode(value) {
const raw = String(value == null ? '' : value).trim();
if (!raw) return '';
const normalized = raw.replace('_', '-');
const pattern = /^[a-z]{2,3}(-[a-z0-9]{2,8})?$/i;
if (!pattern.test(normalized)) return null;
return normalized.toLowerCase();
}
static _normalizeStringArray(values) {
if (!Array.isArray(values)) return [];
const normalized = values
.map((v) => (v == null ? '' : String(v).trim()))
.filter(Boolean);
return [...new Set(normalized)];
}
static _normalizeCategories(categories) {
if (!Array.isArray(categories)) return [];
return categories
.map((item, idx) => {
const id = String(item?.id ?? '').trim() || `category_${idx + 1}`;
const label = String(item?.label ?? id).trim() || id;
const namespaces = I18nPreferencesController._normalizeStringArray(item?.namespaces);
const isCustom = Boolean(item?.isCustom);
return { id, label, namespaces, isCustom };
});
}
static _buildResponse(preferences) {
return {
ok: true,
preferences,
categories: preferences.categories,
globalKeys: preferences.globalKeys,
};
}
static _normalizeLanguagePayload(body) {
const source = (body?.language && typeof body.language === 'object') ? body.language : body;
const rawLanguageCode = source?.languageCode ?? source?.code ?? source?.lang;
const hasAnyLanguageField = rawLanguageCode !== undefined
|| source?.label !== undefined
|| source?.name !== undefined
|| source?.isEnabled !== undefined
|| source?.isCustom !== undefined;
if (!hasAnyLanguageField) return null;
const languageCode = I18nPreferencesController._normalizeLanguageCode(rawLanguageCode);
if (languageCode === null || !languageCode) {
throw new Error('Invalid language payload: languageCode is required');
}
const label = String(source?.label ?? source?.name ?? languageCode.toUpperCase()).trim();
const toBool = (v, fallback) => {
if (v === undefined || v === null) return fallback;
if (typeof v === 'boolean') return v;
const n = String(v).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(n)) return true;
if (['0', 'false', 'no', 'off'].includes(n)) return false;
return fallback;
};
return {
languageCode,
label,
isEnabled: toBool(source?.isEnabled, true),
isCustom: toBool(source?.isCustom, true),
};
}
static _normalizeTranslationsPayload(body, fallbackLanguageCode) {
const raw = body?.translations ?? body?.translationOverrides ?? body?.entries;
if (raw == null) return [];
const normalized = [];
if (Array.isArray(raw)) {
for (const item of raw) {
const languageCode = I18nPreferencesController._normalizeLanguageCode(
item?.languageCode ?? item?.lang ?? fallbackLanguageCode
);
const namespace = String(item?.namespace ?? '').trim();
const key = String(item?.key ?? item?.t_key ?? '').trim();
const value = item?.value ?? item?.t_value;
if (!languageCode || !namespace || !key || value === undefined) continue;
normalized.push({
languageCode,
namespace,
key,
value: String(value),
isCustom: item?.isCustom,
});
}
return normalized;
}
if (typeof raw === 'object') {
for (const [namespace, kv] of Object.entries(raw)) {
if (!kv || typeof kv !== 'object') continue;
for (const [key, value] of Object.entries(kv)) {
const languageCode = I18nPreferencesController._normalizeLanguageCode(fallbackLanguageCode);
if (!languageCode || !namespace || !key || value === undefined) continue;
normalized.push({
languageCode,
namespace: String(namespace).trim(),
key: String(key).trim(),
value: String(value),
isCustom: true,
});
}
}
}
return normalized;
}
static _extractPreferencesPayload(body) {
const source = (body?.preferences && typeof body.preferences === 'object')
? body.preferences
: body;
const hasCategories = Object.prototype.hasOwnProperty.call(source || {}, 'categories');
const hasGlobalKeys = Object.prototype.hasOwnProperty.call(source || {}, 'globalKeys');
return {
categories: hasCategories ? I18nPreferencesController._normalizeCategories(source?.categories) : undefined,
globalKeys: hasGlobalKeys ? I18nPreferencesController._normalizeStringArray(source?.globalKeys) : undefined,
};
}
static async _upsertBundle(req, res) {
try {
const userId = req.user?.userId ?? req.user?.id ?? null;
const language = I18nPreferencesController._normalizeLanguagePayload(req.body || {});
const fallbackLanguageCode = language?.languageCode
|| I18nPreferencesController._normalizeLanguageCode(req.body?.languageCode ?? req.body?.lang);
const translations = I18nPreferencesController._normalizeTranslationsPayload(req.body || {}, fallbackLanguageCode);
const extracted = I18nPreferencesController._extractPreferencesPayload(req.body || {});
const result = await repo.upsertBundle({
categories: extracted.categories,
globalKeys: extracted.globalKeys,
language,
translations,
updatedByUserId: userId,
});
return res.status(200).json({
...I18nPreferencesController._buildResponse(result.preferences),
language: result.language || null,
translationsUpserted: result.translationsUpserted || 0,
});
} catch (error) {
if (String(error?.message || '').includes('Invalid language payload')) {
return res.status(400).json({ ok: false, message: error.message });
}
logger.error('i18nPreferences:upsertBundle:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to upsert i18n language data' });
}
}
static async get(req, res) {
try {
const preferences = await repo.get();
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
} catch (error) {
logger.error('i18nPreferences:get:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to load i18n preferences' });
}
}
static async post(req, res) {
return I18nPreferencesController._upsertBundle(req, res);
}
static async put(req, res) {
return I18nPreferencesController._upsertBundle(req, res);
}
static async delete(req, res) {
try {
const requestedLanguageCode = I18nPreferencesController._normalizeLanguageCode(req.query?.languageCode);
if (requestedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode query parameter' });
}
if (requestedLanguageCode) {
const result = await repo.deleteLanguageEntries(
requestedLanguageCode,
req.user?.userId ?? req.user?.id ?? null
);
return res.status(200).json({
ok: true,
languageCode: requestedLanguageCode,
deletedRows: result.deletedRows,
touchedTables: result.touchedTables,
});
}
const hasBodyReplacement = req.body && (
Object.prototype.hasOwnProperty.call(req.body, 'categories') ||
Object.prototype.hasOwnProperty.call(req.body, 'globalKeys')
);
const preferences = hasBodyReplacement
? await repo.upsert({
categories: I18nPreferencesController._normalizeCategories(req.body?.categories),
globalKeys: I18nPreferencesController._normalizeStringArray(req.body?.globalKeys),
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
})
: await repo.clear(req.user?.userId ?? req.user?.id ?? null);
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
} catch (error) {
logger.error('i18nPreferences:delete:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to delete i18n preferences' });
}
}
static async getTranslations(req, res) {
try {
const languageCodeRaw = req.query?.languageCode ?? req.query?.lang;
const normalizedLanguageCode = languageCodeRaw
? I18nPreferencesController._normalizeLanguageCode(languageCodeRaw)
: '';
if (normalizedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode query parameter' });
}
const namespace = String(req.query?.namespace ?? '').trim() || undefined;
const translations = await repo.listTranslations({
languageCode: normalizedLanguageCode || undefined,
namespace,
});
return res.status(200).json({ ok: true, translations });
} catch (error) {
logger.error('i18nTranslations:get:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to load translations' });
}
}
static async upsertTranslations(req, res) {
try {
const language = I18nPreferencesController._normalizeLanguagePayload(req.body || {});
const fallbackLanguageCode = language?.languageCode
|| I18nPreferencesController._normalizeLanguageCode(req.body?.languageCode ?? req.body?.lang);
const translations = I18nPreferencesController._normalizeTranslationsPayload(req.body || {}, fallbackLanguageCode);
if (!translations.length) {
return res.status(400).json({ ok: false, message: 'No valid translations provided' });
}
const userId = req.user?.userId ?? req.user?.id ?? null;
const result = await repo.upsertTranslations({ translations, updatedByUserId: userId });
if (language) {
await repo.upsertBundle({ language, updatedByUserId: userId });
}
return res.status(200).json({
ok: true,
translationsUpserted: result.upsertedCount || 0,
});
} catch (error) {
if (String(error?.message || '').includes('Invalid language payload')) {
return res.status(400).json({ ok: false, message: error.message });
}
logger.error('i18nTranslations:upsert:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to upsert translations' });
}
}
static async scan(req, res) {
try {
const languageCodeRaw = req.query?.languageCode ?? req.query?.lang ?? req.body?.languageCode ?? req.body?.lang;
const normalizedLanguageCode = languageCodeRaw
? I18nPreferencesController._normalizeLanguageCode(languageCodeRaw)
: '';
if (normalizedLanguageCode === null) {
return res.status(400).json({ ok: false, message: 'Invalid languageCode parameter' });
}
const summary = await repo.getScanSummary({ languageCode: normalizedLanguageCode || undefined });
return res.status(200).json({
ok: true,
...summary,
});
} catch (error) {
logger.error('i18nScan:failed', { error: error?.message });
return res.status(500).json({ ok: false, message: 'Failed to scan i18n data' });
}
}
}
module.exports = I18nPreferencesController;