crashout
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
918deb2b69
commit
0bb230cf66
@ -4,6 +4,16 @@ 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
|
||||
@ -34,6 +44,132 @@ class I18nPreferencesController {
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
@ -45,43 +181,34 @@ class I18nPreferencesController {
|
||||
}
|
||||
|
||||
static async post(req, res) {
|
||||
try {
|
||||
const categories = I18nPreferencesController._normalizeCategories(req.body?.categories);
|
||||
const globalKeys = I18nPreferencesController._normalizeStringArray(req.body?.globalKeys);
|
||||
|
||||
const preferences = await repo.upsert({
|
||||
categories,
|
||||
globalKeys,
|
||||
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
|
||||
});
|
||||
|
||||
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
|
||||
} catch (error) {
|
||||
logger.error('i18nPreferences:post:failed', { error: error?.message });
|
||||
return res.status(500).json({ ok: false, message: 'Failed to save i18n preferences' });
|
||||
}
|
||||
return I18nPreferencesController._upsertBundle(req, res);
|
||||
}
|
||||
|
||||
static async put(req, res) {
|
||||
try {
|
||||
const categories = I18nPreferencesController._normalizeCategories(req.body?.categories);
|
||||
const globalKeys = I18nPreferencesController._normalizeStringArray(req.body?.globalKeys);
|
||||
|
||||
const preferences = await repo.upsert({
|
||||
categories,
|
||||
globalKeys,
|
||||
updatedByUserId: req.user?.userId ?? req.user?.id ?? null,
|
||||
});
|
||||
|
||||
return res.status(200).json(I18nPreferencesController._buildResponse(preferences));
|
||||
} catch (error) {
|
||||
logger.error('i18nPreferences:put:failed', { error: error?.message });
|
||||
return res.status(500).json({ ok: false, message: 'Failed to update i18n preferences' });
|
||||
}
|
||||
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')
|
||||
@ -101,6 +228,81 @@ class I18nPreferencesController {
|
||||
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;
|
||||
|
||||
@ -861,6 +861,50 @@ const createDatabase = async () => {
|
||||
ADD CONSTRAINT \`i18n_preferences_ibfk_1\` FOREIGN KEY (\`updated_by_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
|
||||
);
|
||||
|
||||
// Language metadata used by language-management UI
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS i18n_languages (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
language_code VARCHAR(16) NOT NULL,
|
||||
label VARCHAR(100) NULL,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_custom BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_by_user_id INT NULL,
|
||||
updated_by_user_id INT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_i18n_languages_code UNIQUE (language_code),
|
||||
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
`);
|
||||
console.log('✅ i18n languages table created/verified');
|
||||
|
||||
// Translation overrides/custom values per language
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS i18n_translation_overrides (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
language_code VARCHAR(16) NOT NULL,
|
||||
namespace VARCHAR(128) NOT NULL,
|
||||
t_key VARCHAR(255) NOT NULL,
|
||||
t_value LONGTEXT NOT NULL,
|
||||
is_custom BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_by_user_id INT NULL,
|
||||
updated_by_user_id INT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_i18n_translation_overrides UNIQUE (language_code, namespace, t_key),
|
||||
FOREIGN KEY (language_code) REFERENCES i18n_languages(language_code) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
`);
|
||||
console.log('✅ i18n translation overrides table created/verified');
|
||||
|
||||
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_lang', '`language_code`');
|
||||
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_namespace', '`namespace`');
|
||||
await ensureIndex(connection, 'i18n_translation_overrides', 'idx_i18n_overrides_lang_namespace', '`language_code`, `namespace`');
|
||||
|
||||
// --- Dashboard Platforms (admin managed dashboard cards) ---
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS dashboard_plattforms (
|
||||
|
||||
@ -22,6 +22,15 @@ class I18nPreferencesRepository {
|
||||
};
|
||||
}
|
||||
|
||||
_safeBoolean(value, fallback) {
|
||||
if (value === undefined || value === null) return fallback;
|
||||
if (typeof value === 'boolean') return value;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async get() {
|
||||
const [rows] = await db.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
|
||||
if (!rows.length) {
|
||||
@ -62,6 +71,351 @@ class I18nPreferencesRepository {
|
||||
|
||||
return this.get();
|
||||
}
|
||||
|
||||
async _tableExists(conn, tableName) {
|
||||
const [rows] = await conn.query(
|
||||
`SELECT 1
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
LIMIT 1`,
|
||||
[tableName]
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
async _columnExists(conn, tableName, columnName) {
|
||||
const [rows] = await conn.query(
|
||||
`SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND COLUMN_NAME = ?
|
||||
LIMIT 1`,
|
||||
[tableName, columnName]
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
async deleteLanguageEntries(languageCode, updatedByUserId) {
|
||||
const conn = await db.getConnection();
|
||||
const safeLanguageCode = String(languageCode || '').trim();
|
||||
|
||||
const targets = [
|
||||
// Language metadata
|
||||
{ table: 'i18n_languages', possibleColumns: ['language_code', 'lang', 'code'] },
|
||||
{ table: 'i18n_language_metadata', possibleColumns: ['language_code', 'lang', 'code'] },
|
||||
|
||||
// Translation/custom-value stores
|
||||
{ table: 'i18n_translation_overrides', possibleColumns: ['language_code', 'lang'] },
|
||||
{ table: 'i18n_translations', possibleColumns: ['language_code', 'lang'] },
|
||||
|
||||
// Potential language-scoped preference/link tables
|
||||
{ table: 'i18n_preferences_languages', possibleColumns: ['language_code', 'lang'] },
|
||||
{ table: 'i18n_preference_categories', possibleColumns: ['language_code', 'lang'] },
|
||||
{ table: 'i18n_preference_global_keys', possibleColumns: ['language_code', 'lang'] },
|
||||
];
|
||||
|
||||
let deletedRows = 0;
|
||||
const touchedTables = [];
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
for (const target of targets) {
|
||||
const exists = await this._tableExists(conn, target.table);
|
||||
if (!exists) continue;
|
||||
|
||||
let deleteColumn = null;
|
||||
for (const col of target.possibleColumns) {
|
||||
if (await this._columnExists(conn, target.table, col)) {
|
||||
deleteColumn = col;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!deleteColumn) continue;
|
||||
|
||||
const [result] = await conn.query(
|
||||
`DELETE FROM \`${target.table}\` WHERE \`${deleteColumn}\` = ?`,
|
||||
[safeLanguageCode]
|
||||
);
|
||||
const affected = Number(result?.affectedRows || 0);
|
||||
if (affected > 0) {
|
||||
deletedRows += affected;
|
||||
touchedTables.push(target.table);
|
||||
}
|
||||
}
|
||||
|
||||
if (await this._tableExists(conn, 'i18n_preferences')) {
|
||||
await conn.query(
|
||||
`UPDATE i18n_preferences
|
||||
SET updated_by_user_id = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = 1`,
|
||||
[updatedByUserId || null]
|
||||
);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
return { deletedRows, touchedTables };
|
||||
} catch (error) {
|
||||
await conn.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async upsertLanguage(conn, language, updatedByUserId) {
|
||||
if (!language || !language.languageCode) return null;
|
||||
|
||||
const languageCode = String(language.languageCode).trim().toLowerCase();
|
||||
const label = String(language.label || languageCode).trim();
|
||||
const isEnabled = this._safeBoolean(language.isEnabled, true);
|
||||
const isCustom = this._safeBoolean(language.isCustom, true);
|
||||
|
||||
await conn.query(
|
||||
`INSERT INTO i18n_languages (language_code, label, is_enabled, is_custom, created_by_user_id, updated_by_user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label = VALUES(label),
|
||||
is_enabled = VALUES(is_enabled),
|
||||
is_custom = VALUES(is_custom),
|
||||
updated_by_user_id = VALUES(updated_by_user_id)`,
|
||||
[languageCode, label, isEnabled ? 1 : 0, isCustom ? 1 : 0, updatedByUserId || null, updatedByUserId || null]
|
||||
);
|
||||
|
||||
const [rows] = await conn.query(
|
||||
`SELECT language_code AS languageCode,
|
||||
label,
|
||||
is_enabled AS isEnabled,
|
||||
is_custom AS isCustom,
|
||||
created_at AS createdAt,
|
||||
updated_at AS updatedAt
|
||||
FROM i18n_languages
|
||||
WHERE language_code = ?
|
||||
LIMIT 1`,
|
||||
[languageCode]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async upsertTranslationOverrides(conn, translationEntries, updatedByUserId) {
|
||||
if (!Array.isArray(translationEntries) || !translationEntries.length) {
|
||||
return { upsertedCount: 0 };
|
||||
}
|
||||
|
||||
let upsertedCount = 0;
|
||||
for (const entry of translationEntries) {
|
||||
const languageCode = String(entry.languageCode || '').trim().toLowerCase();
|
||||
const namespace = String(entry.namespace || '').trim();
|
||||
const key = String(entry.key || '').trim();
|
||||
const value = entry.value == null ? '' : String(entry.value);
|
||||
const isCustom = this._safeBoolean(entry.isCustom, true);
|
||||
|
||||
if (!languageCode || !namespace || !key) continue;
|
||||
|
||||
await conn.query(
|
||||
`INSERT INTO i18n_translation_overrides
|
||||
(language_code, namespace, t_key, t_value, is_custom, created_by_user_id, updated_by_user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
t_value = VALUES(t_value),
|
||||
is_custom = VALUES(is_custom),
|
||||
updated_by_user_id = VALUES(updated_by_user_id)`,
|
||||
[
|
||||
languageCode,
|
||||
namespace,
|
||||
key,
|
||||
value,
|
||||
isCustom ? 1 : 0,
|
||||
updatedByUserId || null,
|
||||
updatedByUserId || null,
|
||||
]
|
||||
);
|
||||
upsertedCount += 1;
|
||||
}
|
||||
|
||||
return { upsertedCount };
|
||||
}
|
||||
|
||||
async upsertBundle({ categories, globalKeys, language, translations, updatedByUserId } = {}) {
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const [prefRows] = await conn.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
|
||||
const current = prefRows.length
|
||||
? this._normalizeRow(prefRows[0])
|
||||
: { categories: [], globalKeys: [] };
|
||||
|
||||
const nextCategories = categories !== undefined ? categories : current.categories;
|
||||
const nextGlobalKeys = globalKeys !== undefined ? globalKeys : current.globalKeys;
|
||||
|
||||
await conn.query(
|
||||
`INSERT INTO i18n_preferences (id, categories_json, global_keys_json, updated_by_user_id)
|
||||
VALUES (1, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
categories_json = VALUES(categories_json),
|
||||
global_keys_json = VALUES(global_keys_json),
|
||||
updated_by_user_id = VALUES(updated_by_user_id)`,
|
||||
[JSON.stringify(nextCategories || []), JSON.stringify(nextGlobalKeys || []), updatedByUserId || null]
|
||||
);
|
||||
|
||||
const languagesToEnsure = new Map();
|
||||
if (language && language.languageCode) {
|
||||
languagesToEnsure.set(String(language.languageCode).toLowerCase(), language);
|
||||
}
|
||||
for (const t of (Array.isArray(translations) ? translations : [])) {
|
||||
const code = String(t.languageCode || '').trim().toLowerCase();
|
||||
if (!code) continue;
|
||||
if (!languagesToEnsure.has(code)) {
|
||||
languagesToEnsure.set(code, {
|
||||
languageCode: code,
|
||||
label: code.toUpperCase(),
|
||||
isEnabled: true,
|
||||
isCustom: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let upsertedLanguage = null;
|
||||
for (const [, langPayload] of languagesToEnsure) {
|
||||
const row = await this.upsertLanguage(conn, langPayload, updatedByUserId);
|
||||
if (language && row && row.languageCode === String(language.languageCode).toLowerCase()) {
|
||||
upsertedLanguage = row;
|
||||
}
|
||||
}
|
||||
|
||||
const translationResult = await this.upsertTranslationOverrides(conn, translations, updatedByUserId);
|
||||
|
||||
const [savedRows] = await conn.query('SELECT * FROM i18n_preferences WHERE id = 1 LIMIT 1');
|
||||
const preferences = savedRows.length
|
||||
? this._normalizeRow(savedRows[0])
|
||||
: { categories: [], globalKeys: [] };
|
||||
|
||||
await conn.commit();
|
||||
return {
|
||||
preferences,
|
||||
language: upsertedLanguage,
|
||||
translationsUpserted: translationResult.upsertedCount,
|
||||
};
|
||||
} catch (error) {
|
||||
await conn.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async listTranslations({ languageCode, namespace } = {}) {
|
||||
const filters = [];
|
||||
const params = [];
|
||||
|
||||
if (languageCode) {
|
||||
filters.push('language_code = ?');
|
||||
params.push(String(languageCode).trim().toLowerCase());
|
||||
}
|
||||
if (namespace) {
|
||||
filters.push('namespace = ?');
|
||||
params.push(String(namespace).trim());
|
||||
}
|
||||
|
||||
const whereClause = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
|
||||
const [rows] = await db.query(
|
||||
`SELECT language_code AS languageCode,
|
||||
namespace,
|
||||
t_key AS \`key\`,
|
||||
t_value AS value,
|
||||
is_custom AS isCustom,
|
||||
updated_at AS updatedAt
|
||||
FROM i18n_translation_overrides
|
||||
${whereClause}
|
||||
ORDER BY language_code, namespace, t_key`,
|
||||
params
|
||||
);
|
||||
|
||||
return rows || [];
|
||||
}
|
||||
|
||||
async upsertTranslations({ translations, updatedByUserId } = {}) {
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// Make sure every translation language exists in metadata table.
|
||||
const ensured = new Set();
|
||||
for (const t of (translations || [])) {
|
||||
const code = String(t.languageCode || '').trim().toLowerCase();
|
||||
if (!code || ensured.has(code)) continue;
|
||||
|
||||
await this.upsertLanguage(
|
||||
conn,
|
||||
{
|
||||
languageCode: code,
|
||||
label: code.toUpperCase(),
|
||||
isEnabled: true,
|
||||
isCustom: true,
|
||||
},
|
||||
updatedByUserId
|
||||
);
|
||||
ensured.add(code);
|
||||
}
|
||||
|
||||
const result = await this.upsertTranslationOverrides(conn, translations || [], updatedByUserId);
|
||||
await conn.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await conn.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getScanSummary({ languageCode } = {}) {
|
||||
const code = languageCode ? String(languageCode).trim().toLowerCase() : null;
|
||||
|
||||
const [languages] = await db.query(
|
||||
`SELECT language_code AS languageCode,
|
||||
label,
|
||||
is_enabled AS isEnabled,
|
||||
is_custom AS isCustom
|
||||
FROM i18n_languages
|
||||
ORDER BY language_code`
|
||||
);
|
||||
|
||||
const [namespaces] = await db.query(
|
||||
`SELECT DISTINCT namespace
|
||||
FROM i18n_translation_overrides
|
||||
${code ? 'WHERE language_code = ?' : ''}
|
||||
ORDER BY namespace`,
|
||||
code ? [code] : []
|
||||
);
|
||||
|
||||
const [countsByLanguage] = await db.query(
|
||||
`SELECT language_code AS languageCode, COUNT(*) AS entryCount
|
||||
FROM i18n_translation_overrides
|
||||
${code ? 'WHERE language_code = ?' : ''}
|
||||
GROUP BY language_code
|
||||
ORDER BY language_code`,
|
||||
code ? [code] : []
|
||||
);
|
||||
|
||||
const prefs = await this.get();
|
||||
const categoryNamespaces = Array.isArray(prefs.categories)
|
||||
? [...new Set(prefs.categories.flatMap((c) => (Array.isArray(c?.namespaces) ? c.namespaces : [])))]
|
||||
: [];
|
||||
|
||||
return {
|
||||
languages: languages || [],
|
||||
namespaces: (namespaces || []).map((r) => r.namespace),
|
||||
countsByLanguage: countsByLanguage || [],
|
||||
categories: prefs.categories || [],
|
||||
globalKeys: prefs.globalKeys || [],
|
||||
categoryNamespaces,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = I18nPreferencesRepository;
|
||||
|
||||
@ -11,9 +11,6 @@ const AffiliateController = require('../controller/affiliate/AffiliateController
|
||||
const NewsController = require('../controller/news/NewsController');
|
||||
const PoolController = require('../controller/pool/PoolController');
|
||||
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
|
||||
const { getRouterPathFromApiEnv } = require('../utils/apiPath');
|
||||
|
||||
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
|
||||
|
||||
// Helper middlewares for company-stamp
|
||||
function forceCompanyForAdmin(req, res, next) {
|
||||
@ -41,6 +38,7 @@ router.delete('/admin/news/:id', authMiddleware, adminOnly, NewsController.delet
|
||||
|
||||
// Admin: remove pool members
|
||||
router.delete('/admin/pools/:id/members', authMiddleware, adminOnly, PoolController.removeMembers);
|
||||
router.delete(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.delete);
|
||||
router.delete('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.delete);
|
||||
router.delete('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.delete);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -32,10 +32,8 @@ const DashboardPlatformsController = require('../controller/admin/DashboardPlatf
|
||||
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
|
||||
const LoginController = require('../controller/login/LoginController');
|
||||
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
|
||||
const { getRouterPathFromApiEnv } = require('../utils/apiPath');
|
||||
|
||||
const AUTH_VALIDATE_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_AUTH_VALIDATE_PATH', '/api/auth/validate');
|
||||
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
|
||||
const AUTH_VALIDATE_ROUTE_PATH = '/auth/validate';
|
||||
|
||||
// small helpers copied from original files
|
||||
|
||||
@ -60,7 +58,10 @@ router.get('/users/:id/permissions', authMiddleware, PermissionController.getUse
|
||||
router.get('/admin/users/:id/full', authMiddleware, adminOnly, AdminUserController.getFullUserAccountDetails);
|
||||
router.get('/admin/users/:id/detailed', authMiddleware, adminOnly, AdminUserController.getDetailedUserInfo);
|
||||
router.get('/admin/company-settings', authMiddleware, adminOnly, CompanySettingsController.get);
|
||||
router.get(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.get);
|
||||
router.get('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.get);
|
||||
router.get('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.get);
|
||||
router.get('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.getTranslations);
|
||||
router.get('/i18n/scan', authMiddleware, adminOnly, I18nPreferencesController.scan);
|
||||
router.get('/users/:id/documents', authMiddleware, UserController.getUserDocumentsAndContracts);
|
||||
router.get('/verify-password-reset', (req, res) => { /* Note: was moved from PasswordResetController.verifyPasswordResetToken */ res.status(204).end(); }); // keep placeholder if controller already registered via other verb
|
||||
|
||||
|
||||
@ -33,9 +33,6 @@ const InvoiceController = require('../controller/invoice/InvoiceController'); //
|
||||
const DevManagementController = require('../controller/dev/DevManagementController');
|
||||
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
|
||||
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
|
||||
const { getRouterPathFromApiEnv } = require('../utils/apiPath');
|
||||
|
||||
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
|
||||
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
@ -86,7 +83,10 @@ router.post('/profile/company/complete', authMiddleware, CompanyProfileControlle
|
||||
|
||||
// Admin POSTs (moved from routes/admin.js)
|
||||
router.post('/admin/verify-user/:id', authMiddleware, adminOnly, AdminUserController.verifyUser);
|
||||
router.post(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.post);
|
||||
router.post('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.post);
|
||||
router.post('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.post);
|
||||
router.post('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.upsertTranslations);
|
||||
router.post('/i18n/scan', authMiddleware, adminOnly, I18nPreferencesController.scan);
|
||||
router.post('/admin/send-password-reset/:userId', authMiddleware, adminOnly, async (req, res) => {
|
||||
const userId = req.params.userId;
|
||||
// require here to avoid circular/top-level ordering issues
|
||||
|
||||
@ -10,9 +10,6 @@ const CompanySettingsController = require('../controller/admin/CompanySettingsCo
|
||||
const DashboardPlatformsController = require('../controller/admin/DashboardPlatformsController');
|
||||
const ShippingFeesController = require('../controller/admin/ShippingFeesController');
|
||||
const I18nPreferencesController = require('../controller/admin/I18nPreferencesController');
|
||||
const { getRouterPathFromApiEnv } = require('../utils/apiPath');
|
||||
|
||||
const I18N_PREFERENCES_ROUTE_PATH = getRouterPathFromApiEnv('BACKEND_I18N_PREFERENCES_PATH', '/api/admin/i18n/preferences');
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
@ -32,6 +29,8 @@ router.put('/admin/dashboard-platforms/:id', authMiddleware, adminOnly, Dashboar
|
||||
|
||||
// Admin: update shipping fee for a piece count (60/120)
|
||||
router.put('/admin/shipping-fees/:pieceCount', authMiddleware, adminOnly, ShippingFeesController.updatePrice);
|
||||
router.put(I18N_PREFERENCES_ROUTE_PATH, authMiddleware, adminOnly, I18nPreferencesController.put);
|
||||
router.put('/admin/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.put);
|
||||
router.put('/i18n/preferences', authMiddleware, adminOnly, I18nPreferencesController.put);
|
||||
router.put('/i18n/translations', authMiddleware, adminOnly, I18nPreferencesController.upsertTranslations);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -3,7 +3,6 @@ const UnitOfWork = require('../database/UnitOfWork');
|
||||
const argon2 = require('argon2');
|
||||
|
||||
async function createAdminUser() {
|
||||
return;
|
||||
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com';
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
|
||||
// const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
function toRouterPath(configuredPath, defaultApiPath) {
|
||||
const fallback = String(defaultApiPath || '/').trim() || '/';
|
||||
const raw = String(configuredPath || fallback).trim() || fallback;
|
||||
|
||||
let path = raw.startsWith('/') ? raw : `/${raw}`;
|
||||
|
||||
// Routers are mounted under /api in server.js, so strip an optional /api prefix.
|
||||
if (path === '/api') path = '/';
|
||||
if (path.startsWith('/api/')) path = path.slice(4);
|
||||
|
||||
if (path.length > 1) path = path.replace(/\/+$/, '');
|
||||
return path || '/';
|
||||
}
|
||||
|
||||
function getRouterPathFromApiEnv(envKey, defaultApiPath) {
|
||||
return toRouterPath(process.env[envKey], defaultApiPath);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toRouterPath,
|
||||
getRouterPathFromApiEnv,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user