diff --git a/controller/admin/I18nPreferencesController.js b/controller/admin/I18nPreferencesController.js index 72ca719..8ccc1f5 100644 --- a/controller/admin/I18nPreferencesController.js +++ b/controller/admin/I18nPreferencesController.js @@ -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; diff --git a/database/createDb.js b/database/createDb.js index f98ee49..0ad65a5 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -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 ( diff --git a/repositories/settings/I18nPreferencesRepository.js b/repositories/settings/I18nPreferencesRepository.js index 0b3343b..4b3d6a9 100644 --- a/repositories/settings/I18nPreferencesRepository.js +++ b/repositories/settings/I18nPreferencesRepository.js @@ -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; diff --git a/routes/deleteRoutes.js b/routes/deleteRoutes.js index f49a657..1d98c3d 100644 --- a/routes/deleteRoutes.js +++ b/routes/deleteRoutes.js @@ -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; diff --git a/routes/getRoutes.js b/routes/getRoutes.js index 5937e9f..766b0ca 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -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 diff --git a/routes/postRoutes.js b/routes/postRoutes.js index 12dcfea..cec4238 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -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 diff --git a/routes/putRoutes.js b/routes/putRoutes.js index 62158f7..ce27822 100644 --- a/routes/putRoutes.js +++ b/routes/putRoutes.js @@ -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; diff --git a/scripts/createAdminUser.js b/scripts/createAdminUser.js index c4c6d88..1f25dc8 100644 --- a/scripts/createAdminUser.js +++ b/scripts/createAdminUser.js @@ -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'; diff --git a/utils/apiPath.js b/utils/apiPath.js deleted file mode 100644 index 3c68681..0000000 --- a/utils/apiPath.js +++ /dev/null @@ -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, -};