From 8c2649d9a12e51cdbced463e73630c346ec28fde Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:36:16 +0800 Subject: [PATCH] update --- package-lock.json | 192 +++++++++++++++ packages/backend/package.json | 3 +- packages/backend/src/i18n.ts | 14 +- .../backend/src/locales/en/notifications.json | 64 +++++ .../backend/src/locales/zh/notifications.json | 64 +++++ .../src/services/notification.service.ts | 228 ++++++++++++++---- 6 files changed, 509 insertions(+), 56 deletions(-) create mode 100644 packages/backend/src/locales/en/notifications.json create mode 100644 packages/backend/src/locales/zh/notifications.json diff --git a/package-lock.json b/package-lock.json index f6d7801..2508c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2734,6 +2734,95 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/copyfiles/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/copyfiles/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3142,6 +3231,16 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4972,6 +5071,44 @@ "node": ">=6.0.0" } }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "license": "ISC", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -6445,6 +6582,50 @@ "node": ">=8" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -6884,6 +7065,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/untyped": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", @@ -7372,6 +7563,7 @@ "@types/sqlite3": "^3.1.11", "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", + "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "ts-node-dev": "^2.0.0", "typescript": "^5.0.0" diff --git a/packages/backend/package.json b/packages/backend/package.json index 2f81a19..7bfda59 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,7 +4,7 @@ "private": true, "main": "dist/index.js", "scripts": { - "build": "tsc", + "build": "tsc && copyfiles -u 1 \"src/locales/**/*.json\" dist/src", "start": "node dist/index.js", "dev": "cross-env NODE_ENV=development npx ts-node-dev --respawn --transpile-only src/index.ts" }, @@ -44,6 +44,7 @@ "@types/sqlite3": "^3.1.11", "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", + "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "ts-node-dev": "^2.0.0", "typescript": "^5.0.0" diff --git a/packages/backend/src/i18n.ts b/packages/backend/src/i18n.ts index 38df675..4e5d3a0 100644 --- a/packages/backend/src/i18n.ts +++ b/packages/backend/src/i18n.ts @@ -9,10 +9,11 @@ export const defaultLng = 'en'; i18next .use(Backend) .init({ - // debug: process.env.NODE_ENV === 'development', // 可选:开发模式下开启调试 + debug: process.env.NODE_ENV === 'development', // Enable debug logging in dev supportedLngs, fallbackLng: defaultLng, - lng: defaultLng, // 默认语言 + // lng: defaultLng, // Remove explicit lng setting here, let it be determined later or by detector + preload: supportedLngs, // Preload all supported languages ns: ['notifications'], // 命名空间,用于组织翻译 defaultNS: 'notifications', backend: { @@ -20,8 +21,15 @@ i18next loadPath: path.join(__dirname, 'locales/{{lng}}/{{ns}}.json'), }, interpolation: { - escapeValue: false, // 不对插值进行转义,因为我们可能需要 HTML 或 Markdown + escapeValue: false, // Not needed for react apps }, + }, (err, t) => { // Add init callback + if (err) { + return console.error('[i18next] Error during initialization:', err); + } + console.log('[i18next] Initialization complete. Loaded languages:', Object.keys(i18next.store.data)); + // console.log('[i18next] Example translation (en):', t('testNotification.subject', { lng: 'en' })); // Optional test + // console.log('[i18next] Example translation (zh):', t('testNotification.subject', { lng: 'zh' })); // Optional test }); export default i18next; diff --git a/packages/backend/src/locales/en/notifications.json b/packages/backend/src/locales/en/notifications.json new file mode 100644 index 0000000..e1218e3 --- /dev/null +++ b/packages/backend/src/locales/en/notifications.json @@ -0,0 +1,64 @@ +{ + "testNotification": { + "subject": "Nexus Terminal Test Notification ({eventDisplay})", + "email": { + "body": "This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}", + "bodyHtml": "
This is a test email from Nexus Terminal for event '{{eventDisplay}}'.
If you received this, your SMTP configuration is working.
Timestamp: {{timestamp}}
" + }, + "webhook": { + "detailsMessage": "This is a test notification from Nexus Terminal (Webhook - i18n) for event '{{eventDisplay}}'." + }, + "telegram": { + "detailsMessage": "This is a test notification from Nexus Terminal (Telegram - i18n) for event '{{eventDisplay}}'.", + "bodyTemplate": "*Nexus Terminal Test Notification*\nEvent: `{eventDisplay}`\nTimestamp: {timestamp}\nDetails:\n```\n{details}\n```" + } + }, + "eventDisplay": { + "LOGIN_SUCCESS": "Login Success", + "LOGIN_FAILURE": "Login Failure", + "LOGOUT": "Logout", + "PASSWORD_CHANGED": "Password Changed", + "2FA_ENABLED": "2FA Enabled", + "2FA_DISABLED": "2FA Disabled", + "PASSKEY_REGISTERED": "Passkey Registered", + "PASSKEY_DELETED": "Passkey Deleted", + "CONNECTION_CREATED": "Connection Created", + "CONNECTION_UPDATED": "Connection Updated", + "CONNECTION_DELETED": "Connection Deleted", + "CONNECTION_TESTED": "Connection Tested", + "CONNECTIONS_IMPORTED": "Connections Imported", + "CONNECTIONS_EXPORTED": "Connections Exported", + "PROXY_CREATED": "Proxy Created", + "PROXY_UPDATED": "Proxy Updated", + "PROXY_DELETED": "Proxy Deleted", + "TAG_CREATED": "Tag Created", + "TAG_UPDATED": "Tag Updated", + "TAG_DELETED": "Tag Deleted", + "SETTINGS_UPDATED": "Settings Updated", + "IP_WHITELIST_UPDATED": "IP Whitelist Updated", + "NOTIFICATION_SETTING_CREATED": "Notification Setting Created", + "NOTIFICATION_SETTING_UPDATED": "Notification Setting Updated", + "NOTIFICATION_SETTING_DELETED": "Notification Setting Deleted", + "SFTP_ACTION": "SFTP Action", + "SSH_CONNECT_SUCCESS": "SSH Connection Successful", + "SSH_CONNECT_FAILURE": "SSH Connection Failed", + "SSH_SHELL_FAILURE": "SSH Shell Open Failed", + "SERVER_STARTED": "Server Started", + "SERVER_ERROR": "Server Error", + "DATABASE_MIGRATION": "Database Migration", + "ADMIN_SETUP_COMPLETE": "Initial Admin Setup Completed" + + }, + "eventBody": { + + "SETTINGS_UPDATED": "Event: {{eventDisplay}}\nTimestamp: {{timestamp}}\nDetails:\n{{details}}" + }, + "connection": { + "testSuccess": "Connection test successful for '{{name}}'!", + "testFailed": "Connection test failed for '{{name}}': {{error}}" + }, + "settings": { + "ipWhitelistUpdated": "IP Whitelist updated successfully.", + "updated": "Settings updated successfully." + } +} \ No newline at end of file diff --git a/packages/backend/src/locales/zh/notifications.json b/packages/backend/src/locales/zh/notifications.json new file mode 100644 index 0000000..b9c71d9 --- /dev/null +++ b/packages/backend/src/locales/zh/notifications.json @@ -0,0 +1,64 @@ +{ + "testNotification": { + "subject": "星枢终端测试通知 ({eventDisplay})", + "email": { + "body": "这是一封来自星枢终端关于事件 '{{eventDisplay}}' 的测试邮件。\n\n如果您收到此邮件,表示您的 SMTP 配置工作正常。\n\n时间戳: {{timestamp}}", + "bodyHtml": "这是一封来自 星枢终端 关于事件 '{{eventDisplay}}' 的测试邮件。
如果您收到此邮件,表示您的 SMTP 配置工作正常。
时间戳: {{timestamp}}
" + }, + "webhook": { + "detailsMessage": "这是一条来自星枢终端的测试通知 (Webhook - i18n),事件:'{{eventDisplay}}'。" + }, + "telegram": { + "detailsMessage": "这是一条来自星枢终端的测试通知 (Telegram - i18n),事件:'{{eventDisplay}}'。", + "bodyTemplate": "*星枢终端测试通知*\n事件: `{eventDisplay}`\n时间戳: {timestamp}\n详情:\n```\n{details}\n```" + } + }, + "eventDisplay": { + "LOGIN_SUCCESS": "登录成功", + "LOGIN_FAILURE": "登录失败", + "LOGOUT": "登出", + "PASSWORD_CHANGED": "密码已更改", + "2FA_ENABLED": "两步验证已启用", + "2FA_DISABLED": "两步验证已禁用", + "PASSKEY_REGISTERED": "通行密钥已注册", + "PASSKEY_DELETED": "通行密钥已删除", + "CONNECTION_CREATED": "连接已创建", + "CONNECTION_UPDATED": "连接已更新", + "CONNECTION_DELETED": "连接已删除", + "CONNECTION_TESTED": "连接已测试", + "CONNECTIONS_IMPORTED": "连接已导入", + "CONNECTIONS_EXPORTED": "连接已导出", + "PROXY_CREATED": "代理已创建", + "PROXY_UPDATED": "代理已更新", + "PROXY_DELETED": "代理已删除", + "TAG_CREATED": "标签已创建", + "TAG_UPDATED": "标签已更新", + "TAG_DELETED": "标签已删除", + "SETTINGS_UPDATED": "设置已更新", + "IP_WHITELIST_UPDATED": "IP 白名单已更新", + "NOTIFICATION_SETTING_CREATED": "通知设置已创建", + "NOTIFICATION_SETTING_UPDATED": "通知设置已更新", + "NOTIFICATION_SETTING_DELETED": "通知设置已删除", + "SFTP_ACTION": "SFTP 操作", + "SSH_CONNECT_SUCCESS": "SSH 连接成功", + "SSH_CONNECT_FAILURE": "SSH 连接失败", + "SSH_SHELL_FAILURE": "SSH Shell 打开失败", + "SERVER_STARTED": "服务器已启动", + "SERVER_ERROR": "服务器错误", + "DATABASE_MIGRATION": "数据库迁移", + "ADMIN_SETUP_COMPLETE": "初始管理员设置完成" + + }, + "eventBody": { + + "SETTINGS_UPDATED": "事件: {{eventDisplay}}\n时间戳: {{timestamp}}\n详情:\n{{details}}" + }, + "connection": { + "testSuccess": "连接 '{{name}}' 测试成功!", + "testFailed": "连接 '{{name}}' 测试失败: {{error}}" + }, + "settings": { + "ipWhitelistUpdated": "IP 白名单更新成功。", + "updated": "设置更新成功。" + } +} \ No newline at end of file diff --git a/packages/backend/src/services/notification.service.ts b/packages/backend/src/services/notification.service.ts index b228801..0adb836 100644 --- a/packages/backend/src/services/notification.service.ts +++ b/packages/backend/src/services/notification.service.ts @@ -14,6 +14,16 @@ import * as nodemailer from 'nodemailer'; import Mail from 'nodemailer/lib/mailer'; // Import Mail type for transporter import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import i18next instance and config import { settingsService } from './settings.service'; // Import settings service +// Removed logger import + + +// Define translation keys for test notifications for clarity +const testSubjectKey = 'testNotification.subject'; +const testEmailBodyKey = 'testNotification.email.body'; +const testEmailBodyHtmlKey = 'testNotification.email.bodyHtml'; // Separate key for HTML version +const testWebhookDetailsKey = 'testNotification.webhook.detailsMessage'; +const testTelegramDetailsKey = 'testNotification.telegram.detailsMessage'; +const testTelegramBodyTemplateKey = 'testNotification.telegram.bodyTemplate'; // Key for the template itself export class NotificationService { private repository: NotificationSettingsRepository; @@ -65,10 +75,25 @@ export class NotificationService { // Specific test method for Email private async _testEmailSetting(config: EmailConfig): Promise<{ success: boolean; message: string }> { + console.log('[Notification Test - Email] Starting test...'); // Added log if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { + console.error('[Notification Test - Email] Missing required config.'); // Added log return { success: false, message: '测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 主机, 端口, 发件人)。' }; } + // --- Fetch User Language --- + let userLang = defaultLng; + try { + const langSetting = await settingsService.getSetting('language'); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + console.log(`[Notification Test - Email] Using language: ${userLang}`); // Added log + } catch (error) { + console.error(`[Notification Test - Email] Error fetching language setting, using default (${defaultLng}):`, error); + } + // --- End Fetch User Language --- + // Let TypeScript infer the options type for SMTP const transporterOptions = { host: config.smtpHost, @@ -86,46 +111,72 @@ export class NotificationService { const transporter = nodemailer.createTransport(transporterOptions); + // Translate event display name first + const eventDisplayName = i18next.t(`eventDisplay.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' }); // Hardcoding event for test email + const mailOptions: Mail.Options = { from: config.from, to: config.to, // Use the 'to' from config for testing - subject: 'Nexus Terminal Test Notification', - text: `This is a test email from Nexus Terminal.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: ${new Date().toISOString()}`, - html: `This is a test email from Nexus Terminal.
If you received this, your SMTP configuration is working.
Timestamp: ${new Date().toISOString()}
`, + // Use i18next for subject and body, using fetched user language + subject: i18next.t(testSubjectKey, { lng: userLang, defaultValue: 'Nexus Terminal Test Notification ({eventDisplay})', eventDisplay: eventDisplayName }), + text: i18next.t(testEmailBodyKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, eventDisplay: eventDisplayName }), + html: i18next.t(testEmailBodyHtmlKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{eventDisplay}}'.
If you received this, your SMTP configuration is working.
Timestamp: {{timestamp}}
`, eventDisplay: eventDisplayName }), }; try { - console.log(`[Notification Test] Attempting to send test email via ${config.smtpHost}:${config.smtpPort} to ${config.to}`); + console.log(`[Notification Test - Email] Attempting to send test email via ${config.smtpHost}:${config.smtpPort} to ${config.to}`); // Updated log prefix const info = await transporter.sendMail(mailOptions); - console.log(`[Notification Test] Test email sent successfully: ${info.messageId}`); + console.log(`[Notification Test - Email] Test email sent successfully: ${info.messageId}`); // Updated log prefix // Verify connection if possible (optional) // await transporter.verify(); - // console.log('[Notification Test] SMTP Connection verified.'); + // console.log('[Notification Test - Email] SMTP Connection verified.'); return { success: true, message: '测试邮件发送成功!请检查收件箱。' }; } catch (error: any) { - console.error(`[Notification Test] Error sending test email:`, error); + console.error(`[Notification Test - Email] Error sending test email:`, error); // Updated log prefix return { success: false, message: `测试邮件发送失败: ${error.message || '未知错误'}` }; } } // Specific test method for Webhook private async _testWebhookSetting(config: WebhookConfig): Promise<{ success: boolean; message: string }> { + console.log('[Notification Test - Webhook] Starting test...'); // Added log if (!config.url) { + console.error('[Notification Test - Webhook] Missing URL.'); // Added log return { success: false, message: '测试 Webhook 失败:缺少 URL。' }; } + // --- Fetch User Language --- + let userLang = defaultLng; + try { + const langSetting = await settingsService.getSetting('language'); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + console.log(`[Notification Test - Webhook] Using language: ${userLang}`); // Added log + } catch (error) { + console.error(`[Notification Test - Webhook] Error fetching language setting, using default (${defaultLng}):`, error); + } + // --- End Fetch User Language --- + // Use a valid event type for the test payload const testPayload: NotificationPayload = { event: 'SETTINGS_UPDATED', // Use a valid event type timestamp: Date.now(), - details: { message: 'This is a test notification from Nexus Terminal (Webhook).' } // Add channel type + // Use i18next for the details message, using fetched user language + details: { message: i18next.t(testWebhookDetailsKey, { lng: userLang, defaultValue: 'This is a test notification from Nexus Terminal (Webhook).' }) } }; + // Log the translated message safely + const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property'; + console.log(`[Notification Test - Webhook] Test payload created. Translated details.message:`, translatedWebhookMessage); // Added log with type check // Use the same rendering logic as actual sending + // Translate event display name + const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event }); // Default body for webhook test, using single braces const defaultBody = JSON.stringify(testPayload, null, 2); - const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; // Default template text - const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, testPayload, defaultBody); // Use default template if user input is empty + const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; // Updated default template text + // Pass eventDisplayName to renderTemplate + const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, testPayload, defaultBody, eventDisplayName); const requestConfig: AxiosRequestConfig = { method: config.method || 'POST', @@ -139,55 +190,103 @@ export class NotificationService { }; try { - console.log(`[Notification Test] Sending test Webhook to ${config.url}`); + console.log(`[Notification Test - Webhook] Sending test Webhook to ${config.url}`); // Updated log prefix const response = await axios(requestConfig); - console.log(`[Notification Test] Test Webhook sent successfully to ${config.url}. Status: ${response.status}`); + console.log(`[Notification Test - Webhook] Test Webhook sent successfully to ${config.url}. Status: ${response.status}`); // Updated log prefix return { success: true, message: `测试 Webhook 发送成功 (状态码: ${response.status})。` }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.response?.data || error.message || '未知错误'; - console.error(`[Notification Test] Error sending test Webhook to ${config.url}:`, errorMessage); + console.error(`[Notification Test - Webhook] Error sending test Webhook to ${config.url}:`, errorMessage); // Updated log prefix return { success: false, message: `测试 Webhook 发送失败: ${errorMessage}` }; } } // Specific test method for Telegram private async _testTelegramSetting(config: TelegramConfig): Promise<{ success: boolean; message: string }> { + console.log('[Notification Test - Telegram] Starting test...'); if (!config.botToken || !config.chatId) { + console.error('[Notification Test - Telegram] Missing botToken or chatId.'); return { success: false, message: '测试 Telegram 失败:缺少机器人 Token 或聊天 ID。' }; } + // --- Fetch User Language --- + let userLang = defaultLng; + try { + const langSetting = await settingsService.getSetting('language'); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + console.log(`[Notification Test - Telegram] Using language: ${userLang}`); // Added log + } catch (error) { + console.error(`[Notification Test - Telegram] Error fetching language setting, using default (${defaultLng}):`, error); + } + // --- End Fetch User Language --- + // Use a valid event type for the test payload - const testPayload: NotificationPayload = { - event: 'SETTINGS_UPDATED', // Use a valid event type + // Declare payload first, details will be added after translation + const testPayload: NotificationPayload = { + event: 'SETTINGS_UPDATED', timestamp: Date.now(), - details: { message: 'This is a test notification from Nexus Terminal (Telegram).' } // Add channel type + details: undefined // Initialize details as undefined }; + // --- Translation Start --- + // Log options before calling t() for details message + const detailsOptions = { lng: userLang, defaultValue: 'Fallback: This is a test notification from Nexus Terminal (Telegram).' }; // Use userLang + const keyWithNamespace = `notifications:${testTelegramDetailsKey}`; // Explicitly add namespace + // console.log(`[Notification Test - Telegram] Calling i18next.t for key '${keyWithNamespace}' with options:`, detailsOptions); + const translatedDetailsMessage = i18next.t(keyWithNamespace, detailsOptions); // Use key with namespace + // console.log(`[Notification Test - Telegram] Result from i18next.t for key '${keyWithNamespace}':`, translatedDetailsMessage); + // --- Translation End --- + + // Assign the translated details to the existing payload object + testPayload.details = { message: translatedDetailsMessage }; + + + // Log the translated message safely + const messageFromPayload = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property'; + console.log(`[Notification Test - Telegram] Test payload created. Final details.message in payload:`, messageFromPayload); // Updated log description + // Use the same rendering logic as actual sending - // Default message for Telegram test, using single braces and avoiding Markdown issues - const defaultMessageTemplate = `Nexus Terminal Test Notification\nEvent: {event}\nTimestamp: {timestamp}\nDetails:\n{details}`; - const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplate, testPayload, ''); // Render template, default is now the template itself + // Get the default template from i18n, fallback to a hardcoded default if key not found + // Also explicitly specify namespace and use userLang for the template key + const templateKeyWithNamespace = `notifications:${testTelegramBodyTemplateKey}`; + const defaultMessageTemplateFromI18n = i18next.t(templateKeyWithNamespace, { + lng: userLang, // Use userLang + defaultValue: `Fallback Template: *Nexus Terminal Test Notification*\nEvent: \`{event}\`\nTimestamp: {timestamp}\nDetails:\n\`\`\`\n{details}\n\`\`\`` // Added Fallback prefix + }); + console.log(`[Notification Test - Telegram] Default template from i18n (using lang '${userLang}', key '${templateKeyWithNamespace}'):`, defaultMessageTemplateFromI18n); // Updated log + + // Determine which template to use (user's or default from i18n) + const templateToUse = config.messageTemplate || defaultMessageTemplateFromI18n; + console.log(`[Notification Test - Telegram] Template to render:`, templateToUse); // Added log + + // Translate event display name + const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event }); + // Render the template, passing eventDisplayName + const messageText = this._renderTemplate(templateToUse, testPayload, '', eventDisplayName); + console.log(`[Notification Test - Telegram] Rendered message text:`, messageText); const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; try { - console.log(`[Notification Test] Sending test Telegram message to chat ID ${config.chatId}`); + console.log(`[Notification Test - Telegram] Sending test Telegram message to chat ID ${config.chatId}`); // Updated log prefix const response = await axios.post(telegramApiUrl, { chat_id: config.chatId, text: messageText, - // No parse_mode for testing to avoid issues with braces + parse_mode: 'Markdown' // Add parse_mode for testing consistency }, { timeout: 15000 }); // Slightly longer timeout for testing if (response.data?.ok) { - console.log(`[Notification Test] Test Telegram message sent successfully.`); - return { success: true, message: '测试 Telegram 消息发送成功!' }; - } else { - console.error(`[Notification Test] Telegram API returned error:`, response.data?.description); - return { success: false, message: `测试 Telegram 发送失败: ${response.data?.description || 'API 返回失败'}` }; - } - } catch (error: any) { - const errorMessage = error.response?.data?.description || error.response?.data || error.message || '未知错误'; - console.error(`[Notification Test] Error sending test Telegram message:`, errorMessage); + console.log(`[Notification Test - Telegram] Test Telegram message sent successfully.`); // Updated log prefix + return { success: true, message: '测试 Telegram 消息发送成功!' }; + } else { + console.error(`[Notification Test - Telegram] Telegram API returned error:`, response.data?.description); // Updated log prefix + return { success: false, message: `测试 Telegram 发送失败: ${response.data?.description || 'API 返回失败'}` }; + } + } catch (error: any) { + const errorMessage = error.response?.data?.description || error.response?.data || error.message || '未知错误'; + console.error(`[Notification Test - Telegram] Error sending test Telegram message:`, errorMessage); // Updated log prefix return { success: false, message: `测试 Telegram 发送失败: ${errorMessage}` }; } } @@ -253,11 +352,13 @@ export class NotificationService { // --- Private Sending Helpers --- - private _renderTemplate(template: string | undefined, payload: NotificationPayload, defaultText: string): string { + // Updated to accept eventDisplayName + private _renderTemplate(template: string | undefined, payload: NotificationPayload, defaultText: string, eventDisplayName?: string): string { if (!template) return defaultText; let rendered = template; // Replace single-brace placeholders - rendered = rendered.replace(/\{event\}/g, payload.event); + rendered = rendered.replace(/\{event\}/g, payload.event); // Keep original event code if needed + rendered = rendered.replace(/\{eventDisplay\}/g, eventDisplayName || payload.event); // Use translated name, fallback to original code rendered = rendered.replace(/\{timestamp\}/g, new Date(payload.timestamp).toISOString()); const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2); rendered = rendered.replace(/\{details\}/g, detailsString); @@ -272,16 +373,19 @@ export class NotificationService { return; } + // Translate event display name + const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event }); + // Translate payload details if they match a known key structure const translatedDetails = this._translatePayloadDetails(payload.details, userLang); - const translatedPayload = { ...payload, details: translatedDetails }; + const translatedPayload = { ...payload, details: translatedDetails }; // Keep original payload structure for details translation - const defaultBody = JSON.stringify(translatedPayload, null, 2); + const defaultBody = JSON.stringify(translatedPayload, null, 2); // Default body still uses the potentially translated details // Note: Webhook body templates might need adjustments if they expect specific structures - // or if they should also be translated. For now, we only translate the 'details'. // Use default template text if user hasn't provided one - const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; - const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, translatedPayload, defaultBody); + const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; // Updated placeholder + // Pass eventDisplayName to renderTemplate + const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, translatedPayload, defaultBody, eventDisplayName); const requestConfig: AxiosRequestConfig = { method: config.method || 'POST', @@ -335,20 +439,30 @@ export class NotificationService { i18nOptions.details = payload.details; // Pass non-object details directly if needed } + // Translate event display name first + const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event }); + // Try to translate the event itself for the subject, fallback to event name - const defaultSubjectKey = `event.${payload.event}`; - const defaultSubjectFallback = `星枢终端通知: {event}`; // Use single brace - const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback }); - // Use default subject template if user hasn't provided one - const defaultSubjectTemplate = `Notification: {event}`; - const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, payload, subjectText); + const defaultSubjectKey = `event.${payload.event}`; // This key might not exist, rely on template or default below + const defaultSubjectFallback = `Nexus Terminal Notification: {eventDisplay}`; // Use eventDisplay in fallback + const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); // Pass eventDisplay for interpolation in fallback + + // Use default subject template from i18n if user hasn't provided one + const defaultSubjectTemplateKey = 'testNotification.subject'; // Reuse test subject key structure + const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); + // Render the subject template, passing the translated event display name + const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, payload, subjectText, eventDisplayName); + // Translate the main body content based on event type if a key exists const bodyKey = `eventBody.${payload.event}`; const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2); - const defaultBodyText = `事件: ${payload.event}\n时间: ${new Date(payload.timestamp).toISOString()}\n详情:\n${detailsString}`; - const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText }); - // Note: Email body templates are not implemented in this version. Using translated/default text. + // Use eventDisplay in the default body text + const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${new Date(payload.timestamp).toISOString()}\nDetails:\n${detailsString}`; + // Pass eventDisplay for interpolation if the translation key uses it + const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText, eventDisplay: eventDisplayName }); + // Note: Email body templates are not implemented. Using translated/default text. + // If templates were implemented, we'd use _renderTemplate here too. const mailOptions: Mail.Options = { from: config.from, @@ -383,13 +497,23 @@ export class NotificationService { } else if (payload.details !== undefined) { i18nOptions.details = payload.details; // Pass non-object details directly if needed } - const messageKey = `eventBody.${payload.event}`; // Use same key as email body for consistency - const detailsStr = payload.details ? `\n详情: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : ''; - const defaultMessageTemplate = `*Nexus Terminal Notification*\n\nEvent: \`{event}\`\nTimestamp: {timestamp}${detailsStr}`; // Use single brace - const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageTemplate }); + // Translate event display name first + const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - // Allow template override, use default template if user input is empty - const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplate, payload, translatedBody); + const messageKey = `eventBody.${payload.event}`; // Use same key as email body for consistency + const detailsStr = payload.details ? `\nDetails: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : ''; + // Use eventDisplay in the default message template fallback + const defaultMessageTemplateFallback = `*Nexus Terminal Notification*\n\nEvent: \`{eventDisplay}\`\nTimestamp: {timestamp}${detailsStr}`; + // Pass eventDisplay for interpolation if the translation key uses it + const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageTemplateFallback, eventDisplay: eventDisplayName }); + + // Get the default template from i18n (using the test key structure) + const defaultTemplateKey = `notifications:${testTelegramBodyTemplateKey}`; + const defaultMessageTemplateFromI18n = i18next.t(defaultTemplateKey, { lng: userLang, defaultValue: translatedBody, eventDisplay: eventDisplayName }); + + // Allow template override, use default template from i18n if user input is empty + // Pass eventDisplayName to renderTemplate + const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplateFromI18n, payload, translatedBody, eventDisplayName); const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; try {