From 0cff6224e0d2fc768eda876e116248f8f92b5cfc Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Thu, 6 Aug 2026 10:50:28 +0900 Subject: [PATCH 01/49] fix: add missing MESSAGE_CLEAR_RADIO_SUCCESS key and refactor ClearCommand --- src/commands/ClearCommand.ts | 2 +- src/locales/en-US/commands.json | 1 + src/locales/ko-KR/commands.json | 1 + src/locales/ru-RU/commands.json | 1 + src/locales/sr-RS/commands.json | 1 + src/locales/tr-TR/commands.json | 1 + src/locales/zh-TW/commands.json | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/ClearCommand.ts b/src/commands/ClearCommand.ts index c48ea4a4..3f971b81 100644 --- a/src/commands/ClearCommand.ts +++ b/src/commands/ClearCommand.ts @@ -48,7 +48,7 @@ export class ClearCommand extends BaseCommand { } else { await context.replySuccess(bot, isRadioPlaying - ? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS', { defaultValue: '🧹 Cleared queue and stopped radio playback.' }) + ? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS') : context.t('commands:MESSAGE_CLEAR_SUCCESS')); } } diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index bdbc9372..413dcb6c 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -100,6 +100,7 @@ "ERROR_PLAY_SEARCH": "❌ No results found. ({{reason}})", "ERROR_TIME_EXPIRED": "❌ Time expired.", "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared.", + "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 Cleared queue and stopped radio playback.", "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated.", "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs.", "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs.", diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index 313986a0..85e70d3a 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -100,6 +100,7 @@ "ERROR_PLAY_SEARCH": "❌ 검색결과가 없습니다. 이유: ({{reason}})", "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다.", + "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 재생목록을 초기화하고 라디오 재생을 중지했습니다.", "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다.", "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다.", "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다.", diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 029ac5b5..17ccf2ae 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -100,6 +100,7 @@ "ERROR_PLAY_SEARCH": "❌ Ничего не найдено. ({{reason}})", "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена.", + "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 Очередь очищена, радио остановлено.", "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена.", "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев.", "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев.", diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index adf704d0..9ee58ffc 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -100,6 +100,7 @@ "ERROR_PLAY_SEARCH": "❌ Nema rezultata. ({{reason}})", "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan.", + "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 Red je obrisan i radio je zaustavljen.", "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana.", "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve.", "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve.", diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 25d1177e..af742770 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -100,6 +100,7 @@ "ERROR_PLAY_SEARCH": "❌ Herhangi bir sonuç bulunamadı. ({{reason}})", "ERROR_TIME_EXPIRED": "❌ Süre doldu.", "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi.", + "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 Kuyruk temizlendi ve radyo yayını durduruldu.", "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi.", "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var.", "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir.", diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index 36ea4731..e8975194 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -100,6 +100,7 @@ "ERROR_PLAY_SEARCH": "❌ 找不到結果。({{reason}})", "ERROR_TIME_EXPIRED": "❌ 時間已過期。", "MESSAGE_CLEAR_SUCCESS": "✅ 播放清單已清空。", + "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 已清空播放清單並停止電台播放。", "MESSAGE_DASHBOARD_SUCCESS": "✅ 控制面板已更新。", "MESSAGE_DJ_NO_PERMISSION": "您需要 DJ 或管理員權限才能添加 DJ。", "MESSAGE_DJ_ADMIN_ONLY": "只有管理員可以添加或移除 DJ。", From c5f6388239fecbdcddce9050d139dd240bfba839 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Thu, 6 Aug 2026 11:01:32 +0900 Subject: [PATCH 02/49] fix: resolve update race condition on skip and add embed text length safety checks --- src/lib/DashboardManager.ts | 12 ++++++++++-- src/lib/handlers/SkipButtonHandler.ts | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 91e25410..b14a9b9b 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -57,15 +57,23 @@ export class DashboardManager { } const lng = this.#bot.guildLanguageManager?.get(player.guildId); - const subtitle = await this.#buildSubtitle(player, track, lng); + let subtitle = await this.#buildSubtitle(player, track, lng); const buttons = ButtonsBuilder.createDashboardButtons(player, lng); + const safeTitle = track.title.length > 256 + ? track.title.substring(0, 253) + '...' + : track.title; + + if (subtitle.length > 1024) { + subtitle = subtitle.substring(0, 1021) + '...'; + } + try { await player.dashboardMsg.edit({ embeds: [embeds.dashboard( this.#bot, this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), - track.title, + safeTitle, subtitle, track.uri, track.thumbnail! diff --git a/src/lib/handlers/SkipButtonHandler.ts b/src/lib/handlers/SkipButtonHandler.ts index 630ba825..1754a2e1 100644 --- a/src/lib/handlers/SkipButtonHandler.ts +++ b/src/lib/handlers/SkipButtonHandler.ts @@ -1,7 +1,6 @@ import { RepeatMode } from 'lavashark'; import { MessageFlags } from 'discord.js'; import { DashboardButtonHandler } from './DashboardButtonHandler.js'; -import { ButtonsBuilder } from '../builders/ButtonsBuilder.js'; import { PermissionManager } from '../PermissionManager.js'; import { embeds } from '../../embeds/index.js'; @@ -73,7 +72,8 @@ export class SkipButtonHandler extends DashboardButtonHandler { } catch (_) {} } - const row = ButtonsBuilder.createDashboardButtons(player, lng); - await interaction.update({ components: [row] }); + if (!interaction.replied && !interaction.deferred) { + await interaction.deferUpdate(); + } } } From a18989c87adab999799bec8aff640839281ea042 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Thu, 6 Aug 2026 11:11:05 +0900 Subject: [PATCH 03/49] fix: prevent negative bot ping display using Math.abs --- src/commands/PingCommand.ts | 2 +- src/commands/StatusCommand.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/PingCommand.ts b/src/commands/PingCommand.ts index 2ec80b7c..7a3ed156 100644 --- a/src/commands/PingCommand.ts +++ b/src/commands/PingCommand.ts @@ -28,7 +28,7 @@ export class PingCommand extends BaseCommand { } protected async run(bot: Bot, client: Client, context: CommandContext): Promise { - const botPing = `${Date.now() - context.createdTimestamp}ms`; + const botPing = `${Math.abs(Date.now() - context.createdTimestamp)}ms`; const apiPing = client.ws.ping.toString(); await context.react('👍'); diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index fb6c03ce..2a8c24b9 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -28,7 +28,7 @@ export class StatusCommand extends BaseCommand { } protected async run(bot: Bot, client: Client, context: CommandContext): Promise { - const botPing = `${Date.now() - context.createdTimestamp}ms`; + const botPing = `${Math.abs(Date.now() - context.createdTimestamp)}ms`; const sysload = await sysusage.cpu(); const pingList = await client.lavashark.nodesPing(); From 3ff87a2e150daac58edaac5e548cde64c23e05f1 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Tue, 11 Aug 2026 11:06:31 +0900 Subject: [PATCH 04/49] fix: make playlist lookup case-insensitive, fix playfirst for playlists, and update dashboard on skip --- package-lock.json | 157 +++++++++--------- package.json | 10 +- src/commands/PlayFirstCommand.ts | 33 ++-- src/commands/PlaylistCommand.ts | 25 ++- src/commands/SkipCommand.ts | 10 ++ .../playlist/ShowPlaylistInfoSubcommand.ts | 6 +- src/lib/PlaylistManager.ts | 4 +- src/lib/handlers/SkipButtonHandler.ts | 8 +- 8 files changed, 127 insertions(+), 126 deletions(-) diff --git a/package-lock.json b/package-lock.json index 410b848c..93ad6fee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "bcryptjs": "^3.0.3", - "better-sqlite3": "^13.0.2", + "better-sqlite3": "^13.0.3", "cookie": "^2.0.1", "discord.js": "~14.27.0", "dotenv": "^17.4.2", @@ -25,12 +25,12 @@ "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.6", - "@types/node": "^26.1.2", - "eslint": "^10.8.0", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", "globals": "^17.9.0", - "tsx": "^4.23.5", + "tsx": "^4.23.12", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0" + "typescript-eslint": "^8.67.0" }, "engines": { "node": ">=22.23.2" @@ -941,9 +941,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -994,17 +994,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1017,7 +1017,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1033,16 +1033,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1058,14 +1058,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1080,14 +1080,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1098,9 +1098,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -1115,15 +1115,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1140,9 +1140,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -1154,16 +1154,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1182,16 +1182,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1206,13 +1206,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1306,10 +1306,9 @@ } }, "node_modules/better-sqlite3": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.2.tgz", - "integrity": "sha512-jW6oufeDhXZaiX9Lw5A+oerVClx4iFrI6uDj1zu7SqUAjak9vbJvA0NEcKLNxHiQHb6kYCoFzzXYV0YOauhV3g==", - "hasInstallScript": true, + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", "license": "MIT", "dependencies": { "node-addon-api": "^8.0.0" @@ -1676,9 +1675,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -2939,9 +2938,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz", - "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3016,16 +3015,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", - "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.66.0", - "@typescript-eslint/parser": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0" + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index cf627ef7..d78a2829 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "bcryptjs": "^3.0.3", - "better-sqlite3": "^13.0.2", + "better-sqlite3": "^13.0.3", "cookie": "^2.0.1", "discord.js": "~14.27.0", "dotenv": "^17.4.2", @@ -48,12 +48,12 @@ "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.6", - "@types/node": "^26.1.2", - "eslint": "^10.8.0", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", "globals": "^17.9.0", - "tsx": "^4.23.5", + "tsx": "^4.23.12", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0" + "typescript-eslint": "^8.67.0" }, "engines": { "node": ">=22.23.2" diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 2b3cb14d..3f01360c 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -244,26 +244,23 @@ export class PlayFirstCommand extends BaseCommand { (t as any).requester = requester; } - if (!player.playing) { - const firstTrack = tracks[0]; - const restTracks = tracks.slice(1); + const firstTrack = tracks[0]; + const restTracks = tracks.slice(1); - if (restTracks.length > 0) { - player.queue.tracks.unshift(...restTracks); - } + if (restTracks.length > 0) { + player.queue.tracks.unshift(...restTracks); + } - player.filters.setVolume(curVolume); - await player.prioritizePlay(firstTrack, requester as any) - .catch(async (error) => { - bot.logger.error(bot.shardId, 'Error playing track: ' + error); - await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); - return player.destroy(); - }); - } else { - player.queue.tracks.unshift(...tracks); - if (player.current && client.dashboard) { - await client.dashboard.update(player, player.current); - } + player.filters.setVolume(curVolume); + await player.prioritizePlay(firstTrack, requester as any) + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + + if (player.current && client.dashboard) { + await client.dashboard.update(player, player.current); } } else { diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 1d161616..5d86929f 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -66,6 +66,7 @@ export class PlaylistCommand extends BaseCommand { this.createNamedSubcommand( 'info', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO', + false, ), this.createNamedSubcommand( 'delete', @@ -133,17 +134,12 @@ export class PlaylistCommand extends BaseCommand { return; } - // Normalize the subcommand name from either command source - const name = command.isMessage() - ? command.args[0]?.toLowerCase() - : command.getInteraction().options.getSubcommand(false); - if (!name) { - await command.replyEphemeralError( - bot, - command.t('commands:ERROR_PLAYLIST_SUBCOMMAND_INVALID'), - ); - return; - } + // Normalize the subcommand name from either command source (default to 'list') + const name = ( + command.isMessage() + ? command.args[0]?.toLowerCase() + : command.getInteraction().options.getSubcommand(false) + ) || 'list'; // Find the matching OOP subcommand implementation const subcommand = this.#subcommands.find( @@ -166,11 +162,12 @@ export class PlaylistCommand extends BaseCommand { private createNamedSubcommand( name: PlaylistSubcommandName, descriptionKey: string, + nameRequired: boolean = true, ): ApplicationCommandSubCommandData { return { description: i18next.t(descriptionKey), name, - options: [this.createNameOption()], + options: [this.createNameOption(nameRequired)], type: ApplicationCommandOptionType.Subcommand, }; } @@ -178,11 +175,11 @@ export class PlaylistCommand extends BaseCommand { /** * Create the shared playlist name option */ - private createNameOption(): ApplicationCommandStringOptionData { + private createNameOption(required: boolean = true): ApplicationCommandStringOptionData { return { description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME'), name: 'name', - required: true, + required, type: ApplicationCommandOptionType.String, }; } diff --git a/src/commands/SkipCommand.ts b/src/commands/SkipCommand.ts index e0b2350e..cd284c45 100644 --- a/src/commands/SkipCommand.ts +++ b/src/commands/SkipCommand.ts @@ -1,4 +1,5 @@ import i18next from 'i18next'; +import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory } from '../@types/index.js'; @@ -61,8 +62,17 @@ export class SkipCommand extends BaseCommand { } const hasMoreTracks = player.queue.tracks.length > 0 || player.repeatMode !== 0; + + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + const success = await player.skip(); + if (success && player.current && client.dashboard) { + await client.dashboard.update(player, player.current); + } + if (context.isMessage()) { await context.react(success ? '👍' : '❌'); if (!hasMoreTracks && context.channel && 'send' in context.channel) { diff --git a/src/commands/playlist/ShowPlaylistInfoSubcommand.ts b/src/commands/playlist/ShowPlaylistInfoSubcommand.ts index 3268108f..7a66e996 100644 --- a/src/commands/playlist/ShowPlaylistInfoSubcommand.ts +++ b/src/commands/playlist/ShowPlaylistInfoSubcommand.ts @@ -4,6 +4,7 @@ import { PlaylistButtonId } from '../../@types/index.js'; import { embeds } from '../../embeds/index.js'; import { PLAYLIST_INFO_PAGE_SIZE } from '../../embeds/playlist.embed.js'; import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; +import { ListPlaylistsSubcommand } from './ListPlaylistsSubcommand.js'; import type { PlaylistSubcommandContext, @@ -25,10 +26,7 @@ export class ShowPlaylistInfoSubcommand extends BasePlaylistSubcommand { public async execute(context: PlaylistSubcommandContext): Promise { const name = this.getPlaylistName(context.command); if (!name) { - await context.command.replyEphemeralError( - context.bot, - context.command.t('commands:ERROR_PLAYLIST_NAME_REQUIRED'), - ); + await new ListPlaylistsSubcommand().execute(context); return; } diff --git a/src/lib/PlaylistManager.ts b/src/lib/PlaylistManager.ts index d2b077ac..66697e94 100644 --- a/src/lib/PlaylistManager.ts +++ b/src/lib/PlaylistManager.ts @@ -63,7 +63,7 @@ export class PlaylistManager { const playlistRow = db.prepare(` SELECT id, guild_id as guildId, name, created_at as createdAt FROM playlists - WHERE guild_id = ? AND name = ? + WHERE guild_id = ? AND name = ? COLLATE NOCASE `).get(guildId, name) as Playlist | undefined; if (!playlistRow) return null; @@ -145,7 +145,7 @@ export class PlaylistManager { `); const replacePlaylist = db.transaction(() => { - db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ?').run(guildId, name); + db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ? COLLATE NOCASE').run(guildId, name); const playlistInfo = db.prepare(` INSERT INTO playlists (guild_id, name, created_at) diff --git a/src/lib/handlers/SkipButtonHandler.ts b/src/lib/handlers/SkipButtonHandler.ts index 1754a2e1..4c491a3c 100644 --- a/src/lib/handlers/SkipButtonHandler.ts +++ b/src/lib/handlers/SkipButtonHandler.ts @@ -56,12 +56,10 @@ export class SkipButtonHandler extends DashboardButtonHandler { if (player.repeatMode === RepeatMode.TRACK) { player.setRepeatMode(RepeatMode.OFF); - await player.skip(); - player.setRepeatMode(RepeatMode.TRACK); - } else { - await player.skip(); } + await player.skip(); + if (!hasMoreTracks) { try { await interaction.reply({ @@ -70,6 +68,8 @@ export class SkipButtonHandler extends DashboardButtonHandler { }); return; } catch (_) {} + } else if (player.current && client.dashboard) { + await client.dashboard.update(player, player.current); } if (!interaction.replied && !interaction.deferred) { From a6e184dad570616676ce2814dea253f70e2ba30b Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 09:34:58 +0900 Subject: [PATCH 05/49] i18n proofreads - #125 --- src/locales/en-US/commands.json | 146 ++++++++++++++++---------------- src/locales/en-US/embeds.json | 18 ++-- src/locales/en-US/events.json | 8 +- src/locales/ko-KR/commands.json | 38 ++++----- src/locales/ko-KR/embeds.json | 16 ++-- src/locales/ko-KR/events.json | 6 +- src/locales/ru-RU/commands.json | 105 ++++++++++++----------- src/locales/ru-RU/embeds.json | 8 +- src/locales/ru-RU/events.json | 8 +- src/locales/sr-RS/commands.json | 118 +++++++++++++------------- src/locales/sr-RS/embeds.json | 4 +- src/locales/sr-RS/events.json | 4 +- src/locales/tr-TR/commands.json | 96 ++++++++++----------- src/locales/tr-TR/embeds.json | 32 +++---- src/locales/tr-TR/events.json | 10 +-- src/locales/zh-TW/commands.json | 38 ++++----- src/locales/zh-TW/embeds.json | 10 +-- src/locales/zh-TW/events.json | 12 +-- src/utils/languageUtils.ts | 2 +- 19 files changed, 339 insertions(+), 340 deletions(-) diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index c78aaf74..d6ef0ed3 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -9,10 +9,10 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist.", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist.", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not in the dynamic blacklist.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dynamic Blacklist", - "MESSAGE_BLACKLIST_LIST_EMPTY": "The dynamic blacklist is empty.", - "CONFIG_CLEAR_DESCRIPTION": "Clear all the music in the queue", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not on the blacklist.", + "MESSAGE_BLACKLIST_LIST_TITLE": "Current Blacklist", + "MESSAGE_BLACKLIST_LIST_EMPTY": "The blacklist is currently empty.", + "CONFIG_CLEAR_DESCRIPTION": "Clear the music queue", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Move the dashboard to the bottom", "CONFIG_DASHBOARD_USAGE": "dashboard", @@ -21,25 +21,25 @@ "CONFIG_DJ_OPTION_DESCRIPTION": "The user to add as DJ (optional)", "CONFIG_FILTER_DESCRIPTION": "Set music filter", "CONFIG_FILTER_USAGE": "filter [effect name]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "Select the music filter mode you want to set", - "CONFIG_HELP_DESCRIPTION": "Get commands help", + "CONFIG_FILTER_OPTION_DESCRIPTION": "Select the music filter to apply", + "CONFIG_HELP_DESCRIPTION": "Get help with commands", "CONFIG_HELP_USAGE": "help [command]", - "CONFIG_HELP_OPTION_DESCRIPTION": "which command need help", + "CONFIG_HELP_OPTION_DESCRIPTION": "The command you need help with", "CONFIG_JOIN_DESCRIPTION": "Join current voice channel", "CONFIG_JOIN_USAGE": "join", - "CONFIG_LANG_DESCRIPTION": "Change the Bot locale", - "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Locale name", + "CONFIG_LANG_DESCRIPTION": "Change the bot's language", + "CONFIG_LANG_USAGE": "language ", + "CONFIG_LANG_OPTION_DESCRIPTION": "Select a locale", "CONFIG_LEAVE_DESCRIPTION": "Leave current voice channel", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "Turns the music loop mode on or off", + "CONFIG_LOOP_DESCRIPTION": "Turn the music loop mode on or off", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Music loop mode", "CONFIG_MOVE_DESCRIPTION": "Swap the positions of two songs in the queue", "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "To swap song position 1", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "To swap song position 2", - "CONFIG_NODE_DESCRIPTION": "Show nodes active status", + "CONFIG_MOVE_OPTION_DESCRIPTION": "Track position 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Track position 2", + "CONFIG_NODE_DESCRIPTION": "Show the status of active nodes", "CONFIG_NODE_USAGE": "node [node name]", "CONFIG_NODE_OPTION_DESCRIPTION": "The node name", "CONFIG_NOW_PLAYING_DESCRIPTION": "Show currently playing music", @@ -48,40 +48,40 @@ "CONFIG_PAUSE_USAGE": "pause", "CONFIG_PING_DESCRIPTION": "Get server ping", "CONFIG_PING_USAGE": "ping", - "CONFIG_PLAY_DESCRIPTION": "Enter your song link or song name to play", + "CONFIG_PLAY_DESCRIPTION": "Enter a song link or name to play", "CONFIG_PLAY_USAGE": "play ", "CONFIG_PLAY_OPTION_DESCRIPTION": "The song link or song name", - "CONFIG_PLAYFIRST_DESCRIPTION": "Enter your song link or song name to play first", + "CONFIG_PLAYFIRST_DESCRIPTION": "Enter a song link or name to play first", "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "The song link or song name to play first", "CONFIG_PLAYLAST_DESCRIPTION": "Replay the current or last played song", "CONFIG_PLAYLAST_USAGE": "playlast", "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty.", "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Re-queued the song.", + "MESSAGE_PLAYLAST_SUCCESS": "✅ The track is back in the queue.", "CONFIG_QUEUE_DESCRIPTION": "Show current playlist", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Select a song to remove from the playlist", - "CONFIG_REMOVE_USAGE": "remove [from index to index 2 track]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "track index number", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "from index to index 2 track", + "CONFIG_REMOVE_USAGE": "remove [from to]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Track number", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Track range", "CONFIG_RESUME_DESCRIPTION": "Resume paused track", "CONFIG_RESUME_USAGE": "resume", - "CONFIG_SEARCH_DESCRIPTION": "Enter song name to search", + "CONFIG_SEARCH_DESCRIPTION": "Enter a song name to search", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "The song name", "CONFIG_SEEK_DESCRIPTION": "Seeks to a certain time in the track", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "traget time (ex: 3m20s, 1:20:55)", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Target time (ex: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Show currently active servers", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Shuffle playlist", "CONFIG_SHUFFLE_USAGE": "shuffle", - "CONFIG_SKIP_DESCRIPTION": "Skip currnet track", + "CONFIG_SKIP_DESCRIPTION": "Skip current track", "CONFIG_SKIP_USAGE": "skip", "CONFIG_STATUS_DESCRIPTION": "Show the bot status", "CONFIG_STATUS_USAGE": "status", - "CONFIG_STOP_DESCRIPTION": "Stop the currently playing music and clear the queue, but don't leave the channel", + "CONFIG_STOP_DESCRIPTION": "Stop playback and clear the queue without leaving the channel", "CONFIG_STOP_USAGE": "stop", "CONFIG_VOLUME_DESCRIPTION": "Change bot volume", "CONFIG_VOLUME_USAGE": "v <0-100>", @@ -90,25 +90,25 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [volume]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "The default volume to set (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Command error. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ There is currently no music to be play in the queue.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ There are no tracks in the queue right now.", "ERROR_NO_PLAYING": "❌ There is no music currently playing.", "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if enabled) can also skip.", "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if enabled) can also seek.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if enabled) can also pause.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join voice channel.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join the voice channel.", "ERROR_PLAY_MUSIC": "❌ The service is experiencing some problems, please try again. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ No results found. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Time expired.", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired.", "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared.", "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated.", "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs.", "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs.", "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ.", "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ.", - "MESSAGE_DJ_STATIC_MODE": "DJ permissions are managed through configuration in STATIC mode.", - "MESSAGE_DJ_NO_PLAYER": "No active music player found.", + "MESSAGE_DJ_STATIC_MODE": "In STATIC mode, DJ permissions can only be set through the config file.", + "MESSAGE_DJ_NO_PLAYER": "There's no player currently playing music.", "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ.", - "MESSAGE_DJ_SUCCESS": "Successfully added <@{{userId}}> as DJ.", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> has been added as a DJ.", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ left the channel! Auto-selected new DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ List", "MESSAGE_DJ_LIST_ADMINS": "Admins", @@ -116,18 +116,18 @@ "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dynamic DJs", "MESSAGE_DJ_LIST_NONE": "No DJs configured.", "MESSAGE_DJ_LIST_ERROR": "❌ Failed to load DJ list. Please try again.", - "MESSAGE_DJ_ROLE_NOT_SET": "No DJ role configured", - "MESSAGE_FILTER_NOT_FOUND": "❌ The effect name not found.", - "MESSAGE_FILTER_SELECT_LIST": "Select the music filter mode you want to set. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Select filter mode", - "MESSAGE_HELP_NOT_FOUND": "❌ The command not found.", + "MESSAGE_DJ_ROLE_NOT_SET": "No one has been assigneed to a permanent DJ role.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Effect not found.", + "MESSAGE_FILTER_SELECT_LIST": "Select a music filter: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Select an effect", + "MESSAGE_HELP_NOT_FOUND": "❌ Command not found.", "MESSAGE_HELP_SELECT_LIST": "Choose a command to get help. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Select the help", - "MESSAGE_JOIN_SUCCESS": "✅ Bot Joined.", - "MESSAGE_LANG_ARGS_ERROR": "The locale parameter is wrong, currently available languages: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Currently available languages: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Changed locale to `{{locale}}` successfully", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot left.", + "MESSAGE_HELP_SELECT_MODE": "Select a command", + "MESSAGE_JOIN_SUCCESS": "✅ Joined the voice channel.", + "MESSAGE_LANG_ARGS_ERROR": "Invalid locale code. Available languages: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Available languages: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Locale changed to `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Left the voice channel.", "REPEAT_MODE_OFF": "OFF", "REPEAT_MODE_SINGLE": "SINGLE", "REPEAT_MODE_ALL": "ALL", @@ -136,43 +136,43 @@ "LOOP_CHOICE_ALL": "All", "LOOP_SELECT_PLACEHOLDER": "Select the loop mode", "LOOP_SELECT_DESCRIPTION": "Set loop mode to {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Set loop to `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ The song order was swapped successfully.", - "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter a valid index range. (1 - {{max}})", + "MESSAGE_LOOP_MODE": "✅ Loop mode is now `{{mode}}`.", + "MESSAGE_MOVE_SUCCESS": "✅ Track order changed.", + "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter positions in a valid range. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nand {{length}} other songs", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Save Song", "MESSAGE_NOW_PLAYING_SUBTITLE": "Author: **{{author}}**\nDuration: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Now Playing: {{title}} | Requested by {{requester}}", "UNKNOWN_USER": "Unknown", - "MESSAGE_PAUSE_FAIL": "❌ Music pause failed.", + "MESSAGE_PAUSE_FAIL": "❌ Failed to pause the music.", "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused.", "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Write the name of the music you want to search.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Music added.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches.", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Please provide a song name or link.", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Track added to the queue.", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches found.", "MESSAGE_PLAY_SELECT_DURATION": "Duration: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Select the music", - "MESSAGE_REMOVE_CANCEL": "✅ Cancelled remove.", - "MESSAGE_REMOVE_FAIL": "❌ Music remove failed.", - "MESSAGE_REMOVE_INSTRUCTION": "Choose a song from **1** to **{{length}}** to **remove** or enter others to cancel selection. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ No music in queue after current.", - "MESSAGE_RESUME_FAIL": "❌ Music resume failed.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ The music has been resumed.", + "MESSAGE_REMOVE_CANCEL": "✅ Removal cancelled.", + "MESSAGE_REMOVE_FAIL": "❌ Failed to remove the track.", + "MESSAGE_REMOVE_INSTRUCTION": "Choose a track number from **1** to **{{length}}** to **remove** it, or enter any other number to cancel. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ The queue is empty after the current track.", + "MESSAGE_RESUME_FAIL": "❌ Failed to resume the music.", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ The music has been resumed.", "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed.", "MESSAGE_SEEK_ARGS_ERROR": "❌ Invalid format for the target time.\n(**`ex: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ The seek position is beyond the duration of this track. (`{{duration}}`)\nSkipping to the next song.", - "MESSAGE_SEEK_SUCCESS": "✅ Music has been seeked to `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Music shuffled.", - "MESSAGE_SKIP_FAIL": "❌ Music skip failed.", - "MESSAGE_SKIP_SUCCESS": "✅ Music skipped.", - "MESSAGE_STOP_SUCCESS": "✅ Music stopped.", - "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, with `1` to `{{maxVolume}}` Type a number between.**", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Type a number from `1` to `{{maxVolume}}` to change the volume .**", - "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\\n**Please select the volume you want to set:**", - "MESSAGE_VOLUME_SAME": "❌ The volume you want to change is already the current volume.", + "MESSAGE_SEEK_SKIP": "✅ The requested time (`{{duration}}`) is beyond the track's duration.\nSkipping to the next track.", + "MESSAGE_SEEK_SUCCESS": "✅ Seeked to `{{duration}}`.", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Playlist shuffled.", + "MESSAGE_SKIP_FAIL": "❌ Failed to skip the track.", + "MESSAGE_SKIP_SUCCESS": "✅ Track skipped.", + "MESSAGE_STOP_SUCCESS": "✅ Playback stopped and the queue cleared.", + "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, enter a number from `1` to `{{maxVolume}}`.**", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Enter a number from `1` to `{{maxVolume}}` to change the volume.**", + "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\n**Select the volume you want:**", + "MESSAGE_VOLUME_SAME": "❌ That volume is already set.", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", - "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume has been changed to **{{volume}}%** for this server! (Max: {{maxVolume}}%)", + "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume for this server changed to **{{volume}}%**! (Max: {{maxVolume}}%)", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`.", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume.", "ERROR_QUEUE_LIMIT_REACHED": "❌ You have reached your queue limit! You currently have **{{current}}** songs in the queue (limit: **{{limit}}**). Please wait for some songs to finish before adding more.", @@ -197,13 +197,13 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Blacklist manager is not initialized.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "The blacklist isn't loaded yet. Please try again later.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Select a Music command", "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Utility command", "HELP_COMMAND_ALIASES": "Aliases: {{aliases}}", "HELP_COMMAND_NONE": "None", "LABEL_FILTER_CLEAR": "Clear filters", - "CONFIG_PLAYLIST_DESCRIPTION": "Save, load, edit playlists or import them from M3U url.", + "CONFIG_PLAYLIST_DESCRIPTION": "Manage playlists: save, load, delete, and import from M3U.", "CONFIG_PLAYLIST_USAGE": "playlist ", "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist.", "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist.", @@ -216,7 +216,7 @@ "CONFIG_PLAYLIST_OPTION_SEARCH": "Specific track title to search in the playlist (Optional)", "CONFIG_PLAYLIST_OPTION_URL": "M3U file download link", "CONFIG_PLAYLIST_OPTION_INDEX": "Track index to remove (starts from 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlist manager is not initialized.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlists aren't available yet. Please try again later.", "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track).", "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand.", "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty.", @@ -224,14 +224,14 @@ "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist.", "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty.", "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download M3U file. (HTTP status: {{status}})", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download the M3U file (error code {{status}}).", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ The M3U download was blocked by the security policy. Use a public HTTPS URL up to {{maxSizeMiB}} MiB that responds within {{timeoutSeconds}} seconds.", "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks.", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Failed to parse and save M3U playlist. Check if it's formatted correctly.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Failed to remove track. Check if it is a valid index.", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`.", "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded. (Success: {{added}}, Skipped: {{skipped}})", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded: {{added}} added, {{skipped}} skipped.", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server.", "MESSAGE_PLAYLIST_LIST_TITLE": "Server Playlists", "MESSAGE_PLAYLIST_INFO_TITLE": "Playlist Info: {{name}} ({{count}} tracks)", @@ -246,7 +246,7 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ Usage: `{{prefix}}radio [playlist name] [channel name]` (e.g. `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name.", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Could not find a radio channel matching `{{channelQuery}}` in playlist `{{playlistName}}`.\nAvailable examples: {{samples}} etc.", - "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\n(e.g., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\nThe file must be an M3U playlist with EXTINF tags.", "ERROR_RADIO_NO_MATCH": "❌ Radio channel matching `{{query}}` not found.\nAvailable examples: {{samples}} etc.", "ERROR_RADIO_LOAD_FAILED": "❌ Error occurred during loading radio channel: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`).", @@ -266,4 +266,4 @@ "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty." -} \ No newline at end of file +} diff --git a/src/locales/en-US/embeds.json b/src/locales/en-US/embeds.json index 6dc4ff3d..648175c7 100644 --- a/src/locales/en-US/embeds.json +++ b/src/locales/en-US/embeds.json @@ -4,12 +4,12 @@ "DASHBOARD_SUBTITLE": "Author: **{{author}}**\nDuration: **{{duration}}**\n────────────────────\nVolume: `{{volume}}%`\nLoop: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Requester:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Dynamic DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Voice channel connected successfully.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Connected to the voice channel.", "MAINTAIN_DESCRIPTION": "The bot will begin maintenance in a few minutes. During this time, some features may be temporarily unavailable.", "MAINTAIN_TITLE": "⚠️ Maintenance Notice ⚠️", - "MESSAGE_BLACKLIST": "There are blacklisted user(s) in the voice channel.", + "MESSAGE_BLACKLIST": "There are blacklisted users in the voice channel.", "MESSAGE_COMMAND": "Command **{{command}}**", - "MESSAGE_FILTER": "Set filter mode to **{{effectName}}**", + "MESSAGE_FILTER": "Filter set to: **{{effectName}}**", "NODE_ALL_ACTIVE": "All nodes are active", "NODE_CONNECTED": "Latency: **{{ping}}ms**", "NODE_DISCONNECTED": "**DISCONNECTED**", @@ -19,17 +19,17 @@ "NODE_STATUS_INFO_TITLE": "🏷️ INFO", "NODE_STATUS_INFO_VALUE": "Version: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nBuild time: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 MEMORY", - "NODE_STATUS_MEMORY_VALUE": "Used: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Used: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReserved: **{{reservable}}**\n", "NODE_STATUS_PING": "Latency", "NODE_STATUS_STATS_TITLE": "📊 STATS", - "NODE_STATUS_STATS_VALUE": "uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "Uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ Nodes Status", "NODE_STATUS_TITLE_2": "🛰️ Node '{{nodeName}}' status", "NODE_UNHEALTHY": "⚠️ There are {{unhealthValue}} nodes disconnected", "PING_DESCRIPTION": "Bot: **{{botPing}}**\nAPI: **{{apiPing}}ms**", - "PING_TITLE": "🛰️ LATENCY'", - "QUEUE_ADD_PLAYLIST": "Added Playlist", - "QUEUE_ADD_TRACK": "Added Track", + "PING_TITLE": "🛰️ LATENCY", + "QUEUE_ADD_PLAYLIST": "Playlist added", + "QUEUE_ADD_TRACK": "Track added", "QUEUE_LIST_LOOP_MODE": "Loop: {{repeatMode}}", "QUEUE_LIST_TITLE": "Queue List", "REMOVE_LIST_LOOP_MODE": "Loop: {{repeatMode}}", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Now Playing:**", "QUEUE_EMPTY": "*No songs in queue*", "QUEUE_HEADER": "**Queue:**" -} \ No newline at end of file +} diff --git a/src/locales/en-US/events.json b/src/locales/en-US/events.json index 4db7d42a..9be0f4cc 100644 --- a/src/locales/en-US/events.json +++ b/src/locales/en-US/events.json @@ -8,7 +8,7 @@ "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges.", "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message.", "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled.", - "ERROR_TIME_EXPIRED": "❌ Time expired.", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired.", "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left.", "MESSAGE_BOT_STOP": "✅ Bot stopped.", "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled.", @@ -19,7 +19,7 @@ "LOOP_MODE_OFF": "Off", "LOOP_MODE_SINGLE": "Single", "LOOP_MODE_ALL": "All", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I sent you the name of the music in a private message.", - "MESSAGE_SET_LOOP_MODE": "✅ Set loop to `{{mode}}`", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I've sent you the track name in a private message.", + "MESSAGE_SET_LOOP_MODE": "✅ Loop mode set to `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ You can not execute commands in this channel. Please use the <#{{channelId}}> text channel." -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index d7fc1eae..edb2197a 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -9,9 +9,9 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다.", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다.", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 동적 블랙리스트에 없습니다.", - "MESSAGE_BLACKLIST_LIST_TITLE": "동적 블랙리스트", - "MESSAGE_BLACKLIST_LIST_EMPTY": "동적 블랙리스트가 비어 있습니다.", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 블랙리스트에 없습니다.", + "MESSAGE_BLACKLIST_LIST_TITLE": "블랙리스트", + "MESSAGE_BLACKLIST_LIST_EMPTY": "블랙리스트가 현재 비어 있습니다.", "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다.", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다.", @@ -42,7 +42,7 @@ "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다.", "CONFIG_NODE_USAGE": "node [노드명]", "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요.", - "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악를 표시합니다.", + "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악을 표시합니다.", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다.", "CONFIG_PAUSE_USAGE": "pause", @@ -75,17 +75,17 @@ "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요.", "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다.", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "음악을 랜덤으로 재생할지 선택합니다.", + "CONFIG_SHUFFLE_DESCRIPTION": "재생목록을 섞습니다.", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다.", "CONFIG_SKIP_USAGE": "skip", "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다.", "CONFIG_STATUS_USAGE": "status", - "CONFIG_STOP_DESCRIPTION": "현재 재생 중인 음악을 멈추고 봇을 내보냅니다.", + "CONFIG_STOP_DESCRIPTION": "재생을 멈추고 재생목록을 비웁니다. 채널은 떠나지 않습니다.", "CONFIG_STOP_USAGE": "stop", "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다.", "CONFIG_VOLUME_USAGE": "v [0-200]", - "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정하고자 하는 음량 값을 입력하세요. (1~{{maxVolume}})", + "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정할 음량", "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다.", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [음량]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "설정할 기본 음량 (1-{{maxVolume}})", @@ -96,7 +96,7 @@ "ERROR_SEEK_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 탐색할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 탐색할 수 있습니다.", "ERROR_PAUSE_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 일시정지할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 일시정지할 수 있습니다.", "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다.", - "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 다시 시도해 주세요. 이유: ({{reason}})", + "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ 검색결과가 없습니다. 이유: ({{reason}})", "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다.", @@ -118,10 +118,10 @@ "MESSAGE_DJ_LIST_ERROR": "❌ DJ 목록을 불러오지 못했습니다. 다시 시도해 주세요.", "MESSAGE_DJ_ROLE_NOT_SET": "DJ 역할이 구성되지 않음", "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다.", - "MESSAGE_FILTER_SELECT_LIST": "⬇️ 설정할 재생 효과를 선택합니다. ⬇️", + "MESSAGE_FILTER_SELECT_LIST": "설정할 재생 효과를 선택하세요: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "필터 모드 선택", "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다.", - "MESSAGE_HELP_SELECT_LIST": "⬇️ 도움말을 받으려면 명령어 선택하세요. ⬇️", + "MESSAGE_HELP_SELECT_LIST": "도움말을 받으려면 명령어를 선택하세요. ⬇️", "MESSAGE_HELP_SELECT_MODE": "도움말 선택하기", "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다.", "MESSAGE_LANG_ARGS_ERROR": "언어 변수가 잘못되었습니다. (현재 사용 가능한 언어: {{langList}})", @@ -156,22 +156,22 @@ "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다.", "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다.", "MESSAGE_REMOVE_INSTRUCTION": "⬇️ **1 ~ {{length}}** 중에서 노래를 선택하여 **제거**하거나 다른 문자를 입력하여 선택을 취소합니다. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 대기열에 음악이 없습니다.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 곡 이후 대기열이 비어 있습니다.", "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ 음악이 다시 재생되었습니다.", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 음악이 다시 재생되었습니다.", "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다.", "MESSAGE_SEEK_ARGS_ERROR": "❌ 목표 시간이 잘못된 형식입니다. (예: 3m20s, 1m 50s, 1:20:55, 5:20)", "MESSAGE_SEEK_SKIP": "✅ 해당 위치는 {{duration}}을 초과합니다.\n다음 곡으로 건너뜁니다.", "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 음악을 랜덤으로 재생합니다.", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 재생목록을 섞었습니다.", "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다.", "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다.", - "MESSAGE_STOP_SUCCESS": "✅ 음악을 멈췄습니다.", + "MESSAGE_STOP_SUCCESS": "✅ 재생을 멈추고 재생목록을 비웠습니다.", "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", - "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\\n**설정할 음량을 선택하세요:**", + "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\n**설정할 음량을 선택하세요:**", "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다.", - "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}%**로 변경되었습니다!", + "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 이 서버의 기본 음량이 **{{volume}}%**로 변경되었습니다! (최대: {{maxVolume}}%)", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", @@ -186,7 +186,7 @@ "MESSAGE_DJ_TYPE_STATIC": "(고정)", "MESSAGE_DJ_TYPE_ROLE": "(역할)", "MESSAGE_DJ_TYPE_DYNAMIC": "(동적)", - "LABEL_DELETE_MESSAGE": "메세지 삭제", + "LABEL_DELETE_MESSAGE": "메시지 삭제", "LABEL_CLEAR_QUEUE": "재생목록 초기화", "UNIT_DAYS": "일", "UNIT_HOURS": "시간", @@ -231,7 +231,7 @@ "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 곡 삭제에 실패했습니다. 올바른 트랙 번호인지 확인해주세요.", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다.", "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드가 완료되었습니다. (성공: {{added}}곡, 실패/스킵: {{skipped}}곡)", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드 완료: {{added}}곡 추가, {{skipped}}곡 건너뜀.", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다.", "MESSAGE_PLAYLIST_LIST_TITLE": "서버 플레이리스트 목록", "MESSAGE_PLAYLIST_INFO_TITLE": "플레이리스트 정보: {{name}} (총 {{count}}곡)", @@ -246,7 +246,7 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ 올바른 사용법: `{{prefix}}radio [플레이리스트 이름] [채널명]` (예: `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요.", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 플레이리스트 `{{playlistName}}`에서 `{{channelQuery}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", - "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n(예: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n파일은 EXTINF 태그가 포함된 M3U 형식이어야 합니다.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", "ERROR_RADIO_LOAD_FAILED": "❌ 라디오 채널 로딩 중 오류가 발생했습니다: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다.", diff --git a/src/locales/ko-KR/embeds.json b/src/locales/ko-KR/embeds.json index 3929475b..3bf02bfb 100644 --- a/src/locales/ko-KR/embeds.json +++ b/src/locales/ko-KR/embeds.json @@ -1,10 +1,10 @@ { "DASHBOARD_TITLE": "대시보드", - "DASHBOARD_FINISH_PLAYING": "모든 음악이 재생되었습니다.", + "DASHBOARD_FINISH_PLAYING": "재생목록 재생이 완료되었습니다.", "DASHBOARD_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{duration}}\n────────────────────\n**음량:** `{{volume}}%`\n**반복:** `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**요청자:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성채팅에 성공적으로 연결했습니다.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성 채널에 성공적으로 연결했습니다.", "MAINTAIN_DESCRIPTION": "몇 분 후에 봇이 점검을 시작합니다. 일부 기능을 일시적으로 사용하지 못할 수 있습니다.", "MAINTAIN_TITLE": "⚠️ 유지보수 안내 ⚠️", "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다.", @@ -19,22 +19,22 @@ "NODE_STATUS_INFO_TITLE": "🏷️ 정보", "NODE_STATUS_INFO_VALUE": "**버전:** {{version}}\n**자바:** {{jvm}}\n**Lavaplayer:** {{lavaplayer}}\n**Git:** {{git}}\n**빌드 시간:** {{buildTime}}", "NODE_STATUS_MEMORY_TITLE": "📑 메모리", - "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당 가능:** {{allocated}}\n**예약 가능:** {{reservable}}\n", + "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당됨:** {{allocated}}\n**예약됨:** {{reservable}}\n", "NODE_STATUS_PING": "지연시간", "NODE_STATUS_STATS_TITLE": "📊 상태", - "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**음악을 듣는 서버:** {{playerCount}}개\n**음악을 듣는 인원:** {{playingCount}}명", + "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**플레이어:** {{playerCount}}개\n**재생 중:** {{playingCount}}개", "NODE_STATUS_TITLE": "🛰️ 노드 상태", "NODE_STATUS_TITLE_2": "🛰️ '{{nodeName}}'의 상태", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}의 연결이 끊어졌습니다.", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}개 노드의 연결이 끊어졌습니다.", "PING_DESCRIPTION": "**봇:** {{botPing}}\n**API:** {{apiPing}}ms", "PING_TITLE": "🛰️ 지연시간", "QUEUE_ADD_PLAYLIST": "재생목록 추가", - "QUEUE_ADD_TRACK": "재생목록 추가", + "QUEUE_ADD_TRACK": "음악 추가", "QUEUE_LIST_LOOP_MODE": "반복: {{repeatMode}}", "QUEUE_LIST_TITLE": "재생목록", "REMOVE_LIST_LOOP_MODE": "반복: {{repeatMode}}", - "REMOVE_LIST_TITLE": "제거된 목록", - "REMOVE_TRACK_TITLE": "제거된 목록", + "REMOVE_LIST_TITLE": "제거 목록", + "REMOVE_TRACK_TITLE": "제거된 음악", "SERVER_TITLE": "{{name}}이(가) 가입된 서버", "SERVER_FIELD_DJ_ROLE": "DJ 역할", "SERVER_FIELD_ADMINS": "관리자", diff --git a/src/locales/ko-KR/events.json b/src/locales/ko-KR/events.json index f0fda299..ba936214 100644 --- a/src/locales/ko-KR/events.json +++ b/src/locales/ko-KR/events.json @@ -11,15 +11,15 @@ "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다.", "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다.", - "MESSAGE_MUSIC_SHUFFLE": "✅ 음악을 랜덤으로 재생합니다.", + "MESSAGE_MUSIC_SHUFFLE": "✅ 재생목록을 섞었습니다.", "MESSAGE_NOW_PLAYING_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{label}}\n", "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 페이지 {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 입력 혹은 선택하세요.", + "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 선택하세요.", "LOOP_MODE_OFF": "끄기", "LOOP_MODE_SINGLE": "한 곡 반복", "LOOP_MODE_ALL": "전체 반복", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다.", - "MESSAGE_SET_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다.", + "MESSAGE_SET_LOOP_MODE": "✅ 반복 모드를 `{{mode}}`(으)로 설정했습니다.", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 이 채널에서는 명령을 실행할 수 없습니다. <#{{channelId}}> 채널을 사용하세요." } \ No newline at end of file diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 199f960d..9cf462bb 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -8,8 +8,8 @@ "CONFIG_BLACKLIST_OPTION_USER": "Пользователь для добавления или удаления из чёрного списка", "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список.", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже находится в чёрном списке.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> не внесен(-а) в чёрный список.", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже в чёрном списке.", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> нет в чёрном списке.", "MESSAGE_BLACKLIST_LIST_TITLE": "Текущий чёрный список", "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст.", "CONFIG_CLEAR_DESCRIPTION": "Очистить очередь музыки", @@ -24,12 +24,12 @@ "CONFIG_FILTER_OPTION_DESCRIPTION": "Выберите музыкальный фильтр для наложения", "CONFIG_HELP_DESCRIPTION": "Получить справку по командам", "CONFIG_HELP_USAGE": "help [команда]", - "CONFIG_HELP_OPTION_DESCRIPTION": "Для какой команды нужна подсказка", + "CONFIG_HELP_OPTION_DESCRIPTION": "Команда, по которой нужна справка", "CONFIG_JOIN_DESCRIPTION": "Подключиться к текущему голосовому каналу", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Изменить язык бота", - "CONFIG_LANG_USAGE": "language <языковой код>", - "CONFIG_LANG_OPTION_DESCRIPTION": "Кодовое обозначение языка", + "CONFIG_LANG_USAGE": "language <доступный язык>", + "CONFIG_LANG_OPTION_DESCRIPTION": "Выберите язык", "CONFIG_LEAVE_DESCRIPTION": "Покинуть текущий голосовой канал", "CONFIG_LEAVE_USAGE": "leave", "CONFIG_LOOP_DESCRIPTION": "Включить или выключить режим повтора музыки", @@ -57,12 +57,12 @@ "CONFIG_PLAYLAST_DESCRIPTION": "Повторно воспроизвести текущий или последний трек", "CONFIG_PLAYLAST_USAGE": "playlast", "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно воспроизводившийся трек не найден.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова добавлен в очередь.", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно сыгранный трек не найден.", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова в очереди.", "CONFIG_QUEUE_DESCRIPTION": "Показать текущий плейлист", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Выбрать трек для удаления из плейлиста", - "CONFIG_REMOVE_USAGE": "remove <номер трека> [начальный индекс конечный индекс]", + "CONFIG_REMOVE_USAGE": "remove <номер трека> [от до]", "CONFIG_REMOVE_OPTION_DESCRIPTION": "Номер трека", "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Диапазон треков", "CONFIG_RESUME_DESCRIPTION": "Продолжить воспроизведение", @@ -70,9 +70,9 @@ "CONFIG_SEARCH_DESCRIPTION": "Введите название трека для поиска", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Название трека", - "CONFIG_SEEK_DESCRIPTION": "Перемотать трек на указанное время", - "CONFIG_SEEK_USAGE": "seek <[чч]мм]сс/[чч:мм]:сс> (пример: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "Целевое время (пример: 3m20s, 1:20:55)", + "CONFIG_SEEK_DESCRIPTION": "Перемотать трек к заданному моменту", + "CONFIG_SEEK_USAGE": "seek <[чч]мм]сс/[чч:мм]:сс> (пример: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Время в треке (пример: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Показать активные серверы", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Перемешать плейлист", @@ -90,26 +90,25 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [громкость]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Громкость по умолчанию (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Ошибка команды. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди нет музыки для воспроизведения.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди сейчас нет треков.", "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится.", - "ERROR_SKIP_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может её пропустить. Админы и DJ (если включены) тоже могут пропускать.", - "ERROR_SEEK_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может перематывать её. Админы и DJ (если включены) тоже могут перематывать.", - "ERROR_PAUSE_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может поставить её на паузу. Админы и DJ (если включены) тоже могут ставить на паузу.", + "ERROR_SKIP_NOT_REQUESTER": "❌ Пропустить этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_SEEK_NOT_REQUESTER": "❌ Перематывать этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_PAUSE_NOT_REQUESTER": "❌ Ставить этот трек на паузу может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу.", "ERROR_PLAY_MUSIC": "❌ Сервис испытывает проблемы, попробуйте позже. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Ничего не найдено. ({{reason}})", "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена.", - "MESSAGE_CLEAR_RADIO_SUCCESS": "🧹 Очередь очищена, радио остановлено.", "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена.", "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев.", "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев.", - "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя для добавления на роль диджея.", - "MESSAGE_DJ_NO_BOTS": "Нельзя добавлять ботов на роль диджея.", - "MESSAGE_DJ_STATIC_MODE": "Права DJ управляются через конфигурацию в статическом режиме.", - "MESSAGE_DJ_NO_PLAYER": "Активный музыкальный плеер не найден.", + "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя, которого хотите сделать диджеем.", + "MESSAGE_DJ_NO_BOTS": "Ботов нельзя назначать диджеями.", + "MESSAGE_DJ_STATIC_MODE": "В статическом режиме права DJ настраиваются только через файл конфигурации.", + "MESSAGE_DJ_NO_PLAYER": "Нет плеера, воспроизводящего сейчас музыку.", "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем.", - "MESSAGE_DJ_SUCCESS": "Пользователь <@{{userId}}> назначен на роль диджея.", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> назначен диджеем.", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ покинул канал! Автоматически выбран новый DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Список диджеев", "MESSAGE_DJ_LIST_ADMINS": "Администраторы", @@ -117,17 +116,17 @@ "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Ситуативные диджеи", "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались.", "MESSAGE_DJ_LIST_ERROR": "❌ Не удалось загрузить список диджеев. Попробуйте снова.", - "MESSAGE_DJ_ROLE_NOT_SET": "Роль DJ не настроена", + "MESSAGE_DJ_ROLE_NOT_SET": "Постоянный DJ не назначен.", "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден.", - "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр для наложения: ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Выберите применяемый эффект", + "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Выберите эффект", "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена.", "MESSAGE_HELP_SELECT_LIST": "Выберите команду для получения справки: ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Выберите подсказку", - "MESSAGE_JOIN_SUCCESS": "✅ Бот подключился.", + "MESSAGE_HELP_SELECT_MODE": "Выберите команду", + "MESSAGE_JOIN_SUCCESS": "✅ Подключился к голосовому каналу.", "MESSAGE_LANG_ARGS_ERROR": "Неверный языковой код, доступные языки: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Доступные языки: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Язык успешно изменён на `{{locale}}`", + "MESSAGE_LANG_SUCCESS": "✅ Язык изменён на `{{locale}}`", "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал.", "REPEAT_MODE_OFF": "ВЫКЛ", "REPEAT_MODE_SINGLE": "ОДИНОЧНЫЙ", @@ -137,7 +136,7 @@ "LOOP_CHOICE_ALL": "Все", "LOOP_SELECT_PLACEHOLDER": "Выберите режим повтора", "LOOP_SELECT_DESCRIPTION": "Установить режим повтора: {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Режим повтора установлен: `{{mode}}`.", + "MESSAGE_LOOP_MODE": "✅ Режим повтора теперь `{{mode}}`.", "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена.", "MESSAGE_MOVE_WRONG_INDEX": "❌ Укажите позиции в корректном диапазоне. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nи ещё {{length}} треков", @@ -149,37 +148,37 @@ "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу.", "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу.", "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название искомой музыки.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Музыка добавлена.", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название трека или ссылку.", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Трек добавлен в очередь.", "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено.", "MESSAGE_PLAY_SELECT_DURATION": "Длительность: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Выберите музыку", "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено.", - "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить музыку.", - "MESSAGE_REMOVE_INSTRUCTION": "Выберите позицию трека от **1** до **{{length}}** для **удаления** или введите другое значение для отмены. ⬇️", + "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить трек.", + "MESSAGE_REMOVE_INSTRUCTION": "Выберите номер трека от **1** до **{{length}}**, чтобы **удалить** его, или введите любой другой номер для отмены. ⬇️", "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста.", "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку.", "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена.", "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено.", "MESSAGE_SEEK_ARGS_ERROR": "❌ Неверный формат времени.\n(**`пример: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Позиция перемотки превышает длительность трека. (`{{duration}}`)\nПереход к следующей песне.", + "MESSAGE_SEEK_SKIP": "✅ Указанная позиция (`{{duration}}`) больше длительности трека\nПерехожу к следующему треку.", "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`.", "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан.", "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку.", "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен.", - "MESSAGE_STOP_SUCCESS": "✅ Музыка остановлена.", + "MESSAGE_STOP_SUCCESS": "✅ Воспроизведение остановлено, очередь очищена.", "MESSAGE_VOLUME_ARGS_ERROR": "Текущая громкость: **{{volume}}** 🔊\n**Чтобы изменить громкость, введите число от `1` до `{{maxVolume}}`.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Введите число от `1` до `{{maxVolume}}` для изменения громкости.**", - "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\\n**Выберите желаемую громкость:**", + "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\n**Выберите желаемую громкость:**", "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена.", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Громкость по умолчанию для этого сервера изменена на **{{volume}}%**! (Макс: {{maxVolume}}%)", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`.", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию.", - "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас у вас **{{current}}** треков в очереди (лимит: **{{limit}}**). Подождите, пока некоторые треки доиграют.", - "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в очереди **{{total}}** треков (глобальный лимит: **{{limit}}**). Подождите, пока некоторые треки доиграют.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен частично: добавлено **{{added}}** треков, пропущено **{{skipped}}** из-за лимита очереди (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен частично: добавлено **{{added}}** треков, пропущено **{{skipped}}** из-за глобального лимита (**{{total}}/{{limit}}**).", + "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас в ней **{{current}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", + "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в ней **{{total}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за лимита очереди (**{{current}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за глобального лимита (**{{total}}/{{limit}}**).", "MESSAGE_SERVER_GUILD_ID": "ID сервера", "MESSAGE_SERVER_GUILD": "Сервер", "MESSAGE_SERVER_MEMBERS": "Участники", @@ -198,7 +197,7 @@ "UNIT_MB": "МБ", "UNIT_GB": "ГБ", "UNIT_TB": "ТБ", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Менеджер чёрного списка не инициализирован.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Чёрный список ещё не загружен, попробуйте позже.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Выберите музыкальную команду", "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите утилиту", "HELP_COMMAND_ALIASES": "Алиасы: {{aliases}}", @@ -214,10 +213,10 @@ "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U.", "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста.", "CONFIG_PLAYLIST_OPTION_NAME": "Имя плейлиста", - "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (опционально)", + "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (необязательно)", "CONFIG_PLAYLIST_OPTION_URL": "Ссылка на скачивание файла M3U", "CONFIG_PLAYLIST_OPTION_INDEX": "Номер трека для удаления (начиная с 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Менеджер плейлистов не инициализирован.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Плейлисты пока недоступны, попробуйте позже.", "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track).", "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда.", "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста.", @@ -225,21 +224,21 @@ "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист.", "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст.", "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U. (Код ошибки: {{status}})", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U (код ошибки {{status}}).", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Загрузка M3U заблокирована политикой безопасности. Используйте общедоступный HTTPS URL размером до {{maxSizeMiB}} МиБ, отвечающий в течение {{timeoutSeconds}} секунд.", "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков.", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Ошибка разбора или сохранения M3U плейлиста. Убедитесь, что формат правильный.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Не удалось удалить трек. Убедитесь, что это правильный номер.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Сохранено {{count}} треков из текущей очереди в плейлист `{{name}}`.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Плейлист `{{name}}` сохранён — {{count}} треков из текущей очереди.", "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен. (Добавлено: {{added}}, Пропущено: {{skipped}})", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен: добавлено {{added}}, пропущено {{skipped}}.", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов.", "MESSAGE_PLAYLIST_LIST_TITLE": "Плейлисты сервера", "MESSAGE_PLAYLIST_INFO_TITLE": "Информация о плейлисте: {{name}} ({{count}} треков)", - "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и еще {{remaining}} треков.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` успешно удален.", - "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний плейлист M3U успешно импортирован!\nЗарегистрировано {{count}} треков в плейлисте `{{name}}`.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Удален трек №{{index}} из плейлиста `{{name}}`.", + "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и ещё {{remaining}} треков.*", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` удалён.", + "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний M3U-плейлист импортирован!\nВ плейлист `{{name}}` добавлено {{count}} треков.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Трек №{{index}} удалён из плейлиста `{{name}}`.", "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста.", "CONFIG_RADIO_USAGE": "radio [название]", "CONFIG_RADIO_OPTION_PLAYLIST": "Название плейлиста для поиска", @@ -247,7 +246,7 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ Использование: `{{prefix}}radio [название плейлиста] [название канала]` (например, `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала.", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ В плейлисте `{{playlistName}}` не найден радиоканал, соответствующий `{{channelQuery}}`.\nДоступные примеры: {{samples}} и др.", - "ERROR_RADIO_NOT_READY": "❌ Плейлист радио не готов.\nСначала импортируйте плейлист радио с помощью `/playlist import {{name}} `.\n(Например: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Радио пока не настроено.\nСначала импортируйте радио-плейлист командой `/playlist import {{name}} `.\nФайл должен быть в формате M3U с тегами EXTINF.", "ERROR_RADIO_NO_MATCH": "❌ Радиоканал по запросу `{{query}}` не найден.\nДоступные варианты: {{samples}} и др.", "ERROR_RADIO_LOAD_FAILED": "❌ Ошибка загрузки радиоканала: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`).", @@ -262,9 +261,9 @@ "LABEL_OVERWRITE": "Перезаписать", "LABEL_CANCEL": "Отмена", "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменен.", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменён.", "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Пропущен текущий трек. Очередь пуста." -} \ No newline at end of file + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста." +} diff --git a/src/locales/ru-RU/embeds.json b/src/locales/ru-RU/embeds.json index 2052c20d..e435c504 100644 --- a/src/locales/ru-RU/embeds.json +++ b/src/locales/ru-RU/embeds.json @@ -5,9 +5,9 @@ "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Заказал:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Ситуативный DJ:** {{djDisplay}}", "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу.", - "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время некоторые функции окажутся временно недоступны.", + "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время часть функций будет недоступна.", "MAINTAIN_TITLE": "⚠️ Техническое обслуживание ⚠️", - "MESSAGE_BLACKLIST": "В голосовом канале есть пользователь(и) из чёрного списка.", + "MESSAGE_BLACKLIST": "В голосовом канале есть пользователи из чёрного списка.", "MESSAGE_COMMAND": "Команда **{{command}}**", "MESSAGE_FILTER": "Установлен фильтр: **{{effectName}}**", "NODE_ALL_ACTIVE": "Все узлы активны", @@ -19,7 +19,7 @@ "NODE_STATUS_INFO_TITLE": "🏷️ ИНФОРМАЦИЯ", "NODE_STATUS_INFO_VALUE": "Версия: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nВремя сборки: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 ПАМЯТЬ", - "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nРезервируемо: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nЗарезервировано: **{{reservable}}**\n", "NODE_STATUS_PING": "Задержка", "NODE_STATUS_STATS_TITLE": "📊 СТАТИСТИКА", "NODE_STATUS_STATS_VALUE": "Время работы: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} мс**\nПлееров: **{{playerCount}}**\nВоспроизводящих: **{{playingCount}}**", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Сейчас играет:**", "QUEUE_EMPTY": "*Нет треков в очереди*", "QUEUE_HEADER": "**Очередь:**" -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/events.json b/src/locales/ru-RU/events.json index fe14dd73..7d9a5a61 100644 --- a/src/locales/ru-RU/events.json +++ b/src/locales/ru-RU/events.json @@ -1,7 +1,7 @@ { "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша.", "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в указанный голосовой канал <#{{channelId}}> и вызовите меня.", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в голосовой канал <#{{channelId}}> и позовите меня.", "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу.", "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится.", "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора.", @@ -20,6 +20,6 @@ "LOOP_MODE_SINGLE": "Один трек", "LOOP_MODE_ALL": "Все", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения.", - "MESSAGE_SET_LOOP_MODE": "✅ Установлен режим повтора: `{{mode}}`", - "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Вы не можете использовать команды в этом канале. Используйте текстовый канал <#{{channelId}}>" -} \ No newline at end of file + "MESSAGE_SET_LOOP_MODE": "✅ Выбран режим повтора: `{{mode}}`", + "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ В этом канале нельзя использовать команды. Пожалуйста, перейдите в текстовый канал <#{{channelId}}>." +} diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 91fc7d07..9e678800 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -1,6 +1,6 @@ { "CONFIG_BLACKLIST_DESCRIPTION": "Upravljaj crnom listom bota", - "CONFIG_BLACKLIST_USAGE": "blacklist [user]", + "CONFIG_BLACKLIST_USAGE": "blacklist [korisnik]", "CONFIG_BLACKLIST_OPTION_SUBCOMMAND": "Radnja koja će se izvršiti", "CONFIG_BLACKLIST_OPTION_ADD": "Dodaj korisnika na crnu listu", "CONFIG_BLACKLIST_OPTION_REMOVE": "Ukloni korisnika sa crne liste", @@ -9,38 +9,38 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu.", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste.", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na dinamičnoj crnoj listi.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dinamična crna lista", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Dinamična crna lista je prazna.", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na crnoj listi.", + "MESSAGE_BLACKLIST_LIST_TITLE": "Crna lista", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Crna lista je trenutno prazna.", "CONFIG_CLEAR_DESCRIPTION": "Obriši svu muziku iz reda", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Premesti kontrolnu tablu na dno", "CONFIG_DASHBOARD_USAGE": "dashboard", "CONFIG_DJ_DESCRIPTION": "Dodaj korisnika kao DJ-a ili prikaži listu DJ-eva", - "CONFIG_DJ_USAGE": "dj [@user]", + "CONFIG_DJ_USAGE": "dj [@korisnik]", "CONFIG_DJ_OPTION_DESCRIPTION": "Korisnik koji će biti dodat kao DJ (opciono)", "CONFIG_FILTER_DESCRIPTION": "Podesi muzički filter", - "CONFIG_FILTER_USAGE": "filter [effect name]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "Izaberi režim muzičkog filtera koji želiš da podesiš", + "CONFIG_FILTER_USAGE": "filter [ime efekta]", + "CONFIG_FILTER_OPTION_DESCRIPTION": "Izaberi muzički filter koji želiš da podesiš", "CONFIG_HELP_DESCRIPTION": "Prikaži pomoć za komande", - "CONFIG_HELP_USAGE": "help [command]", - "CONFIG_HELP_OPTION_DESCRIPTION": "za koju komandu trebaš pomoć", + "CONFIG_HELP_USAGE": "help [komanda]", + "CONFIG_HELP_OPTION_DESCRIPTION": "Komanda za koju trebaš pomoć", "CONFIG_JOIN_DESCRIPTION": "Pridruži se trenutnom govornom kanalu", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Promeni jezik bota", "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Naziv jezika", + "CONFIG_LANG_OPTION_DESCRIPTION": "Izaberi jezik", "CONFIG_LEAVE_DESCRIPTION": "Napusti trenutni govorni kanal", "CONFIG_LEAVE_USAGE": "leave", "CONFIG_LOOP_DESCRIPTION": "Uključi ili isključi režim ponavljanja muzike", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Režim ponavljanja muzike", "CONFIG_MOVE_DESCRIPTION": "Zameni pozicije dve pesme u redu", - "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "Pozicija pesme 1 za zamenu", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Pozicija pesme 2 za zamenu", + "CONFIG_MOVE_USAGE": "move ", + "CONFIG_MOVE_OPTION_DESCRIPTION": "Pozicija pesme 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Pozicija pesme 2", "CONFIG_NODE_DESCRIPTION": "Prikaži status aktivnih nodova", - "CONFIG_NODE_USAGE": "node [node name]", + "CONFIG_NODE_USAGE": "node [ime noda]", "CONFIG_NODE_OPTION_DESCRIPTION": "Naziv noda", "CONFIG_NOW_PLAYING_DESCRIPTION": "Prikaži trenutno puštenu muziku", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", @@ -49,10 +49,10 @@ "CONFIG_PING_DESCRIPTION": "Prikaži ping servera", "CONFIG_PING_USAGE": "ping", "CONFIG_PLAY_DESCRIPTION": "Unesi link pesme ili naziv pesme za puštanje", - "CONFIG_PLAY_USAGE": "play ", + "CONFIG_PLAY_USAGE": "play ", "CONFIG_PLAY_OPTION_DESCRIPTION": "Link pesme ili naziv pesme", "CONFIG_PLAYFIRST_DESCRIPTION": "Unesi link pesme ili naziv pesme da bi se pustila prva", - "CONFIG_PLAYFIRST_USAGE": "playfirst ", + "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Link pesme ili naziv pesme koja će se pustiti prva", "CONFIG_PLAYLAST_DESCRIPTION": "Ponovo pusti trenutnu ili poslednju puštenu pesmu", "CONFIG_PLAYLAST_USAGE": "playlast", @@ -62,17 +62,17 @@ "CONFIG_QUEUE_DESCRIPTION": "Prikaži trenutnu listu pesama", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Izaberi pesmu za uklanjanje iz liste pesama", - "CONFIG_REMOVE_USAGE": "remove [from index to index 2 track]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "broj indeksa pesme", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "od indeksa do indeksa 2 pesme", + "CONFIG_REMOVE_USAGE": "remove [od do]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Broj pesme", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Raspon pesama", "CONFIG_RESUME_DESCRIPTION": "Nastavi pauziranu pesmu", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "Unesi naziv pesme za pretragu", - "CONFIG_SEARCH_USAGE": "search ", + "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Naziv pesme", "CONFIG_SEEK_DESCRIPTION": "Premotaj pesmu na određeno vreme", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "ciljno vreme (npr: 3m20s, 1:20:55)", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (npr: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Ciljno vreme (npr: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Prikaži trenutno aktivne servere", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Izmešaj listu pesama", @@ -90,7 +90,7 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [jačina]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Podrazumevana jačina zvuka (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Greška u komandi. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Trenutno nema muzike za puštanje u redu.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ U redu trenutno nema pesama.", "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika.", "ERROR_SKIP_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je preskoči. Administratori i DJ-evi (ako je omogućeno) takođe mogu da preskoče.", "ERROR_SEEK_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je premotava. Administratori i DJ-evi (ako je omogućeno) takođe mogu da premotavaju.", @@ -108,26 +108,26 @@ "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu.", "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer.", "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ.", - "MESSAGE_DJ_SUCCESS": "Uspešno dodat <@{{userId}}> kao DJ.", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> je dodat kao DJ.", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ je napustio kanal! Automatski izabran novi DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Lista DJ-eva", "MESSAGE_DJ_LIST_ADMINS": "Administratori", - "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi bazirani na ulogama", + "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi po ulozi", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamični DJ-evi", "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva.", "MESSAGE_DJ_LIST_ERROR": "❌ Neuspešno učitavanje liste DJ-eva. Molim te pokušaj ponovo.", - "MESSAGE_DJ_ROLE_NOT_SET": "Nije podešena DJ uloga", - "MESSAGE_FILTER_NOT_FOUND": "❌ Naziv efekta nije pronađen.", - "MESSAGE_FILTER_SELECT_LIST": "Izaberi režim muzičkog filtera koji želiš da podesiš. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Izaberi režim filtera", + "MESSAGE_DJ_ROLE_NOT_SET": "Nije podešena DJ uloga.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekat nije pronađen.", + "MESSAGE_FILTER_SELECT_LIST": "Izaberi muzički filter: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Izaberi efekat", "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena.", "MESSAGE_HELP_SELECT_LIST": "Izaberi komandu da bi dobio pomoć. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Izaberi pomoć", - "MESSAGE_JOIN_SUCCESS": "✅ Bot se pridružio.", - "MESSAGE_LANG_ARGS_ERROR": "Parametar jezika je pogrešan, trenutno dostupni jezici: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Trenutno dostupni jezici: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Uspešno promenjen jezik na `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot je napustio.", + "MESSAGE_HELP_SELECT_MODE": "Izaberi komandu", + "MESSAGE_JOIN_SUCCESS": "✅ Pridružio sam se glasovnom kanalu.", + "MESSAGE_LANG_ARGS_ERROR": "Pogrešan jezički kod. Dostupni jezici: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Dostupni jezici: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Jezik promenjen na `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Napustio sam glasovni kanal.", "REPEAT_MODE_OFF": "ISKLJUČENO", "REPEAT_MODE_SINGLE": "POJEDINAČNO", "REPEAT_MODE_ALL": "SVE", @@ -136,7 +136,7 @@ "LOOP_CHOICE_ALL": "Sve", "LOOP_SELECT_PLACEHOLDER": "Izaberi režim ponavljanja", "LOOP_SELECT_DESCRIPTION": "Postavi režim ponavljanja na {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Postavljeno ponavljanje na `{{mode}}`.", + "MESSAGE_LOOP_MODE": "✅ Režim ponavljanja je sada `{{mode}}`.", "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen.", "MESSAGE_MOVE_WRONG_INDEX": "❌ Molim te unesi važeći opseg indeksa. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\ni {{length}} drugih pesama", @@ -148,33 +148,33 @@ "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo.", "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana.", "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Napiši naziv muzike koju želiš da pretražiš.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Muzika dodata.", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Unesi naziv pesme ili link.", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Pesma dodata u red.", "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja.", "MESSAGE_PLAY_SELECT_DURATION": "Trajanje: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Izaberi muziku", - "MESSAGE_REMOVE_CANCEL": "✅ Otkazano uklanjanje.", + "MESSAGE_REMOVE_CANCEL": "✅ Uklanjanje otkazano.", "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo.", - "MESSAGE_REMOVE_INSTRUCTION": "Izaberi pesmu od **1** do **{{length}}** za **uklanjanje** ili unesi bilo šta drugo za otkazivanje izbora. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Nema muzike u redu nakon trenutne.", + "MESSAGE_REMOVE_INSTRUCTION": "Unesi broj pesme od **1** do **{{length}}** za **uklanjanje**, ili bilo koji drugi broj za otkazivanje. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Red je prazan nakon trenutne pesme.", "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ Muzika je nastavljena.", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Muzika je nastavljena.", "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena.", "MESSAGE_SEEK_ARGS_ERROR": "❌ Nevažeći format za ciljno vreme.\n(**`npr: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Pozicija premotavanja je izvan trajanja ove pesme. (`{{duration}}`)\nPreskakanje na sledeću pesmu.", - "MESSAGE_SEEK_SUCCESS": "✅ Muzika je premotana na `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Muzika izmešana.", + "MESSAGE_SEEK_SKIP": "✅ Navedena pozicija (`{{duration}}`) je duža od trajanja pesme.\nPrelazim na sledeću pesmu.", + "MESSAGE_SEEK_SUCCESS": "✅ Premotano na `{{duration}}`.", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Lista pesama izmešana.", "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo.", - "MESSAGE_SKIP_SUCCESS": "✅ Muzika preskočena.", - "MESSAGE_STOP_SUCCESS": "✅ Muzika zaustavljena.", - "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Da bi promenio jačinu, unesi broj između `1` i `{{maxVolume}}`.**", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Unesi broj od `1` do `{{maxVolume}}` da bi promenio jačinu.**", - "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\\n**Molim te izaberi jačinu koju želiš da podesiš:**", - "MESSAGE_VOLUME_SAME": "❌ Jačina koju želiš da podesiš je već trenutna jačina.", + "MESSAGE_SKIP_SUCCESS": "✅ Pesma preskočena.", + "MESSAGE_STOP_SUCCESS": "✅ Reprodukcija zaustavljena, red obrisan.", + "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", + "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\n**Molim te izaberi jačinu koju želiš da podesiš:**", + "MESSAGE_VOLUME_SAME": "❌ Ta jačina je već podešena.", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Podrazumevana jačina zvuka je promenjena na **{{volume}}%** za ovaj server! (Maks: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nDa bi promenio, unesi broj između `1` i `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Unesi broj od `1` do `{{maxVolume}}` da bi promenio podrazumevanu jačinu.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nZa promenu unesi broj od `1` do `{{maxVolume}}`.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Za promenu podrazumevane jačine unesi broj od `1` do `{{maxVolume}}`.", "ERROR_QUEUE_LIMIT_REACHED": "❌ Dostigao si limit reda! Trenutno imaš **{{current}}** pesama u redu (limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Dostignut je globalni limit reda! Red trenutno ima **{{total}}** pesama (globalni limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**).", @@ -192,14 +192,14 @@ "UNIT_HOURS": "č", "UNIT_MINUTES": "m", "UNIT_SECONDS": "s", - "UNIT_BYTES": "Bajta", + "UNIT_BYTES": "Bajt", "UNIT_KB": "KB", "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Blacklist manager nije inicijalizovan.", - "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi Muzičku komandu", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberi Utility komandu", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Crna lista još nije učitana. Pokušaj ponovo kasnije.", + "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi muzičku komandu", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberi utility komandu", "HELP_COMMAND_ALIASES": "Aliasi: {{aliases}}", "HELP_COMMAND_NONE": "Nijedan", "LABEL_FILTER_CLEAR": "Obriši filtere", @@ -231,7 +231,7 @@ "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Uklanjanje pesme nije uspelo. Proverite da li je indeks ispravan.", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`.", "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana. (Uspešno: {{added}}, Preskočeno: {{skipped}})", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana: dodato {{added}}, preskočeno {{skipped}}.", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru.", "MESSAGE_PLAYLIST_LIST_TITLE": "Plejliste na serveru", "MESSAGE_PLAYLIST_INFO_TITLE": "Informacije o plejlisti: {{name}} ({{count}} pesama)", @@ -246,7 +246,7 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ Upotreba: `{{prefix}}radio [naziv plejliste] [naziv kanala]` (npr. `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala.", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Radio kanal koji odgovara `{{channelQuery}}` u plejlisti `{{playlistName}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", - "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\n(npr., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\nFajl mora biti u M3U formatu sa EXTINF tagovima.", "ERROR_RADIO_NO_MATCH": "❌ Radio kanal koji odgovara `{{query}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", "ERROR_RADIO_LOAD_FAILED": "❌ Greška pri učitavanju radio kanala: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`).", diff --git a/src/locales/sr-RS/embeds.json b/src/locales/sr-RS/embeds.json index 8f52cec4..4f769ebc 100644 --- a/src/locales/sr-RS/embeds.json +++ b/src/locales/sr-RS/embeds.json @@ -22,7 +22,7 @@ "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nRezervisano: **{{reservable}}**\n", "NODE_STATUS_PING": "Kašnjenje", "NODE_STATUS_STATS_TITLE": "📊 STATISTIKA", - "NODE_STATUS_STATS_VALUE": "vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "Vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ Status nodova", "NODE_STATUS_TITLE_2": "🛰️ Status noda '{{nodeName}}'", "NODE_UNHEALTHY": "⚠️ Ima {{unhealthValue}} isključenih nodova", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Trenutno svira:**", "QUEUE_EMPTY": "*Nema pesama u redu*", "QUEUE_HEADER": "**Red:**" -} \ No newline at end of file +} diff --git a/src/locales/sr-RS/events.json b/src/locales/sr-RS/events.json index 80e46384..04f26327 100644 --- a/src/locales/sr-RS/events.json +++ b/src/locales/sr-RS/events.json @@ -11,7 +11,7 @@ "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal.", "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Muzika izmešana.", + "MESSAGE_MUSIC_SHUFFLE": "✅ Red je izmešan.", "MESSAGE_NOW_PLAYING_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Strana {{curPage}}/{{maxPage}} -----", @@ -20,6 +20,6 @@ "LOOP_MODE_SINGLE": "Jedna", "LOOP_MODE_ALL": "Sve", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci.", - "MESSAGE_SET_LOOP_MODE": "✅ Ponavljanje postavljeno na `{{mode}}`", + "MESSAGE_SET_LOOP_MODE": "✅ Izabran režim ponavljanja: `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Ne možeš izvršavati komande u ovom kanalu. Molim te koristi <#{{channelId}}> tekstualni kanal." } \ No newline at end of file diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 775b7af1..f29f03e2 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -9,9 +9,9 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi.", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı.", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> dinamik kara listede değil.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dinamik Kara Liste", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Dinamik kara liste boş.", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> kara listede değil.", + "MESSAGE_BLACKLIST_LIST_TITLE": "Kara Liste", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Kara liste şu anda boş.", "CONFIG_CLEAR_DESCRIPTION": "Kuyruktaki tüm parçaları temizle", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Dashboard'u alta al", @@ -22,35 +22,35 @@ "CONFIG_FILTER_DESCRIPTION": "Müzik filtresini belirleme", "CONFIG_FILTER_USAGE": "filter [efekt adı]", "CONFIG_FILTER_OPTION_DESCRIPTION": "Belirlemek istediğiniz filtreyi seçin", - "CONFIG_HELP_DESCRIPTION": "Yardım komutunu kullanma", + "CONFIG_HELP_DESCRIPTION": "Komutlar hakkında yardım al", "CONFIG_HELP_USAGE": "help [komut]", - "CONFIG_HELP_OPTION_DESCRIPTION": "yardıma ihtiyaç duyduğunuz komut", + "CONFIG_HELP_OPTION_DESCRIPTION": "Hakkında yardım almak istediğiniz komut", "CONFIG_JOIN_DESCRIPTION": "Seçilen ses kanalına bağlan", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Bot'un dilini değiştir", "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Yerelleştirme kodu", + "CONFIG_LANG_OPTION_DESCRIPTION": "Bir dil seçin", "CONFIG_LEAVE_DESCRIPTION": "Ses kanalını terket", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "Döngü modunu açma", + "CONFIG_LOOP_DESCRIPTION": "Döngü modunu aç veya kapat", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Döngü modu", "CONFIG_MOVE_DESCRIPTION": "Kuyruktaki 2 parçanın sırasını değiştir", "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "1. sıraya alındı", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "2. sıraya alındı", - "CONFIG_NODE_DESCRIPTION": "Aktif bağlantı noktalarının adını göster", - "CONFIG_NODE_USAGE": "node [bağlantı noktası adı]", - "CONFIG_NODE_OPTION_DESCRIPTION": "Bağlantı noktası adı", + "CONFIG_MOVE_OPTION_DESCRIPTION": "1. parçanın konumu", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "2. parçanın konumu", + "CONFIG_NODE_DESCRIPTION": "Aktif düğümlerin durumunu göster", + "CONFIG_NODE_USAGE": "node [düğüm adı]", + "CONFIG_NODE_OPTION_DESCRIPTION": "Düğüm adı", "CONFIG_NOW_PLAYING_DESCRIPTION": "Şuan çalan parçayı göster", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", "CONFIG_PAUSE_DESCRIPTION": "Şuan çalan parçayı durdur", "CONFIG_PAUSE_USAGE": "pause", - "CONFIG_PING_DESCRIPTION": "Sunucu gecikmesi", + "CONFIG_PING_DESCRIPTION": "Sunucu gecikmesini öğren", "CONFIG_PING_USAGE": "ping", "CONFIG_PLAY_DESCRIPTION": "Çalınacak parçanın adını ya da linkini girin", "CONFIG_PLAY_USAGE": "play ", - "CONFIG_PLAY_OPTION_DESCRIPTION": "Parçanın linki ya da adını girin", + "CONFIG_PLAY_OPTION_DESCRIPTION": "Parçanın linki ya da adı", "CONFIG_PLAYFIRST_DESCRIPTION": "Kuyrukta başa eklenecek parçanın adını ya da linkini girin", "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Kuyrukta başa alınacak parçanın linki ya da adı", @@ -62,35 +62,35 @@ "CONFIG_QUEUE_DESCRIPTION": "Şuanki oynatma listesini göster", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Oynatma kuyruğundan silmek için bir parça seçin", - "CONFIG_REMOVE_USAGE": "remove [baştan sona kadar 2 parça]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "parça sırası", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "baştan sona kadar 2 parça", + "CONFIG_REMOVE_USAGE": "remove [başlangıç bitiş]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Parça numarası", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Parça aralığı", "CONFIG_RESUME_DESCRIPTION": "Duraklatılan parçayı devam ettir", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "Aramak istediğiniz parçanın adını girin", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Parça adı", "CONFIG_SEEK_DESCRIPTION": "Parçada belirlenen bir zamana atlama", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (örn: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "hedeflenen zaman (örn: 3m20s, 1:20:55)", - "CONFIG_SERVER_DESCRIPTION": "Aktif olan sunucular", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (örn: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Hedef zaman (örn: 165, 3m20s, 1:20:55)", + "CONFIG_SERVER_DESCRIPTION": "Aktif sunucuları göster", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Oynatma listesini karıştır", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "Parçayı atlama", "CONFIG_SKIP_USAGE": "skip", - "CONFIG_STATUS_DESCRIPTION": "Bot'un durumunu öğrenme", + "CONFIG_STATUS_DESCRIPTION": "Bot'un durumunu göster", "CONFIG_STATUS_USAGE": "status", "CONFIG_STOP_DESCRIPTION": "Şu an çalan müziği ve kuyruğu temizle ancak kanaldan ayrılma", "CONFIG_STOP_USAGE": "stop", - "CONFIG_VOLUME_DESCRIPTION": "Bot'un ses sesviyesini belirleme", + "CONFIG_VOLUME_DESCRIPTION": "Bot'un ses seviyesini değiştir", "CONFIG_VOLUME_USAGE": "v <0-100>", "CONFIG_VOLUME_OPTION_DESCRIPTION": "Ses seviyesi", "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "Bu sunucu için varsayılan ses seviyesini değiştir", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [ses-seviyesi]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Belirlenecek varsayılan ses seviyesi (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Komut hatası. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta çalınacak başka bir parça bulunmuyor.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta şu anda parça yok.", "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", "ERROR_SKIP_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi atlayabilir. Yöneticiler ve DJ'ler (etkinse) de atlayabilir.", "ERROR_SEEK_NOT_REQUESTER": "❌ Bu şarkıda yalnızca isteyen kişi zaman atlayabilir. Yöneticiler ve DJ'ler (etkinse) de zaman atlayabilir.", @@ -116,18 +116,18 @@ "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamik DJ'ler", "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok.", "MESSAGE_DJ_LIST_ERROR": "❌ DJ listesi yüklenemedi. Lütfen tekrar deneyin.", - "MESSAGE_DJ_ROLE_NOT_SET": "DJ rolü yapılandırılmamış", - "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt adı bulunamadı.", - "MESSAGE_FILTER_SELECT_LIST": "Belirlemek istediğiniz filtreyi seçin. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Filtre modunu seçin", + "MESSAGE_DJ_ROLE_NOT_SET": "DJ rolü yapılandırılmamış.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt bulunamadı.", + "MESSAGE_FILTER_SELECT_LIST": "Bir müzik filtresi seçin: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Bir efekt seçin", "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı.", "MESSAGE_HELP_SELECT_LIST": "Yardım almak için bir komut seçin. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Yardımı seçin", - "MESSAGE_JOIN_SUCCESS": "✅ Bot Katıldı.", - "MESSAGE_LANG_ARGS_ERROR": "Dil parametresi yanlış, şu anda mevcut diller: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Şu anda mevcut diller: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Dil başarıyla `{{locale}}` olarak değiştirildi", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot ayrıldı.", + "MESSAGE_HELP_SELECT_MODE": "Bir komut seçin", + "MESSAGE_JOIN_SUCCESS": "✅ Ses kanalına katıldım.", + "MESSAGE_LANG_ARGS_ERROR": "Yanlış dil kodu. Mevcut diller: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Mevcut diller: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Dil `{{locale}}` olarak değiştirildi", + "MESSAGE_LEAVE_SUCCESS": "✅ Ses kanalından ayrıldım.", "REPEAT_MODE_OFF": "KAPALI", "REPEAT_MODE_SINGLE": "TEK", "REPEAT_MODE_ALL": "HEPSİ", @@ -148,29 +148,29 @@ "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı.", "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı.", "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Aramak istediğiniz müziğin adını yazın.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Müzik eklendi.", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Parça adı veya linki girin.", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Parça kuyruğa eklendi.", "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok.", "MESSAGE_PLAY_SELECT_DURATION": "Süre: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Müziği seçin", "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi.", - "MESSAGE_REMOVE_FAIL": "❌ Müzik kaldırılamadı.", - "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir şarkı seçerek **kaldırın** veya seçimi iptal etmek için başka bir şey girin. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcuttan sonra kuyrukta müzik yok.", + "MESSAGE_REMOVE_FAIL": "❌ Parça kaldırılamadı.", + "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir parça numarası seçerek **kaldırın** veya iptal için başka bir numara girin. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcut parçadan sonra kuyruk boş.", "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ Müzik devam ettirildi.", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Müzik devam ettirildi.", "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi.", "MESSAGE_SEEK_ARGS_ERROR": "❌ Hedef zaman için geçersiz format.\n(**`örn: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Arama konumu bu parçanın süresinin ötesinde. (`{{duration}}`)\nSıradaki şarkıya geçiliyor.", + "MESSAGE_SEEK_SKIP": "✅ Belirtilen konum (`{{duration}}`), parçanın süresinden uzun.\nSıradaki parçaya geçiyorum.", "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Müzik karıştırıldı.", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Çalma listesi karıştırıldı.", "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı.", - "MESSAGE_SKIP_SUCCESS": "✅ Müzik atlandı.", - "MESSAGE_STOP_SUCCESS": "✅ Müzik durduruldu.", + "MESSAGE_SKIP_SUCCESS": "✅ Parça atlandı.", + "MESSAGE_STOP_SUCCESS": "✅ Çalma durduruldu, kuyruk temizlendi.", "MESSAGE_VOLUME_ARGS_ERROR": "Mevcut ses seviyesi: **{{volume}}** 🔊\n**Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", - "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", - "MESSAGE_VOLUME_SAME": "❌ Değiştirmek istediğiniz ses seviyesi zaten mevcut ses seviyesiyle aynı.", + "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", + "MESSAGE_VOLUME_SAME": "❌ Bu ses seviyesi zaten ayarlı.", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Bu sunucu için varsayılan ses seviyesi **{{volume}}%** olarak değiştirildi! (En fazla: {{maxVolume}}%)", "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", @@ -197,7 +197,7 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste yöneticisi başlatılmadı.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste henüz yüklenmedi, sonra tekrar deneyin.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Bir Müzik komutu seçin", "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Yardımcı komut seçin", "HELP_COMMAND_ALIASES": "Takma Adlar: {{aliases}}", @@ -231,7 +231,7 @@ "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Parça kaldırılamadı. Geçerli bir indeks girdiğinizden emin olun.", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi.", "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi. (Başarılı: {{added}}, Atlanan: {{skipped}})", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi: {{added}} eklendi, {{skipped}} atlandı.", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok.", "MESSAGE_PLAYLIST_LIST_TITLE": "Sunucu Çalma Listeleri", "MESSAGE_PLAYLIST_INFO_TITLE": "Çalma Listesi Bilgisi: {{name}} ({{count}} parça)", @@ -246,7 +246,7 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ Kullanım: `{{prefix}}radio [çalma listesi adı] [kanal adı]` (örn. `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin.", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ `{{playlistName}}` çalma listesinde `{{channelQuery}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", - "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\n(örn., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\nDosya, EXTINF etiketleri içeren bir M3U formatında olmalı.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", "ERROR_RADIO_LOAD_FAILED": "❌ Radyo kanalı yüklenirken hata oluştu: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`).", diff --git a/src/locales/tr-TR/embeds.json b/src/locales/tr-TR/embeds.json index 9a4dc7d3..c9402ff8 100644 --- a/src/locales/tr-TR/embeds.json +++ b/src/locales/tr-TR/embeds.json @@ -1,41 +1,41 @@ { "DASHBOARD_TITLE": "Panel", - "DASHBOARD_FINISH_PLAYING": "Kuyruk sonlandı.", + "DASHBOARD_FINISH_PLAYING": "Kuyruğun çalınması tamamlandı.", "DASHBOARD_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{duration}}**\n────────────────────\nSes Yüksekliği: `{{volume}}%`\nDöngü: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Talep Eden:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı.", "MAINTAIN_DESCRIPTION": "Bot birkaç dakika içinde bakım moduna alınacaktır. Bu süre boyunca bazı özellikler geçici olarak kullanılamayabilir.", "MAINTAIN_TITLE": "⚠️ Bakım Notu ⚠️", - "MESSAGE_BLACKLIST": "Ses kanalında kara listeye alınmış kullanıcı(lar) bulunuyor.", + "MESSAGE_BLACKLIST": "Ses kanalında kara listedeki kullanıcılar var.", "MESSAGE_COMMAND": "Komut **{{command}}**", - "MESSAGE_FILTER": "Filtre modu seçiliyor **{{effectName}}**", - "NODE_ALL_ACTIVE": "Tüm bağlantı noktaları etkin", + "MESSAGE_FILTER": "Filtre **{{effectName}}** olarak ayarlandı", + "NODE_ALL_ACTIVE": "Tüm düğümler etkin", "NODE_CONNECTED": "Gecikme: **{{ping}}ms**", "NODE_DISCONNECTED": "**BAĞLANTI KESİLDİ**", - "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir bağlantı noktası seçin.", + "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir düğüm adı girin.", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Çekirdekler: **{{cores}}**\nSistem yükü: **{{systemLoad}}**\nLavalink yükü: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ Bilgi", - "NODE_STATUS_INFO_VALUE": "Versiyon: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nBuild time: **{{buildTime}}**", + "NODE_STATUS_INFO_VALUE": "Versiyon: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nDerleme süresi: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 Hafıza", - "NODE_STATUS_MEMORY_VALUE": "Kullanılan: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Kullanılan: **{{used}}**\nBoş: **{{free}}**\nAyrılan: **{{allocated}}**\nRezerve: **{{reservable}}**\n", "NODE_STATUS_PING": "Gecikme", "NODE_STATUS_STATS_TITLE": "📊 İstatistikler", - "NODE_STATUS_STATS_VALUE": "Bağlantı Süresi: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", - "NODE_STATUS_TITLE": "🛰️ Bağlantı Noktası", - "NODE_STATUS_TITLE_2": "🛰️ Node '{{nodeName}}' durumu", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}} tane bağlantı noktasının bağlantısı kesildi", + "NODE_STATUS_STATS_VALUE": "Bağlantı Süresi: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nOynatıcı: **{{playerCount}}**\nÇalan oynatıcılar: **{{playingCount}}**", + "NODE_STATUS_TITLE": "🛰️ Düğüm Durumu", + "NODE_STATUS_TITLE_2": "🛰️ Düğüm '{{nodeName}}' durumu", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}} düğümün bağlantısı kesildi", "PING_DESCRIPTION": "Bot: **{{botPing}}**\nAPI: **{{apiPing}}ms**", - "PING_TITLE": "🛰️ Gecikme'", + "PING_TITLE": "🛰️ Gecikme", "QUEUE_ADD_PLAYLIST": "Oynatma listesi eklendi", "QUEUE_ADD_TRACK": "Parça eklendi", "QUEUE_LIST_LOOP_MODE": "Döngü: {{repeatMode}}", - "QUEUE_LIST_TITLE": "Kuyruktaki Liste", + "QUEUE_LIST_TITLE": "Kuyruk Listesi", "REMOVE_LIST_LOOP_MODE": "Döngü: {{repeatMode}}", - "REMOVE_LIST_TITLE": "Liste kaldırıldı.", - "REMOVE_TRACK_TITLE": "Parça kaldırıldı.", - "SERVER_TITLE": "**{{name}}** bulunan sunucular", + "REMOVE_LIST_TITLE": "Kaldırma Listesi", + "REMOVE_TRACK_TITLE": "Kaldırılan Parça", + "SERVER_TITLE": "**{{name}}** olan sunucular", "SERVER_FIELD_DJ_ROLE": "DJ Rolü", "SERVER_FIELD_ADMINS": "Yöneticiler", "SERVER_FIELD_DJ_USERS": "DJ Kullanıcıları", diff --git a/src/locales/tr-TR/events.json b/src/locales/tr-TR/events.json index ed921011..e7dca23c 100644 --- a/src/locales/tr-TR/events.json +++ b/src/locales/tr-TR/events.json @@ -1,5 +1,5 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ ⚠ Sunucu verileri önbelleğe alınamadı!", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Sunucu verileri önbellekten alınamadı.", "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz.", "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın.", "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz.", @@ -16,10 +16,10 @@ "MESSAGE_NOW_PLAYING_TITLE": "Şuan Çalıyor: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Sayfa {{curPage}}/{{maxPage}} -----", "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz.", - "LOOP_MODE_OFF": "Off", - "LOOP_MODE_SINGLE": "Single", - "LOOP_MODE_ALL": "All", + "LOOP_MODE_OFF": "Kapalı", + "LOOP_MODE_SINGLE": "Tek", + "LOOP_MODE_ALL": "Tümü", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim.", - "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}`", + "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}` olarak ayarlandı", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Bu kanalda komut kullanamazsınız! Lütfen <#{{channelId}}> metin kanalını kullanın." } \ No newline at end of file diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index c82fb8d1..fda67fe1 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -9,12 +9,12 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> 已被加入黑名單。", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> 已從黑名單中移除。", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> 已在黑名單中。", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> 不在動態黑名單中。", - "MESSAGE_BLACKLIST_LIST_TITLE": "動態黑名單", - "MESSAGE_BLACKLIST_LIST_EMPTY": "動態黑名單為空。", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> 不在黑名單中。", + "MESSAGE_BLACKLIST_LIST_TITLE": "黑名單", + "MESSAGE_BLACKLIST_LIST_EMPTY": "黑名單目前是空的。", "CONFIG_CLEAR_DESCRIPTION": "清除隊列中的所有音樂", "CONFIG_CLEAR_USAGE": "clear", - "CONFIG_DASHBOARD_DESCRIPTION": "將儀表板更新到底部", + "CONFIG_DASHBOARD_DESCRIPTION": "將儀表板移至底部", "CONFIG_DASHBOARD_USAGE": "dashboard", "CONFIG_DJ_DESCRIPTION": "將使用者添加為 DJ 或顯示 DJ 列表", "CONFIG_DJ_USAGE": "dj [@使用者]", @@ -37,8 +37,8 @@ "CONFIG_LOOP_OPTION_DESCRIPTION": "音樂循環模式", "CONFIG_MOVE_DESCRIPTION": "交換隊列中兩首歌曲的位置", "CONFIG_MOVE_USAGE": "move <位置1> <位置2>", - "CONFIG_MOVE_OPTION_DESCRIPTION": "交換歌曲位置1", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "交換歌曲位置2", + "CONFIG_MOVE_OPTION_DESCRIPTION": "歌曲位置 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "歌曲位置 2", "CONFIG_NODE_DESCRIPTION": "顯示節點的運作狀態", "CONFIG_NODE_USAGE": "node [節點名稱]", "CONFIG_NODE_OPTION_DESCRIPTION": "節點名稱", @@ -62,9 +62,9 @@ "CONFIG_QUEUE_DESCRIPTION": "顯示當前播放列表", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "從播放列表中選擇要移除的歌曲", - "CONFIG_REMOVE_USAGE": "remove <曲目索引號> [從索引到索引2的曲目]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "曲目索引號", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "從索引到索引2的曲目", + "CONFIG_REMOVE_USAGE": "remove <曲目編號> [起始到結束]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "曲目編號", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "曲目範圍", "CONFIG_RESUME_DESCRIPTION": "恢復播放已暫停的曲目", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "輸入歌曲名稱以搜尋", @@ -75,7 +75,7 @@ "CONFIG_SEEK_OPTION_DESCRIPTION": "目標時間 (例如: 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "顯示當前活躍的伺服器", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "隨機播放列表", + "CONFIG_SHUFFLE_DESCRIPTION": "隨機打亂播放清單", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "跳過當前曲目", "CONFIG_SKIP_USAGE": "skip", @@ -98,7 +98,7 @@ "ERROR_PLAY_JOIN_CHANNEL": "❌ 我無法加入語音頻道。", "ERROR_PLAY_MUSIC": "❌ 服務遇到一些問題,請稍後再試。({{reason}})", "ERROR_PLAY_SEARCH": "❌ 找不到結果。({{reason}})", - "ERROR_TIME_EXPIRED": "❌ 時間已過期。", + "ERROR_TIME_EXPIRED": "❌ 等待時間已逾時。", "MESSAGE_CLEAR_SUCCESS": "✅ 播放清單已清空。", "MESSAGE_DASHBOARD_SUCCESS": "✅ 控制面板已更新。", "MESSAGE_DJ_NO_PERMISSION": "您需要 DJ 或管理員權限才能添加 DJ。", @@ -156,20 +156,20 @@ "MESSAGE_REMOVE_CANCEL": "✅ 已取消移除。", "MESSAGE_REMOVE_FAIL": "❌ 音樂移除失敗。", "MESSAGE_REMOVE_INSTRUCTION": "從 **1** 到 **{{length}}** 中選擇一首歌曲來 **移除**,或輸入其他內容以取消選擇。 ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 當前播放之後沒有音樂。", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 目前曲目之後的佇列是空的。", "MESSAGE_RESUME_FAIL": "❌ 音樂恢復失敗。", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ 音樂已恢復播放。", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 音樂已恢復播放。", "MESSAGE_RESUME_SUCCESS": "▶️ 音樂已恢復播放。", "MESSAGE_SEEK_ARGS_ERROR": "❌ 目標時間格式無效。\n(**`例如: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ 搜尋位置超出了此曲目的時長。(`{{duration}}`)\n正在跳到下一首歌曲。", + "MESSAGE_SEEK_SKIP": "✅ 指定的位置 (`{{duration}}`) 超出此曲目的時長。\n正在跳到下一首歌曲。", "MESSAGE_SEEK_SUCCESS": "✅ 音樂已跳轉至 `{{duration}}`。", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 音樂已隨機播放。", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 播放清單已打亂。", "MESSAGE_SKIP_FAIL": "❌ 音樂跳過失敗。", "MESSAGE_SKIP_SUCCESS": "✅ 音樂已跳過。", - "MESSAGE_STOP_SUCCESS": "✅ 音樂已停止。", + "MESSAGE_STOP_SUCCESS": "✅ 已停止播放並清空佇列。", "MESSAGE_VOLUME_ARGS_ERROR": "當前音量:**{{volume}}** 🔊\n**要更改音量,請輸入 `1` 到 `{{maxVolume}}` 之間的數字。**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **輸入 `1` 到 `{{maxVolume}}` 之間的數字以更改音量。**", - "MESSAGE_VOLUME_SELECT": "當前音量:**{{volume}}%** 🔊\\n**請選擇您要設置的音量:**", + "MESSAGE_VOLUME_SELECT": "當前音量:**{{volume}}%** 🔊\n**請選擇您要設置的音量:**", "MESSAGE_VOLUME_SAME": "❌ 您要更改的音量已經是當前音量。", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 此伺服器的預設音量已更改為 **{{volume}}%**! (最大值: {{maxVolume}}%)", @@ -192,7 +192,7 @@ "UNIT_HOURS": "小時", "UNIT_MINUTES": "分", "UNIT_SECONDS": "秒", - "UNIT_BYTES": "Bytes", + "UNIT_BYTES": "位元組", "UNIT_KB": "KB", "UNIT_MB": "MB", "UNIT_GB": "GB", @@ -246,7 +246,7 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ 正確用法: `{{prefix}}radio [播放清單名稱] [電台名稱]` (例如: `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ 請同時輸入播放清單名稱與電台頻道名稱。", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 在播放清單 `{{playlistName}}` 中找不到與 `{{channelQuery}}` 符合的電台頻道。\n可用範例: {{samples}} 等", - "ERROR_RADIO_NOT_READY": "❌ 電台播放清單尚未就緒。\n請先使用 `/playlist import {{name}} ` 指令匯入電台網址清單。\n(例如: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ 電台播放清單尚未就緒。\n請先使用 `/playlist import {{name}} ` 指令匯入電台網址清單。\n檔案必須是包含 EXTINF 標籤的 M3U 格式。", "ERROR_RADIO_NO_MATCH": "❌ 找不到與 `{{query}}` 符合的電台頻道。\n可用範例: {{samples}} 等", "ERROR_RADIO_LOAD_FAILED": "❌ 載入電台頻道時發生錯誤: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ 無法載入或找不到電台串流 (`{{title}}`)。", diff --git a/src/locales/zh-TW/embeds.json b/src/locales/zh-TW/embeds.json index 60f295d2..0f005bc4 100644 --- a/src/locales/zh-TW/embeds.json +++ b/src/locales/zh-TW/embeds.json @@ -1,6 +1,6 @@ { "DASHBOARD_TITLE": "控制面板", - "DASHBOARD_FINISH_PLAYING": "音樂播放已結束", + "DASHBOARD_FINISH_PLAYING": "播放清單播放已結束", "DASHBOARD_SUBTITLE": "作者: **{{author}}**\n時長: **{{duration}}**\n────────────────────\n音量: `{{volume}}%`\n循環模式: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**請求者:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", @@ -9,7 +9,7 @@ "MAINTAIN_TITLE": "⚠️ 維護通知 ⚠️", "MESSAGE_BLACKLIST": "語音頻道中有被列入黑名單的用戶。", "MESSAGE_COMMAND": "指令 **{{command}}**", - "MESSAGE_FILTER": "將效果模式設置為 **{{effectName}}**", + "MESSAGE_FILTER": "效果已設置為:**{{effectName}}**", "NODE_ALL_ACTIVE": "所有節點均已啟用", "NODE_CONNECTED": "延遲: **{{ping}}ms**", "NODE_DISCONNECTED": "**已斷線**", @@ -19,10 +19,10 @@ "NODE_STATUS_INFO_TITLE": "🏷️ 資訊", "NODE_STATUS_INFO_VALUE": "版本: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\n構建時間: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 記憶體", - "NODE_STATUS_MEMORY_VALUE": "已使用: **{{used}}**\n可用: **{{free}}**\n分配: **{{allocated}}**\n可保留: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "已使用: **{{used}}**\n可用: **{{free}}**\n分配: **{{allocated}}**\n已保留: **{{reservable}}**\n", "NODE_STATUS_PING": "延遲", "NODE_STATUS_STATS_TITLE": "📊 統計", - "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n正在播放的播放器: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n播放中: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ 節點狀態", "NODE_STATUS_TITLE_2": "🛰️ 節點 '{{nodeName}}' 狀態", "NODE_UNHEALTHY": "⚠️ 有 {{unhealthValue}} 個節點已斷線", @@ -42,7 +42,7 @@ "STATUS_DESCRIPTION": "**• 服務於 {{serverCount}} 個伺服器**\n**• 正在 {{playingCount}} 個伺服器上播放**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "STATUS_LATENCY_TITLE": "🛰️ 延遲", "STATUS_LATENCY_VALUE": "機器人: **{{botPing}}**\nAPI: **{{apiPing}} 毫秒**", - "STATUS_SYSTEM_TITLE": "⚙️ SYSTEM", + "STATUS_SYSTEM_TITLE": "⚙️ 系統", "STATUS_SYSTEM_VALUE": "作業系統: **{{os_version}}**\nNode.js: **{{node_version}}**\nDiscord.js: **{{dc_version}}**\nLavaShark: **{{shark_version}}**\nCPU: **{{cpu}}**\n運行時間: **{{uptime}}**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "STATUS_USAGE_TITLE": "📊 資源使用", "STATUS_USAGE_VALUE": "CPU: **{{cpuUsage}}**\nRam: **{{ramUsage}}**\nHeap: **{{heapUsage}}**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", diff --git a/src/locales/zh-TW/events.json b/src/locales/zh-TW/events.json index 79a6ca21..3e987074 100644 --- a/src/locales/zh-TW/events.json +++ b/src/locales/zh-TW/events.json @@ -11,15 +11,15 @@ "ERROR_TIME_EXPIRED": "❌ 時間已過期。", "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 機器人已離開頻道。", "MESSAGE_BOT_STOP": "✅ 機器人已停止。", - "MESSAGE_MUSIC_SHUFFLE": "✅ 音樂已隨機播放。", + "MESSAGE_MUSIC_SHUFFLE": "✅ 播放清單已打亂。", "MESSAGE_NOW_PLAYING_SUBTITLE": "作者: **{{author}}**\n時長: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "正在播放: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 第 {{curPage}}/{{maxPage}} 頁 -----", "MESSAGE_SELECT_LOOP_MODE": "選擇歌曲循環模式。", - "LOOP_MODE_OFF": "Off", - "LOOP_MODE_SINGLE": "Single", - "LOOP_MODE_ALL": "All", + "LOOP_MODE_OFF": "關閉", + "LOOP_MODE_SINGLE": "單曲", + "LOOP_MODE_ALL": "全部", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ 我已將歌曲名稱發送至您的私人訊息中。", - "MESSAGE_SET_LOOP_MODE": "✅ 循環模式設置為 `{{mode}}`", - "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。\n" + "MESSAGE_SET_LOOP_MODE": "✅ 已選擇循環模式:`{{mode}}`", + "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。" } \ No newline at end of file diff --git a/src/utils/languageUtils.ts b/src/utils/languageUtils.ts index 4caaf54c..174849a4 100644 --- a/src/utils/languageUtils.ts +++ b/src/utils/languageUtils.ts @@ -2,7 +2,7 @@ const LANGUAGE_NAMES: Record = { 'en-US': 'English (en-US)', 'ko-KR': '한국어 (ko-KR)', 'ru-RU': 'Русский (ru-RU)', - 'sr-RS': 'Српски (sr-RS)', + 'sr-RS': 'Srpski (sr-RS)', 'tr-TR': 'Türkçe (tr-TR)', 'zh-TW': '繁體中文 (zh-TW)', 'ar': 'العربية (ar)', From 4364eee4e5538cfd2a153ca1edab8981d33412f1 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 09:40:31 +0900 Subject: [PATCH 06/49] fix: prevent voice connection flood and sanitize fallback search in play playlist command --- .../playlist/PlayPlaylistSubcommand.ts | 99 ++++++++++++------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 754e02be..b850bf52 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -185,7 +185,9 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { ? command.getMessage() : command.getInteraction(); try { - await player.connect(); + if (!player.voiceChannelId || player.voiceChannelId !== voiceChannel.id) { + await player.connect(); + } player.metadata = metadata; } catch (error) { bot.logger.error( @@ -227,21 +229,43 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { context: PlaylistSubcommandContext, playlistTrack: PlaylistTrack, ) { - const queries = [ - playlistTrack.encoded, - playlistTrack.url, - `ytsearch:${playlistTrack.title}`, - ].filter((query): query is string => Boolean(query)); + if (playlistTrack.encoded) { + try { + const result = await context.client.lavashark.search(playlistTrack.encoded); + if (result.tracks.length > 0) return result.tracks[0]; + } catch { + // Try next query + } + } - for (const query of new Set(queries)) { + if (playlistTrack.url && (playlistTrack.url.startsWith('http://') || playlistTrack.url.startsWith('https://'))) { try { - const result = await context.client.lavashark.search(query); + const result = await context.client.lavashark.search(playlistTrack.url); if (result.tracks.length > 0) return result.tracks[0]; } catch { - // Try the next fallback query. + // Try next query } } + const cleanTitle = playlistTrack.title + ?.replace(/\.(mp3|flac|wav|aac|ogg|m4a|webm)$/i, '') + .trim(); + + if (!cleanTitle || cleanTitle.length < 3) { + return null; + } + + const searchQuery = playlistTrack.author + ? `ytsearch:${playlistTrack.author} - ${cleanTitle}` + : `ytsearch:${cleanTitle}`; + + try { + const result = await context.client.lavashark.search(searchQuery); + if (result.tracks.length > 0) return result.tracks[0]; + } catch { + // Track search failed + } + return null; } @@ -256,38 +280,39 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { ): Promise { let added = 0; let skipped = 0; + const tracks = playlist.tracks ?? []; + const BATCH_SIZE = 5; + + for (let i = 0; i < tracks.length; i += BATCH_SIZE) { + const chunk = tracks.slice(i, i + BATCH_SIZE); + const promises = chunk.map(async (playlistTrack) => { + const limit = QueueLimitManager.canAddSongs( + context.bot, + player, + context.command.user.id, + member, + 1, + ); + if (!limit.canAdd) { + return null; + } - for (const playlistTrack of playlist.tracks ?? []) { - // Check both per-user and global limits before each search - const limit = QueueLimitManager.canAddSongs( - context.bot, - player, - context.command.user.id, - member, - 1, - ); - if (!limit.canAdd) { - skipped++; - continue; - } + try { + return await this.findTrack(context, playlistTrack); + } catch { + return null; + } + }); - try { - const track = await this.findTrack(context, playlistTrack); - if (!track) { + const results = await Promise.all(promises); + for (const track of results) { + if (track) { + const requester = context.command.user as unknown as PlayerRequester; + player.addTracks(track, requester); + added++; + } else { skipped++; - continue; } - - // LavaShark resolves discord.js through a separate type entry point. - const requester = context.command.user as unknown as PlayerRequester; - player.addTracks(track, requester); - added++; - } catch (error) { - context.bot.logger.error( - context.bot.shardId, - `[PlaylistCommand] Error loading track ${playlistTrack.title}: ${error}`, - ); - skipped++; } } From 1e98ba9e4df6070479400b990c413a1d30590eb6 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 09:42:51 +0900 Subject: [PATCH 07/49] feat: add M3U playlist import toggle flag and command with disabled-by-default policy --- config.js | 6 +++ src/@types/index.ts | 8 +++- src/commands/PlaylistCommand.ts | 17 ++++++++ .../playlist/BasePlaylistSubcommand.ts | 3 +- .../playlist/ImportPlaylistSubcommand.ts | 8 ++++ src/commands/playlist/ToggleM3uSubcommand.ts | 43 +++++++++++++++++++ src/locales/en-US/commands.json | 6 ++- src/locales/ko-KR/commands.json | 6 ++- src/locales/ru-RU/commands.json | 6 ++- src/locales/sr-RS/commands.json | 6 ++- src/locales/tr-TR/commands.json | 6 ++- src/locales/zh-TW/commands.json | 6 ++- 12 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 src/commands/playlist/ToggleM3uSubcommand.ts diff --git a/config.js b/config.js index 071f67f1..ff5a279c 100644 --- a/config.js +++ b/config.js @@ -177,6 +177,12 @@ const config = { enabled : false // Enable/disable persistent queue storage }, + // Playlist settings + playlist: { + enabled : true, // Enable/disable playlist feature + allowM3uImport : false // Enable/disable M3U playlist import (disabled by default) + }, + // SQLite database settings database: { path : './data/database.db' // Path to SQLite database file diff --git a/src/@types/index.ts b/src/@types/index.ts index 88f7f871..b259bea7 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -114,12 +114,18 @@ export type Config = { bot: BotConfig; nodeList: NodeOptions[]; spotify: SpotifyConfig; - blacklist: string[], + blacklist: string[]; webDashboard: WebDashboardConfig; localNode: LocalNodeConfig; command: CommandConfig; database: DatabaseConfig; queuePersistence: QueuePersistenceConfig; + playlist: PlaylistConfig; +}; + +export type PlaylistConfig = { + enabled: boolean; + allowM3uImport: boolean; }; export type BotConfig = { diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 5d86929f..5b58929b 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -10,6 +10,7 @@ import { PlayPlaylistSubcommand } from './playlist/PlayPlaylistSubcommand.js'; import { RemovePlaylistTrackSubcommand } from './playlist/RemovePlaylistTrackSubcommand.js'; import { SavePlaylistSubcommand } from './playlist/SavePlaylistSubcommand.js'; import { ShowPlaylistInfoSubcommand } from './playlist/ShowPlaylistInfoSubcommand.js'; +import { ToggleM3uSubcommand } from './playlist/ToggleM3uSubcommand.js'; import type { ApplicationCommandStringOptionData, @@ -36,6 +37,7 @@ export class PlaylistCommand extends BaseCommand { new RemovePlaylistTrackSubcommand(), new SavePlaylistSubcommand(), new ShowPlaylistInfoSubcommand(), + new ToggleM3uSubcommand(), ]; /** @@ -108,6 +110,13 @@ export class PlaylistCommand extends BaseCommand { ], type: ApplicationCommandOptionType.Subcommand, }, + { + description: i18next.t( + 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U', + ), + name: 'toggle-m3u', + type: ApplicationCommandOptionType.Subcommand, + }, ], sendTyping: true, showHelp: true, @@ -124,6 +133,14 @@ export class PlaylistCommand extends BaseCommand { client: Client, command: CommandContext, ): Promise { + if (bot.config.playlist?.enabled === false) { + await command.replyEphemeralError( + bot, + command.t('commands:ERROR_PLAYLIST_DISABLED'), + ); + return; + } + // Ensure playlist storage is available before dispatching const playlistManager = bot.playlistManager; if (!playlistManager) { diff --git a/src/commands/playlist/BasePlaylistSubcommand.ts b/src/commands/playlist/BasePlaylistSubcommand.ts index 1a327a84..f8da9ce8 100644 --- a/src/commands/playlist/BasePlaylistSubcommand.ts +++ b/src/commands/playlist/BasePlaylistSubcommand.ts @@ -20,7 +20,8 @@ export type PlaylistSubcommandName = | 'list' | 'play' | 'remove-track' - | 'save'; + | 'save' + | 'toggle-m3u'; /** * Dependencies shared by playlist subcommand executions diff --git a/src/commands/playlist/ImportPlaylistSubcommand.ts b/src/commands/playlist/ImportPlaylistSubcommand.ts index 73962555..07d39f57 100644 --- a/src/commands/playlist/ImportPlaylistSubcommand.ts +++ b/src/commands/playlist/ImportPlaylistSubcommand.ts @@ -23,6 +23,14 @@ export class ImportPlaylistSubcommand extends BasePlaylistSubcommand { * Validate import arguments and confirm replacement when required */ public async execute(context: PlaylistSubcommandContext): Promise { + if (!context.bot.config.playlist?.allowM3uImport) { + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_M3U_DISABLED'), + ); + return; + } + const args = this.getImportArguments(context.command); if (!args) { await context.command.replyEphemeralError( diff --git a/src/commands/playlist/ToggleM3uSubcommand.ts b/src/commands/playlist/ToggleM3uSubcommand.ts new file mode 100644 index 00000000..59dc4a1a --- /dev/null +++ b/src/commands/playlist/ToggleM3uSubcommand.ts @@ -0,0 +1,43 @@ +import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; +import { PermissionFlagsBits } from 'discord.js'; + +import type { + PlaylistSubcommandContext, + PlaylistSubcommandName, +} from './BasePlaylistSubcommand.js'; + +export class ToggleM3uSubcommand extends BasePlaylistSubcommand { + public readonly name: PlaylistSubcommandName = 'toggle-m3u'; + + public async execute(context: PlaylistSubcommandContext): Promise { + const { bot, command } = context; + const member = command.member; + + const isAdmin = bot.config.bot.admin.includes(command.user.id) || + member?.permissions.has(PermissionFlagsBits.Administrator) || + member?.permissions.has(PermissionFlagsBits.ManageGuild); + + if (!isAdmin) { + await command.replyEphemeralError( + bot, + command.t('commands:MESSAGE_DJ_ADMIN_ONLY'), + ); + return; + } + + if (!bot.config.playlist) { + bot.config.playlist = { + enabled: true, + allowM3uImport: false, + }; + } + + bot.config.playlist.allowM3uImport = !bot.config.playlist.allowM3uImport; + const state = bot.config.playlist.allowM3uImport ? 'ON' : 'OFF'; + + await command.replySuccess( + bot, + command.t('commands:MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS', { state }), + ); + } +} diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index d6ef0ed3..4946955f 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ Importing M3U playlists is currently disabled.", + "ERROR_PLAYLIST_DISABLED": "❌ Playlist features are currently disabled.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Toggle allowing M3U playlist imports.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U playlist import permission has been set to `{{state}}`." } diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index edb2197a..f582aa43 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U 플레이리스트 가져오기 기능이 비활성화되어 있습니다.", + "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U 플레이리스트 가져오기 기능 허용 여부를 설정합니다.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U 플레이리스트 가져오기 기능이 `{{state}}`(으)로 설정되었습니다." } \ No newline at end of file diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 9cf462bb..d733bfc3 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ Импорт M3U-плейлистов отключён.", + "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Переключить разрешение на импорт M3U-плейлистов.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Разрешение на импорт M3U-плейлистов установлено в `{{state}}`." } diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 9e678800..832c5e6e 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ Uvoz M3U plejlisti je trenutno onemogućen.", + "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Uključi/isključi dozvolu za uvoz M3U plejlisti.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Dozvola za uvoz M3U plejlisti je postavljena na `{{state}}`." } \ No newline at end of file diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index f29f03e2..319ec212 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U çalma listelerini içe aktarma şu anda devre dışı.", + "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U çalma listesi içe aktarma iznini değiştirin.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U çalma listesi içe aktarma izni `{{state}}` olarak ayarlandı." } \ No newline at end of file diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index fda67fe1..513776e1 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 儲存確認逾時。", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。" + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ 目前已停用匯入 M3U 播放清單功能。", + "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "切換是否允許匯入 M3U 播放清單。", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ 匯入 M3U 播放清單權限已設定為 `{{state}}`。" } \ No newline at end of file From 92697a47b6f6ce6f0f14b0457ee6bbf6e23ff654 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 09:50:07 +0900 Subject: [PATCH 08/49] fix: resolve zombie player leaks, optimize queue restore batching, and handle command execution errors --- src/commands/JoinCommand.ts | 1 + src/commands/PlayCommand.ts | 1 + src/commands/PlayFirstCommand.ts | 1 + src/commands/RadioCommand.ts | 1 + src/commands/SearchCommand.ts | 1 + .../playlist/PlayPlaylistSubcommand.ts | 1 + src/events/discord/InteractionCreateEvent.ts | 16 +++- src/lib/QueuePersistence.ts | 73 +++++++++++-------- 8 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/commands/JoinCommand.ts b/src/commands/JoinCommand.ts index 8ee56e04..b4577544 100644 --- a/src/commands/JoinCommand.ts +++ b/src/commands/JoinCommand.ts @@ -62,6 +62,7 @@ export class JoinCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return; } diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index e7f2967e..050017d3 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -141,6 +141,7 @@ export class PlayCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return null; } diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 3f01360c..e64af035 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -141,6 +141,7 @@ export class PlayFirstCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return null; } diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index be045102..1731be59 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -128,6 +128,7 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; } catch (error) { bot.logger.error(bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return; } diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index f6921a79..8b46a63e 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -150,6 +150,7 @@ export class SearchCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return null; } diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index b850bf52..c4429d2e 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -198,6 +198,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { bot, command.t('commands:ERROR_PLAY_JOIN_CHANNEL'), ); + await player.destroy(); return null; } diff --git a/src/events/discord/InteractionCreateEvent.ts b/src/events/discord/InteractionCreateEvent.ts index cfbb6f18..abb25204 100644 --- a/src/events/discord/InteractionCreateEvent.ts +++ b/src/events/discord/InteractionCreateEvent.ts @@ -245,10 +245,20 @@ export class InteractionCreateEvent extends BaseDiscordEvent {}); + } + } } } diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 92c4fc13..6373324a 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -290,48 +290,59 @@ export class QueuePersistence { } } - // Restore tracks - try encoded track string first, fall back to URI search - for (const serializedTrack of queueData.tracks) { - try { - let result = null; - - // Try loading by encoded track string first + // Restore tracks in batch chunks to prevent performance bottlenecks + const BATCH_SIZE = 5; + for (let i = 0; i < queueData.tracks.length; i += BATCH_SIZE) { + const chunk = queueData.tracks.slice(i, i + BATCH_SIZE); + const restoredTracks = await Promise.all(chunk.map(async (serializedTrack) => { try { - result = await client.lavashark.search(serializedTrack.track); - } catch (_) { - // Encoded string failed, will try fallback - } + let result = null; - // Fallback: search by URI - if (!result || !result.tracks || result.tracks.length === 0) { + // Try loading by encoded track string first try { - result = await client.lavashark.search(serializedTrack.info.uri); + result = await client.lavashark.search(serializedTrack.track); } catch (_) { - // URI search failed, will try title search + // Encoded string failed, will try fallback } - } - // Final fallback: search by title + author - if (!result || !result.tracks || result.tracks.length === 0) { - try { - result = await client.lavashark.search(`ytsearch:${serializedTrack.info.title} ${serializedTrack.info.author}`); - } catch (_) { - // All methods failed + // Fallback: search by URI + if (!result || !result.tracks || result.tracks.length === 0) { + try { + result = await client.lavashark.search(serializedTrack.info.uri); + } catch (_) { + // URI search failed, will try title search + } } + + // Final fallback: search by title + author + if (!result || !result.tracks || result.tracks.length === 0) { + try { + result = await client.lavashark.search(`ytsearch:${serializedTrack.info.title} ${serializedTrack.info.author}`); + } catch (_) { + // All methods failed + } + } + + if (result && result.tracks && result.tracks.length > 0) { + const track = result.tracks[0]; + track.requester = { + id: serializedTrack.requesterId, + tag: serializedTrack.requesterTag + } as any; + return track; + } else { + this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); + } + } catch (error) { + this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); } + return null; + })); - if (result && result.tracks && result.tracks.length > 0) { - const track = result.tracks[0]; - track.requester = { - id: serializedTrack.requesterId, - tag: serializedTrack.requesterTag - } as any; + for (const track of restoredTracks) { + if (track) { player.queue.add(track); - } else { - this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); } - } catch (error) { - this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); } } From e4fd15a7dfa7a45b20c636c726f1cffb8644b896 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 09:55:17 +0900 Subject: [PATCH 09/49] fix: replace loose equality, resolve collector race conditions, and guard async player cleanup --- src/App.ts | 8 ++++++-- src/commands/RemoveCommand.ts | 5 ++--- src/commands/SearchCommand.ts | 6 +++--- src/events/discord/VoiceStateUpdateEvent.ts | 5 ++++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/App.ts b/src/App.ts index 18fe02f3..9b318a89 100644 --- a/src/App.ts +++ b/src/App.ts @@ -227,6 +227,10 @@ const main = async () => { main(); -process.on('unhandledRejection', (error) => { - console.error('Unhandled promise rejection:', error); +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Promise Rejection at:', promise, 'reason:', reason); +}); + +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception thrown:', error); }); diff --git a/src/commands/RemoveCommand.ts b/src/commands/RemoveCommand.ts index a5bd541c..4d39a336 100644 --- a/src/commands/RemoveCommand.ts +++ b/src/commands/RemoveCommand.ts @@ -209,6 +209,8 @@ export class RemoveCommand extends BaseCommand { }); collector.on('collect', async (query: Message) => { + collector.stop(); + const index = parseInt(query.content); if (!index || index <= 0 || index > tracks.length) { @@ -224,7 +226,6 @@ export class RemoveCommand extends BaseCommand { allowedMentions: { repliedUser: false } }); } - collector.stop(); return; } @@ -243,8 +244,6 @@ export class RemoveCommand extends BaseCommand { msg.delete().catch(() => bot.logger.discord( bot.shardId, 'Failed to delete message.') ); - - collector.stop(); }); collector.on('end', async (collected: ReadonlyCollection>, reason: string) => { diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index 8b46a63e..d0e26571 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -298,7 +298,7 @@ export class SearchCommand extends BaseCommand { }); collector.on('collect', async (i: StringSelectMenuInteraction) => { - if (i.customId != SelectButtonId.Music) return; + if (i.customId !== SelectButtonId.Music) return; // Check queue limits before adding selected track const checkResult = QueueLimitManager.canAddSongs(bot, player, userId, guildMember, 1); @@ -318,7 +318,7 @@ export class SearchCommand extends BaseCommand { const requester = context.isMessage() ? context.getMessage().author : context.getInteraction().user; const curVolume = player.setting.volume ?? bot.guildVolumeManager?.get(player.guildId) ?? bot.config.bot.volume.default; - player.addTracks(res.tracks.find((x: any) => x.uri == i.values[0])!, requester as any); + player.addTracks(res.tracks.find((x: any) => x.uri === i.values[0])!, requester as any); if (!player.playing) { player.filters.setVolume(curVolume); @@ -345,7 +345,7 @@ export class SearchCommand extends BaseCommand { }); collector.on('end', async (collected: Collection, reason: string) => { - if (reason == 'time' && collected.size == 0) { + if (reason === 'time' && collected.size === 0) { if (!player.playing) { player.destroy(); } diff --git a/src/events/discord/VoiceStateUpdateEvent.ts b/src/events/discord/VoiceStateUpdateEvent.ts index 672674ff..d94676e2 100644 --- a/src/events/discord/VoiceStateUpdateEvent.ts +++ b/src/events/discord/VoiceStateUpdateEvent.ts @@ -211,7 +211,10 @@ export class VoiceStateUpdateEvent extends BaseDiscordEvent Date: Wed, 12 Aug 2026 10:10:05 +0900 Subject: [PATCH 10/49] feat: Add dropdown select menu and choices to nodestatus command --- src/commands/NodeStatusCommand.ts | 60 +++++++++++++++- src/events/discord/InteractionCreateEvent.ts | 2 + src/lib/handlers/NodeSelectHandler.ts | 76 ++++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/lib/handlers/NodeSelectHandler.ts diff --git a/src/commands/NodeStatusCommand.ts b/src/commands/NodeStatusCommand.ts index 397272f0..7a059124 100644 --- a/src/commands/NodeStatusCommand.ts +++ b/src/commands/NodeStatusCommand.ts @@ -1,3 +1,4 @@ +import { ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js'; import i18next from 'i18next'; import { NodeState } from 'lavashark'; @@ -11,7 +12,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NodeStatusCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(bot: Bot): CommandMetadata { + const choices = (bot.config.nodeList || []).slice(0, 25).map(node => ({ + name: node.id, + value: node.id + })); + return { name: 'nodestatus', aliases: ['node', 'nodes', 'nodesstatus'], @@ -26,7 +32,8 @@ export class NodeStatusCommand extends BaseCommand { name: 'nodename', description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION'), type: 3, - required: false + required: false, + choices: choices.length > 0 ? choices : undefined } ] }; @@ -72,8 +79,32 @@ export class NodeStatusCommand extends BaseCommand { bot.logger.log( bot.shardId, 'nodesStatus: ' + JSON.stringify(nodesStatus)); + const components = []; + if (nodes.length > 0) { + const selectOptions = nodes.slice(0, 25).map((node, i) => { + const ping = pingList[i]; + const isConnected = ping !== -1; + return { + label: node.identifier, + value: node.identifier, + description: isConnected + ? `${context.t('embeds:NODE_STATUS_PING')}: ${ping}ms` + : context.t('embeds:NODE_DISCONNECTED').replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(context.t('commands:CONFIG_NODE_OPTION_DESCRIPTION')) + .addOptions(selectOptions); + + components.push(new ActionRowBuilder().addComponents(selectMenu)); + } + await context.reply({ embeds: [embeds.nodesStatus(bot, unhealthValue, nodesStatus, context.language)], + components, allowedMentions: { repliedUser: false } }); } @@ -108,8 +139,32 @@ export class NodeStatusCommand extends BaseCommand { bot.logger.log( bot.shardId, 'nodeStats: ' + JSON.stringify(nodeStats)); bot.logger.log( bot.shardId, 'nodePing: ' + nodePing + 'ms'); + const components = []; + if (nodes.length > 0) { + const selectOptions = nodes.slice(0, 25).map(n => { + const isConnected = n.state === NodeState.CONNECTED; + return { + label: n.identifier, + value: n.identifier, + default: n.identifier === nodeName, + description: isConnected + ? `${context.t('embeds:NODE_STATUS_PING')}: ${nodePing}ms` + : context.t('embeds:NODE_DISCONNECTED').replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(context.t('commands:CONFIG_NODE_OPTION_DESCRIPTION')) + .addOptions(selectOptions); + + components.push(new ActionRowBuilder().addComponents(selectMenu)); + } + await context.reply({ embeds: [embeds.nodeStatus(bot, nodeName, nodeInfo, nodeStats, nodePing, context.language)], + components, allowedMentions: { repliedUser: false } }); return; @@ -128,3 +183,4 @@ export class NodeStatusCommand extends BaseCommand { }); } } + diff --git a/src/events/discord/InteractionCreateEvent.ts b/src/events/discord/InteractionCreateEvent.ts index abb25204..318cbd8b 100644 --- a/src/events/discord/InteractionCreateEvent.ts +++ b/src/events/discord/InteractionCreateEvent.ts @@ -13,6 +13,7 @@ import { ShuffleButtonHandler } from '../../lib/handlers/ShuffleButtonHandler.js import { MusicSaveButtonHandler } from '../../lib/handlers/MusicSaveButtonHandler.js'; import { QueueButtonHandler } from '../../lib/handlers/QueueButtonHandler.js'; import { LanguageSelectHandler } from '../../lib/handlers/LanguageSelectHandler.js'; +import { NodeSelectHandler } from '../../lib/handlers/NodeSelectHandler.js'; import { DashboardButtonId, QueueButtonId, MusicButtonId } from '../../@types/index.js'; import type { Client } from 'discord.js'; @@ -40,6 +41,7 @@ export class InteractionCreateEvent extends BaseDiscordEvent { + if (interaction.customId !== 'select_node') return; + + const lng = bot.guildLanguageManager?.get(interaction.guildId!) || bot.config.bot.i18n.defaultLocale; + const nodeName = interaction.values[0]; + const nodes = client.lavashark.nodes; + + const targetNode = nodes.find(node => node.identifier === nodeName); + + if (!targetNode) { + await interaction.reply({ + embeds: [embeds.validNodeName(bot, nodes.map(n => `\`${n.identifier}\``).join('\n'), lng)], + flags: MessageFlags.Ephemeral + }); + return; + } + + if (targetNode.state !== NodeState.CONNECTED) { + await interaction.reply({ + embeds: [embeds.nodeDisconnected(bot, nodeName, lng)], + flags: MessageFlags.Ephemeral + }); + return; + } + + await interaction.deferUpdate(); + + try { + const [nodeInfo, nodeStats, nodePing] = await Promise.all([ + targetNode.getInfo(), + targetNode.getStats(), + client.lavashark.nodePing(targetNode) + ]); + + const selectOptions = nodes.slice(0, 25).map(node => { + const isConnected = node.state === NodeState.CONNECTED; + return { + label: node.identifier, + value: node.identifier, + default: node.identifier === nodeName, + description: isConnected + ? (bot.i18n.t('embeds:NODE_STATUS_PING', { lng }) + `: ${nodePing}ms`) + : bot.i18n.t('embeds:NODE_DISCONNECTED', { lng }).replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(bot.i18n.t('commands:CONFIG_NODE_OPTION_DESCRIPTION', { lng })) + .addOptions(selectOptions); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + await interaction.editReply({ + embeds: [embeds.nodeStatus(bot, nodeName, nodeInfo, nodeStats, nodePing, lng)], + components: [row] + }); + } catch (error) { + bot.logger.error(bot.shardId, '[NodeSelectHandler] Error fetching node status: ' + error); + } + } +} From 1f92faacf213ba493cc1e6bff25ea7433f21d0de Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 10:43:19 +0900 Subject: [PATCH 11/49] Revert wrong reservable correction - #125 --- src/locales/en-US/commands.json | 6 +----- src/locales/en-US/embeds.json | 2 +- src/locales/ko-KR/commands.json | 6 +----- src/locales/ko-KR/embeds.json | 2 +- src/locales/ru-RU/commands.json | 6 +----- src/locales/ru-RU/embeds.json | 2 +- src/locales/sr-RS/commands.json | 6 +----- src/locales/sr-RS/embeds.json | 2 +- src/locales/tr-TR/commands.json | 6 +----- src/locales/zh-TW/commands.json | 6 +----- src/locales/zh-TW/embeds.json | 2 +- 11 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index 4946955f..d6ef0ed3 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -265,9 +265,5 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ Importing M3U playlists is currently disabled.", - "ERROR_PLAYLIST_DISABLED": "❌ Playlist features are currently disabled.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Toggle allowing M3U playlist imports.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U playlist import permission has been set to `{{state}}`." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty." } diff --git a/src/locales/en-US/embeds.json b/src/locales/en-US/embeds.json index 648175c7..95560f74 100644 --- a/src/locales/en-US/embeds.json +++ b/src/locales/en-US/embeds.json @@ -19,7 +19,7 @@ "NODE_STATUS_INFO_TITLE": "🏷️ INFO", "NODE_STATUS_INFO_VALUE": "Version: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nBuild time: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 MEMORY", - "NODE_STATUS_MEMORY_VALUE": "Used: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReserved: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Used: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", "NODE_STATUS_PING": "Latency", "NODE_STATUS_STATS_TITLE": "📊 STATS", "NODE_STATUS_STATS_VALUE": "Uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index f582aa43..edb2197a 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -265,9 +265,5 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U 플레이리스트 가져오기 기능이 비활성화되어 있습니다.", - "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U 플레이리스트 가져오기 기능 허용 여부를 설정합니다.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U 플레이리스트 가져오기 기능이 `{{state}}`(으)로 설정되었습니다." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다." } \ No newline at end of file diff --git a/src/locales/ko-KR/embeds.json b/src/locales/ko-KR/embeds.json index 3bf02bfb..126bae26 100644 --- a/src/locales/ko-KR/embeds.json +++ b/src/locales/ko-KR/embeds.json @@ -19,7 +19,7 @@ "NODE_STATUS_INFO_TITLE": "🏷️ 정보", "NODE_STATUS_INFO_VALUE": "**버전:** {{version}}\n**자바:** {{jvm}}\n**Lavaplayer:** {{lavaplayer}}\n**Git:** {{git}}\n**빌드 시간:** {{buildTime}}", "NODE_STATUS_MEMORY_TITLE": "📑 메모리", - "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당됨:** {{allocated}}\n**예약됨:** {{reservable}}\n", + "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당됨:** {{allocated}}\n**예약 가능:** {{reservable}}\n", "NODE_STATUS_PING": "지연시간", "NODE_STATUS_STATS_TITLE": "📊 상태", "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**플레이어:** {{playerCount}}개\n**재생 중:** {{playingCount}}개", diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index d733bfc3..9cf462bb 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -265,9 +265,5 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ Импорт M3U-плейлистов отключён.", - "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Переключить разрешение на импорт M3U-плейлистов.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Разрешение на импорт M3U-плейлистов установлено в `{{state}}`." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста." } diff --git a/src/locales/ru-RU/embeds.json b/src/locales/ru-RU/embeds.json index e435c504..5b78f87f 100644 --- a/src/locales/ru-RU/embeds.json +++ b/src/locales/ru-RU/embeds.json @@ -19,7 +19,7 @@ "NODE_STATUS_INFO_TITLE": "🏷️ ИНФОРМАЦИЯ", "NODE_STATUS_INFO_VALUE": "Версия: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nВремя сборки: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 ПАМЯТЬ", - "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nЗарезервировано: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nДоступно: **{{reservable}}**\n", "NODE_STATUS_PING": "Задержка", "NODE_STATUS_STATS_TITLE": "📊 СТАТИСТИКА", "NODE_STATUS_STATS_VALUE": "Время работы: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} мс**\nПлееров: **{{playerCount}}**\nВоспроизводящих: **{{playingCount}}**", diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 832c5e6e..9e678800 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -265,9 +265,5 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ Uvoz M3U plejlisti je trenutno onemogućen.", - "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Uključi/isključi dozvolu za uvoz M3U plejlisti.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Dozvola za uvoz M3U plejlisti je postavljena na `{{state}}`." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan." } \ No newline at end of file diff --git a/src/locales/sr-RS/embeds.json b/src/locales/sr-RS/embeds.json index 4f769ebc..d27edc74 100644 --- a/src/locales/sr-RS/embeds.json +++ b/src/locales/sr-RS/embeds.json @@ -19,7 +19,7 @@ "NODE_STATUS_INFO_TITLE": "🏷️ INFO", "NODE_STATUS_INFO_VALUE": "Verzija: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nVreme izgradnje: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 MEMORIJA", - "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nRezervisano: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nDostupno: **{{reservable}}**\n", "NODE_STATUS_PING": "Kašnjenje", "NODE_STATUS_STATS_TITLE": "📊 STATISTIKA", "NODE_STATUS_STATS_VALUE": "Vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 319ec212..f29f03e2 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -265,9 +265,5 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U çalma listelerini içe aktarma şu anda devre dışı.", - "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U çalma listesi içe aktarma iznini değiştirin.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U çalma listesi içe aktarma izni `{{state}}` olarak ayarlandı." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş." } \ No newline at end of file diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index 513776e1..fda67fe1 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -265,9 +265,5 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 儲存確認逾時。", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ 目前已停用匯入 M3U 播放清單功能。", - "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "切換是否允許匯入 M3U 播放清單。", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ 匯入 M3U 播放清單權限已設定為 `{{state}}`。" + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。" } \ No newline at end of file diff --git a/src/locales/zh-TW/embeds.json b/src/locales/zh-TW/embeds.json index 0f005bc4..a1fe62eb 100644 --- a/src/locales/zh-TW/embeds.json +++ b/src/locales/zh-TW/embeds.json @@ -19,7 +19,7 @@ "NODE_STATUS_INFO_TITLE": "🏷️ 資訊", "NODE_STATUS_INFO_VALUE": "版本: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\n構建時間: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 記憶體", - "NODE_STATUS_MEMORY_VALUE": "已使用: **{{used}}**\n可用: **{{free}}**\n分配: **{{allocated}}**\n已保留: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "已使用: **{{used}}**\n可用: **{{free}}**\n分配: **{{allocated}}**\n可保留: **{{reservable}}**\n", "NODE_STATUS_PING": "延遲", "NODE_STATUS_STATS_TITLE": "📊 統計", "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n播放中: **{{playingCount}}**", From f8f114d3c42c4686093daa971883f6b93c0debdb Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 10:48:41 +0900 Subject: [PATCH 12/49] fix(: restore missing playlist translation keys across locales --- src/locales/en-US/commands.json | 6 +++++- src/locales/ko-KR/commands.json | 6 +++++- src/locales/ru-RU/commands.json | 6 +++++- src/locales/sr-RS/commands.json | 6 +++++- src/locales/tr-TR/commands.json | 6 +++++- src/locales/zh-TW/commands.json | 6 +++++- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index d6ef0ed3..1b380849 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U playlist import is disabled.", + "ERROR_PLAYLIST_DISABLED": "❌ Playlist feature is disabled.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Toggle permission for importing M3U playlists.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U playlist import permission set to `{{state}}`." } diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index edb2197a..fb4c04b6 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U 플레이리스트 가져오기 기능이 비활성화되어 있습니다.", + "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U 플레이리스트 가져오기 허용 여부를 설정합니다.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U 플레이리스트 가져오기 허용 여부가 `{{state}}`(으)로 설정되었습니다." } \ No newline at end of file diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 9cf462bb..d733bfc3 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ Импорт M3U-плейлистов отключён.", + "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Переключить разрешение на импорт M3U-плейлистов.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Разрешение на импорт M3U-плейлистов установлено в `{{state}}`." } diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 9e678800..832c5e6e 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ Uvoz M3U plejlisti je trenutno onemogućen.", + "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Uključi/isključi dozvolu za uvoz M3U plejlisti.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Dozvola za uvoz M3U plejlisti je postavljena na `{{state}}`." } \ No newline at end of file diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index f29f03e2..319ec212 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U çalma listelerini içe aktarma şu anda devre dışı.", + "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U çalma listesi içe aktarma iznini değiştirin.", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U çalma listesi içe aktarma izni `{{state}}` olarak ayarlandı." } \ No newline at end of file diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index fda67fe1..513776e1 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -265,5 +265,9 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 儲存確認逾時。", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。" + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。", + "ERROR_PLAYLIST_M3U_DISABLED": "❌ 目前已停用匯入 M3U 播放清單功能。", + "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "切換是否允許匯入 M3U 播放清單。", + "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ 匯入 M3U 播放清單權限已設定為 `{{state}}`。" } \ No newline at end of file From 5ca6dd2409ad9b377996d3873b5793b2c061c000 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 10:52:28 +0900 Subject: [PATCH 13/49] fix: pass requested language context to help command embed and command metadata --- src/commands/BlacklistCommand.ts | 15 +++++++------ src/commands/ClearCommand.ts | 6 +++--- src/commands/DashboardCommand.ts | 8 +++---- src/commands/DjCommand.ts | 8 +++---- src/commands/FilterCommand.ts | 8 +++---- src/commands/HelpCommand.ts | 24 ++++++++++----------- src/commands/JoinCommand.ts | 8 +++---- src/commands/LanguageCommand.ts | 10 ++++----- src/commands/LeaveCommand.ts | 6 +++--- src/commands/LoopCommand.ts | 8 +++---- src/commands/MoveCommand.ts | 10 ++++----- src/commands/NodeStatusCommand.ts | 8 +++---- src/commands/NowPlayingCommand.ts | 6 +++--- src/commands/PauseCommand.ts | 6 +++--- src/commands/PingCommand.ts | 6 +++--- src/commands/PlayCommand.ts | 8 +++---- src/commands/PlayFirstCommand.ts | 8 +++---- src/commands/PlayLastCommand.ts | 6 +++--- src/commands/PlaylistCommand.ts | 32 ++++++++++++++++++++-------- src/commands/QueueCommand.ts | 6 +++--- src/commands/RadioCommand.ts | 10 ++++----- src/commands/RemoveCommand.ts | 10 ++++----- src/commands/ResumeCommand.ts | 6 +++--- src/commands/SearchCommand.ts | 8 +++---- src/commands/SeekCommand.ts | 8 +++---- src/commands/ServerCommand.ts | 6 +++--- src/commands/ShuffleCommand.ts | 6 +++--- src/commands/SkipCommand.ts | 6 +++--- src/commands/StatusCommand.ts | 6 +++--- src/commands/StopCommand.ts | 6 +++--- src/commands/VolumeCommand.ts | 8 +++---- src/commands/VolumeDefaultCommand.ts | 8 +++---- src/commands/base/BaseCommand.ts | 2 +- src/commands/base/CommandRegistry.ts | 4 ++-- 34 files changed, 154 insertions(+), 137 deletions(-) diff --git a/src/commands/BlacklistCommand.ts b/src/commands/BlacklistCommand.ts index 19a85c7b..747faca3 100644 --- a/src/commands/BlacklistCommand.ts +++ b/src/commands/BlacklistCommand.ts @@ -24,30 +24,32 @@ export class BlacklistCommand extends BaseCommand { /** * Build blacklist command metadata and slash command options */ - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { aliases: [], category: CommandCategory.UTILITY, - description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION'), + description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION', { lng }), name: 'blacklist', options: [ this.createUserSubcommand( 'add', 'commands:CONFIG_BLACKLIST_OPTION_ADD', + lng, ), this.createUserSubcommand( 'remove', 'commands:CONFIG_BLACKLIST_OPTION_REMOVE', + lng, ), { - description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST'), + description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST', { lng }), name: 'list', type: ApplicationCommandOptionType.Subcommand, }, ], sendTyping: true, showHelp: true, - usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE'), + usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE', { lng }), voiceChannel: false, }; } @@ -99,13 +101,14 @@ export class BlacklistCommand extends BaseCommand { private createUserSubcommand( name: BlacklistMutationAction, descriptionKey: string, + lng?: string ): ApplicationCommandSubCommandData { return { - description: i18next.t(descriptionKey), + description: i18next.t(descriptionKey, { lng }), name, options: [ { - description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER'), + description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER', { lng }), name: 'user', required: true, type: ApplicationCommandOptionType.User, diff --git a/src/commands/ClearCommand.ts b/src/commands/ClearCommand.ts index 3f971b81..376aa800 100644 --- a/src/commands/ClearCommand.ts +++ b/src/commands/ClearCommand.ts @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ClearCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'clear', aliases: ['cls'], - description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_CLEAR_USAGE'), + description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_CLEAR_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/DashboardCommand.ts b/src/commands/DashboardCommand.ts index 7ac4cec5..c2ace976 100644 --- a/src/commands/DashboardCommand.ts +++ b/src/commands/DashboardCommand.ts @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class DashboardCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'dashboard', - aliases: ['d', 'console'], - description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE'), + aliases: ['db'], + description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/DjCommand.ts b/src/commands/DjCommand.ts index ca9d33bf..a52f82e8 100644 --- a/src/commands/DjCommand.ts +++ b/src/commands/DjCommand.ts @@ -12,12 +12,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class DjCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'dj', aliases: ['adddj'], - description: i18next.t('commands:CONFIG_DJ_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_DJ_USAGE'), + description: i18next.t('commands:CONFIG_DJ_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_DJ_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -25,7 +25,7 @@ export class DjCommand extends BaseCommand { options: [ { name: 'user', - description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION', { lng }), type: ApplicationCommandOptionType.User, required: false } diff --git a/src/commands/FilterCommand.ts b/src/commands/FilterCommand.ts index b1af98f8..d2d24152 100644 --- a/src/commands/FilterCommand.ts +++ b/src/commands/FilterCommand.ts @@ -18,12 +18,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class FilterCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'filter', aliases: ['eq', 'equalizer'], - description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_FILTER_USAGE'), + description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_FILTER_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -31,7 +31,7 @@ export class FilterCommand extends BaseCommand { options: [ { name: 'filter', - description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION', { lng }), type: 3, required: true, choices: [ diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index e56929a4..a5967ee2 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -17,12 +17,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class HelpCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'help', aliases: ['h'], - description: i18next.t('commands:CONFIG_HELP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_HELP_USAGE'), + description: i18next.t('commands:CONFIG_HELP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_HELP_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -30,7 +30,7 @@ export class HelpCommand extends BaseCommand { options: [ { name: 'command', - description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION', { lng }), type: 3, required: false } @@ -60,14 +60,14 @@ export class HelpCommand extends BaseCommand { */ async #showCommandList(bot: Bot, client: Client, context: CommandContext): Promise { const title = client.user?.username; - const commands = client.commands.getHelpCommands(bot); + const commands = client.commands.getHelpCommands(bot, context.language); const musicCommands = commands.filter(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); return metadata.category === CommandCategory.MUSIC; }); const utilityCommands = commands.filter(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); return metadata.category === CommandCategory.UTILITY; }); @@ -76,7 +76,7 @@ export class HelpCommand extends BaseCommand { .setCustomId(SelectButtonId.HelpMusic) .setPlaceholder(context.t('commands:HELP_SELECT_MUSIC_PLACEHOLDER')) .setOptions(musicCommands.map(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE'); return { label: metadata.name, @@ -89,7 +89,7 @@ export class HelpCommand extends BaseCommand { .setCustomId(SelectButtonId.HelpUtility) .setPlaceholder(context.t('commands:HELP_SELECT_UTILITY_PLACEHOLDER')) .setOptions(utilityCommands.map(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE'); return { label: metadata.name, @@ -120,7 +120,7 @@ export class HelpCommand extends BaseCommand { const selectedCommand = client.commands.get(i.values[0]); if (!selectedCommand) return; - const metadata = selectedCommand.getMetadata(bot); + const metadata = selectedCommand.getMetadata(bot, context.language); const usage = `${metadata.description}\n\`\`\`${bot.config.bot.prefix}${metadata.usage}\`\`\``; await i.deferUpdate(); @@ -150,11 +150,11 @@ export class HelpCommand extends BaseCommand { */ async #showCommandHelp(bot: Bot, client: Client, context: CommandContext, commandName: string): Promise { const prefix = bot.config.bot.prefix; - const commands = client.commands.getHelpCommands(bot); + const commands = client.commands.getHelpCommands(bot, context.language); let found = false; for (const cmd of commands) { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); if (commandName === metadata.name || (metadata.aliases && metadata.aliases.includes(commandName))) { const description = `${metadata.description}\n\`\`\`${prefix}${metadata.usage}\`\`\``; diff --git a/src/commands/JoinCommand.ts b/src/commands/JoinCommand.ts index b4577544..86e8ac2b 100644 --- a/src/commands/JoinCommand.ts +++ b/src/commands/JoinCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class JoinCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'join', - aliases: ['add', 'summon'], - description: i18next.t('commands:CONFIG_JOIN_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_JOIN_USAGE'), + aliases: ['j', 'connect'], + description: i18next.t('commands:CONFIG_JOIN_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_JOIN_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/LanguageCommand.ts b/src/commands/LanguageCommand.ts index f8bc5ad7..4e0c651f 100644 --- a/src/commands/LanguageCommand.ts +++ b/src/commands/LanguageCommand.ts @@ -11,7 +11,7 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class LanguageCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { const choices = bot.lang.languages.slice(0, 25).map(lang => ({ name: getLanguageDisplayName(lang), value: lang @@ -20,16 +20,16 @@ export class LanguageCommand extends BaseCommand { return { name: 'language', aliases: ['lang', 'locale'], - description: i18next.t('commands:CONFIG_LANG_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LANG_USAGE'), + description: i18next.t('commands:CONFIG_LANG_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LANG_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, sendTyping: false, options: [ { - name: 'locale', - description: i18next.t('commands:CONFIG_LANG_OPTION_DESCRIPTION'), + name: 'language', + description: i18next.t('commands:CONFIG_LANG_OPTION_DESCRIPTION', { lng }), type: 3, required: false, choices diff --git a/src/commands/LeaveCommand.ts b/src/commands/LeaveCommand.ts index f06d5a99..209d4171 100644 --- a/src/commands/LeaveCommand.ts +++ b/src/commands/LeaveCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class LeaveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'leave', aliases: [], - description: i18next.t('commands:CONFIG_LEAVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LEAVE_USAGE'), + description: i18next.t('commands:CONFIG_LEAVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LEAVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/LoopCommand.ts b/src/commands/LoopCommand.ts index 54887ad0..05af6c94 100644 --- a/src/commands/LoopCommand.ts +++ b/src/commands/LoopCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class LoopCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'loop', aliases: ['lp'], - description: i18next.t('commands:CONFIG_LOOP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LOOP_USAGE'), + description: i18next.t('commands:CONFIG_LOOP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LOOP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,7 +23,7 @@ export class LoopCommand extends BaseCommand { options: [ { name: 'mode', - description: i18next.t('commands:CONFIG_LOOP_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_LOOP_OPTION_DESCRIPTION', { lng }), type: 3, required: true, choices: [ diff --git a/src/commands/MoveCommand.ts b/src/commands/MoveCommand.ts index a63155f2..4a2f7288 100644 --- a/src/commands/MoveCommand.ts +++ b/src/commands/MoveCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class MoveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'move', aliases: ['mv', 'swap', 'change'], - description: i18next.t('commands:CONFIG_MOVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_MOVE_USAGE'), + description: i18next.t('commands:CONFIG_MOVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_MOVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -22,14 +22,14 @@ export class MoveCommand extends BaseCommand { options: [ { name: 'moveindex1', - description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION', { lng }), type: 4, required: true, min_value: 1 }, { name: 'moveindex2', - description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION_2'), + description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION_2', { lng }), type: 4, required: true, min_value: 1 diff --git a/src/commands/NodeStatusCommand.ts b/src/commands/NodeStatusCommand.ts index 7a059124..db0b014a 100644 --- a/src/commands/NodeStatusCommand.ts +++ b/src/commands/NodeStatusCommand.ts @@ -12,7 +12,7 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NodeStatusCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { const choices = (bot.config.nodeList || []).slice(0, 25).map(node => ({ name: node.id, value: node.id @@ -21,8 +21,8 @@ export class NodeStatusCommand extends BaseCommand { return { name: 'nodestatus', aliases: ['node', 'nodes', 'nodesstatus'], - description: i18next.t('commands:CONFIG_NODE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_NODE_USAGE'), + description: i18next.t('commands:CONFIG_NODE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_NODE_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -30,7 +30,7 @@ export class NodeStatusCommand extends BaseCommand { options: [ { name: 'nodename', - description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION', { lng }), type: 3, required: false, choices: choices.length > 0 ? choices : undefined diff --git a/src/commands/NowPlayingCommand.ts b/src/commands/NowPlayingCommand.ts index 66d63367..6139edb9 100644 --- a/src/commands/NowPlayingCommand.ts +++ b/src/commands/NowPlayingCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NowPlayingCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'nowplaying', aliases: ['np', 'save'], - description: i18next.t('commands:CONFIG_NOW_PLAYING_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_NOW_PLAYING_USAGE'), + description: i18next.t('commands:CONFIG_NOW_PLAYING_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_NOW_PLAYING_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: false, showHelp: true, diff --git a/src/commands/PauseCommand.ts b/src/commands/PauseCommand.ts index 5f1546dd..7417881b 100644 --- a/src/commands/PauseCommand.ts +++ b/src/commands/PauseCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Pause command - Pauses music playback */ export class PauseCommand extends BaseCommand { - getMetadata(_bot: Bot): CommandMetadata { + getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'pause', aliases: [], - description: i18next.t('commands:CONFIG_PAUSE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PAUSE_USAGE'), + description: i18next.t('commands:CONFIG_PAUSE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PAUSE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/PingCommand.ts b/src/commands/PingCommand.ts index 7a3ed156..789f4c54 100644 --- a/src/commands/PingCommand.ts +++ b/src/commands/PingCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Ping command - Shows bot latency */ export class PingCommand extends BaseCommand { - getMetadata(_bot: Bot): CommandMetadata { + getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'ping', aliases: [], - description: i18next.t('commands:CONFIG_PING_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PING_USAGE'), + description: i18next.t('commands:CONFIG_PING_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PING_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index 050017d3..7e78a10a 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -14,12 +14,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class PlayCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'play', aliases: ['p'], - description: i18next.t('commands:CONFIG_PLAY_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAY_USAGE'), + description: i18next.t('commands:CONFIG_PLAY_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAY_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -27,7 +27,7 @@ export class PlayCommand extends BaseCommand { options: [ { name: 'play', - description: i18next.t('commands:CONFIG_PLAY_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAY_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index e64af035..5211b410 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -14,12 +14,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class PlayFirstCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'playfirst', aliases: ['pf', 'pp', 'prioritizePlay'], - description: i18next.t('commands:CONFIG_PLAYFIRST_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAYFIRST_USAGE'), + description: i18next.t('commands:CONFIG_PLAYFIRST_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAYFIRST_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -27,7 +27,7 @@ export class PlayFirstCommand extends BaseCommand { options: [ { name: 'playfirst', - description: i18next.t('commands:CONFIG_PLAYFIRST_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAYFIRST_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/PlayLastCommand.ts b/src/commands/PlayLastCommand.ts index de218bd3..813d9970 100644 --- a/src/commands/PlayLastCommand.ts +++ b/src/commands/PlayLastCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * PlayLast command - Replay the current or last played song */ export class PlayLastCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'playlast', aliases: [], - description: i18next.t('commands:CONFIG_PLAYLAST_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAYLAST_USAGE'), + description: i18next.t('commands:CONFIG_PLAYLAST_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAYLAST_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: false, showHelp: true, diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 5b58929b..8e9f1387 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -43,24 +43,29 @@ export class PlaylistCommand extends BaseCommand { /** * Build playlist command metadata and slash command options */ - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { aliases: ['pl'], category: CommandCategory.MUSIC, - description: i18next.t('commands:CONFIG_PLAYLIST_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAYLIST_DESCRIPTION', { lng }), name: 'playlist', options: [ this.createNamedSubcommand( 'save', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE', + true, + lng ), this.createNamedSubcommand( 'play', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY', + true, + lng ), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST', + { lng } ), name: 'list', type: ApplicationCommandOptionType.Subcommand, @@ -69,21 +74,26 @@ export class PlaylistCommand extends BaseCommand { 'info', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO', false, + lng ), this.createNamedSubcommand( 'delete', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE', + true, + lng ), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT', + { lng } ), name: 'import', options: [ - this.createNameOption(), + this.createNameOption(true, lng), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_URL', + { lng } ), name: 'url', required: true, @@ -95,13 +105,15 @@ export class PlaylistCommand extends BaseCommand { { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK', + { lng } ), name: 'remove-track', options: [ - this.createNameOption(), + this.createNameOption(true, lng), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_INDEX', + { lng } ), name: 'index', required: true, @@ -113,6 +125,7 @@ export class PlaylistCommand extends BaseCommand { { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U', + { lng } ), name: 'toggle-m3u', type: ApplicationCommandOptionType.Subcommand, @@ -120,7 +133,7 @@ export class PlaylistCommand extends BaseCommand { ], sendTyping: true, showHelp: true, - usage: i18next.t('commands:CONFIG_PLAYLIST_USAGE'), + usage: i18next.t('commands:CONFIG_PLAYLIST_USAGE', { lng }), voiceChannel: false, }; } @@ -180,11 +193,12 @@ export class PlaylistCommand extends BaseCommand { name: PlaylistSubcommandName, descriptionKey: string, nameRequired: boolean = true, + lng?: string ): ApplicationCommandSubCommandData { return { - description: i18next.t(descriptionKey), + description: i18next.t(descriptionKey, { lng }), name, - options: [this.createNameOption(nameRequired)], + options: [this.createNameOption(nameRequired, lng)], type: ApplicationCommandOptionType.Subcommand, }; } @@ -192,9 +206,9 @@ export class PlaylistCommand extends BaseCommand { /** * Create the shared playlist name option */ - private createNameOption(required: boolean = true): ApplicationCommandStringOptionData { + private createNameOption(required: boolean = true, lng?: string): ApplicationCommandStringOptionData { return { - description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME'), + description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME', { lng }), name: 'name', required, type: ApplicationCommandOptionType.String, diff --git a/src/commands/QueueCommand.ts b/src/commands/QueueCommand.ts index a199e1ac..704ba6f8 100644 --- a/src/commands/QueueCommand.ts +++ b/src/commands/QueueCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class QueueCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'queue', aliases: ['q', 'list'], - description: i18next.t('commands:CONFIG_QUEUE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_QUEUE_USAGE'), + description: i18next.t('commands:CONFIG_QUEUE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_QUEUE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 1731be59..2071372e 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -10,12 +10,12 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class RadioCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'radio', aliases: ['record'], - description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_RADIO_USAGE'), + description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_RADIO_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,13 +23,13 @@ export class RadioCommand extends BaseCommand { options: [ { name: 'playlist', - description: i18next.t('commands:CONFIG_RADIO_OPTION_PLAYLIST'), + description: i18next.t('commands:CONFIG_RADIO_OPTION_PLAYLIST', { lng }), type: ApplicationCommandOptionType.String, required: true }, { name: 'channel', - description: i18next.t('commands:CONFIG_RADIO_OPTION_CHANNEL'), + description: i18next.t('commands:CONFIG_RADIO_OPTION_CHANNEL', { lng }), type: ApplicationCommandOptionType.String, required: true } diff --git a/src/commands/RemoveCommand.ts b/src/commands/RemoveCommand.ts index 4d39a336..669dca2b 100644 --- a/src/commands/RemoveCommand.ts +++ b/src/commands/RemoveCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class RemoveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'remove', aliases: ['rm'], - description: i18next.t('commands:CONFIG_REMOVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_REMOVE_USAGE'), + description: i18next.t('commands:CONFIG_REMOVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_REMOVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,13 +23,13 @@ export class RemoveCommand extends BaseCommand { options: [ { name: 'index', - description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION', { lng }), type: 10, required: false }, { name: 'index2', - description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION_2'), + description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION_2', { lng }), type: 10, required: false } diff --git a/src/commands/ResumeCommand.ts b/src/commands/ResumeCommand.ts index 8ead4338..1d412913 100644 --- a/src/commands/ResumeCommand.ts +++ b/src/commands/ResumeCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ResumeCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'resume', aliases: [], - description: i18next.t('commands:CONFIG_RESUME_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_RESUME_USAGE'), + description: i18next.t('commands:CONFIG_RESUME_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_RESUME_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index d0e26571..5cd484a4 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -21,12 +21,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class SearchCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'search', aliases: ['find'], - description: i18next.t('commands:CONFIG_SEARCH_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SEARCH_USAGE'), + description: i18next.t('commands:CONFIG_SEARCH_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SEARCH_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -34,7 +34,7 @@ export class SearchCommand extends BaseCommand { options: [ { name: 'search', - description: i18next.t('commands:CONFIG_SEARCH_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_SEARCH_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/SeekCommand.ts b/src/commands/SeekCommand.ts index 9fa19ed7..e215e0ba 100644 --- a/src/commands/SeekCommand.ts +++ b/src/commands/SeekCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class SeekCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'seek', aliases: [], - description: i18next.t('commands:CONFIG_SEEK_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SEEK_USAGE'), + description: i18next.t('commands:CONFIG_SEEK_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SEEK_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -24,7 +24,7 @@ export class SeekCommand extends BaseCommand { options: [ { name: 'seek', - description: i18next.t('commands:CONFIG_SEEK_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_SEEK_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/ServerCommand.ts b/src/commands/ServerCommand.ts index d9fb0926..05349102 100644 --- a/src/commands/ServerCommand.ts +++ b/src/commands/ServerCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ServerCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'server', aliases: [], - description: i18next.t('commands:CONFIG_SERVER_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SERVER_USAGE'), + description: i18next.t('commands:CONFIG_SERVER_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SERVER_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, diff --git a/src/commands/ShuffleCommand.ts b/src/commands/ShuffleCommand.ts index f05c17e0..ae85a949 100644 --- a/src/commands/ShuffleCommand.ts +++ b/src/commands/ShuffleCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ShuffleCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'shuffle', aliases: ['random'], - description: i18next.t('commands:CONFIG_SHUFFLE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SHUFFLE_USAGE'), + description: i18next.t('commands:CONFIG_SHUFFLE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SHUFFLE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/SkipCommand.ts b/src/commands/SkipCommand.ts index cd284c45..08631a17 100644 --- a/src/commands/SkipCommand.ts +++ b/src/commands/SkipCommand.ts @@ -14,12 +14,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Skip command - Skips current track */ export class SkipCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'skip', aliases: ['s'], - description: i18next.t('commands:CONFIG_SKIP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SKIP_USAGE'), + description: i18next.t('commands:CONFIG_SKIP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SKIP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index 2a8c24b9..ffc223a7 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata, SystemStatus } from '../@types/index.js'; export class StatusCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'status', aliases: ['info'], - description: i18next.t('commands:CONFIG_STATUS_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_STATUS_USAGE'), + description: i18next.t('commands:CONFIG_STATUS_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_STATUS_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, diff --git a/src/commands/StopCommand.ts b/src/commands/StopCommand.ts index c7dbded1..aef16cf9 100644 --- a/src/commands/StopCommand.ts +++ b/src/commands/StopCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class StopCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'stop', aliases: [], - description: i18next.t('commands:CONFIG_STOP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_STOP_USAGE'), + description: i18next.t('commands:CONFIG_STOP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_STOP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/VolumeCommand.ts b/src/commands/VolumeCommand.ts index 39b607c7..f7ec42a0 100644 --- a/src/commands/VolumeCommand.ts +++ b/src/commands/VolumeCommand.ts @@ -19,12 +19,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class VolumeCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { return { name: 'volume', aliases: ['v'], - description: i18next.t('commands:CONFIG_VOLUME_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_VOLUME_USAGE'), + description: i18next.t('commands:CONFIG_VOLUME_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_VOLUME_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -32,7 +32,7 @@ export class VolumeCommand extends BaseCommand { options: [ { name: 'volume', - description: bot.i18n.t('commands:CONFIG_VOLUME_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max }), + description: bot.i18n.t('commands:CONFIG_VOLUME_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max, lng }), type: 4, required: false, min_value: 1, diff --git a/src/commands/VolumeDefaultCommand.ts b/src/commands/VolumeDefaultCommand.ts index bddf7ab8..9beb8fce 100644 --- a/src/commands/VolumeDefaultCommand.ts +++ b/src/commands/VolumeDefaultCommand.ts @@ -8,12 +8,12 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class VolumeDefaultCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { return { name: 'volume-default', aliases: ['vdef', 'voldef'], - description: i18next.t('commands:CONFIG_VOLUME_DEFAULT_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_VOLUME_DEFAULT_USAGE'), + description: i18next.t('commands:CONFIG_VOLUME_DEFAULT_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_VOLUME_DEFAULT_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -21,7 +21,7 @@ export class VolumeDefaultCommand extends BaseCommand { options: [ { name: 'volume', - description: bot.i18n.t('commands:CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max }), + description: bot.i18n.t('commands:CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max, lng }), type: 4, required: false, min_value: 1, diff --git a/src/commands/base/BaseCommand.ts b/src/commands/base/BaseCommand.ts index 67eab573..a08b671b 100644 --- a/src/commands/base/BaseCommand.ts +++ b/src/commands/base/BaseCommand.ts @@ -12,7 +12,7 @@ export abstract class BaseCommand { /** * Get command metadata */ - public abstract getMetadata(bot: Bot): CommandMetadata; + public abstract getMetadata(bot: Bot, lng?: string): CommandMetadata; /** * Execute command logic (implemented by subclasses) diff --git a/src/commands/base/CommandRegistry.ts b/src/commands/base/CommandRegistry.ts index 3a74d7c9..f43f4be0 100644 --- a/src/commands/base/CommandRegistry.ts +++ b/src/commands/base/CommandRegistry.ts @@ -77,9 +77,9 @@ export class CommandRegistry { /** * Get all commands that should be shown in help */ - public getHelpCommands(bot: Bot): BaseCommand[] { + public getHelpCommands(bot: Bot, lng?: string): BaseCommand[] { return this.getAll().filter(cmd => - cmd.getMetadata(bot).showHelp + cmd.getMetadata(bot, lng).showHelp ); } } From 4ec99cabd25118ec2dfce62e7e860e8b126255c0 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 10:55:44 +0900 Subject: [PATCH 14/49] fix: display selected command name instead of bot username in select menu embed --- src/commands/HelpCommand.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index a5967ee2..4032a194 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -59,7 +59,6 @@ export class HelpCommand extends BaseCommand { * @private */ async #showCommandList(bot: Bot, client: Client, context: CommandContext): Promise { - const title = client.user?.username; const commands = client.commands.getHelpCommands(bot, context.language); const musicCommands = commands.filter(cmd => { @@ -125,7 +124,7 @@ export class HelpCommand extends BaseCommand { await i.deferUpdate(); await msg.edit({ - embeds: [embeds.help(bot, title!, usage, context.language)], + embeds: [embeds.help(bot, metadata.name, usage, context.language)], components: [], allowedMentions: { repliedUser: false } }).catch(() => bot.logger.discord( bot.shardId, 'Failed to edit deleted message.')); From 2c900d67a6c0b5e9d76fe33417bce65807ecbc32 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 11:10:48 +0900 Subject: [PATCH 15/49] refactor: restrict M3U import to Admin/DJ and disallow full queueing --- config.js | 3 +- src/@types/index.ts | 1 - src/commands/PlayCommand.ts | 7 +++ src/commands/PlayFirstCommand.ts | 7 +++ src/commands/PlaylistCommand.ts | 10 ----- .../playlist/BasePlaylistSubcommand.ts | 3 +- .../playlist/ImportPlaylistSubcommand.ts | 9 +++- .../playlist/PlayPlaylistSubcommand.ts | 8 ++++ src/commands/playlist/ToggleM3uSubcommand.ts | 45 +------------------ src/lib/DatabaseManager.ts | 8 ++++ src/lib/PlaylistManager.ts | 31 ++++++++----- src/locales/en-US/commands.json | 5 +-- src/locales/ko-KR/commands.json | 6 +-- src/locales/ru-RU/commands.json | 5 +-- src/locales/sr-RS/commands.json | 5 +-- src/locales/tr-TR/commands.json | 5 +-- src/locales/zh-TW/commands.json | 5 +-- 17 files changed, 74 insertions(+), 89 deletions(-) diff --git a/config.js b/config.js index ff5a279c..782c1d0d 100644 --- a/config.js +++ b/config.js @@ -179,8 +179,7 @@ const config = { // Playlist settings playlist: { - enabled : true, // Enable/disable playlist feature - allowM3uImport : false // Enable/disable M3U playlist import (disabled by default) + enabled : true // Enable/disable playlist feature }, // SQLite database settings diff --git a/src/@types/index.ts b/src/@types/index.ts index b259bea7..9f07dbac 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -125,7 +125,6 @@ export type Config = { export type PlaylistConfig = { enabled: boolean; - allowM3uImport: boolean; }; export type BotConfig = { diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index 7e78a10a..71221da5 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -46,6 +46,13 @@ export class PlayCommand extends BaseCommand { return; } + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str.trim()); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str.trim()); + if (isM3uUrl || existingPlaylist?.isM3u) { + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); + return; + } + // Search for tracks let res; try { diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 5211b410..02c598c0 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -46,6 +46,13 @@ export class PlayFirstCommand extends BaseCommand { return; } + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str.trim()); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str.trim()); + if (isM3uUrl || existingPlaylist?.isM3u) { + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); + return; + } + // Search for tracks let res; try { diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 8e9f1387..6abdbe77 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -10,7 +10,6 @@ import { PlayPlaylistSubcommand } from './playlist/PlayPlaylistSubcommand.js'; import { RemovePlaylistTrackSubcommand } from './playlist/RemovePlaylistTrackSubcommand.js'; import { SavePlaylistSubcommand } from './playlist/SavePlaylistSubcommand.js'; import { ShowPlaylistInfoSubcommand } from './playlist/ShowPlaylistInfoSubcommand.js'; -import { ToggleM3uSubcommand } from './playlist/ToggleM3uSubcommand.js'; import type { ApplicationCommandStringOptionData, @@ -37,7 +36,6 @@ export class PlaylistCommand extends BaseCommand { new RemovePlaylistTrackSubcommand(), new SavePlaylistSubcommand(), new ShowPlaylistInfoSubcommand(), - new ToggleM3uSubcommand(), ]; /** @@ -122,14 +120,6 @@ export class PlaylistCommand extends BaseCommand { ], type: ApplicationCommandOptionType.Subcommand, }, - { - description: i18next.t( - 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U', - { lng } - ), - name: 'toggle-m3u', - type: ApplicationCommandOptionType.Subcommand, - }, ], sendTyping: true, showHelp: true, diff --git a/src/commands/playlist/BasePlaylistSubcommand.ts b/src/commands/playlist/BasePlaylistSubcommand.ts index f8da9ce8..1a327a84 100644 --- a/src/commands/playlist/BasePlaylistSubcommand.ts +++ b/src/commands/playlist/BasePlaylistSubcommand.ts @@ -20,8 +20,7 @@ export type PlaylistSubcommandName = | 'list' | 'play' | 'remove-track' - | 'save' - | 'toggle-m3u'; + | 'save'; /** * Dependencies shared by playlist subcommand executions diff --git a/src/commands/playlist/ImportPlaylistSubcommand.ts b/src/commands/playlist/ImportPlaylistSubcommand.ts index 07d39f57..92747443 100644 --- a/src/commands/playlist/ImportPlaylistSubcommand.ts +++ b/src/commands/playlist/ImportPlaylistSubcommand.ts @@ -1,4 +1,5 @@ import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; +import { DJManager } from '../../lib/DjManager.js'; import { PLAYLIST_TRACK_LIMIT } from '../../lib/PlaylistManager.js'; import { DEFAULT_REMOTE_TEXT_MAX_BYTES, @@ -23,10 +24,14 @@ export class ImportPlaylistSubcommand extends BasePlaylistSubcommand { * Validate import arguments and confirm replacement when required */ public async execute(context: PlaylistSubcommandContext): Promise { - if (!context.bot.config.playlist?.allowM3uImport) { + const userId = context.command.user.id; + const member = context.command.member; + const player = context.client.lavashark.getPlayer(context.command.guild!.id); + + if (!DJManager.isDJ(context.bot, userId, member, player ?? undefined)) { await context.command.replyEphemeralError( context.bot, - context.command.t('commands:ERROR_PLAYLIST_M3U_DISABLED'), + context.command.t('commands:ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY'), ); return; } diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index c4429d2e..92ee89ee 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -52,6 +52,14 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { return; } + if (playlist.isM3u) { + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL'), + ); + return; + } + await this.play(context, playlist); } diff --git a/src/commands/playlist/ToggleM3uSubcommand.ts b/src/commands/playlist/ToggleM3uSubcommand.ts index 59dc4a1a..9cec1e73 100644 --- a/src/commands/playlist/ToggleM3uSubcommand.ts +++ b/src/commands/playlist/ToggleM3uSubcommand.ts @@ -1,43 +1,2 @@ -import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; -import { PermissionFlagsBits } from 'discord.js'; - -import type { - PlaylistSubcommandContext, - PlaylistSubcommandName, -} from './BasePlaylistSubcommand.js'; - -export class ToggleM3uSubcommand extends BasePlaylistSubcommand { - public readonly name: PlaylistSubcommandName = 'toggle-m3u'; - - public async execute(context: PlaylistSubcommandContext): Promise { - const { bot, command } = context; - const member = command.member; - - const isAdmin = bot.config.bot.admin.includes(command.user.id) || - member?.permissions.has(PermissionFlagsBits.Administrator) || - member?.permissions.has(PermissionFlagsBits.ManageGuild); - - if (!isAdmin) { - await command.replyEphemeralError( - bot, - command.t('commands:MESSAGE_DJ_ADMIN_ONLY'), - ); - return; - } - - if (!bot.config.playlist) { - bot.config.playlist = { - enabled: true, - allowM3uImport: false, - }; - } - - bot.config.playlist.allowM3uImport = !bot.config.playlist.allowM3uImport; - const state = bot.config.playlist.allowM3uImport ? 'ON' : 'OFF'; - - await command.replySuccess( - bot, - command.t('commands:MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS', { state }), - ); - } -} +// Deprecated subcommand, replaced by Admin/DJ permission check on import and radio-only station selection. +export {}; diff --git a/src/lib/DatabaseManager.ts b/src/lib/DatabaseManager.ts index 9c13d4b2..6df4f56f 100644 --- a/src/lib/DatabaseManager.ts +++ b/src/lib/DatabaseManager.ts @@ -113,6 +113,7 @@ export class DatabaseManager { guild_id TEXT NOT NULL, name TEXT NOT NULL, created_at INTEGER NOT NULL, + is_m3u INTEGER DEFAULT 0, UNIQUE(guild_id, name) ); @@ -128,5 +129,12 @@ export class DatabaseManager { FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE ); `); + + const columns = this.db.prepare('PRAGMA table_info(playlists)').all() as Array<{ name: string }>; + const hasIsM3u = columns.some((col) => col.name === 'is_m3u'); + + if (!hasIsM3u) { + this.db.exec('ALTER TABLE playlists ADD COLUMN is_m3u INTEGER DEFAULT 0;'); + } } } diff --git a/src/lib/PlaylistManager.ts b/src/lib/PlaylistManager.ts index 66697e94..f8e59541 100644 --- a/src/lib/PlaylistManager.ts +++ b/src/lib/PlaylistManager.ts @@ -15,11 +15,14 @@ export interface PlaylistTrack { position: number; } + + export interface Playlist { id: number; guildId: string; name: string; createdAt: number; + isM3u?: boolean; tracks?: PlaylistTrack[]; } @@ -61,21 +64,29 @@ export class PlaylistManager { try { const playlistRow = db.prepare(` - SELECT id, guild_id as guildId, name, created_at as createdAt + SELECT id, guild_id as guildId, name, created_at as createdAt, is_m3u as isM3u FROM playlists WHERE guild_id = ? AND name = ? COLLATE NOCASE - `).get(guildId, name) as Playlist | undefined; + `).get(guildId, name) as any; if (!playlistRow) return null; + const playlist: Playlist = { + id: Number(playlistRow.id), + guildId: String(playlistRow.guildId), + name: String(playlistRow.name), + createdAt: Number(playlistRow.createdAt), + isM3u: Boolean(playlistRow.isM3u), + }; + const tracks = db.prepare(` SELECT id, playlist_id as playlistId, title, url, encoded, author, duration, position FROM playlist_tracks WHERE playlist_id = ? ORDER BY position ASC - `).all(playlistRow.id) as any[]; + `).all(playlist.id) as any[]; - playlistRow.tracks = tracks.map(t => ({ + playlist.tracks = tracks.map(t => ({ id: t.id, playlistId: t.playlistId, title: t.title, @@ -86,7 +97,7 @@ export class PlaylistManager { position: t.position })); - return playlistRow; + return playlist; } catch (error) { this.bot.logger.error(this.bot.shardId, `[PlaylistManager] Error getting playlist ${name}: ${error}`); return null; @@ -134,7 +145,7 @@ export class PlaylistManager { } } - public saveCurrentQueue(guildId: string, name: string, tracks: Omit[]): boolean { + public saveCurrentQueue(guildId: string, name: string, tracks: Omit[], isM3u: boolean = false): boolean { const db = this.db; if (!db || tracks.length === 0 || tracks.length > PLAYLIST_TRACK_LIMIT) return false; @@ -148,9 +159,9 @@ export class PlaylistManager { db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ? COLLATE NOCASE').run(guildId, name); const playlistInfo = db.prepare(` - INSERT INTO playlists (guild_id, name, created_at) - VALUES (?, ?, ?) - `).run(guildId, name, Date.now()); + INSERT INTO playlists (guild_id, name, created_at, is_m3u) + VALUES (?, ?, ?, ?) + `).run(guildId, name, Date.now(), isM3u ? 1 : 0); const playlistId = Number(playlistInfo.lastInsertRowid); for (let i = 0; i < tracks.length; i++) { @@ -255,7 +266,7 @@ export class PlaylistManager { author: null })); - const success = this.saveCurrentQueue(guildId, name, playlistTracks); + const success = this.saveCurrentQueue(guildId, name, playlistTracks, true); return success ? {success: true, trackCount: playlistTracks.length} : {success: false, reason: 'SAVE_FAILED'}; diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index 1b380849..fed39f15 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -266,8 +266,7 @@ "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U playlist import is disabled.", "ERROR_PLAYLIST_DISABLED": "❌ Playlist feature is disabled.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Toggle permission for importing M3U playlists.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U playlist import permission set to `{{state}}`." + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U playlists cannot be queued entirely. Please use the `/radio` command to select a specific station.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U playlist import is restricted to Admins and DJs only." } diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index fb4c04b6..323d6530 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -265,9 +265,7 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U 플레이리스트 가져오기 기능이 비활성화되어 있습니다.", "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U 플레이리스트 가져오기 허용 여부를 설정합니다.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U 플레이리스트 가져오기 허용 여부가 `{{state}}`(으)로 설정되었습니다." + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U 플레이리스트 전체는 재생할 수 없습니다. `/radio` 명령어를 사용하여 원하는 라디오 채널을 선택해 주세요.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U 플레이리스트 가져오기는 관리자 또는 DJ 권한이 있는 사용자만 수행할 수 있습니다." } \ No newline at end of file diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index d733bfc3..07805056 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -266,8 +266,7 @@ "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ Импорт M3U-плейлистов отключён.", "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Переключить разрешение на импорт M3U-плейлистов.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Разрешение на импорт M3U-плейлистов установлено в `{{state}}`." + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U-плейлисты нельзя добавлять в очередь целиком. Используйте команду `/radio` для выбора радиостанции.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Импорт M3U-плейлистов доступен только администраторам и DJ." } diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 832c5e6e..098ea7a3 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -266,8 +266,7 @@ "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ Uvoz M3U plejlisti je trenutno onemogućen.", "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "Uključi/isključi dozvolu za uvoz M3U plejlisti.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ Dozvola za uvoz M3U plejlisti je postavljena na `{{state}}`." + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U plejliste se ne mogu staviti u red u celosti. Koristite komandu `/radio` za izbor stanice.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Uvoz M3U plejlisti je ograničen samo na admine i DJ-eve." } \ No newline at end of file diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 319ec212..3d21ae7f 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -266,8 +266,7 @@ "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ M3U çalma listelerini içe aktarma şu anda devre dışı.", "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "M3U çalma listesi içe aktarma iznini değiştirin.", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ M3U çalma listesi içe aktarma izni `{{state}}` olarak ayarlandı." + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U çalma listeleri tamamen sıraya eklenemez. Lütfen belirli bir istasyon seçmek için `/radio` komutunu kullanın.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U çalma listesi içe aktarma yalnızca Yöneticiler ve DJ'ler ile sınırlıdır." } \ No newline at end of file diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index 513776e1..c6f9d936 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -266,8 +266,7 @@ "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 儲存確認逾時。", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。", - "ERROR_PLAYLIST_M3U_DISABLED": "❌ 目前已停用匯入 M3U 播放清單功能。", "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_TOGGLE_M3U": "切換是否允許匯入 M3U 播放清單。", - "MESSAGE_PLAYLIST_TOGGLE_M3U_SUCCESS": "✅ 匯入 M3U 播放清單權限已設定為 `{{state}}`。" + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ 無法將 M3U 播放清單整本加入佇列。請使用 `/radio` 指令選擇特定電台頻道。", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ 匯入 M3U 播放清單僅限管理員及 DJ 權限使用者使用。" } \ No newline at end of file From 517546ba722c9a7616b67dba771425a05218a593 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 11:12:37 +0900 Subject: [PATCH 16/49] fix:: restore missing MESSAGE_SKIP_EMPTY_DISCONNECT key in ko-KR locale --- src/locales/ko-KR/commands.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index 323d6530..6496dc82 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -265,6 +265,7 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U 플레이리스트 전체는 재생할 수 없습니다. `/radio` 명령어를 사용하여 원하는 라디오 채널을 선택해 주세요.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U 플레이리스트 가져오기는 관리자 또는 DJ 권한이 있는 사용자만 수행할 수 있습니다." From d402f363c9eb13fb119da2f3eb81eca620dd54ef Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Wed, 12 Aug 2026 14:03:32 +0900 Subject: [PATCH 17/49] refactor: relocate generateRandomKey to utils and remove obsolete hashGenerator --- src/lib/api/routes/AuthRouter.ts | 8 +++---- src/lib/hashGenerator.ts | 30 ------------------------ src/utils/functions/generateRandomKey.ts | 5 ++++ 3 files changed, 9 insertions(+), 34 deletions(-) delete mode 100644 src/lib/hashGenerator.ts create mode 100644 src/utils/functions/generateRandomKey.ts diff --git a/src/lib/api/routes/AuthRouter.ts b/src/lib/api/routes/AuthRouter.ts index c56667b0..77f5e343 100644 --- a/src/lib/api/routes/AuthRouter.ts +++ b/src/lib/api/routes/AuthRouter.ts @@ -5,7 +5,7 @@ import { parseCookie } from 'cookie'; import undici from 'undici'; import { LoginTypeEnum } from '../../../@types/index.js'; -import { hashGenerator } from '../../hashGenerator.js'; +import { generateRandomKey } from '../../../utils/functions/generateRandomKey.js'; import { loginSchema } from '../validators/schemas.js'; import { noContent, ok, problem } from '../http.js'; import { validateBody } from '../validators/validate.js'; @@ -126,7 +126,7 @@ export class AuthRouter extends BaseRouter { if (username === siteConfig.user.username && passwordMatch) { this.sessionManager.unblockIP(userIP); - const sessionId = hashGenerator.generateRandomKey(); + const sessionId = generateRandomKey(); this.sessionManager.createSession(sessionId); res.cookie('sessionId', sessionId, SESSION_COOKIE_OPTIONS); @@ -245,7 +245,7 @@ export class AuthRouter extends BaseRouter { }); } - const state = hashGenerator.generateRandomKey(); + const state = generateRandomKey(); this.#oauthStates.set(state, { expiresAt: Date.now() + OAUTH2_STATE_TTL_MS, requesterIp, @@ -344,7 +344,7 @@ export class AuthRouter extends BaseRouter { if (this.bot.config.bot.admin.includes(user.id)) { this.sessionManager.unblockIP(userIP); - const sessionId = hashGenerator.generateRandomKey(); + const sessionId = generateRandomKey(); this.sessionManager.createSession(sessionId); res.cookie('sessionId', sessionId, SESSION_COOKIE_OPTIONS); diff --git a/src/lib/hashGenerator.ts b/src/lib/hashGenerator.ts deleted file mode 100644 index 1385744d..00000000 --- a/src/lib/hashGenerator.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createHash, randomBytes } from 'crypto'; - - -const hashGenerator = { - /** - * Generates a random 32-byte key in hexadecimal format. - * @returns {string} - A randomly generated key. - */ - generateRandomKey: (): string => { - return randomBytes(32).toString('hex'); - }, - - /** - * Generates a hash using the SHA-512 algorithm - * and a provided password along with a pre-generated salt. - * @param {string} password - The password to be hashed. - * @returns {string} - The hashed password. - */ - generateHash: (password: string): string => { - return createHash('sha512') - .update(password + salt, 'utf-8') - .digest('hex'); - } -}; - -export { hashGenerator }; - - -// pre-generated salt -const salt = hashGenerator.generateRandomKey(); diff --git a/src/utils/functions/generateRandomKey.ts b/src/utils/functions/generateRandomKey.ts new file mode 100644 index 00000000..12b577da --- /dev/null +++ b/src/utils/functions/generateRandomKey.ts @@ -0,0 +1,5 @@ +import { randomBytes } from 'crypto'; + +export const generateRandomKey = (): string => { + return randomBytes(32).toString('hex'); +}; From 8845f8b65b48beeddf942a3d3dd6ece55e9309c2 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 10:12:45 +0900 Subject: [PATCH 18/49] fix: resolve playlist info duplication, playfirst loop conflict, and remove record alias --- src/commands/PlayFirstCommand.ts | 5 +++++ src/commands/PlaylistCommand.ts | 2 +- src/commands/RadioCommand.ts | 2 +- src/commands/playlist/ShowPlaylistInfoSubcommand.ts | 6 ++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 02c598c0..0781f6b2 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -1,4 +1,5 @@ import i18next from 'i18next'; +import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType } from '../@types/index.js'; @@ -245,6 +246,10 @@ export class PlayFirstCommand extends BaseCommand { const requester = context.isMessage() ? context.getMessage().author : context.getInteraction().user; const curVolume = player.setting.volume ?? bot.guildVolumeManager?.get(player.guildId) ?? bot.config.bot.volume.default; + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + if (res.loadType === LoadType.PLAYLIST) { const tracks = tracksToAdd !== undefined ? res.tracks.slice(0, tracksToAdd) : res.tracks; diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 6abdbe77..a58fc94f 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -71,7 +71,7 @@ export class PlaylistCommand extends BaseCommand { this.createNamedSubcommand( 'info', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO', - false, + true, lng ), this.createNamedSubcommand( diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 2071372e..071b6240 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -13,7 +13,7 @@ export class RadioCommand extends BaseCommand { public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'radio', - aliases: ['record'], + aliases: [], description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION', { lng }), usage: i18next.t('commands:CONFIG_RADIO_USAGE', { lng }), category: CommandCategory.MUSIC, diff --git a/src/commands/playlist/ShowPlaylistInfoSubcommand.ts b/src/commands/playlist/ShowPlaylistInfoSubcommand.ts index 7a66e996..3268108f 100644 --- a/src/commands/playlist/ShowPlaylistInfoSubcommand.ts +++ b/src/commands/playlist/ShowPlaylistInfoSubcommand.ts @@ -4,7 +4,6 @@ import { PlaylistButtonId } from '../../@types/index.js'; import { embeds } from '../../embeds/index.js'; import { PLAYLIST_INFO_PAGE_SIZE } from '../../embeds/playlist.embed.js'; import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; -import { ListPlaylistsSubcommand } from './ListPlaylistsSubcommand.js'; import type { PlaylistSubcommandContext, @@ -26,7 +25,10 @@ export class ShowPlaylistInfoSubcommand extends BasePlaylistSubcommand { public async execute(context: PlaylistSubcommandContext): Promise { const name = this.getPlaylistName(context.command); if (!name) { - await new ListPlaylistsSubcommand().execute(context); + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_NAME_REQUIRED'), + ); return; } From bd48b12494a6d7d829bb6e670d33890875aaf6dd Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 10:37:46 +0900 Subject: [PATCH 19/49] fix: resolve playlist subcommand conflicts, playfirst loop issue, and dashboard spam --- src/commands/playlist/PlayPlaylistSubcommand.ts | 10 ++++++++-- src/events/lavashark/TrackAddEvent.ts | 12 +++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 92ee89ee..14d395e0 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -292,6 +292,8 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const tracks = playlist.tracks ?? []; const BATCH_SIZE = 5; + const resolvedTracks: any[] = []; + for (let i = 0; i < tracks.length; i += BATCH_SIZE) { const chunk = tracks.slice(i, i + BATCH_SIZE); const promises = chunk.map(async (playlistTrack) => { @@ -316,8 +318,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const results = await Promise.all(promises); for (const track of results) { if (track) { - const requester = context.command.user as unknown as PlayerRequester; - player.addTracks(track, requester); + resolvedTracks.push(track); added++; } else { skipped++; @@ -325,6 +326,11 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { } } + if (resolvedTracks.length > 0) { + const requester = context.command.user as unknown as PlayerRequester; + player.addTracks(resolvedTracks, requester); + } + return { added, skipped }; } diff --git a/src/events/lavashark/TrackAddEvent.ts b/src/events/lavashark/TrackAddEvent.ts index 04a23ac4..c287df71 100644 --- a/src/events/lavashark/TrackAddEvent.ts +++ b/src/events/lavashark/TrackAddEvent.ts @@ -33,14 +33,12 @@ export class TrackAddEvent extends BaseLavaSharkEvent<'trackAdd'> { } // Refresh dashboard - try { - if (player.dashboardMsg) await player.dashboardMsg.delete(); - } catch (error) { - bot.logger.error( bot.shardId, 'Dashboard delete error:' + error); + if (player.current) { + if (!player.dashboardMsg) { + await client.dashboard.initialize((player.metadata as Message), player); + } + await client.dashboard.update(player, player.current); } - - await client.dashboard.initialize((player.metadata as Message), player); - await client.dashboard.update(player, player.current!); } /** From a1e001cabdcc916f9c34706f9ca2bdd5990223c7 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 10:51:29 +0900 Subject: [PATCH 20/49] fix: optimize graceful shutdown handling and sqlite WAL checkpointing --- src/App.ts | 27 ++++++++++++++++++--------- src/lib/DatabaseManager.ts | 5 +++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/App.ts b/src/App.ts index 9b318a89..33f758a1 100644 --- a/src/App.ts +++ b/src/App.ts @@ -134,7 +134,12 @@ class App { * @private */ #setShutdownSignalHandlers(): void { + let isShuttingDown = false; + const shutdown = async (signal: string) => { + if (isShuttingDown) return; + isShuttingDown = true; + this.bot.logger.log( this.bot.shardId, `Received ${signal}. Closing server gracefully...`); const timeout = setTimeout(() => { @@ -143,29 +148,33 @@ class App { }, 30 * 1000); try { - // Stop periodic saves and save all active queues before shutdown + // Stop periodic saves and save all active queues in parallel before shutdown if (this.bot.config.queuePersistence.enabled && this.#client.queuePersistence) { this.bot.logger.log( this.bot.shardId, 'Saving active queues before shutdown...'); this.#client.queuePersistence.stopAllPeriodicSaves(); - for (const player of this.#client.lavashark.players.values()) { - await this.#client.queuePersistence.saveQueue(player); - } + const players = Array.from(this.#client.lavashark?.players?.values() ?? []); + await Promise.allSettled( + players.map(player => this.#client.queuePersistence!.saveQueue(player)) + ); } // Close the lavashark players connections this.bot.logger.log( this.bot.shardId, 'Closing voice channel connection...'); + const activePlayers = Array.from(this.#client.lavashark?.players?.values() ?? []); await Promise.allSettled( - Array.from(this.#client.lavashark.players.values()).map(player => player.destroy()) + activePlayers.map(player => player.destroy()) ); - await new Promise(resolve => setTimeout(resolve, 2000)); this.bot.logger.log( this.bot.shardId, 'Voice channel connection closed.'); // Close the lavashark nodes connections this.bot.logger.log( this.bot.shardId, 'Closing lavashark nodes...'); - for (const node of this.#client.lavashark.nodes) { - node.disconnect(); + if (this.#client.lavashark?.nodes) { + for (const node of this.#client.lavashark.nodes) { + try { + node.disconnect(); + } catch (_) {} + } } - await new Promise(resolve => setTimeout(resolve, 2000)); this.bot.logger.log( this.bot.shardId, 'Lavashark nodes closed.'); // Close discord.js connection diff --git a/src/lib/DatabaseManager.ts b/src/lib/DatabaseManager.ts index 6df4f56f..6eec5c66 100644 --- a/src/lib/DatabaseManager.ts +++ b/src/lib/DatabaseManager.ts @@ -59,6 +59,10 @@ export class DatabaseManager { return; } + try { + this.db.pragma('wal_checkpoint(TRUNCATE)'); + } catch (_) {} + this.db.close(); this.db = null; this.bot.logger.log(this.bot.shardId, '[DatabaseManager] Database connection closed.'); @@ -71,6 +75,7 @@ export class DatabaseManager { this.db.pragma('journal_mode = WAL'); // Default is DELETE this.db.pragma('synchronous = NORMAL'); // Default is FULL + this.db.pragma('temp_store = MEMORY'); this.db.pragma('foreign_keys = ON'); // Default is OFF this.db.pragma('busy_timeout = 5000'); // Default is 0 (no timeout) } From 10faa7366a9203837f17f025309cd66b22a1fc20 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 10:54:56 +0900 Subject: [PATCH 21/49] fix: allow direct text channel instances in DashboardManager.initialize --- src/lib/DashboardManager.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index b14a9b9b..96c5bd84 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -26,22 +26,28 @@ export class DashboardManager { /** * Initialize dashboard in a channel */ - public async initialize(interactionOrMessage: ChatInputCommandInteraction | Message, player: Player): Promise { + public async initialize( + target: ChatInputCommandInteraction | Message | any, + player: Player + ): Promise { let channel; - if (interactionOrMessage instanceof Message) { - channel = (interactionOrMessage as Message).channel; + if (target instanceof Message) { + channel = target.channel; } - else if (interactionOrMessage instanceof ChatInputCommandInteraction) { - channel = (interactionOrMessage as ChatInputCommandInteraction).channel; + else if (target instanceof ChatInputCommandInteraction) { + channel = target.channel; + } + else if (target && typeof target.send === 'function') { + channel = target; } else { - throw new TypeError('Invalid Interaction or Message type'); + throw new TypeError('Invalid Interaction, Message, or Channel type'); } const lng = this.#bot.guildLanguageManager?.get(player.guildId); - player.dashboardMsg = await (channel as any /* discord.js type error ? (v14.16.2) */).send({ + player.dashboardMsg = await (channel as any).send({ embeds: [embeds.connected(this.#bot, lng)], components: [] }); From 83a526ecd5a0258aaf563dfff3f8aebf2e06e8d0 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 10:58:49 +0900 Subject: [PATCH 22/49] feat: re-send dashboard to the bottom of the channel on update --- src/lib/DashboardManager.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 96c5bd84..36a39106 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -54,14 +54,9 @@ export class DashboardManager { } /** - * Update dashboard with current track and player state + * Update dashboard with current track and player state (re-sends to the bottom of the channel) */ public async update(player: Player, track: Track): Promise { - if (!player.dashboardMsg) { - this.#bot.logger.error( this.#bot.shardId, 'Dashboard update called but dashboard is null'); - return; - } - const lng = this.#bot.guildLanguageManager?.get(player.guildId); let subtitle = await this.#buildSubtitle(player, track, lng); const buttons = ButtonsBuilder.createDashboardButtons(player, lng); @@ -74,8 +69,24 @@ export class DashboardManager { subtitle = subtitle.substring(0, 1021) + '...'; } + const channel = player.dashboardMsg?.channel + ?? (player.textChannelId ? this.#client.channels.cache.get(player.textChannelId) : null); + + if (!channel || typeof (channel as any).send !== 'function') { + this.#bot.logger.error(this.#bot.shardId, 'Dashboard update called but channel is missing or invalid'); + return; + } + + const oldMsg = player.dashboardMsg; + + if (oldMsg) { + try { + await oldMsg.delete(); + } catch (_) {} + } + try { - await player.dashboardMsg.edit({ + player.dashboardMsg = await (channel as any).send({ embeds: [embeds.dashboard( this.#bot, this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), @@ -87,7 +98,7 @@ export class DashboardManager { components: [buttons] }); } catch (error) { - this.#bot.logger.error( this.#bot.shardId, 'Dashboard update error: ' + error); + this.#bot.logger.error(this.#bot.shardId, 'Dashboard update error: ' + error); } } From 7ee9b0795edce146be5501089cc24c205991ebca Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:03:05 +0900 Subject: [PATCH 23/49] fix: collect all restored tracks before queuing to prevent queue truncation --- src/lib/QueuePersistence.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 6373324a..77d285bc 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -292,9 +292,11 @@ export class QueuePersistence { // Restore tracks in batch chunks to prevent performance bottlenecks const BATCH_SIZE = 5; + const allRestoredTracks: any[] = []; + for (let i = 0; i < queueData.tracks.length; i += BATCH_SIZE) { const chunk = queueData.tracks.slice(i, i + BATCH_SIZE); - const restoredTracks = await Promise.all(chunk.map(async (serializedTrack) => { + const restoredChunk = await Promise.all(chunk.map(async (serializedTrack) => { try { let result = null; @@ -339,19 +341,23 @@ export class QueuePersistence { return null; })); - for (const track of restoredTracks) { + for (const track of restoredChunk) { if (track) { - player.queue.add(track); + allRestoredTracks.push(track); } } } + for (const track of allRestoredTracks) { + player.queue.add(track); + } + // Restore settings player.setRepeatMode(queueData.repeatMode); // Connect and play await player.connect(); - if (!player.playing && !player.paused) { + if (!player.playing && !player.paused && player.queue.tracks.length > 0) { await player.play(); } From 86e3639ebecb09a6463c9ea956346e60f2e76990 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:07:04 +0900 Subject: [PATCH 24/49] fix: implement smart edit and conditional re-send for dashboard messages --- src/lib/DashboardManager.ts | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 36a39106..8d1a7a5e 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -54,7 +54,7 @@ export class DashboardManager { } /** - * Update dashboard with current track and player state (re-sends to the bottom of the channel) + * Update dashboard with current track and player state (smart edit or re-send to bottom) */ public async update(player: Player, track: Track): Promise { const lng = this.#bot.guildLanguageManager?.get(player.guildId); @@ -77,26 +77,35 @@ export class DashboardManager { return; } - const oldMsg = player.dashboardMsg; + const embedPayload = { + embeds: [embeds.dashboard( + this.#bot, + this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), + safeTitle, + subtitle, + track.uri, + track.thumbnail! + )], + components: [buttons] + }; + + if (player.dashboardMsg) { + try { + const lastMsgId = (channel as any).lastMessageId; + if (!lastMsgId || lastMsgId === player.dashboardMsg.id) { + await player.dashboardMsg.edit(embedPayload); + return; + } + } catch (_) {} - if (oldMsg) { try { - await oldMsg.delete(); + await player.dashboardMsg.delete(); } catch (_) {} + player.dashboardMsg = null; } try { - player.dashboardMsg = await (channel as any).send({ - embeds: [embeds.dashboard( - this.#bot, - this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), - safeTitle, - subtitle, - track.uri, - track.thumbnail! - )], - components: [buttons] - }); + player.dashboardMsg = await (channel as any).send(embedPayload); } catch (error) { this.#bot.logger.error(this.#bot.shardId, 'Dashboard update error: ' + error); } From 2a8e6c30a6f97ff92a87190f9b0e70e2dac82f44 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:09:33 +0900 Subject: [PATCH 25/49] fix: ensure unencoded queue tracks are properly serialized and persisted --- src/lib/QueuePersistence.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 77d285bc..ccc1404e 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -87,18 +87,19 @@ export class QueuePersistence { } const serializeTrack = (track: Track): SerializedTrack | null => { - if (!('encoded' in track)) return null; + if (!track) return null; + const encodedTrack = ('encoded' in track && typeof track.encoded === 'string') ? track.encoded : ((track as any).track ?? ''); return { - track: track.encoded, + track: encodedTrack, info: { - identifier: track.identifier, - title: track.title, - author: track.author, + identifier: track.identifier || '', + title: track.title || '', + author: track.author || '', length: typeof track.duration === 'number' ? track.duration : 0, - uri: track.uri, + uri: track.uri || '', sourceName: (track as any).sourceName || 'youtube', - isSeekable: track.isSeekable, - isStream: track.isStream + isSeekable: track.isSeekable ?? true, + isStream: track.isStream ?? false }, requesterId: track.requester?.id || '', requesterTag: track.requester?.tag || '' @@ -115,10 +116,8 @@ export class QueuePersistence { // Append queue tracks for (const track of player.queue.tracks) { - if ('encoded' in track) { - const s = serializeTrack(track as Track); - if (s) serializedTracks.push(s); - } + const s = serializeTrack(track as Track); + if (s) serializedTracks.push(s); } const queueData: PersistedQueue = { From a82a2ba2f49931912bce9100c53c2286a305d7cb Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:12:55 +0900 Subject: [PATCH 26/49] fix: prevent race conditions and duplicate updates during track skip --- src/commands/SkipCommand.ts | 4 ---- src/lib/DashboardManager.ts | 6 ++++++ src/lib/handlers/SkipButtonHandler.ts | 2 -- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/SkipCommand.ts b/src/commands/SkipCommand.ts index 08631a17..ddead2bc 100644 --- a/src/commands/SkipCommand.ts +++ b/src/commands/SkipCommand.ts @@ -69,10 +69,6 @@ export class SkipCommand extends BaseCommand { const success = await player.skip(); - if (success && player.current && client.dashboard) { - await client.dashboard.update(player, player.current); - } - if (context.isMessage()) { await context.react(success ? '👍' : '❌'); if (!hasMoreTracks && context.channel && 'send' in context.channel) { diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 8d1a7a5e..15198554 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -17,6 +17,7 @@ import type { Client } from 'discord.js'; export class DashboardManager { #bot: Bot; #client: Client; + #updatePromise: Promise = Promise.resolve(); constructor(bot: Bot, client: Client) { this.#bot = bot; @@ -57,6 +58,11 @@ export class DashboardManager { * Update dashboard with current track and player state (smart edit or re-send to bottom) */ public async update(player: Player, track: Track): Promise { + this.#updatePromise = this.#updatePromise.then(() => this.#performUpdate(player, track)).catch(() => {}); + return this.#updatePromise; + } + + async #performUpdate(player: Player, track: Track): Promise { const lng = this.#bot.guildLanguageManager?.get(player.guildId); let subtitle = await this.#buildSubtitle(player, track, lng); const buttons = ButtonsBuilder.createDashboardButtons(player, lng); diff --git a/src/lib/handlers/SkipButtonHandler.ts b/src/lib/handlers/SkipButtonHandler.ts index 4c491a3c..a2883947 100644 --- a/src/lib/handlers/SkipButtonHandler.ts +++ b/src/lib/handlers/SkipButtonHandler.ts @@ -68,8 +68,6 @@ export class SkipButtonHandler extends DashboardButtonHandler { }); return; } catch (_) {} - } else if (player.current && client.dashboard) { - await client.dashboard.update(player, player.current); } if (!interaction.replied && !interaction.deferred) { From 227ef12b66e59aec5835672dbd44d66cf64515ff Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:15:45 +0900 Subject: [PATCH 27/49] fix: prevent empty search query from resolving default YouTube trending video --- src/lib/QueuePersistence.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index ccc1404e..423668fe 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -300,14 +300,16 @@ export class QueuePersistence { let result = null; // Try loading by encoded track string first - try { - result = await client.lavashark.search(serializedTrack.track); - } catch (_) { - // Encoded string failed, will try fallback + if (serializedTrack.track && serializedTrack.track.trim() !== '') { + try { + result = await client.lavashark.search(serializedTrack.track); + } catch (_) { + // Encoded string failed, will try fallback + } } // Fallback: search by URI - if (!result || !result.tracks || result.tracks.length === 0) { + if ((!result || !result.tracks || result.tracks.length === 0) && serializedTrack.info?.uri && serializedTrack.info.uri.trim() !== '') { try { result = await client.lavashark.search(serializedTrack.info.uri); } catch (_) { @@ -316,9 +318,12 @@ export class QueuePersistence { } // Final fallback: search by title + author - if (!result || !result.tracks || result.tracks.length === 0) { + if ((!result || !result.tracks || result.tracks.length === 0) && serializedTrack.info?.title && serializedTrack.info.title.trim() !== '') { try { - result = await client.lavashark.search(`ytsearch:${serializedTrack.info.title} ${serializedTrack.info.author}`); + const query = serializedTrack.info.author + ? `${serializedTrack.info.title} ${serializedTrack.info.author}`.trim() + : serializedTrack.info.title.trim(); + result = await client.lavashark.search(`ytsearch:${query}`); } catch (_) { // All methods failed } From 3837d842b0a1c98ed8583ae2e5f87d21eef9dbe9 Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:18:02 +0900 Subject: [PATCH 28/49] fix: delete old dashboard message during initialization to prevent duplicate --- src/lib/DashboardManager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 15198554..acf41ddc 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -46,6 +46,13 @@ export class DashboardManager { throw new TypeError('Invalid Interaction, Message, or Channel type'); } + if (player.dashboardMsg) { + try { + await player.dashboardMsg.delete(); + } catch (_) {} + player.dashboardMsg = null; + } + const lng = this.#bot.guildLanguageManager?.get(player.guildId); player.dashboardMsg = await (channel as any).send({ From d54fb1efa4e10147ca30b75c8e58d6bb4e88e63c Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:19:29 +0900 Subject: [PATCH 29/49] fix: preserve custom volume level across restarts and dashboard updates --- src/lib/DashboardManager.ts | 4 +++- src/lib/QueuePersistence.ts | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index acf41ddc..de16159f 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -153,10 +153,12 @@ export class DashboardManager { async #buildSubtitle(player: Player, track: Track, lng?: string): Promise { const repeatModeLabel = this.#getRepeatModeLabel(player.repeatMode, lng); + const currentVolume = player.volume ?? (player.setting as any)?.volume ?? this.#bot.guildVolumeManager?.get(player.guildId) ?? this.#bot.config.bot.volume.default; + let subtitle = this.#bot.i18n.t('embeds:DASHBOARD_SUBTITLE', { author: track.author, duration: track.duration.label, - volume: player.volume, + volume: currentVolume, repeatMode: repeatModeLabel, lng }); diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 423668fe..3b8bb6fe 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -120,13 +120,15 @@ export class QueuePersistence { if (s) serializedTracks.push(s); } + const currentVolume = player.volume ?? (player.setting as any)?.volume ?? this.bot.guildVolumeManager?.get(player.guildId) ?? this.bot.config.bot.volume.default; + const queueData: PersistedQueue = { guildId: player.guildId, voiceChannelId: player.voiceChannelId || '', textChannelId: player.textChannelId || '', tracks: serializedTracks, currentTrackIndex: 0, - volume: (player as any).volume || 100, + volume: currentVolume, repeatMode: player.repeatMode, paused: player.paused, position: player.position, @@ -365,7 +367,16 @@ export class QueuePersistence { await player.play(); } - // Set volume after playback starts (Player.volume is a read-only getter) + if (queueData.paused && player.playing) { + await player.pause(); + } + + // Set volume after playback starts + if (!player.setting) { + player.setting = { queuePage: null, volume: queueData.volume, fairQueueRotation: [] }; + } else { + player.setting.volume = queueData.volume; + } player.filters.setVolume(queueData.volume); // Seek to saved position after playback starts From 37d832cfafe603b26969db4e6ac58fa2a5f1f28e Mon Sep 17 00:00:00 2001 From: hoehoetvhs Date: Fri, 14 Aug 2026 11:37:49 +0900 Subject: [PATCH 30/49] fix: resolve option name mismatch in language command --- src/commands/LanguageCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/LanguageCommand.ts b/src/commands/LanguageCommand.ts index 4e0c651f..2f0f68cb 100644 --- a/src/commands/LanguageCommand.ts +++ b/src/commands/LanguageCommand.ts @@ -40,7 +40,7 @@ export class LanguageCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { const locale = context.isInteraction() - ? context.getStringOption('locale') + ? (context.getStringOption('language') || context.getStringOption('locale')) : context.args.join(' '); if (!locale) { From 278fc12b939a69968614c59515e8f72cdafcfd72 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:03:50 +0300 Subject: [PATCH 31/49] feat: add M3U playlist import with toggle and admin/DJ restriction Co-authored-by: hoehoetvhss --- src/@types/index.ts | 7 ++++- src/commands/PlayCommand.ts | 7 +++++ src/commands/PlayFirstCommand.ts | 7 +++++ src/commands/PlaylistCommand.ts | 8 +++++ .../playlist/ImportPlaylistSubcommand.ts | 13 ++++++++ .../playlist/PlayPlaylistSubcommand.ts | 8 +++++ src/commands/playlist/ToggleM3uSubcommand.ts | 2 ++ src/lib/DatabaseManager.ts | 8 +++++ src/lib/PlaylistManager.ts | 31 +++++++++++++------ src/locales/en-US/commands.json | 5 ++- src/locales/ko-KR/commands.json | 4 ++- src/locales/ru-RU/commands.json | 5 ++- src/locales/sr-RS/commands.json | 5 ++- src/locales/tr-TR/commands.json | 5 ++- src/locales/zh-TW/commands.json | 5 ++- 15 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 src/commands/playlist/ToggleM3uSubcommand.ts diff --git a/src/@types/index.ts b/src/@types/index.ts index 88f7f871..9f07dbac 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -114,12 +114,17 @@ export type Config = { bot: BotConfig; nodeList: NodeOptions[]; spotify: SpotifyConfig; - blacklist: string[], + blacklist: string[]; webDashboard: WebDashboardConfig; localNode: LocalNodeConfig; command: CommandConfig; database: DatabaseConfig; queuePersistence: QueuePersistenceConfig; + playlist: PlaylistConfig; +}; + +export type PlaylistConfig = { + enabled: boolean; }; export type BotConfig = { diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index e7f2967e..58f1746a 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -46,6 +46,13 @@ export class PlayCommand extends BaseCommand { return; } + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str.trim()); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str.trim()); + if (isM3uUrl || existingPlaylist?.isM3u) { + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); + return; + } + // Search for tracks let res; try { diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 2b3cb14d..18f8a25f 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -46,6 +46,13 @@ export class PlayFirstCommand extends BaseCommand { return; } + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str.trim()); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str.trim()); + if (isM3uUrl || existingPlaylist?.isM3u) { + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); + return; + } + // Search for tracks let res; try { diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 1d161616..deb80bda 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -123,6 +123,14 @@ export class PlaylistCommand extends BaseCommand { client: Client, command: CommandContext, ): Promise { + if (bot.config.playlist?.enabled === false) { + await command.replyEphemeralError( + bot, + command.t('commands:ERROR_PLAYLIST_DISABLED'), + ); + return; + } + // Ensure playlist storage is available before dispatching const playlistManager = bot.playlistManager; if (!playlistManager) { diff --git a/src/commands/playlist/ImportPlaylistSubcommand.ts b/src/commands/playlist/ImportPlaylistSubcommand.ts index 73962555..92747443 100644 --- a/src/commands/playlist/ImportPlaylistSubcommand.ts +++ b/src/commands/playlist/ImportPlaylistSubcommand.ts @@ -1,4 +1,5 @@ import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; +import { DJManager } from '../../lib/DjManager.js'; import { PLAYLIST_TRACK_LIMIT } from '../../lib/PlaylistManager.js'; import { DEFAULT_REMOTE_TEXT_MAX_BYTES, @@ -23,6 +24,18 @@ export class ImportPlaylistSubcommand extends BasePlaylistSubcommand { * Validate import arguments and confirm replacement when required */ public async execute(context: PlaylistSubcommandContext): Promise { + const userId = context.command.user.id; + const member = context.command.member; + const player = context.client.lavashark.getPlayer(context.command.guild!.id); + + if (!DJManager.isDJ(context.bot, userId, member, player ?? undefined)) { + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY'), + ); + return; + } + const args = this.getImportArguments(context.command); if (!args) { await context.command.replyEphemeralError( diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 754e02be..b4cce769 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -52,6 +52,14 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { return; } + if (playlist.isM3u) { + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL'), + ); + return; + } + await this.play(context, playlist); } diff --git a/src/commands/playlist/ToggleM3uSubcommand.ts b/src/commands/playlist/ToggleM3uSubcommand.ts new file mode 100644 index 00000000..9cec1e73 --- /dev/null +++ b/src/commands/playlist/ToggleM3uSubcommand.ts @@ -0,0 +1,2 @@ +// Deprecated subcommand, replaced by Admin/DJ permission check on import and radio-only station selection. +export {}; diff --git a/src/lib/DatabaseManager.ts b/src/lib/DatabaseManager.ts index 9c13d4b2..6df4f56f 100644 --- a/src/lib/DatabaseManager.ts +++ b/src/lib/DatabaseManager.ts @@ -113,6 +113,7 @@ export class DatabaseManager { guild_id TEXT NOT NULL, name TEXT NOT NULL, created_at INTEGER NOT NULL, + is_m3u INTEGER DEFAULT 0, UNIQUE(guild_id, name) ); @@ -128,5 +129,12 @@ export class DatabaseManager { FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE ); `); + + const columns = this.db.prepare('PRAGMA table_info(playlists)').all() as Array<{ name: string }>; + const hasIsM3u = columns.some((col) => col.name === 'is_m3u'); + + if (!hasIsM3u) { + this.db.exec('ALTER TABLE playlists ADD COLUMN is_m3u INTEGER DEFAULT 0;'); + } } } diff --git a/src/lib/PlaylistManager.ts b/src/lib/PlaylistManager.ts index d2b077ac..6cc4b3a4 100644 --- a/src/lib/PlaylistManager.ts +++ b/src/lib/PlaylistManager.ts @@ -15,11 +15,14 @@ export interface PlaylistTrack { position: number; } + + export interface Playlist { id: number; guildId: string; name: string; createdAt: number; + isM3u?: boolean; tracks?: PlaylistTrack[]; } @@ -61,21 +64,29 @@ export class PlaylistManager { try { const playlistRow = db.prepare(` - SELECT id, guild_id as guildId, name, created_at as createdAt + SELECT id, guild_id as guildId, name, created_at as createdAt, is_m3u as isM3u FROM playlists WHERE guild_id = ? AND name = ? - `).get(guildId, name) as Playlist | undefined; + `).get(guildId, name) as any; if (!playlistRow) return null; + const playlist: Playlist = { + id: Number(playlistRow.id), + guildId: String(playlistRow.guildId), + name: String(playlistRow.name), + createdAt: Number(playlistRow.createdAt), + isM3u: Boolean(playlistRow.isM3u), + }; + const tracks = db.prepare(` SELECT id, playlist_id as playlistId, title, url, encoded, author, duration, position FROM playlist_tracks WHERE playlist_id = ? ORDER BY position ASC - `).all(playlistRow.id) as any[]; + `).all(playlist.id) as any[]; - playlistRow.tracks = tracks.map(t => ({ + playlist.tracks = tracks.map(t => ({ id: t.id, playlistId: t.playlistId, title: t.title, @@ -86,7 +97,7 @@ export class PlaylistManager { position: t.position })); - return playlistRow; + return playlist; } catch (error) { this.bot.logger.error(this.bot.shardId, `[PlaylistManager] Error getting playlist ${name}: ${error}`); return null; @@ -134,7 +145,7 @@ export class PlaylistManager { } } - public saveCurrentQueue(guildId: string, name: string, tracks: Omit[]): boolean { + public saveCurrentQueue(guildId: string, name: string, tracks: Omit[], isM3u: boolean = false): boolean { const db = this.db; if (!db || tracks.length === 0 || tracks.length > PLAYLIST_TRACK_LIMIT) return false; @@ -148,9 +159,9 @@ export class PlaylistManager { db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ?').run(guildId, name); const playlistInfo = db.prepare(` - INSERT INTO playlists (guild_id, name, created_at) - VALUES (?, ?, ?) - `).run(guildId, name, Date.now()); + INSERT INTO playlists (guild_id, name, created_at, is_m3u) + VALUES (?, ?, ?, ?) + `).run(guildId, name, Date.now(), isM3u ? 1 : 0); const playlistId = Number(playlistInfo.lastInsertRowid); for (let i = 0; i < tracks.length; i++) { @@ -255,7 +266,7 @@ export class PlaylistManager { author: null })); - const success = this.saveCurrentQueue(guildId, name, playlistTracks); + const success = this.saveCurrentQueue(guildId, name, playlistTracks, true); return success ? {success: true, trackCount: playlistTracks.length} : {success: false, reason: 'SAVE_FAILED'}; diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index c78aaf74..bb10eddf 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -265,5 +265,8 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", + "ERROR_PLAYLIST_DISABLED": "❌ Playlist features are currently disabled.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U playlists cannot be queued entirely. Please use the `/radio` command to select a specific station.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U playlist import is restricted to Admins and DJs only." } \ No newline at end of file diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index d7fc1eae..a02420f2 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -265,5 +265,7 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다." + "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U 플레이리스트 전체는 재생할 수 없습니다. `/radio` 명령어를 사용하여 원하는 라디오 채널을 선택해 주세요.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U 플레이리스트 가져오기는 관리자 또는 DJ 권한이 있는 사용자만 수행할 수 있습니다." } \ No newline at end of file diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 72869773..21019e3e 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -265,5 +265,8 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Пропущен текущий трек. Очередь пуста." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", + "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U-плейлисты нельзя добавлять в очередь целиком. Используйте команду `/radio` для выбора радиостанции.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Импорт M3U-плейлистов доступен только администраторам и DJ." } \ No newline at end of file diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 91fc7d07..2408dc25 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -265,5 +265,8 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", + "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U plejliste se ne mogu staviti u red u celosti. Koristite komandu `/radio` za izbor stanice.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Uvoz M3U plejlisti je ograničen samo na admine i DJ-eve." } \ No newline at end of file diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 775b7af1..8de4a7e9 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -265,5 +265,8 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş." + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", + "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U çalma listeleri tamamen sıraya eklenemez. Lütfen belirli bir istasyon seçmek için `/radio` komutunu kullanın.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U çalma listesi içe aktarma yalnızca Yöneticiler ve DJ'ler ile sınırlıdır." } \ No newline at end of file diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index c82fb8d1..a7cc0cd6 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -265,5 +265,8 @@ "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 儲存確認逾時。", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。" + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。", + "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ 無法將 M3U 播放清單整本加入佇列。請使用 `/radio` 指令選擇特定電台頻道。", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ 匯入 M3U 播放清單僅限管理員及 DJ 權限使用者使用。" } \ No newline at end of file From c52523dee646a569d7ac20a605b782c1ea3fcb06 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:09:23 +0300 Subject: [PATCH 32/49] fix: make saved queue restore reliable - bulk decode, rate-limit retries, preserved metadata Co-authored-by: hoehoetvhss --- src/events/lavashark/ErrorEvent.ts | 7 +- src/lib/QueuePersistence.ts | 221 ++++++++++++++++++------ src/utils/functions/lavasharkRequest.ts | 79 +++++++++ 3 files changed, 252 insertions(+), 55 deletions(-) create mode 100644 src/utils/functions/lavasharkRequest.ts diff --git a/src/events/lavashark/ErrorEvent.ts b/src/events/lavashark/ErrorEvent.ts index 1612b342..4bdc6ac1 100644 --- a/src/events/lavashark/ErrorEvent.ts +++ b/src/events/lavashark/ErrorEvent.ts @@ -15,6 +15,11 @@ export class ErrorEvent extends BaseLavaSharkEvent<'error'> { } public execute(bot: Bot, _client: Client, node: Node, error: any): void { - bot.logger.error( bot.shardId, `[LavaShark] ${node.identifier} error: ${error.message}`); + // `node` can be null at runtime: lavashark may emit 'error' after the + // node was detached from a destroyed player + bot.logger.error( + bot.shardId, + `[LavaShark] ${node?.identifier ?? 'unknown'} error: ${error?.message ?? error}`, + ); } } diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 92c4fc13..30d19009 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -3,6 +3,12 @@ import { ChannelType } from 'discord.js'; import type { Client } from 'discord.js'; import type { Player, Track } from 'lavashark'; import type { Bot, QueueTableRow } from '../@types/index.js'; +import { + decodeTracksWithRetry, + decodeTrackWithRetry, + searchWithRetry, + sleep, +} from '../utils/functions/lavasharkRequest.js'; /** @@ -40,6 +46,27 @@ interface SerializedTrack { requesterTag: string; } +/** + * Format milliseconds as a duration label (mirrors lavashark's formatTime) + */ +const formatDurationLabel = (milliseconds: number): string => { + const seconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let timeString = ''; + if (days > 0) { + timeString += `${days}d `; + } + if (hours % 24 > 0) { + timeString += `${(hours % 24).toString().padStart(2, '0')}:`; + } + timeString += `${(minutes % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`; + + return timeString; +}; + /** * Manager for persisting queue state to SQLite database */ @@ -87,18 +114,19 @@ export class QueuePersistence { } const serializeTrack = (track: Track): SerializedTrack | null => { - if (!('encoded' in track)) return null; + if (!track) return null; + const encodedTrack = ('encoded' in track && typeof track.encoded === 'string') ? track.encoded : ((track as any).track ?? ''); return { - track: track.encoded, + track: encodedTrack, info: { - identifier: track.identifier, - title: track.title, - author: track.author, - length: typeof track.duration === 'number' ? track.duration : 0, - uri: track.uri, + identifier: track.identifier || '', + title: track.title || '', + author: track.author || '', + length: typeof track.duration === 'number' ? track.duration : (track.duration?.value ?? 0), + uri: track.uri || '', sourceName: (track as any).sourceName || 'youtube', - isSeekable: track.isSeekable, - isStream: track.isStream + isSeekable: track.isSeekable ?? true, + isStream: track.isStream ?? false }, requesterId: track.requester?.id || '', requesterTag: track.requester?.tag || '' @@ -115,19 +143,19 @@ export class QueuePersistence { // Append queue tracks for (const track of player.queue.tracks) { - if ('encoded' in track) { - const s = serializeTrack(track as Track); - if (s) serializedTracks.push(s); - } + const s = serializeTrack(track as Track); + if (s) serializedTracks.push(s); } + const currentVolume = player.volume ?? (player.setting as any)?.volume ?? this.bot.guildVolumeManager?.get(player.guildId) ?? this.bot.config.bot.volume.default; + const queueData: PersistedQueue = { guildId: player.guildId, voiceChannelId: player.voiceChannelId || '', textChannelId: player.textChannelId || '', tracks: serializedTracks, currentTrackIndex: 0, - volume: (player as any).volume || 100, + volume: currentVolume, repeatMode: player.repeatMode, paused: player.paused, position: player.position, @@ -290,49 +318,51 @@ export class QueuePersistence { } } - // Restore tracks - try encoded track string first, fall back to URI search - for (const serializedTrack of queueData.tracks) { - try { - let result = null; - - // Try loading by encoded track string first - try { - result = await client.lavashark.search(serializedTrack.track); - } catch (_) { - // Encoded string failed, will try fallback - } + // Bulk-decode saved encoded tracks in a single request — Lavalink + // nodes rate-limit REST calls (HTTP 429), so per-track requests + // would exhaust the node budget. Fall back to paced per-track + // restore for anything the bulk decode did not cover. + const serializedEntries = queueData.tracks.map((track, index) => ({ track, index })); + const restoredByIndex: Map = new Map(); + const TRACK_REQUEST_DELAY_MS = 400; - // Fallback: search by URI - if (!result || !result.tracks || result.tracks.length === 0) { - try { - result = await client.lavashark.search(serializedTrack.info.uri); - } catch (_) { - // URI search failed, will try title search + const encodedEntries = serializedEntries.filter(({ track }) => + track.track && track.track.trim() !== '', + ); + if (encodedEntries.length > 0) { + const decoded = await decodeTracksWithRetry( + client.lavashark, + encodedEntries.map(({ track }) => track.track.trim()), + ); + if (decoded && decoded.length === encodedEntries.length) { + encodedEntries.forEach(({ track, index }, i) => { + if (decoded[i]) { + restoredByIndex.set( + index, + this.applySerializedMetadata(decoded[i], track), + ); } - } + }); + } + } - // Final fallback: search by title + author - if (!result || !result.tracks || result.tracks.length === 0) { - try { - result = await client.lavashark.search(`ytsearch:${serializedTrack.info.title} ${serializedTrack.info.author}`); - } catch (_) { - // All methods failed - } - } + const allRestoredTracks: any[] = []; + for (const { track, index } of serializedEntries) { + const restored = restoredByIndex.get(index); + if (restored) { + allRestoredTracks.push(restored); + continue; + } - if (result && result.tracks && result.tracks.length > 0) { - const track = result.tracks[0]; - track.requester = { - id: serializedTrack.requesterId, - tag: serializedTrack.requesterTag - } as any; - player.queue.add(track); - } else { - this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); - } - } catch (error) { - this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); + const fallback = await this.restoreTrack(client, track); + if (fallback) { + allRestoredTracks.push(fallback); } + await sleep(TRACK_REQUEST_DELAY_MS); + } + + for (const track of allRestoredTracks) { + player.queue.add(track); } // Restore settings @@ -340,11 +370,20 @@ export class QueuePersistence { // Connect and play await player.connect(); - if (!player.playing && !player.paused) { + if (!player.playing && !player.paused && player.queue.tracks.length > 0) { await player.play(); } - // Set volume after playback starts (Player.volume is a read-only getter) + if (queueData.paused && player.playing) { + await player.pause(); + } + + // Set volume after playback starts + if (!player.setting) { + player.setting = { queuePage: null, volume: queueData.volume, fairQueueRotation: [] }; + } else { + player.setting.volume = queueData.volume; + } player.filters.setVolume(queueData.volume); // Seek to saved position after playback starts @@ -368,6 +407,80 @@ export class QueuePersistence { } } + /** + * Resolve a single serialized track back into a playable track. + * Decodes the saved encoded string first (exact restore), then falls back + * to searching by URI, then by title + author. Requests are retried once + * each because Lavalink nodes rate-limit REST calls (HTTP 429). + */ + private async restoreTrack( + client: Client, + serializedTrack: SerializedTrack, + ): Promise { + try { + let resolvedTrack: any = null; + + // 1) Decode the saved encoded track exactly (preserves the original track data) + if (serializedTrack.track && serializedTrack.track.trim() !== '') { + resolvedTrack = await decodeTrackWithRetry( + client.lavashark, + serializedTrack.track, + ); + } + + // 2) Fallback: search by URI + if (!resolvedTrack && serializedTrack.info?.uri && serializedTrack.info.uri.trim() !== '') { + const result = await searchWithRetry(client.lavashark, serializedTrack.info.uri); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + resolvedTrack = result.tracks[0]; + } + } + + // 3) Final fallback: search by title + author + if (!resolvedTrack && serializedTrack.info?.title && serializedTrack.info.title.trim() !== '') { + const query = serializedTrack.info.author + ? `${serializedTrack.info.title} ${serializedTrack.info.author}`.trim() + : serializedTrack.info.title.trim(); + const result = await searchWithRetry(client.lavashark, `ytsearch:${query}`); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + resolvedTrack = result.tracks[0]; + } + } + + if (resolvedTrack) { + return this.applySerializedMetadata(resolvedTrack, serializedTrack); + } + + this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); + } catch (error) { + this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); + } + return null; + } + + /** + * Restore the originally saved metadata so names are never replaced by + * whatever the fallback search returned + */ + private applySerializedMetadata(track: any, serializedTrack: SerializedTrack): any { + const info = serializedTrack.info ?? {}; + if (info.title) track.title = info.title; + if (info.author) track.author = info.author; + if (info.uri) track.uri = info.uri; + if (info.identifier) track.identifier = info.identifier; + if (typeof info.length === 'number' && info.length > 0) { + track.duration = { + label: formatDurationLabel(info.length), + value: info.length, + }; + } + track.requester = { + id: serializedTrack.requesterId, + tag: serializedTrack.requesterTag + } as any; + return track; + } + /** * Check if any persisted queues exist for a specific voice channel * @param voiceChannelId - Voice channel ID to check diff --git a/src/utils/functions/lavasharkRequest.ts b/src/utils/functions/lavasharkRequest.ts new file mode 100644 index 00000000..e5fef773 --- /dev/null +++ b/src/utils/functions/lavasharkRequest.ts @@ -0,0 +1,79 @@ +import type { LavaShark } from 'lavashark'; + + +const DEFAULT_RETRY_ATTEMPTS = 3; +const DEFAULT_RETRY_DELAY_MS = 2000; + +/** + * Wait for the given number of milliseconds + */ +export const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Search a track with retries. + * Lavalink nodes enforce REST rate limits (HTTP 429); a single retry after a + * short delay lets transient rate limiting resolve without skipping tracks. + */ +export async function searchWithRetry( + lavashark: LavaShark, + query: string, + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.search(query); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} + +/** + * Decode a track from its encoded string with retries + */ +export async function decodeTrackWithRetry( + lavashark: LavaShark, + encoded: string, + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.decodeTrack(encoded); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} + +/** + * Decode many tracks in a single request. + * Lavalink rate limits REST calls (HTTP 429) aggressively; bulk-decoding a + * whole saved queue with one `/decodetracks` request avoids exhausting the + * node's request budget entirely. + */ +export async function decodeTracksWithRetry( + lavashark: LavaShark, + encodedTracks: string[], + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.decodeTracks(encodedTracks); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} From f172e990f5ed4d9a34a99d5bb50b37fe8e94c4a6 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:10:18 +0300 Subject: [PATCH 33/49] fix: correct saved playlist playback - voice channel join, mix expansion, title fallbacks Co-authored-by: hoehoetvhss --- src/commands/PlayFirstCommand.ts | 38 ++-- src/commands/PlaylistCommand.ts | 52 +++--- src/commands/RadioCommand.ts | 2 +- .../playlist/PlayPlaylistSubcommand.ts | 166 +++++++++++++----- 4 files changed, 174 insertions(+), 84 deletions(-) diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 18f8a25f..2009598b 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -1,4 +1,5 @@ import i18next from 'i18next'; +import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType } from '../@types/index.js'; @@ -244,6 +245,10 @@ export class PlayFirstCommand extends BaseCommand { const requester = context.isMessage() ? context.getMessage().author : context.getInteraction().user; const curVolume = player.setting.volume ?? bot.guildVolumeManager?.get(player.guildId) ?? bot.config.bot.volume.default; + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + if (res.loadType === LoadType.PLAYLIST) { const tracks = tracksToAdd !== undefined ? res.tracks.slice(0, tracksToAdd) : res.tracks; @@ -251,26 +256,23 @@ export class PlayFirstCommand extends BaseCommand { (t as any).requester = requester; } - if (!player.playing) { - const firstTrack = tracks[0]; - const restTracks = tracks.slice(1); + const firstTrack = tracks[0]; + const restTracks = tracks.slice(1); - if (restTracks.length > 0) { - player.queue.tracks.unshift(...restTracks); - } + if (restTracks.length > 0) { + player.queue.tracks.unshift(...restTracks); + } - player.filters.setVolume(curVolume); - await player.prioritizePlay(firstTrack, requester as any) - .catch(async (error) => { - bot.logger.error(bot.shardId, 'Error playing track: ' + error); - await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); - return player.destroy(); - }); - } else { - player.queue.tracks.unshift(...tracks); - if (player.current && client.dashboard) { - await client.dashboard.update(player, player.current); - } + player.filters.setVolume(curVolume); + await player.prioritizePlay(firstTrack, requester as any) + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + + if (player.current && client.dashboard) { + await client.dashboard.update(player, player.current); } } else { diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index deb80bda..a58fc94f 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -41,24 +41,29 @@ export class PlaylistCommand extends BaseCommand { /** * Build playlist command metadata and slash command options */ - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { aliases: ['pl'], category: CommandCategory.MUSIC, - description: i18next.t('commands:CONFIG_PLAYLIST_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAYLIST_DESCRIPTION', { lng }), name: 'playlist', options: [ this.createNamedSubcommand( 'save', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE', + true, + lng ), this.createNamedSubcommand( 'play', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY', + true, + lng ), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST', + { lng } ), name: 'list', type: ApplicationCommandOptionType.Subcommand, @@ -66,21 +71,27 @@ export class PlaylistCommand extends BaseCommand { this.createNamedSubcommand( 'info', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO', + true, + lng ), this.createNamedSubcommand( 'delete', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE', + true, + lng ), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT', + { lng } ), name: 'import', options: [ - this.createNameOption(), + this.createNameOption(true, lng), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_URL', + { lng } ), name: 'url', required: true, @@ -92,13 +103,15 @@ export class PlaylistCommand extends BaseCommand { { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK', + { lng } ), name: 'remove-track', options: [ - this.createNameOption(), + this.createNameOption(true, lng), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_INDEX', + { lng } ), name: 'index', required: true, @@ -110,7 +123,7 @@ export class PlaylistCommand extends BaseCommand { ], sendTyping: true, showHelp: true, - usage: i18next.t('commands:CONFIG_PLAYLIST_USAGE'), + usage: i18next.t('commands:CONFIG_PLAYLIST_USAGE', { lng }), voiceChannel: false, }; } @@ -141,17 +154,12 @@ export class PlaylistCommand extends BaseCommand { return; } - // Normalize the subcommand name from either command source - const name = command.isMessage() - ? command.args[0]?.toLowerCase() - : command.getInteraction().options.getSubcommand(false); - if (!name) { - await command.replyEphemeralError( - bot, - command.t('commands:ERROR_PLAYLIST_SUBCOMMAND_INVALID'), - ); - return; - } + // Normalize the subcommand name from either command source (default to 'list') + const name = ( + command.isMessage() + ? command.args[0]?.toLowerCase() + : command.getInteraction().options.getSubcommand(false) + ) || 'list'; // Find the matching OOP subcommand implementation const subcommand = this.#subcommands.find( @@ -174,11 +182,13 @@ export class PlaylistCommand extends BaseCommand { private createNamedSubcommand( name: PlaylistSubcommandName, descriptionKey: string, + nameRequired: boolean = true, + lng?: string ): ApplicationCommandSubCommandData { return { - description: i18next.t(descriptionKey), + description: i18next.t(descriptionKey, { lng }), name, - options: [this.createNameOption()], + options: [this.createNameOption(nameRequired, lng)], type: ApplicationCommandOptionType.Subcommand, }; } @@ -186,11 +196,11 @@ export class PlaylistCommand extends BaseCommand { /** * Create the shared playlist name option */ - private createNameOption(): ApplicationCommandStringOptionData { + private createNameOption(required: boolean = true, lng?: string): ApplicationCommandStringOptionData { return { - description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME'), + description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME', { lng }), name: 'name', - required: true, + required, type: ApplicationCommandOptionType.String, }; } diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index be045102..7e9aad15 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -13,7 +13,7 @@ export class RadioCommand extends BaseCommand { public getMetadata(_bot: Bot): CommandMetadata { return { name: 'radio', - aliases: ['record'], + aliases: [], description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION'), usage: i18next.t('commands:CONFIG_RADIO_USAGE'), category: CommandCategory.MUSIC, diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index b4cce769..aa3b98e0 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -1,8 +1,14 @@ -import { DJModeEnum } from '../../@types/index.js'; +import { DJModeEnum, LoadType } from '../../@types/index.js'; import { embeds } from '../../embeds/index.js'; import { DJManager } from '../../lib/DjManager.js'; import { QueueLimitManager } from '../../lib/QueueLimitManager.js'; import { isUserInBlacklist } from '../../utils/functions/isUserInBlacklist.js'; +import { + decodeTracksWithRetry, + decodeTrackWithRetry, + searchWithRetry, + sleep, +} from '../../utils/functions/lavasharkRequest.js'; import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; import type { GuildMember, VoiceBasedChannel } from 'discord.js'; @@ -193,6 +199,12 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { ? command.getMessage() : command.getInteraction(); try { + if (player.voiceChannelId !== voiceChannel.id) { + player.setVoiceChannel(voiceChannel.id); + } + // connect() is idempotent: it only sends a voice state update when + // the player is not already connected, so always call it — a fresh + // player would otherwise never join the voice channel await player.connect(); player.metadata = metadata; } catch (error) { @@ -229,24 +241,41 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { } /** - * Resolve a stored track using encoded data, URL, then title search + * Resolve a stored track using encoded data, URL, then title search. + * YouTube mix/playlist URLs (`watch?v=X&list=...`) resolve as a + * PLAYLIST result, so the whole playlist is expanded into the queue. */ private async findTrack( context: PlaylistSubcommandContext, playlistTrack: PlaylistTrack, - ) { + ): Promise { + if (playlistTrack.encoded) { + const track = await decodeTrackWithRetry( + context.client.lavashark, + playlistTrack.encoded, + ); + if (track) return [track]; + } + const queries = [ - playlistTrack.encoded, playlistTrack.url, `ytsearch:${playlistTrack.title}`, ].filter((query): query is string => Boolean(query)); for (const query of new Set(queries)) { - try { - const result = await context.client.lavashark.search(query); - if (result.tracks.length > 0) return result.tracks[0]; - } catch { - // Try the next fallback query. + const result = await searchWithRetry(context.client.lavashark, query); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + if (result.loadType === LoadType.PLAYLIST) { + // Mix/playlist expansion keeps the real track names + return result.tracks; + } + const track = result.tracks[0]; + // Keep the name exactly as it was saved, even if the fallback + // search resolved to a different video + if (playlistTrack.title) track.title = playlistTrack.title; + if (playlistTrack.author) track.author = playlistTrack.author; + if (playlistTrack.url) track.uri = playlistTrack.url; + return [track]; } } @@ -254,7 +283,11 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { } /** - * Resolve and enqueue tracks while respecting queue limits + * Resolve and enqueue tracks while respecting queue limits. + * Tracks carrying a saved encoded string are bulk-decoded in a single + * request (`/decodetracks`) — Lavalink nodes rate-limit REST calls + * (HTTP 429), and bursting per-track requests makes tracks get skipped. + * Entries without an encoded string fall back to paced per-track lookup. */ private async loadTracks( context: PlaylistSubcommandContext, @@ -264,39 +297,69 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { ): Promise { let added = 0; let skipped = 0; - - for (const playlistTrack of playlist.tracks ?? []) { - // Check both per-user and global limits before each search - const limit = QueueLimitManager.canAddSongs( - context.bot, - player, - context.command.user.id, - member, - 1, + const tracks = playlist.tracks ?? []; + const TRACK_REQUEST_DELAY_MS = 400; + + const canAdd = (): boolean => QueueLimitManager.canAddSongs( + context.bot, + player, + context.command.user.id, + member, + 1, + ).canAdd; + + const entries = tracks.map((track, index) => ({ track, index })); + const resolvedByIndex: Map = new Map(); + + // Bulk-decode entries that carry a saved encoded track string + const encodedEntries = entries.filter(({ track }) => + track.encoded && track.encoded.trim() !== '', + ); + if (encodedEntries.length > 0) { + const decoded = await decodeTracksWithRetry( + context.client.lavashark, + encodedEntries.map(({ track }) => track.encoded!.trim()), ); - if (!limit.canAdd) { - skipped++; + if (decoded && decoded.length === encodedEntries.length) { + encodedEntries.forEach(({ track, index }, i) => { + if (decoded[i] && canAdd()) { + // Keep the names exactly as they were saved + if (track.title) decoded[i].title = track.title; + if (track.author) decoded[i].author = track.author; + if (track.url) decoded[i].uri = track.url; + resolvedByIndex.set(index, [decoded[i]]); + added++; + } + }); + } + } + + // Per-track fallback for entries not covered by the bulk decode + for (const { track, index } of entries) { + if (resolvedByIndex.has(index)) { continue; } - try { - const track = await this.findTrack(context, playlistTrack); - if (!track) { - skipped++; - continue; - } + if (!canAdd()) { + skipped++; + await sleep(TRACK_REQUEST_DELAY_MS); + continue; + } - // LavaShark resolves discord.js through a separate type entry point. - const requester = context.command.user as unknown as PlayerRequester; - player.addTracks(track, requester); - added++; - } catch (error) { - context.bot.logger.error( - context.bot.shardId, - `[PlaylistCommand] Error loading track ${playlistTrack.title}: ${error}`, - ); + const result = await this.findTrack(context, track); + if (result && result.length > 0) { + resolvedByIndex.set(index, result); + added += result.length; + } else { skipped++; } + await sleep(TRACK_REQUEST_DELAY_MS); + } + + const resolvedTracks = entries.flatMap(({ index }) => resolvedByIndex.get(index) ?? []); + if (resolvedTracks.length > 0) { + const requester = context.command.user as unknown as PlayerRequester; + player.addTracks(resolvedTracks, requester); } return { added, skipped }; @@ -315,14 +378,29 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { context.bot.config.bot.volume.default; player.filters.setVolume(volume); - try { - await player.play(); - } catch (error) { - context.bot.logger.error( - context.bot.shardId, - `[PlaylistCommand] Error playing track: ${error}`, - ); - await player.destroy(); + // Retry playback a few times so a transient rate limit (HTTP 429) + // does not destroy the player right after loading a playlist + const PLAY_RETRY_ATTEMPTS = 3; + const PLAY_RETRY_DELAY_MS = 2000; + for (let attempt = 0; attempt < PLAY_RETRY_ATTEMPTS; attempt++) { + try { + await player.play(); + return; + } catch (error) { + if (attempt < PLAY_RETRY_ATTEMPTS - 1) { + context.bot.logger.log( + context.bot.shardId, + `[PlaylistCommand] Error playing track (attempt ${attempt + 1}/${PLAY_RETRY_ATTEMPTS}): ${error}`, + ); + await sleep(PLAY_RETRY_DELAY_MS); + continue; + } + context.bot.logger.error( + context.bot.shardId, + `[PlaylistCommand] Error playing track: ${error}`, + ); + await player.destroy(); + } } return; } From fad66430df09b2c59603a15837c89c8dac2dead8 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:10:23 +0300 Subject: [PATCH 34/49] fix: stabilize dashboard messages - smart edit, duplicate prevention, skip race conditions Co-authored-by: hoehoetvhss --- src/commands/SkipCommand.ts | 6 ++ src/events/lavashark/TrackAddEvent.ts | 12 ++- src/lib/DashboardManager.ts | 101 +++++++++++++++++++------- src/lib/handlers/SkipButtonHandler.ts | 12 ++- 4 files changed, 91 insertions(+), 40 deletions(-) diff --git a/src/commands/SkipCommand.ts b/src/commands/SkipCommand.ts index e0b2350e..873f2fc4 100644 --- a/src/commands/SkipCommand.ts +++ b/src/commands/SkipCommand.ts @@ -1,4 +1,5 @@ import i18next from 'i18next'; +import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory } from '../@types/index.js'; @@ -61,6 +62,11 @@ export class SkipCommand extends BaseCommand { } const hasMoreTracks = player.queue.tracks.length > 0 || player.repeatMode !== 0; + + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + const success = await player.skip(); if (context.isMessage()) { diff --git a/src/events/lavashark/TrackAddEvent.ts b/src/events/lavashark/TrackAddEvent.ts index 04a23ac4..c287df71 100644 --- a/src/events/lavashark/TrackAddEvent.ts +++ b/src/events/lavashark/TrackAddEvent.ts @@ -33,14 +33,12 @@ export class TrackAddEvent extends BaseLavaSharkEvent<'trackAdd'> { } // Refresh dashboard - try { - if (player.dashboardMsg) await player.dashboardMsg.delete(); - } catch (error) { - bot.logger.error( bot.shardId, 'Dashboard delete error:' + error); + if (player.current) { + if (!player.dashboardMsg) { + await client.dashboard.initialize((player.metadata as Message), player); + } + await client.dashboard.update(player, player.current); } - - await client.dashboard.initialize((player.metadata as Message), player); - await client.dashboard.update(player, player.current!); } /** diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 91e25410..de16159f 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -17,6 +17,7 @@ import type { Client } from 'discord.js'; export class DashboardManager { #bot: Bot; #client: Client; + #updatePromise: Promise = Promise.resolve(); constructor(bot: Bot, client: Client) { this.#bot = bot; @@ -26,54 +27,100 @@ export class DashboardManager { /** * Initialize dashboard in a channel */ - public async initialize(interactionOrMessage: ChatInputCommandInteraction | Message, player: Player): Promise { + public async initialize( + target: ChatInputCommandInteraction | Message | any, + player: Player + ): Promise { let channel; - if (interactionOrMessage instanceof Message) { - channel = (interactionOrMessage as Message).channel; + if (target instanceof Message) { + channel = target.channel; } - else if (interactionOrMessage instanceof ChatInputCommandInteraction) { - channel = (interactionOrMessage as ChatInputCommandInteraction).channel; + else if (target instanceof ChatInputCommandInteraction) { + channel = target.channel; + } + else if (target && typeof target.send === 'function') { + channel = target; } else { - throw new TypeError('Invalid Interaction or Message type'); + throw new TypeError('Invalid Interaction, Message, or Channel type'); + } + + if (player.dashboardMsg) { + try { + await player.dashboardMsg.delete(); + } catch (_) {} + player.dashboardMsg = null; } const lng = this.#bot.guildLanguageManager?.get(player.guildId); - player.dashboardMsg = await (channel as any /* discord.js type error ? (v14.16.2) */).send({ + player.dashboardMsg = await (channel as any).send({ embeds: [embeds.connected(this.#bot, lng)], components: [] }); } /** - * Update dashboard with current track and player state + * Update dashboard with current track and player state (smart edit or re-send to bottom) */ public async update(player: Player, track: Track): Promise { - if (!player.dashboardMsg) { - this.#bot.logger.error( this.#bot.shardId, 'Dashboard update called but dashboard is null'); - return; - } + this.#updatePromise = this.#updatePromise.then(() => this.#performUpdate(player, track)).catch(() => {}); + return this.#updatePromise; + } + async #performUpdate(player: Player, track: Track): Promise { const lng = this.#bot.guildLanguageManager?.get(player.guildId); - const subtitle = await this.#buildSubtitle(player, track, lng); + let subtitle = await this.#buildSubtitle(player, track, lng); const buttons = ButtonsBuilder.createDashboardButtons(player, lng); + const safeTitle = track.title.length > 256 + ? track.title.substring(0, 253) + '...' + : track.title; + + if (subtitle.length > 1024) { + subtitle = subtitle.substring(0, 1021) + '...'; + } + + const channel = player.dashboardMsg?.channel + ?? (player.textChannelId ? this.#client.channels.cache.get(player.textChannelId) : null); + + if (!channel || typeof (channel as any).send !== 'function') { + this.#bot.logger.error(this.#bot.shardId, 'Dashboard update called but channel is missing or invalid'); + return; + } + + const embedPayload = { + embeds: [embeds.dashboard( + this.#bot, + this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), + safeTitle, + subtitle, + track.uri, + track.thumbnail! + )], + components: [buttons] + }; + + if (player.dashboardMsg) { + try { + const lastMsgId = (channel as any).lastMessageId; + if (!lastMsgId || lastMsgId === player.dashboardMsg.id) { + await player.dashboardMsg.edit(embedPayload); + return; + } + } catch (_) {} + + try { + await player.dashboardMsg.delete(); + } catch (_) {} + player.dashboardMsg = null; + } + try { - await player.dashboardMsg.edit({ - embeds: [embeds.dashboard( - this.#bot, - this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), - track.title, - subtitle, - track.uri, - track.thumbnail! - )], - components: [buttons] - }); + player.dashboardMsg = await (channel as any).send(embedPayload); } catch (error) { - this.#bot.logger.error( this.#bot.shardId, 'Dashboard update error: ' + error); + this.#bot.logger.error(this.#bot.shardId, 'Dashboard update error: ' + error); } } @@ -106,10 +153,12 @@ export class DashboardManager { async #buildSubtitle(player: Player, track: Track, lng?: string): Promise { const repeatModeLabel = this.#getRepeatModeLabel(player.repeatMode, lng); + const currentVolume = player.volume ?? (player.setting as any)?.volume ?? this.#bot.guildVolumeManager?.get(player.guildId) ?? this.#bot.config.bot.volume.default; + let subtitle = this.#bot.i18n.t('embeds:DASHBOARD_SUBTITLE', { author: track.author, duration: track.duration.label, - volume: player.volume, + volume: currentVolume, repeatMode: repeatModeLabel, lng }); diff --git a/src/lib/handlers/SkipButtonHandler.ts b/src/lib/handlers/SkipButtonHandler.ts index 630ba825..a2883947 100644 --- a/src/lib/handlers/SkipButtonHandler.ts +++ b/src/lib/handlers/SkipButtonHandler.ts @@ -1,7 +1,6 @@ import { RepeatMode } from 'lavashark'; import { MessageFlags } from 'discord.js'; import { DashboardButtonHandler } from './DashboardButtonHandler.js'; -import { ButtonsBuilder } from '../builders/ButtonsBuilder.js'; import { PermissionManager } from '../PermissionManager.js'; import { embeds } from '../../embeds/index.js'; @@ -57,12 +56,10 @@ export class SkipButtonHandler extends DashboardButtonHandler { if (player.repeatMode === RepeatMode.TRACK) { player.setRepeatMode(RepeatMode.OFF); - await player.skip(); - player.setRepeatMode(RepeatMode.TRACK); - } else { - await player.skip(); } + await player.skip(); + if (!hasMoreTracks) { try { await interaction.reply({ @@ -73,7 +70,8 @@ export class SkipButtonHandler extends DashboardButtonHandler { } catch (_) {} } - const row = ButtonsBuilder.createDashboardButtons(player, lng); - await interaction.update({ components: [row] }); + if (!interaction.replied && !interaction.deferred) { + await interaction.deferUpdate(); + } } } From 20981c47e9196f3e54e8bab6b849fe325bd44041 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:10:30 +0300 Subject: [PATCH 35/49] feat: add node status dropdown menu Co-authored-by: hoehoetvhss --- src/commands/NodeStatusCommand.ts | 60 +++++++++++++++- src/events/discord/InteractionCreateEvent.ts | 2 + src/lib/handlers/NodeSelectHandler.ts | 76 ++++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/lib/handlers/NodeSelectHandler.ts diff --git a/src/commands/NodeStatusCommand.ts b/src/commands/NodeStatusCommand.ts index 397272f0..7a059124 100644 --- a/src/commands/NodeStatusCommand.ts +++ b/src/commands/NodeStatusCommand.ts @@ -1,3 +1,4 @@ +import { ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js'; import i18next from 'i18next'; import { NodeState } from 'lavashark'; @@ -11,7 +12,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NodeStatusCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(bot: Bot): CommandMetadata { + const choices = (bot.config.nodeList || []).slice(0, 25).map(node => ({ + name: node.id, + value: node.id + })); + return { name: 'nodestatus', aliases: ['node', 'nodes', 'nodesstatus'], @@ -26,7 +32,8 @@ export class NodeStatusCommand extends BaseCommand { name: 'nodename', description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION'), type: 3, - required: false + required: false, + choices: choices.length > 0 ? choices : undefined } ] }; @@ -72,8 +79,32 @@ export class NodeStatusCommand extends BaseCommand { bot.logger.log( bot.shardId, 'nodesStatus: ' + JSON.stringify(nodesStatus)); + const components = []; + if (nodes.length > 0) { + const selectOptions = nodes.slice(0, 25).map((node, i) => { + const ping = pingList[i]; + const isConnected = ping !== -1; + return { + label: node.identifier, + value: node.identifier, + description: isConnected + ? `${context.t('embeds:NODE_STATUS_PING')}: ${ping}ms` + : context.t('embeds:NODE_DISCONNECTED').replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(context.t('commands:CONFIG_NODE_OPTION_DESCRIPTION')) + .addOptions(selectOptions); + + components.push(new ActionRowBuilder().addComponents(selectMenu)); + } + await context.reply({ embeds: [embeds.nodesStatus(bot, unhealthValue, nodesStatus, context.language)], + components, allowedMentions: { repliedUser: false } }); } @@ -108,8 +139,32 @@ export class NodeStatusCommand extends BaseCommand { bot.logger.log( bot.shardId, 'nodeStats: ' + JSON.stringify(nodeStats)); bot.logger.log( bot.shardId, 'nodePing: ' + nodePing + 'ms'); + const components = []; + if (nodes.length > 0) { + const selectOptions = nodes.slice(0, 25).map(n => { + const isConnected = n.state === NodeState.CONNECTED; + return { + label: n.identifier, + value: n.identifier, + default: n.identifier === nodeName, + description: isConnected + ? `${context.t('embeds:NODE_STATUS_PING')}: ${nodePing}ms` + : context.t('embeds:NODE_DISCONNECTED').replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(context.t('commands:CONFIG_NODE_OPTION_DESCRIPTION')) + .addOptions(selectOptions); + + components.push(new ActionRowBuilder().addComponents(selectMenu)); + } + await context.reply({ embeds: [embeds.nodeStatus(bot, nodeName, nodeInfo, nodeStats, nodePing, context.language)], + components, allowedMentions: { repliedUser: false } }); return; @@ -128,3 +183,4 @@ export class NodeStatusCommand extends BaseCommand { }); } } + diff --git a/src/events/discord/InteractionCreateEvent.ts b/src/events/discord/InteractionCreateEvent.ts index cfbb6f18..5d6b4212 100644 --- a/src/events/discord/InteractionCreateEvent.ts +++ b/src/events/discord/InteractionCreateEvent.ts @@ -13,6 +13,7 @@ import { ShuffleButtonHandler } from '../../lib/handlers/ShuffleButtonHandler.js import { MusicSaveButtonHandler } from '../../lib/handlers/MusicSaveButtonHandler.js'; import { QueueButtonHandler } from '../../lib/handlers/QueueButtonHandler.js'; import { LanguageSelectHandler } from '../../lib/handlers/LanguageSelectHandler.js'; +import { NodeSelectHandler } from '../../lib/handlers/NodeSelectHandler.js'; import { DashboardButtonId, QueueButtonId, MusicButtonId } from '../../@types/index.js'; import type { Client } from 'discord.js'; @@ -40,6 +41,7 @@ export class InteractionCreateEvent extends BaseDiscordEvent { + if (interaction.customId !== 'select_node') return; + + const lng = bot.guildLanguageManager?.get(interaction.guildId!) || bot.config.bot.i18n.defaultLocale; + const nodeName = interaction.values[0]; + const nodes = client.lavashark.nodes; + + const targetNode = nodes.find(node => node.identifier === nodeName); + + if (!targetNode) { + await interaction.reply({ + embeds: [embeds.validNodeName(bot, nodes.map(n => `\`${n.identifier}\``).join('\n'), lng)], + flags: MessageFlags.Ephemeral + }); + return; + } + + if (targetNode.state !== NodeState.CONNECTED) { + await interaction.reply({ + embeds: [embeds.nodeDisconnected(bot, nodeName, lng)], + flags: MessageFlags.Ephemeral + }); + return; + } + + await interaction.deferUpdate(); + + try { + const [nodeInfo, nodeStats, nodePing] = await Promise.all([ + targetNode.getInfo(), + targetNode.getStats(), + client.lavashark.nodePing(targetNode) + ]); + + const selectOptions = nodes.slice(0, 25).map(node => { + const isConnected = node.state === NodeState.CONNECTED; + return { + label: node.identifier, + value: node.identifier, + default: node.identifier === nodeName, + description: isConnected + ? (bot.i18n.t('embeds:NODE_STATUS_PING', { lng }) + `: ${nodePing}ms`) + : bot.i18n.t('embeds:NODE_DISCONNECTED', { lng }).replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(bot.i18n.t('commands:CONFIG_NODE_OPTION_DESCRIPTION', { lng })) + .addOptions(selectOptions); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + await interaction.editReply({ + embeds: [embeds.nodeStatus(bot, nodeName, nodeInfo, nodeStats, nodePing, lng)], + components: [row] + }); + } catch (error) { + bot.logger.error(bot.shardId, '[NodeSelectHandler] Error fetching node status: ' + error); + } + } +} From 2f726d98a203e8e0c8898553c15389897bf9f1e3 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:12:02 +0300 Subject: [PATCH 36/49] fix: polish commands - language context, ping display, help select menu Co-authored-by: hoehoetvhss --- src/commands/BlacklistCommand.ts | 15 +++++++++------ src/commands/ClearCommand.ts | 8 ++++---- src/commands/DashboardCommand.ts | 8 ++++---- src/commands/DjCommand.ts | 8 ++++---- src/commands/FilterCommand.ts | 8 ++++---- src/commands/HelpCommand.ts | 27 +++++++++++++-------------- src/commands/JoinCommand.ts | 8 ++++---- src/commands/LanguageCommand.ts | 12 ++++++------ src/commands/LeaveCommand.ts | 6 +++--- src/commands/LoopCommand.ts | 8 ++++---- src/commands/MoveCommand.ts | 10 +++++----- src/commands/NodeStatusCommand.ts | 8 ++++---- src/commands/NowPlayingCommand.ts | 6 +++--- src/commands/PauseCommand.ts | 6 +++--- src/commands/PingCommand.ts | 8 ++++---- src/commands/PlayCommand.ts | 8 ++++---- src/commands/PlayFirstCommand.ts | 8 ++++---- src/commands/PlayLastCommand.ts | 6 +++--- src/commands/QueueCommand.ts | 6 +++--- src/commands/RadioCommand.ts | 10 +++++----- src/commands/RemoveCommand.ts | 15 +++++++-------- src/commands/ResumeCommand.ts | 6 +++--- src/commands/SearchCommand.ts | 14 +++++++------- src/commands/SeekCommand.ts | 8 ++++---- src/commands/ServerCommand.ts | 6 +++--- src/commands/ShuffleCommand.ts | 6 +++--- src/commands/SkipCommand.ts | 6 +++--- src/commands/StatusCommand.ts | 8 ++++---- src/commands/StopCommand.ts | 6 +++--- src/commands/VolumeCommand.ts | 8 ++++---- src/commands/VolumeDefaultCommand.ts | 8 ++++---- src/commands/base/BaseCommand.ts | 2 +- src/commands/base/CommandRegistry.ts | 4 ++-- 33 files changed, 141 insertions(+), 140 deletions(-) diff --git a/src/commands/BlacklistCommand.ts b/src/commands/BlacklistCommand.ts index 19a85c7b..747faca3 100644 --- a/src/commands/BlacklistCommand.ts +++ b/src/commands/BlacklistCommand.ts @@ -24,30 +24,32 @@ export class BlacklistCommand extends BaseCommand { /** * Build blacklist command metadata and slash command options */ - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { aliases: [], category: CommandCategory.UTILITY, - description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION'), + description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION', { lng }), name: 'blacklist', options: [ this.createUserSubcommand( 'add', 'commands:CONFIG_BLACKLIST_OPTION_ADD', + lng, ), this.createUserSubcommand( 'remove', 'commands:CONFIG_BLACKLIST_OPTION_REMOVE', + lng, ), { - description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST'), + description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST', { lng }), name: 'list', type: ApplicationCommandOptionType.Subcommand, }, ], sendTyping: true, showHelp: true, - usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE'), + usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE', { lng }), voiceChannel: false, }; } @@ -99,13 +101,14 @@ export class BlacklistCommand extends BaseCommand { private createUserSubcommand( name: BlacklistMutationAction, descriptionKey: string, + lng?: string ): ApplicationCommandSubCommandData { return { - description: i18next.t(descriptionKey), + description: i18next.t(descriptionKey, { lng }), name, options: [ { - description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER'), + description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER', { lng }), name: 'user', required: true, type: ApplicationCommandOptionType.User, diff --git a/src/commands/ClearCommand.ts b/src/commands/ClearCommand.ts index c48ea4a4..376aa800 100644 --- a/src/commands/ClearCommand.ts +++ b/src/commands/ClearCommand.ts @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ClearCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'clear', aliases: ['cls'], - description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_CLEAR_USAGE'), + description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_CLEAR_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -48,7 +48,7 @@ export class ClearCommand extends BaseCommand { } else { await context.replySuccess(bot, isRadioPlaying - ? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS', { defaultValue: '🧹 Cleared queue and stopped radio playback.' }) + ? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS') : context.t('commands:MESSAGE_CLEAR_SUCCESS')); } } diff --git a/src/commands/DashboardCommand.ts b/src/commands/DashboardCommand.ts index 7ac4cec5..c2ace976 100644 --- a/src/commands/DashboardCommand.ts +++ b/src/commands/DashboardCommand.ts @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class DashboardCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'dashboard', - aliases: ['d', 'console'], - description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE'), + aliases: ['db'], + description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/DjCommand.ts b/src/commands/DjCommand.ts index ca9d33bf..a52f82e8 100644 --- a/src/commands/DjCommand.ts +++ b/src/commands/DjCommand.ts @@ -12,12 +12,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class DjCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'dj', aliases: ['adddj'], - description: i18next.t('commands:CONFIG_DJ_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_DJ_USAGE'), + description: i18next.t('commands:CONFIG_DJ_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_DJ_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -25,7 +25,7 @@ export class DjCommand extends BaseCommand { options: [ { name: 'user', - description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION', { lng }), type: ApplicationCommandOptionType.User, required: false } diff --git a/src/commands/FilterCommand.ts b/src/commands/FilterCommand.ts index b1af98f8..d2d24152 100644 --- a/src/commands/FilterCommand.ts +++ b/src/commands/FilterCommand.ts @@ -18,12 +18,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class FilterCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'filter', aliases: ['eq', 'equalizer'], - description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_FILTER_USAGE'), + description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_FILTER_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -31,7 +31,7 @@ export class FilterCommand extends BaseCommand { options: [ { name: 'filter', - description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION', { lng }), type: 3, required: true, choices: [ diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index e56929a4..4032a194 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -17,12 +17,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class HelpCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'help', aliases: ['h'], - description: i18next.t('commands:CONFIG_HELP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_HELP_USAGE'), + description: i18next.t('commands:CONFIG_HELP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_HELP_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -30,7 +30,7 @@ export class HelpCommand extends BaseCommand { options: [ { name: 'command', - description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION', { lng }), type: 3, required: false } @@ -59,15 +59,14 @@ export class HelpCommand extends BaseCommand { * @private */ async #showCommandList(bot: Bot, client: Client, context: CommandContext): Promise { - const title = client.user?.username; - const commands = client.commands.getHelpCommands(bot); + const commands = client.commands.getHelpCommands(bot, context.language); const musicCommands = commands.filter(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); return metadata.category === CommandCategory.MUSIC; }); const utilityCommands = commands.filter(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); return metadata.category === CommandCategory.UTILITY; }); @@ -76,7 +75,7 @@ export class HelpCommand extends BaseCommand { .setCustomId(SelectButtonId.HelpMusic) .setPlaceholder(context.t('commands:HELP_SELECT_MUSIC_PLACEHOLDER')) .setOptions(musicCommands.map(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE'); return { label: metadata.name, @@ -89,7 +88,7 @@ export class HelpCommand extends BaseCommand { .setCustomId(SelectButtonId.HelpUtility) .setPlaceholder(context.t('commands:HELP_SELECT_UTILITY_PLACEHOLDER')) .setOptions(utilityCommands.map(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE'); return { label: metadata.name, @@ -120,12 +119,12 @@ export class HelpCommand extends BaseCommand { const selectedCommand = client.commands.get(i.values[0]); if (!selectedCommand) return; - const metadata = selectedCommand.getMetadata(bot); + const metadata = selectedCommand.getMetadata(bot, context.language); const usage = `${metadata.description}\n\`\`\`${bot.config.bot.prefix}${metadata.usage}\`\`\``; await i.deferUpdate(); await msg.edit({ - embeds: [embeds.help(bot, title!, usage, context.language)], + embeds: [embeds.help(bot, metadata.name, usage, context.language)], components: [], allowedMentions: { repliedUser: false } }).catch(() => bot.logger.discord( bot.shardId, 'Failed to edit deleted message.')); @@ -150,11 +149,11 @@ export class HelpCommand extends BaseCommand { */ async #showCommandHelp(bot: Bot, client: Client, context: CommandContext, commandName: string): Promise { const prefix = bot.config.bot.prefix; - const commands = client.commands.getHelpCommands(bot); + const commands = client.commands.getHelpCommands(bot, context.language); let found = false; for (const cmd of commands) { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); if (commandName === metadata.name || (metadata.aliases && metadata.aliases.includes(commandName))) { const description = `${metadata.description}\n\`\`\`${prefix}${metadata.usage}\`\`\``; diff --git a/src/commands/JoinCommand.ts b/src/commands/JoinCommand.ts index 8ee56e04..926666c5 100644 --- a/src/commands/JoinCommand.ts +++ b/src/commands/JoinCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class JoinCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'join', - aliases: ['add', 'summon'], - description: i18next.t('commands:CONFIG_JOIN_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_JOIN_USAGE'), + aliases: ['j', 'connect'], + description: i18next.t('commands:CONFIG_JOIN_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_JOIN_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/LanguageCommand.ts b/src/commands/LanguageCommand.ts index f8bc5ad7..2f0f68cb 100644 --- a/src/commands/LanguageCommand.ts +++ b/src/commands/LanguageCommand.ts @@ -11,7 +11,7 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class LanguageCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { const choices = bot.lang.languages.slice(0, 25).map(lang => ({ name: getLanguageDisplayName(lang), value: lang @@ -20,16 +20,16 @@ export class LanguageCommand extends BaseCommand { return { name: 'language', aliases: ['lang', 'locale'], - description: i18next.t('commands:CONFIG_LANG_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LANG_USAGE'), + description: i18next.t('commands:CONFIG_LANG_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LANG_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, sendTyping: false, options: [ { - name: 'locale', - description: i18next.t('commands:CONFIG_LANG_OPTION_DESCRIPTION'), + name: 'language', + description: i18next.t('commands:CONFIG_LANG_OPTION_DESCRIPTION', { lng }), type: 3, required: false, choices @@ -40,7 +40,7 @@ export class LanguageCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { const locale = context.isInteraction() - ? context.getStringOption('locale') + ? (context.getStringOption('language') || context.getStringOption('locale')) : context.args.join(' '); if (!locale) { diff --git a/src/commands/LeaveCommand.ts b/src/commands/LeaveCommand.ts index f06d5a99..209d4171 100644 --- a/src/commands/LeaveCommand.ts +++ b/src/commands/LeaveCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class LeaveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'leave', aliases: [], - description: i18next.t('commands:CONFIG_LEAVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LEAVE_USAGE'), + description: i18next.t('commands:CONFIG_LEAVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LEAVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/LoopCommand.ts b/src/commands/LoopCommand.ts index 54887ad0..05af6c94 100644 --- a/src/commands/LoopCommand.ts +++ b/src/commands/LoopCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class LoopCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'loop', aliases: ['lp'], - description: i18next.t('commands:CONFIG_LOOP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LOOP_USAGE'), + description: i18next.t('commands:CONFIG_LOOP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LOOP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,7 +23,7 @@ export class LoopCommand extends BaseCommand { options: [ { name: 'mode', - description: i18next.t('commands:CONFIG_LOOP_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_LOOP_OPTION_DESCRIPTION', { lng }), type: 3, required: true, choices: [ diff --git a/src/commands/MoveCommand.ts b/src/commands/MoveCommand.ts index a63155f2..4a2f7288 100644 --- a/src/commands/MoveCommand.ts +++ b/src/commands/MoveCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class MoveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'move', aliases: ['mv', 'swap', 'change'], - description: i18next.t('commands:CONFIG_MOVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_MOVE_USAGE'), + description: i18next.t('commands:CONFIG_MOVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_MOVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -22,14 +22,14 @@ export class MoveCommand extends BaseCommand { options: [ { name: 'moveindex1', - description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION', { lng }), type: 4, required: true, min_value: 1 }, { name: 'moveindex2', - description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION_2'), + description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION_2', { lng }), type: 4, required: true, min_value: 1 diff --git a/src/commands/NodeStatusCommand.ts b/src/commands/NodeStatusCommand.ts index 7a059124..db0b014a 100644 --- a/src/commands/NodeStatusCommand.ts +++ b/src/commands/NodeStatusCommand.ts @@ -12,7 +12,7 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NodeStatusCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { const choices = (bot.config.nodeList || []).slice(0, 25).map(node => ({ name: node.id, value: node.id @@ -21,8 +21,8 @@ export class NodeStatusCommand extends BaseCommand { return { name: 'nodestatus', aliases: ['node', 'nodes', 'nodesstatus'], - description: i18next.t('commands:CONFIG_NODE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_NODE_USAGE'), + description: i18next.t('commands:CONFIG_NODE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_NODE_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -30,7 +30,7 @@ export class NodeStatusCommand extends BaseCommand { options: [ { name: 'nodename', - description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION', { lng }), type: 3, required: false, choices: choices.length > 0 ? choices : undefined diff --git a/src/commands/NowPlayingCommand.ts b/src/commands/NowPlayingCommand.ts index 66d63367..6139edb9 100644 --- a/src/commands/NowPlayingCommand.ts +++ b/src/commands/NowPlayingCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NowPlayingCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'nowplaying', aliases: ['np', 'save'], - description: i18next.t('commands:CONFIG_NOW_PLAYING_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_NOW_PLAYING_USAGE'), + description: i18next.t('commands:CONFIG_NOW_PLAYING_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_NOW_PLAYING_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: false, showHelp: true, diff --git a/src/commands/PauseCommand.ts b/src/commands/PauseCommand.ts index 5f1546dd..7417881b 100644 --- a/src/commands/PauseCommand.ts +++ b/src/commands/PauseCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Pause command - Pauses music playback */ export class PauseCommand extends BaseCommand { - getMetadata(_bot: Bot): CommandMetadata { + getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'pause', aliases: [], - description: i18next.t('commands:CONFIG_PAUSE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PAUSE_USAGE'), + description: i18next.t('commands:CONFIG_PAUSE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PAUSE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/PingCommand.ts b/src/commands/PingCommand.ts index 2ec80b7c..789f4c54 100644 --- a/src/commands/PingCommand.ts +++ b/src/commands/PingCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Ping command - Shows bot latency */ export class PingCommand extends BaseCommand { - getMetadata(_bot: Bot): CommandMetadata { + getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'ping', aliases: [], - description: i18next.t('commands:CONFIG_PING_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PING_USAGE'), + description: i18next.t('commands:CONFIG_PING_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PING_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -28,7 +28,7 @@ export class PingCommand extends BaseCommand { } protected async run(bot: Bot, client: Client, context: CommandContext): Promise { - const botPing = `${Date.now() - context.createdTimestamp}ms`; + const botPing = `${Math.abs(Date.now() - context.createdTimestamp)}ms`; const apiPing = client.ws.ping.toString(); await context.react('👍'); diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index 58f1746a..a032288c 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -14,12 +14,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class PlayCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'play', aliases: ['p'], - description: i18next.t('commands:CONFIG_PLAY_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAY_USAGE'), + description: i18next.t('commands:CONFIG_PLAY_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAY_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -27,7 +27,7 @@ export class PlayCommand extends BaseCommand { options: [ { name: 'play', - description: i18next.t('commands:CONFIG_PLAY_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAY_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 2009598b..ff0fc200 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class PlayFirstCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'playfirst', aliases: ['pf', 'pp', 'prioritizePlay'], - description: i18next.t('commands:CONFIG_PLAYFIRST_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAYFIRST_USAGE'), + description: i18next.t('commands:CONFIG_PLAYFIRST_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAYFIRST_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -28,7 +28,7 @@ export class PlayFirstCommand extends BaseCommand { options: [ { name: 'playfirst', - description: i18next.t('commands:CONFIG_PLAYFIRST_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAYFIRST_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/PlayLastCommand.ts b/src/commands/PlayLastCommand.ts index de218bd3..813d9970 100644 --- a/src/commands/PlayLastCommand.ts +++ b/src/commands/PlayLastCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * PlayLast command - Replay the current or last played song */ export class PlayLastCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'playlast', aliases: [], - description: i18next.t('commands:CONFIG_PLAYLAST_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAYLAST_USAGE'), + description: i18next.t('commands:CONFIG_PLAYLAST_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAYLAST_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: false, showHelp: true, diff --git a/src/commands/QueueCommand.ts b/src/commands/QueueCommand.ts index a199e1ac..704ba6f8 100644 --- a/src/commands/QueueCommand.ts +++ b/src/commands/QueueCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class QueueCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'queue', aliases: ['q', 'list'], - description: i18next.t('commands:CONFIG_QUEUE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_QUEUE_USAGE'), + description: i18next.t('commands:CONFIG_QUEUE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_QUEUE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 7e9aad15..de1a8c59 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -10,12 +10,12 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class RadioCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'radio', aliases: [], - description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_RADIO_USAGE'), + description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_RADIO_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,13 +23,13 @@ export class RadioCommand extends BaseCommand { options: [ { name: 'playlist', - description: i18next.t('commands:CONFIG_RADIO_OPTION_PLAYLIST'), + description: i18next.t('commands:CONFIG_RADIO_OPTION_PLAYLIST', { lng }), type: ApplicationCommandOptionType.String, required: true }, { name: 'channel', - description: i18next.t('commands:CONFIG_RADIO_OPTION_CHANNEL'), + description: i18next.t('commands:CONFIG_RADIO_OPTION_CHANNEL', { lng }), type: ApplicationCommandOptionType.String, required: true } diff --git a/src/commands/RemoveCommand.ts b/src/commands/RemoveCommand.ts index a5bd541c..669dca2b 100644 --- a/src/commands/RemoveCommand.ts +++ b/src/commands/RemoveCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class RemoveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'remove', aliases: ['rm'], - description: i18next.t('commands:CONFIG_REMOVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_REMOVE_USAGE'), + description: i18next.t('commands:CONFIG_REMOVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_REMOVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,13 +23,13 @@ export class RemoveCommand extends BaseCommand { options: [ { name: 'index', - description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION', { lng }), type: 10, required: false }, { name: 'index2', - description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION_2'), + description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION_2', { lng }), type: 10, required: false } @@ -209,6 +209,8 @@ export class RemoveCommand extends BaseCommand { }); collector.on('collect', async (query: Message) => { + collector.stop(); + const index = parseInt(query.content); if (!index || index <= 0 || index > tracks.length) { @@ -224,7 +226,6 @@ export class RemoveCommand extends BaseCommand { allowedMentions: { repliedUser: false } }); } - collector.stop(); return; } @@ -243,8 +244,6 @@ export class RemoveCommand extends BaseCommand { msg.delete().catch(() => bot.logger.discord( bot.shardId, 'Failed to delete message.') ); - - collector.stop(); }); collector.on('end', async (collected: ReadonlyCollection>, reason: string) => { diff --git a/src/commands/ResumeCommand.ts b/src/commands/ResumeCommand.ts index 8ead4338..1d412913 100644 --- a/src/commands/ResumeCommand.ts +++ b/src/commands/ResumeCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ResumeCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'resume', aliases: [], - description: i18next.t('commands:CONFIG_RESUME_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_RESUME_USAGE'), + description: i18next.t('commands:CONFIG_RESUME_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_RESUME_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index f6921a79..fff7a3c0 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -21,12 +21,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class SearchCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'search', aliases: ['find'], - description: i18next.t('commands:CONFIG_SEARCH_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SEARCH_USAGE'), + description: i18next.t('commands:CONFIG_SEARCH_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SEARCH_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -34,7 +34,7 @@ export class SearchCommand extends BaseCommand { options: [ { name: 'search', - description: i18next.t('commands:CONFIG_SEARCH_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_SEARCH_OPTION_DESCRIPTION', { lng }), type: 3, required: true } @@ -297,7 +297,7 @@ export class SearchCommand extends BaseCommand { }); collector.on('collect', async (i: StringSelectMenuInteraction) => { - if (i.customId != SelectButtonId.Music) return; + if (i.customId !== SelectButtonId.Music) return; // Check queue limits before adding selected track const checkResult = QueueLimitManager.canAddSongs(bot, player, userId, guildMember, 1); @@ -317,7 +317,7 @@ export class SearchCommand extends BaseCommand { const requester = context.isMessage() ? context.getMessage().author : context.getInteraction().user; const curVolume = player.setting.volume ?? bot.guildVolumeManager?.get(player.guildId) ?? bot.config.bot.volume.default; - player.addTracks(res.tracks.find((x: any) => x.uri == i.values[0])!, requester as any); + player.addTracks(res.tracks.find((x: any) => x.uri === i.values[0])!, requester as any); if (!player.playing) { player.filters.setVolume(curVolume); @@ -344,7 +344,7 @@ export class SearchCommand extends BaseCommand { }); collector.on('end', async (collected: Collection, reason: string) => { - if (reason == 'time' && collected.size == 0) { + if (reason === 'time' && collected.size === 0) { if (!player.playing) { player.destroy(); } diff --git a/src/commands/SeekCommand.ts b/src/commands/SeekCommand.ts index 9fa19ed7..e215e0ba 100644 --- a/src/commands/SeekCommand.ts +++ b/src/commands/SeekCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class SeekCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'seek', aliases: [], - description: i18next.t('commands:CONFIG_SEEK_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SEEK_USAGE'), + description: i18next.t('commands:CONFIG_SEEK_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SEEK_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -24,7 +24,7 @@ export class SeekCommand extends BaseCommand { options: [ { name: 'seek', - description: i18next.t('commands:CONFIG_SEEK_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_SEEK_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/ServerCommand.ts b/src/commands/ServerCommand.ts index d9fb0926..05349102 100644 --- a/src/commands/ServerCommand.ts +++ b/src/commands/ServerCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ServerCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'server', aliases: [], - description: i18next.t('commands:CONFIG_SERVER_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SERVER_USAGE'), + description: i18next.t('commands:CONFIG_SERVER_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SERVER_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, diff --git a/src/commands/ShuffleCommand.ts b/src/commands/ShuffleCommand.ts index f05c17e0..ae85a949 100644 --- a/src/commands/ShuffleCommand.ts +++ b/src/commands/ShuffleCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ShuffleCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'shuffle', aliases: ['random'], - description: i18next.t('commands:CONFIG_SHUFFLE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SHUFFLE_USAGE'), + description: i18next.t('commands:CONFIG_SHUFFLE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SHUFFLE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/SkipCommand.ts b/src/commands/SkipCommand.ts index 873f2fc4..ddead2bc 100644 --- a/src/commands/SkipCommand.ts +++ b/src/commands/SkipCommand.ts @@ -14,12 +14,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Skip command - Skips current track */ export class SkipCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'skip', aliases: ['s'], - description: i18next.t('commands:CONFIG_SKIP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SKIP_USAGE'), + description: i18next.t('commands:CONFIG_SKIP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SKIP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index fb6c03ce..ffc223a7 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata, SystemStatus } from '../@types/index.js'; export class StatusCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'status', aliases: ['info'], - description: i18next.t('commands:CONFIG_STATUS_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_STATUS_USAGE'), + description: i18next.t('commands:CONFIG_STATUS_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_STATUS_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -28,7 +28,7 @@ export class StatusCommand extends BaseCommand { } protected async run(bot: Bot, client: Client, context: CommandContext): Promise { - const botPing = `${Date.now() - context.createdTimestamp}ms`; + const botPing = `${Math.abs(Date.now() - context.createdTimestamp)}ms`; const sysload = await sysusage.cpu(); const pingList = await client.lavashark.nodesPing(); diff --git a/src/commands/StopCommand.ts b/src/commands/StopCommand.ts index c7dbded1..aef16cf9 100644 --- a/src/commands/StopCommand.ts +++ b/src/commands/StopCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class StopCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'stop', aliases: [], - description: i18next.t('commands:CONFIG_STOP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_STOP_USAGE'), + description: i18next.t('commands:CONFIG_STOP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_STOP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/VolumeCommand.ts b/src/commands/VolumeCommand.ts index 39b607c7..f7ec42a0 100644 --- a/src/commands/VolumeCommand.ts +++ b/src/commands/VolumeCommand.ts @@ -19,12 +19,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class VolumeCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { return { name: 'volume', aliases: ['v'], - description: i18next.t('commands:CONFIG_VOLUME_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_VOLUME_USAGE'), + description: i18next.t('commands:CONFIG_VOLUME_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_VOLUME_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -32,7 +32,7 @@ export class VolumeCommand extends BaseCommand { options: [ { name: 'volume', - description: bot.i18n.t('commands:CONFIG_VOLUME_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max }), + description: bot.i18n.t('commands:CONFIG_VOLUME_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max, lng }), type: 4, required: false, min_value: 1, diff --git a/src/commands/VolumeDefaultCommand.ts b/src/commands/VolumeDefaultCommand.ts index bddf7ab8..9beb8fce 100644 --- a/src/commands/VolumeDefaultCommand.ts +++ b/src/commands/VolumeDefaultCommand.ts @@ -8,12 +8,12 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class VolumeDefaultCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { return { name: 'volume-default', aliases: ['vdef', 'voldef'], - description: i18next.t('commands:CONFIG_VOLUME_DEFAULT_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_VOLUME_DEFAULT_USAGE'), + description: i18next.t('commands:CONFIG_VOLUME_DEFAULT_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_VOLUME_DEFAULT_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -21,7 +21,7 @@ export class VolumeDefaultCommand extends BaseCommand { options: [ { name: 'volume', - description: bot.i18n.t('commands:CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max }), + description: bot.i18n.t('commands:CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max, lng }), type: 4, required: false, min_value: 1, diff --git a/src/commands/base/BaseCommand.ts b/src/commands/base/BaseCommand.ts index 67eab573..a08b671b 100644 --- a/src/commands/base/BaseCommand.ts +++ b/src/commands/base/BaseCommand.ts @@ -12,7 +12,7 @@ export abstract class BaseCommand { /** * Get command metadata */ - public abstract getMetadata(bot: Bot): CommandMetadata; + public abstract getMetadata(bot: Bot, lng?: string): CommandMetadata; /** * Execute command logic (implemented by subclasses) diff --git a/src/commands/base/CommandRegistry.ts b/src/commands/base/CommandRegistry.ts index 3a74d7c9..f43f4be0 100644 --- a/src/commands/base/CommandRegistry.ts +++ b/src/commands/base/CommandRegistry.ts @@ -77,9 +77,9 @@ export class CommandRegistry { /** * Get all commands that should be shown in help */ - public getHelpCommands(bot: Bot): BaseCommand[] { + public getHelpCommands(bot: Bot, lng?: string): BaseCommand[] { return this.getAll().filter(cmd => - cmd.getMetadata(bot).showHelp + cmd.getMetadata(bot, lng).showHelp ); } } From 982feb7adf27b3b4bbe5f22a43094fe0b45b242b Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:12:08 +0300 Subject: [PATCH 37/49] fix: harden shutdown and player lifecycle - zombie leaks, WAL checkpointing Co-authored-by: hoehoetvhss --- src/App.ts | 35 +++++++++++++------ src/commands/JoinCommand.ts | 1 + src/commands/PlayCommand.ts | 1 + src/commands/PlayFirstCommand.ts | 1 + src/commands/RadioCommand.ts | 1 + src/commands/SearchCommand.ts | 1 + .../playlist/PlayPlaylistSubcommand.ts | 1 + src/events/discord/InteractionCreateEvent.ts | 16 +++++++-- src/events/discord/VoiceStateUpdateEvent.ts | 5 ++- src/lib/DatabaseManager.ts | 5 +++ 10 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/App.ts b/src/App.ts index 18fe02f3..33f758a1 100644 --- a/src/App.ts +++ b/src/App.ts @@ -134,7 +134,12 @@ class App { * @private */ #setShutdownSignalHandlers(): void { + let isShuttingDown = false; + const shutdown = async (signal: string) => { + if (isShuttingDown) return; + isShuttingDown = true; + this.bot.logger.log( this.bot.shardId, `Received ${signal}. Closing server gracefully...`); const timeout = setTimeout(() => { @@ -143,29 +148,33 @@ class App { }, 30 * 1000); try { - // Stop periodic saves and save all active queues before shutdown + // Stop periodic saves and save all active queues in parallel before shutdown if (this.bot.config.queuePersistence.enabled && this.#client.queuePersistence) { this.bot.logger.log( this.bot.shardId, 'Saving active queues before shutdown...'); this.#client.queuePersistence.stopAllPeriodicSaves(); - for (const player of this.#client.lavashark.players.values()) { - await this.#client.queuePersistence.saveQueue(player); - } + const players = Array.from(this.#client.lavashark?.players?.values() ?? []); + await Promise.allSettled( + players.map(player => this.#client.queuePersistence!.saveQueue(player)) + ); } // Close the lavashark players connections this.bot.logger.log( this.bot.shardId, 'Closing voice channel connection...'); + const activePlayers = Array.from(this.#client.lavashark?.players?.values() ?? []); await Promise.allSettled( - Array.from(this.#client.lavashark.players.values()).map(player => player.destroy()) + activePlayers.map(player => player.destroy()) ); - await new Promise(resolve => setTimeout(resolve, 2000)); this.bot.logger.log( this.bot.shardId, 'Voice channel connection closed.'); // Close the lavashark nodes connections this.bot.logger.log( this.bot.shardId, 'Closing lavashark nodes...'); - for (const node of this.#client.lavashark.nodes) { - node.disconnect(); + if (this.#client.lavashark?.nodes) { + for (const node of this.#client.lavashark.nodes) { + try { + node.disconnect(); + } catch (_) {} + } } - await new Promise(resolve => setTimeout(resolve, 2000)); this.bot.logger.log( this.bot.shardId, 'Lavashark nodes closed.'); // Close discord.js connection @@ -227,6 +236,10 @@ const main = async () => { main(); -process.on('unhandledRejection', (error) => { - console.error('Unhandled promise rejection:', error); +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Promise Rejection at:', promise, 'reason:', reason); +}); + +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception thrown:', error); }); diff --git a/src/commands/JoinCommand.ts b/src/commands/JoinCommand.ts index 926666c5..86e8ac2b 100644 --- a/src/commands/JoinCommand.ts +++ b/src/commands/JoinCommand.ts @@ -62,6 +62,7 @@ export class JoinCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return; } diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index a032288c..71221da5 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -148,6 +148,7 @@ export class PlayCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return null; } diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index ff0fc200..0781f6b2 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -149,6 +149,7 @@ export class PlayFirstCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return null; } diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index de1a8c59..071b6240 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -128,6 +128,7 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; } catch (error) { bot.logger.error(bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return; } diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index fff7a3c0..5cd484a4 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -150,6 +150,7 @@ export class SearchCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return null; } diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index aa3b98e0..15b673d1 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -216,6 +216,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { bot, command.t('commands:ERROR_PLAY_JOIN_CHANNEL'), ); + await player.destroy(); return null; } diff --git a/src/events/discord/InteractionCreateEvent.ts b/src/events/discord/InteractionCreateEvent.ts index 5d6b4212..318cbd8b 100644 --- a/src/events/discord/InteractionCreateEvent.ts +++ b/src/events/discord/InteractionCreateEvent.ts @@ -247,10 +247,20 @@ export class InteractionCreateEvent extends BaseDiscordEvent {}); + } + } } } diff --git a/src/events/discord/VoiceStateUpdateEvent.ts b/src/events/discord/VoiceStateUpdateEvent.ts index 672674ff..d94676e2 100644 --- a/src/events/discord/VoiceStateUpdateEvent.ts +++ b/src/events/discord/VoiceStateUpdateEvent.ts @@ -211,7 +211,10 @@ export class VoiceStateUpdateEvent extends BaseDiscordEvent Date: Sat, 15 Aug 2026 02:12:15 +0300 Subject: [PATCH 38/49] chore: i18n proofreads and missing locale keys Co-authored-by: hoehoetvhss --- src/locales/en-US/commands.json | 260 ++++++++++++++--------------- src/locales/en-US/embeds.json | 18 +-- src/locales/en-US/events.json | 34 ++-- src/locales/ko-KR/commands.json | 279 ++++++++++++++++---------------- src/locales/ko-KR/embeds.json | 26 +-- src/locales/ko-KR/events.json | 34 ++-- src/locales/ru-RU/commands.json | 226 +++++++++++++------------- src/locales/ru-RU/embeds.json | 12 +- src/locales/ru-RU/events.json | 36 ++--- src/locales/sr-RS/commands.json | 246 ++++++++++++++-------------- src/locales/sr-RS/embeds.json | 12 +- src/locales/sr-RS/events.json | 34 ++-- src/locales/tr-TR/commands.json | 236 +++++++++++++-------------- src/locales/tr-TR/embeds.json | 36 ++--- src/locales/tr-TR/events.json | 40 ++--- src/locales/zh-TW/commands.json | 50 +++--- src/locales/zh-TW/embeds.json | 10 +- src/locales/zh-TW/events.json | 14 +- src/utils/languageUtils.ts | 2 +- 19 files changed, 803 insertions(+), 802 deletions(-) diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index bb10eddf..4eeff197 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -6,13 +6,13 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Remove a user from the blacklist", "CONFIG_BLACKLIST_OPTION_LIST": "List all blacklisted users", "CONFIG_BLACKLIST_OPTION_USER": "User to add or remove from blacklist", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not in the dynamic blacklist.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dynamic Blacklist", - "MESSAGE_BLACKLIST_LIST_EMPTY": "The dynamic blacklist is empty.", - "CONFIG_CLEAR_DESCRIPTION": "Clear all the music in the queue", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not on the blacklist", + "MESSAGE_BLACKLIST_LIST_TITLE": "Current Blacklist", + "MESSAGE_BLACKLIST_LIST_EMPTY": "The blacklist is currently empty", + "CONFIG_CLEAR_DESCRIPTION": "Clear the music queue", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Move the dashboard to the bottom", "CONFIG_DASHBOARD_USAGE": "dashboard", @@ -21,25 +21,25 @@ "CONFIG_DJ_OPTION_DESCRIPTION": "The user to add as DJ (optional)", "CONFIG_FILTER_DESCRIPTION": "Set music filter", "CONFIG_FILTER_USAGE": "filter [effect name]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "Select the music filter mode you want to set", - "CONFIG_HELP_DESCRIPTION": "Get commands help", + "CONFIG_FILTER_OPTION_DESCRIPTION": "Select the music filter to apply", + "CONFIG_HELP_DESCRIPTION": "Get help with commands", "CONFIG_HELP_USAGE": "help [command]", - "CONFIG_HELP_OPTION_DESCRIPTION": "which command need help", + "CONFIG_HELP_OPTION_DESCRIPTION": "The command you need help with", "CONFIG_JOIN_DESCRIPTION": "Join current voice channel", "CONFIG_JOIN_USAGE": "join", - "CONFIG_LANG_DESCRIPTION": "Change the Bot locale", - "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Locale name", + "CONFIG_LANG_DESCRIPTION": "Change the bot's language", + "CONFIG_LANG_USAGE": "language ", + "CONFIG_LANG_OPTION_DESCRIPTION": "Select a locale", "CONFIG_LEAVE_DESCRIPTION": "Leave current voice channel", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "Turns the music loop mode on or off", + "CONFIG_LOOP_DESCRIPTION": "Turn the music loop mode on or off", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Music loop mode", "CONFIG_MOVE_DESCRIPTION": "Swap the positions of two songs in the queue", "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "To swap song position 1", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "To swap song position 2", - "CONFIG_NODE_DESCRIPTION": "Show nodes active status", + "CONFIG_MOVE_OPTION_DESCRIPTION": "Track position 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Track position 2", + "CONFIG_NODE_DESCRIPTION": "Show the status of active nodes", "CONFIG_NODE_USAGE": "node [node name]", "CONFIG_NODE_OPTION_DESCRIPTION": "The node name", "CONFIG_NOW_PLAYING_DESCRIPTION": "Show currently playing music", @@ -48,40 +48,40 @@ "CONFIG_PAUSE_USAGE": "pause", "CONFIG_PING_DESCRIPTION": "Get server ping", "CONFIG_PING_USAGE": "ping", - "CONFIG_PLAY_DESCRIPTION": "Enter your song link or song name to play", + "CONFIG_PLAY_DESCRIPTION": "Enter a song link or name to play", "CONFIG_PLAY_USAGE": "play ", "CONFIG_PLAY_OPTION_DESCRIPTION": "The song link or song name", - "CONFIG_PLAYFIRST_DESCRIPTION": "Enter your song link or song name to play first", + "CONFIG_PLAYFIRST_DESCRIPTION": "Enter a song link or name to play first", "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "The song link or song name to play first", "CONFIG_PLAYLAST_DESCRIPTION": "Replay the current or last played song", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Re-queued the song.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found", + "MESSAGE_PLAYLAST_SUCCESS": "✅ The track is back in the queue", "CONFIG_QUEUE_DESCRIPTION": "Show current playlist", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Select a song to remove from the playlist", - "CONFIG_REMOVE_USAGE": "remove [from index to index 2 track]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "track index number", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "from index to index 2 track", + "CONFIG_REMOVE_USAGE": "remove [from to]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Track number", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Track range", "CONFIG_RESUME_DESCRIPTION": "Resume paused track", "CONFIG_RESUME_USAGE": "resume", - "CONFIG_SEARCH_DESCRIPTION": "Enter song name to search", + "CONFIG_SEARCH_DESCRIPTION": "Enter a song name to search", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "The song name", "CONFIG_SEEK_DESCRIPTION": "Seeks to a certain time in the track", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "traget time (ex: 3m20s, 1:20:55)", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Target time (ex: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Show currently active servers", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Shuffle playlist", "CONFIG_SHUFFLE_USAGE": "shuffle", - "CONFIG_SKIP_DESCRIPTION": "Skip currnet track", + "CONFIG_SKIP_DESCRIPTION": "Skip current track", "CONFIG_SKIP_USAGE": "skip", "CONFIG_STATUS_DESCRIPTION": "Show the bot status", "CONFIG_STATUS_USAGE": "status", - "CONFIG_STOP_DESCRIPTION": "Stop the currently playing music and clear the queue, but don't leave the channel", + "CONFIG_STOP_DESCRIPTION": "Stop playback and clear the queue without leaving the channel", "CONFIG_STOP_USAGE": "stop", "CONFIG_VOLUME_DESCRIPTION": "Change bot volume", "CONFIG_VOLUME_USAGE": "v <0-100>", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [volume]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "The default volume to set (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Command error. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ There is currently no music to be play in the queue.", - "ERROR_NO_PLAYING": "❌ There is no music currently playing.", - "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if enabled) can also skip.", - "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if enabled) can also seek.", - "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if enabled) can also pause.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join voice channel.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ There are no tracks in the queue right now", + "ERROR_NO_PLAYING": "❌ There is no music currently playing", + "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if present) can also skip.", + "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if present) can also seek.", + "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if present) can also pause.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join the voice channel", "ERROR_PLAY_MUSIC": "❌ The service is experiencing some problems, please try again. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ No results found. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Time expired.", - "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated.", - "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs.", - "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs.", - "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ.", - "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ.", - "MESSAGE_DJ_STATIC_MODE": "DJ permissions are managed through configuration in STATIC mode.", - "MESSAGE_DJ_NO_PLAYER": "No active music player found.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ.", - "MESSAGE_DJ_SUCCESS": "Successfully added <@{{userId}}> as DJ.", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired", + "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated", + "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs", + "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs", + "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ", + "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ", + "MESSAGE_DJ_STATIC_MODE": "In STATIC mode, DJ permissions can only be set through the config file", + "MESSAGE_DJ_NO_PLAYER": "There's no player currently playing music", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> has been added as a DJ", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ left the channel! Auto-selected new DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ List", "MESSAGE_DJ_LIST_ADMINS": "Admins", "MESSAGE_DJ_LIST_ROLE_DJS": "Role-based DJs", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dynamic DJs", - "MESSAGE_DJ_LIST_NONE": "No DJs configured.", + "MESSAGE_DJ_LIST_NONE": "No DJs configured", "MESSAGE_DJ_LIST_ERROR": "❌ Failed to load DJ list. Please try again.", - "MESSAGE_DJ_ROLE_NOT_SET": "No DJ role configured", - "MESSAGE_FILTER_NOT_FOUND": "❌ The effect name not found.", - "MESSAGE_FILTER_SELECT_LIST": "Select the music filter mode you want to set. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Select filter mode", - "MESSAGE_HELP_NOT_FOUND": "❌ The command not found.", + "MESSAGE_DJ_ROLE_NOT_SET": "No one has been assigneed to a permanent DJ role", + "MESSAGE_FILTER_NOT_FOUND": "❌ Effect not found", + "MESSAGE_FILTER_SELECT_LIST": "Select a music filter: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Select an effect", + "MESSAGE_HELP_NOT_FOUND": "❌ Command not found", "MESSAGE_HELP_SELECT_LIST": "Choose a command to get help. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Select the help", - "MESSAGE_JOIN_SUCCESS": "✅ Bot Joined.", - "MESSAGE_LANG_ARGS_ERROR": "The locale parameter is wrong, currently available languages: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Currently available languages: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Changed locale to `{{locale}}` successfully", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot left.", + "MESSAGE_HELP_SELECT_MODE": "Select a command", + "MESSAGE_JOIN_SUCCESS": "✅ Joined the voice channel", + "MESSAGE_LANG_ARGS_ERROR": "Invalid locale code. Available languages: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Available languages: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Locale changed to `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Left the voice channel", "REPEAT_MODE_OFF": "OFF", "REPEAT_MODE_SINGLE": "SINGLE", "REPEAT_MODE_ALL": "ALL", @@ -136,49 +136,49 @@ "LOOP_CHOICE_ALL": "All", "LOOP_SELECT_PLACEHOLDER": "Select the loop mode", "LOOP_SELECT_DESCRIPTION": "Set loop mode to {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Set loop to `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ The song order was swapped successfully.", - "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter a valid index range. (1 - {{max}})", + "MESSAGE_LOOP_MODE": "✅ Loop mode is now `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Track order changed", + "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter positions in a valid range. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nand {{length}} other songs", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Save Song", "MESSAGE_NOW_PLAYING_SUBTITLE": "Author: **{{author}}**\nDuration: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Now Playing: {{title}} | Requested by {{requester}}", "UNKNOWN_USER": "Unknown", - "MESSAGE_PAUSE_FAIL": "❌ Music pause failed.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Write the name of the music you want to search.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Music added.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches.", + "MESSAGE_PAUSE_FAIL": "❌ Failed to pause the music", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Please provide a song name or link", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Track added to the queue", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches found", "MESSAGE_PLAY_SELECT_DURATION": "Duration: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Select the music", - "MESSAGE_REMOVE_CANCEL": "✅ Cancelled remove.", - "MESSAGE_REMOVE_FAIL": "❌ Music remove failed.", - "MESSAGE_REMOVE_INSTRUCTION": "Choose a song from **1** to **{{length}}** to **remove** or enter others to cancel selection. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ No music in queue after current.", - "MESSAGE_RESUME_FAIL": "❌ Music resume failed.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ The music has been resumed.", - "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed.", + "MESSAGE_REMOVE_CANCEL": "✅ Removal cancelled", + "MESSAGE_REMOVE_FAIL": "❌ Failed to remove the track", + "MESSAGE_REMOVE_INSTRUCTION": "Choose a track number from **1** to **{{length}}** to **remove** it, or enter any other number to cancel. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ The queue is empty after the current track", + "MESSAGE_RESUME_FAIL": "❌ Failed to resume the music", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ The music has been resumed", + "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed", "MESSAGE_SEEK_ARGS_ERROR": "❌ Invalid format for the target time.\n(**`ex: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ The seek position is beyond the duration of this track. (`{{duration}}`)\nSkipping to the next song.", - "MESSAGE_SEEK_SUCCESS": "✅ Music has been seeked to `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Music shuffled.", - "MESSAGE_SKIP_FAIL": "❌ Music skip failed.", - "MESSAGE_SKIP_SUCCESS": "✅ Music skipped.", - "MESSAGE_STOP_SUCCESS": "✅ Music stopped.", - "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, with `1` to `{{maxVolume}}` Type a number between.**", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Type a number from `1` to `{{maxVolume}}` to change the volume .**", - "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\\n**Please select the volume you want to set:**", - "MESSAGE_VOLUME_SAME": "❌ The volume you want to change is already the current volume.", + "MESSAGE_SEEK_SKIP": "✅ The requested time (`{{duration}}`) is beyond the track's duration.\nSkipping to the next track.", + "MESSAGE_SEEK_SUCCESS": "✅ Seeked to `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Playlist shuffled", + "MESSAGE_SKIP_FAIL": "❌ Failed to skip the track", + "MESSAGE_SKIP_SUCCESS": "✅ Track skipped", + "MESSAGE_STOP_SUCCESS": "✅ Playback stopped and the queue cleared", + "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, enter a number from `1` to `{{maxVolume}}`.**", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Enter a number from `1` to `{{maxVolume}}` to change the volume.**", + "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\n**Select the volume you want:**", + "MESSAGE_VOLUME_SAME": "❌ That volume is already set", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", - "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume has been changed to **{{volume}}%** for this server! (Max: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume.", + "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume for this server changed to **{{volume}}%**! (Max: {{maxVolume}}%)", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume", "ERROR_QUEUE_LIMIT_REACHED": "❌ You have reached your queue limit! You currently have **{{current}}** songs in the queue (limit: **{{limit}}**). Please wait for some songs to finish before adding more.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ The global queue limit has been reached! The queue currently has **{{total}}** songs (global limit: **{{limit}}**). Please wait for some songs to finish before adding more.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to your queue limit (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to the global queue limit (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to your queue limit (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to the global queue limit (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "Guild ID", "MESSAGE_SERVER_GUILD": "Guild", "MESSAGE_SERVER_MEMBERS": "Members", @@ -197,76 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Blacklist manager is not initialized.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "The blacklist isn't loaded yet. Please try again later.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Select a Music command", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Utility command", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Service command", "HELP_COMMAND_ALIASES": "Aliases: {{aliases}}", "HELP_COMMAND_NONE": "None", "LABEL_FILTER_CLEAR": "Clear filters", - "CONFIG_PLAYLIST_DESCRIPTION": "Save, load, edit playlists or import them from M3U url.", + "CONFIG_PLAYLIST_DESCRIPTION": "Manage playlists: save, load, delete, and import from M3U", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "List all playlists in the server.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Show the tracks in a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Delete a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Import a playlist from a remote M3U file.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Remove a track from a playlist.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "List all playlists in the server", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Show the tracks in a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Delete a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Import a playlist from a remote M3U file", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Remove a track from a playlist", "CONFIG_PLAYLIST_OPTION_NAME": "Playlist name", "CONFIG_PLAYLIST_OPTION_SEARCH": "Specific track title to search in the playlist (Optional)", "CONFIG_PLAYLIST_OPTION_URL": "M3U file download link", "CONFIG_PLAYLIST_OPTION_INDEX": "Track index to remove (starts from 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlist manager is not initialized.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Please specify a playlist name.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download M3U file. (HTTP status: {{status}})", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlists aren't available yet. Please try again later.", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand", + "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Please specify a playlist name", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download the M3U file (error code {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ The M3U download was blocked by the security policy. Use a public HTTPS URL up to {{maxSizeMiB}} MiB that responds within {{timeoutSeconds}} seconds.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Failed to parse and save M3U playlist. Check if it's formatted correctly.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Failed to remove track. Check if it is a valid index.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded. (Success: {{added}}, Skipped: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded: {{added}} added, {{skipped}} skipped", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server", "MESSAGE_PLAYLIST_LIST_TITLE": "Server Playlists", "MESSAGE_PLAYLIST_INFO_TITLE": "Playlist Info: {{name}} ({{count}} tracks)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*and {{remaining}} more tracks.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Deleted playlist `{{name}}`.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Deleted playlist `{{name}}`", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ External M3U playlist imported!\nRegistered {{count}} tracks in `{{name}}` playlist.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Removed track #{{index}} from playlist `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Search and play a specific genre/channel from imported radio playlist.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Removed track #{{index}} from playlist `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Search and play a specific genre/channel from imported radio playlist", "CONFIG_RADIO_USAGE": "radio [channel]", "CONFIG_RADIO_OPTION_PLAYLIST": "Playlist name to search", "CONFIG_RADIO_OPTION_CHANNEL": "Radio channel or genre to play (e.g. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Usage: `{{prefix}}radio [playlist name] [channel name]` (e.g. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Could not find a radio channel matching `{{channelQuery}}` in playlist `{{playlistName}}`.\nAvailable examples: {{samples}} etc.", - "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\n(e.g., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\nThe file must be an M3U playlist with EXTINF tags.", "ERROR_RADIO_NO_MATCH": "❌ Radio channel matching `{{query}}` not found.\nAvailable examples: {{samples}} etc.", "ERROR_RADIO_LOAD_FAILED": "❌ Error occurred during loading radio channel: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Searching radio channel **{{title}}** in playlist **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Searching radio channel **{{title}}** in playlist **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Started playing radio channel **{{title}}** from playlist **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Page {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Only the command user can navigate pages.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Only the command user can confirm this action.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Only the command user can navigate pages", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Only the command user can confirm this action", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Playlist Exists", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "A playlist named **`{{name}}`** ({{count}} tracks) already exists.\nDo you want to overwrite it with the imported playlist?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "A playlist named **`{{name}}`** ({{count}} tracks) already exists.\nDo you want to overwrite it with the current queue?", "LABEL_OVERWRITE": "Overwrite", "LABEL_CANCEL": "Cancel", - "MESSAGE_PLAYLIST_OVERWRITING": "Overwriting playlist...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Playlist import cancelled.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", + "MESSAGE_PLAYLIST_OVERWRITING": "Overwriting playlist..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Playlist import cancelled", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", - "ERROR_PLAYLIST_DISABLED": "❌ Playlist features are currently disabled.", + "ERROR_PLAYLIST_DISABLED": "❌ Playlist feature is disabled.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U playlists cannot be queued entirely. Please use the `/radio` command to select a specific station.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U playlist import is restricted to Admins and DJs only." -} \ No newline at end of file +} diff --git a/src/locales/en-US/embeds.json b/src/locales/en-US/embeds.json index 6dc4ff3d..0027ed9f 100644 --- a/src/locales/en-US/embeds.json +++ b/src/locales/en-US/embeds.json @@ -4,16 +4,16 @@ "DASHBOARD_SUBTITLE": "Author: **{{author}}**\nDuration: **{{duration}}**\n────────────────────\nVolume: `{{volume}}%`\nLoop: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Requester:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Dynamic DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Voice channel connected successfully.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Connected to the voice channel", "MAINTAIN_DESCRIPTION": "The bot will begin maintenance in a few minutes. During this time, some features may be temporarily unavailable.", "MAINTAIN_TITLE": "⚠️ Maintenance Notice ⚠️", - "MESSAGE_BLACKLIST": "There are blacklisted user(s) in the voice channel.", + "MESSAGE_BLACKLIST": "There are blacklisted users in the voice channel", "MESSAGE_COMMAND": "Command **{{command}}**", - "MESSAGE_FILTER": "Set filter mode to **{{effectName}}**", + "MESSAGE_FILTER": "Filter set to: **{{effectName}}**", "NODE_ALL_ACTIVE": "All nodes are active", "NODE_CONNECTED": "Latency: **{{ping}}ms**", "NODE_DISCONNECTED": "**DISCONNECTED**", - "NODE_STATUS_ARGS_ERROR": "❌ Please enter a valid node name.", + "NODE_STATUS_ARGS_ERROR": "❌ Please enter a valid node name", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Cores: **{{cores}}**\nSystem load: **{{systemLoad}}**\nLavalink load: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ INFO", @@ -22,14 +22,14 @@ "NODE_STATUS_MEMORY_VALUE": "Used: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", "NODE_STATUS_PING": "Latency", "NODE_STATUS_STATS_TITLE": "📊 STATS", - "NODE_STATUS_STATS_VALUE": "uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "Uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ Nodes Status", "NODE_STATUS_TITLE_2": "🛰️ Node '{{nodeName}}' status", "NODE_UNHEALTHY": "⚠️ There are {{unhealthValue}} nodes disconnected", "PING_DESCRIPTION": "Bot: **{{botPing}}**\nAPI: **{{apiPing}}ms**", - "PING_TITLE": "🛰️ LATENCY'", - "QUEUE_ADD_PLAYLIST": "Added Playlist", - "QUEUE_ADD_TRACK": "Added Track", + "PING_TITLE": "🛰️ LATENCY", + "QUEUE_ADD_PLAYLIST": "Playlist added", + "QUEUE_ADD_TRACK": "Track added", "QUEUE_LIST_LOOP_MODE": "Loop: {{repeatMode}}", "QUEUE_LIST_TITLE": "Queue List", "REMOVE_LIST_LOOP_MODE": "Loop: {{repeatMode}}", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Now Playing:**", "QUEUE_EMPTY": "*No songs in queue*", "QUEUE_HEADER": "**Queue:**" -} \ No newline at end of file +} diff --git a/src/locales/en-US/events.json b/src/locales/en-US/events.json index 4db7d42a..89ab1c11 100644 --- a/src/locales/en-US/events.json +++ b/src/locales/en-US/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Unable to get guild data in cache.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ You are not on the same audio channel as me.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Please join the specified voice channel <#{{channelId}}> and call me.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ You are not connected to an audio channel.", - "ERROR_NOT_PLAYING": "❌ There is no music currently playing.", - "ERROR_REQUIRE_ADMIN": "❌ This command requires administrator privileges.", - "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message.", - "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled.", - "ERROR_TIME_EXPIRED": "❌ Time expired.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left.", - "MESSAGE_BOT_STOP": "✅ Bot stopped.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Unable to get guild data in cache", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ You are not on the same audio channel as me", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Please join the specified voice channel <#{{channelId}}> and call me", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ You are not connected to an audio channel", + "ERROR_NOT_PLAYING": "❌ There is no music currently playing", + "ERROR_REQUIRE_ADMIN": "❌ This command requires administrator privileges", + "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message", + "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left", + "MESSAGE_BOT_STOP": "✅ Bot stopped", + "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled", "MESSAGE_NOW_PLAYING_SUBTITLE": "Author: **{{author}}**\nDuration: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Page {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Select song loop mode.", + "MESSAGE_SELECT_LOOP_MODE": "Select song loop mode", "LOOP_MODE_OFF": "Off", "LOOP_MODE_SINGLE": "Single", "LOOP_MODE_ALL": "All", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I sent you the name of the music in a private message.", - "MESSAGE_SET_LOOP_MODE": "✅ Set loop to `{{mode}}`", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I've sent you the track name in a private message", + "MESSAGE_SET_LOOP_MODE": "✅ Loop mode set to `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ You can not execute commands in this channel. Please use the <#{{channelId}}> text channel." -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index a02420f2..7e891761 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -6,138 +6,138 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "사용자를 블랙리스트에서 제거합니다", "CONFIG_BLACKLIST_OPTION_LIST": "블랙리스트 사용자 목록을 조회합니다", "CONFIG_BLACKLIST_OPTION_USER": "블랙리스트에 추가하거나 제거할 사용자", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 동적 블랙리스트에 없습니다.", - "MESSAGE_BLACKLIST_LIST_TITLE": "동적 블랙리스트", - "MESSAGE_BLACKLIST_LIST_EMPTY": "동적 블랙리스트가 비어 있습니다.", - "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 블랙리스트에 없습니다", + "MESSAGE_BLACKLIST_LIST_TITLE": "블랙리스트", + "MESSAGE_BLACKLIST_LIST_EMPTY": "블랙리스트가 현재 비어 있습니다", + "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다", "CONFIG_CLEAR_USAGE": "clear", - "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다.", + "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다", "CONFIG_DASHBOARD_USAGE": "dashboard", - "CONFIG_DJ_DESCRIPTION": "사용자를 DJ로 추가하거나 DJ 목록을 표시합니다.", + "CONFIG_DJ_DESCRIPTION": "사용자를 DJ로 추가하거나 DJ 목록을 표시합니다", "CONFIG_DJ_USAGE": "dj [@사용자]", "CONFIG_DJ_OPTION_DESCRIPTION": "DJ로 추가할 사용자 (선택사항)", - "CONFIG_FILTER_DESCRIPTION": "재생 효과를 변경합니다.", + "CONFIG_FILTER_DESCRIPTION": "재생 효과를 변경합니다", "CONFIG_FILTER_USAGE": "filter [효과명]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "원하는 음악 효과 모드를 입력 및 선택하세요.", - "CONFIG_HELP_DESCRIPTION": "명령어 도움말을 확인합니다.", + "CONFIG_FILTER_OPTION_DESCRIPTION": "원하는 음악 효과 모드를 입력 및 선택하세요", + "CONFIG_HELP_DESCRIPTION": "명령어 도움말을 확인합니다", "CONFIG_HELP_USAGE": "help [명령어]", - "CONFIG_HELP_OPTION_DESCRIPTION": "도움이 필요한 명령어를 입력하세요.", - "CONFIG_JOIN_DESCRIPTION": "현재 음성 채널에 봇을 참여시킵니다.", + "CONFIG_HELP_OPTION_DESCRIPTION": "도움이 필요한 명령어를 입력하세요", + "CONFIG_JOIN_DESCRIPTION": "현재 음성 채널에 봇을 참여시킵니다", "CONFIG_JOIN_USAGE": "join", - "CONFIG_LANG_DESCRIPTION": "봇 언어를 변경합니다.", + "CONFIG_LANG_DESCRIPTION": "봇 언어를 변경합니다", "CONFIG_LANG_USAGE": "language [언어]", "CONFIG_LANG_OPTION_DESCRIPTION": "언어", - "CONFIG_LEAVE_DESCRIPTION": "현재 음성 채널에서 봇을 내보냅니다.", + "CONFIG_LEAVE_DESCRIPTION": "현재 음성 채널에서 봇을 내보냅니다", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "반복 모드를 켜거나 끕니다.", + "CONFIG_LOOP_DESCRIPTION": "반복 모드를 켜거나 끕니다", "CONFIG_LOOP_USAGE": "loop [끄기/한 곡 반복/전체 반복]", - "CONFIG_LOOP_OPTION_DESCRIPTION": "반복 모드를 선택하세요.", - "CONFIG_MOVE_DESCRIPTION": "재생목록에 있는 두 곡의 위치를 바꿉니다.", + "CONFIG_LOOP_OPTION_DESCRIPTION": "반복 모드를 선택하세요", + "CONFIG_MOVE_DESCRIPTION": "재생목록에 있는 두 곡의 위치를 바꿉니다", "CONFIG_MOVE_USAGE": "move [번호 1] [번호 2]", "CONFIG_MOVE_OPTION_DESCRIPTION": "바뀔 재생목록 번호", "CONFIG_MOVE_OPTION_DESCRIPTION_2": "바꾸고자 하는 재생목록 번호", - "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다.", + "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다", "CONFIG_NODE_USAGE": "node [노드명]", - "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요.", - "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악를 표시합니다.", + "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요", + "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악을 표시합니다", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", - "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다.", + "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다", "CONFIG_PAUSE_USAGE": "pause", - "CONFIG_PING_DESCRIPTION": "서버와 봇의 지연시간을 확인합니다.", + "CONFIG_PING_DESCRIPTION": "서버와 봇의 지연시간을 확인합니다", "CONFIG_PING_USAGE": "ping", - "CONFIG_PLAY_DESCRIPTION": "재생할 음악 링크 또는 음악 이름을 입력하세요.", + "CONFIG_PLAY_DESCRIPTION": "재생할 음악 링크 또는 음악 이름을 입력하세요", "CONFIG_PLAY_USAGE": "play [음악 링크/음악 이름]", - "CONFIG_PLAY_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요.", - "CONFIG_PLAYFIRST_DESCRIPTION": "먼저 재생할 음악 링크 혹은 음악 이름을 입력하세요.", + "CONFIG_PLAY_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요", + "CONFIG_PLAYFIRST_DESCRIPTION": "먼저 재생할 음악 링크 혹은 음악 이름을 입력하세요", "CONFIG_PLAYFIRST_USAGE": "playfirst [음악 링크/음악 이름]", - "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요.", + "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요", "CONFIG_PLAYLAST_DESCRIPTION": "현재 또는 마지막으로 재생된 곡을 다시 재생합니다", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ 이 명령은 대기열이 비어있을 때만 사용할 수 있습니다.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ 최근 재생된 곡을 찾을 수 없습니다.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ 곡이 대기열에 다시 추가되었습니다.", - "CONFIG_QUEUE_DESCRIPTION": "현재 재생중인 음악을 확인합니다.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ 이 명령은 대기열이 비어있을 때만 사용할 수 있습니다", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ 최근 재생된 곡을 찾을 수 없습니다", + "MESSAGE_PLAYLAST_SUCCESS": "✅ 곡이 대기열에 다시 추가되었습니다", + "CONFIG_QUEUE_DESCRIPTION": "현재 재생중인 음악을 확인합니다", "CONFIG_QUEUE_USAGE": "queue", - "CONFIG_REMOVE_DESCRIPTION": "재생목록에서 제거할 음악을 선택합니다.", + "CONFIG_REMOVE_DESCRIPTION": "재생목록에서 제거할 음악을 선택합니다", "CONFIG_REMOVE_USAGE": "remove [재생목록 번호]", "CONFIG_REMOVE_OPTION_DESCRIPTION": "재생목록 번호", "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "재생목록 번호", - "CONFIG_RESUME_DESCRIPTION": "일시중지된 음악을 재생합니다.", + "CONFIG_RESUME_DESCRIPTION": "일시중지된 음악을 재생합니다", "CONFIG_RESUME_USAGE": "resume", - "CONFIG_SEARCH_DESCRIPTION": "검색할 음악 이름 혹은 링크를 입력하세요.", + "CONFIG_SEARCH_DESCRIPTION": "검색할 음악 이름 혹은 링크를 입력하세요", "CONFIG_SEARCH_USAGE": "search [음악 이름/음악 링크]", - "CONFIG_SEARCH_OPTION_DESCRIPTION": "음악 이름 혹은 음악 링크를 입력하세요.", - "CONFIG_SEEK_DESCRIPTION": "음악을 특정 시간으로 이동합니다.", + "CONFIG_SEARCH_OPTION_DESCRIPTION": "음악 이름 혹은 음악 링크를 입력하세요", + "CONFIG_SEEK_DESCRIPTION": "음악을 특정 시간으로 이동합니다", "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (예시: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요.", - "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다.", + "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요", + "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "음악을 랜덤으로 재생할지 선택합니다.", + "CONFIG_SHUFFLE_DESCRIPTION": "재생목록을 섞습니다", "CONFIG_SHUFFLE_USAGE": "shuffle", - "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다.", + "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다", "CONFIG_SKIP_USAGE": "skip", - "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다.", + "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다", "CONFIG_STATUS_USAGE": "status", - "CONFIG_STOP_DESCRIPTION": "현재 재생 중인 음악을 멈추고 봇을 내보냅니다.", + "CONFIG_STOP_DESCRIPTION": "재생을 멈추고 재생목록을 비웁니다. 채널은 떠나지 않습니다.", "CONFIG_STOP_USAGE": "stop", - "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다.", + "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다", "CONFIG_VOLUME_USAGE": "v [0-200]", - "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정하고자 하는 음량 값을 입력하세요. (1~{{maxVolume}})", - "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다.", + "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정할 음량", + "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [음량]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "설정할 기본 음량 (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ 명령에 오류가 발생했습니다. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ 현재 대기열에 재생할 음악이 없습니다.", - "ERROR_NO_PLAYING": "❌ 현재 재생 중인 음악이 없습니다.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ 현재 대기열에 재생할 음악이 없습니다", + "ERROR_NO_PLAYING": "❌ 현재 재생 중인 음악이 없습니다", "ERROR_SKIP_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 건너뛸 수 있습니다. 관리자 및 DJ(활성화된 경우)도 건너뛸 수 있습니다.", "ERROR_SEEK_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 탐색할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 탐색할 수 있습니다.", "ERROR_PAUSE_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 일시정지할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 일시정지할 수 있습니다.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다.", - "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 다시 시도해 주세요. 이유: ({{reason}})", + "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다", + "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ 검색결과가 없습니다. 이유: ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", - "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다.", - "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다.", - "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다.", - "MESSAGE_DJ_MENTION_USER": "DJ로 추가할 사용자를 멘션해주세요.", - "MESSAGE_DJ_NO_BOTS": "봇을 DJ로 추가할 수 없습니다.", - "MESSAGE_DJ_STATIC_MODE": "고정 모드에서는 설정을 통해 DJ 권한이 관리됩니다.", - "MESSAGE_DJ_NO_PLAYER": "활성화된 음악 플레이어를 찾을 수 없습니다.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}>는 이미 DJ입니다.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}>를 DJ로 성공적으로 추가했습니다.", + "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다", + "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다", + "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다", + "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다", + "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다", + "MESSAGE_DJ_MENTION_USER": "DJ로 추가할 사용자를 멘션해주세요", + "MESSAGE_DJ_NO_BOTS": "봇을 DJ로 추가할 수 없습니다", + "MESSAGE_DJ_STATIC_MODE": "고정 모드에서는 설정을 통해 DJ 권한이 관리됩니다", + "MESSAGE_DJ_NO_PLAYER": "활성화된 음악 플레이어를 찾을 수 없습니다", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}>는 이미 DJ입니다", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}>를 DJ로 성공적으로 추가했습니다", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ가 채널을 떠났습니다! 새로운 DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ 목록", "MESSAGE_DJ_LIST_ADMINS": "관리자", "MESSAGE_DJ_LIST_ROLE_DJS": "역할 기반 DJ", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "동적 DJ", - "MESSAGE_DJ_LIST_NONE": "구성된 DJ가 없습니다.", + "MESSAGE_DJ_LIST_NONE": "구성된 DJ가 없습니다", "MESSAGE_DJ_LIST_ERROR": "❌ DJ 목록을 불러오지 못했습니다. 다시 시도해 주세요.", "MESSAGE_DJ_ROLE_NOT_SET": "DJ 역할이 구성되지 않음", - "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다.", - "MESSAGE_FILTER_SELECT_LIST": "⬇️ 설정할 재생 효과를 선택합니다. ⬇️", + "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다", + "MESSAGE_FILTER_SELECT_LIST": "설정할 재생 효과를 선택하세요: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "필터 모드 선택", - "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다.", - "MESSAGE_HELP_SELECT_LIST": "⬇️ 도움말을 받으려면 명령어 선택하세요. ⬇️", + "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다", + "MESSAGE_HELP_SELECT_LIST": "도움말을 받으려면 명령어를 선택하세요. ⬇️", "MESSAGE_HELP_SELECT_MODE": "도움말 선택하기", - "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다.", + "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다", "MESSAGE_LANG_ARGS_ERROR": "언어 변수가 잘못되었습니다. (현재 사용 가능한 언어: {{langList}})", "MESSAGE_LANG_AVAILABLE_LIST": "현재 사용 가능한 언어: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ 봇의 언어가 `{{locale}}`(으)로 성공적으로 변경되었습니다.", - "MESSAGE_LEAVE_SUCCESS": "✅ 봇이 떠났습니다.", + "MESSAGE_LANG_SUCCESS": "✅ 봇의 언어가 `{{locale}}`(으)로 성공적으로 변경되었습니다", + "MESSAGE_LEAVE_SUCCESS": "✅ 봇이 떠났습니다", "REPEAT_MODE_OFF": "끄기", "REPEAT_MODE_SINGLE": "한 곡 반복", "REPEAT_MODE_ALL": "전체 반복", "LOOP_CHOICE_OFF": "끄기", "LOOP_CHOICE_ONE": "한 곡 반복", "LOOP_CHOICE_ALL": "전체 반복", - "LOOP_SELECT_PLACEHOLDER": "반복 모드를 선택하세요.", - "LOOP_SELECT_DESCRIPTION": "반복 모드를 {{mode}}(으)로 설정합니다.", - "MESSAGE_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다.", - "MESSAGE_MOVE_SUCCESS": "✅ 음악 순서가 성공적으로 변경되었습니다.", + "LOOP_SELECT_PLACEHOLDER": "반복 모드를 선택하세요", + "LOOP_SELECT_DESCRIPTION": "반복 모드를 {{mode}}(으)로 설정합니다", + "MESSAGE_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다", + "MESSAGE_MOVE_SUCCESS": "✅ 음악 순서가 성공적으로 변경되었습니다", "MESSAGE_MOVE_WRONG_INDEX": "❌ 유효한 범위를 입력하세요. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\n그 외 {{length}}개의 곡", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "음악 저장", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 현재 재생 중: {{title}} | {{requester}} 요청", "UNKNOWN_USER": "알 수 없음", - "MESSAGE_PAUSE_FAIL": "❌ 음악 일시 중지에 실패했습니다.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ 음악이 일시정지되었습니다.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ 음악이 일시정지되었습니다.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ 검색하려는 음악의 이름을 입력하세요.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ 음악이 추가되었습니다.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ 검색결과가 없습니다.", + "MESSAGE_PAUSE_FAIL": "❌ 음악 일시 중지에 실패했습니다", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ 음악이 일시정지되었습니다", + "MESSAGE_PAUSE_SUCCESS": "⏸️ 음악이 일시정지되었습니다", + "MESSAGE_PLAY_ARGS_ERROR": "❌ 검색하려는 음악의 이름을 입력하세요", + "MESSAGE_PLAY_MUSIC_ADD": "✅ 음악이 추가되었습니다", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ 검색결과가 없습니다", "MESSAGE_PLAY_SELECT_DURATION": "**길이:** {{label}}", - "MESSAGE_PLAY_SELECT_TITLE": "음악을 선택하세요.", - "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다.", - "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다.", + "MESSAGE_PLAY_SELECT_TITLE": "음악을 선택하세요", + "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다", + "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다", "MESSAGE_REMOVE_INSTRUCTION": "⬇️ **1 ~ {{length}}** 중에서 노래를 선택하여 **제거**하거나 다른 문자를 입력하여 선택을 취소합니다. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 대기열에 음악이 없습니다.", - "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ 음악이 다시 재생되었습니다.", - "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 곡 이후 대기열이 비어 있습니다", + "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 음악이 다시 재생되었습니다", + "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다", "MESSAGE_SEEK_ARGS_ERROR": "❌ 목표 시간이 잘못된 형식입니다. (예: 3m20s, 1m 50s, 1:20:55, 5:20)", "MESSAGE_SEEK_SKIP": "✅ 해당 위치는 {{duration}}을 초과합니다.\n다음 곡으로 건너뜁니다.", - "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 음악을 랜덤으로 재생합니다.", - "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다.", - "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다.", - "MESSAGE_STOP_SUCCESS": "✅ 음악을 멈췄습니다.", - "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", - "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\\n**설정할 음량을 선택하세요:**", - "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다.", - "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}%**로 변경되었습니다!", + "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 재생목록을 섞었습니다", + "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다", + "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다", + "MESSAGE_STOP_SUCCESS": "✅ 재생을 멈추고 재생목록을 비웠습니다", + "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다", + "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\n**설정할 음량을 선택하세요:**", + "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다", + "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 이 서버의 기본 음량이 **{{volume}}%**로 변경되었습니다! (최대: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요", "ERROR_QUEUE_LIMIT_REACHED": "❌ 대기열 한도에 도달했습니다! 현재 대기열에 **{{current}}**개의 음악이 있습니다 (한도: **{{limit}}**). 더 추가하려면 일부 음악이 끝날 때까지 기다려 주세요.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ 전역 대기열 한도에 도달했습니다! 대기열에 현재 **{{total}}**개의 음악이 있습니다 (전역 한도: **{{limit}}**). 더 추가하려면 일부 음악이 끝날 때까지 기다려 주세요.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (대기열 한도: **{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (전역 대기열 한도: **{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (대기열 한도: **{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (전역 대기열 한도: **{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "서버 ID", "MESSAGE_SERVER_GUILD": "서버", "MESSAGE_SERVER_MEMBERS": "멤버", @@ -186,7 +186,7 @@ "MESSAGE_DJ_TYPE_STATIC": "(고정)", "MESSAGE_DJ_TYPE_ROLE": "(역할)", "MESSAGE_DJ_TYPE_DYNAMIC": "(동적)", - "LABEL_DELETE_MESSAGE": "메세지 삭제", + "LABEL_DELETE_MESSAGE": "메시지 삭제", "LABEL_CLEAR_QUEUE": "재생목록 초기화", "UNIT_DAYS": "일", "UNIT_HOURS": "시간", @@ -197,75 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "블랙리스트 관리자가 초기화되지 않았습니다.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "블랙리스트 관리자가 초기화되지 않았습니다", "HELP_SELECT_MUSIC_PLACEHOLDER": "음악 명령어 선택", - "HELP_SELECT_UTILITY_PLACEHOLDER": "유틸리티 명령어 선택", + "HELP_SELECT_UTILITY_PLACEHOLDER": "서비스 명령어를 선택하세요", "HELP_COMMAND_ALIASES": "별칭: {{aliases}}", "HELP_COMMAND_NONE": "없음", "LABEL_FILTER_CLEAR": "효과 초기화", - "CONFIG_PLAYLIST_DESCRIPTION": "플레이리스트를 저장, 로드, 편집 또는 외부 M3U에서 가져옵니다.", + "CONFIG_PLAYLIST_DESCRIPTION": "플레이리스트를 저장, 로드, 편집 또는 외부 M3U에서 가져옵니다", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "현재 대기열을 플레이리스트로 저장합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "저장된 플레이리스트를 대기열에 추가하고 재생합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "서버의 플레이리스트 목록을 조회합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "플레이리스트에 저장된 곡 목록을 확인합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "플레이리스트를 삭제합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "외부 M3U 파일 주소에서 플레이리스트를 가져옵니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "플레이리스트에서 특정 트랙을 삭제합니다.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "현재 대기열을 플레이리스트로 저장합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "저장된 플레이리스트를 대기열에 추가하고 재생합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "서버의 플레이리스트 목록을 조회합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "플레이리스트에 저장된 곡 목록을 확인합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "플레이리스트를 삭제합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "외부 M3U 파일 주소에서 플레이리스트를 가져옵니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "플레이리스트에서 특정 트랙을 삭제합니다", "CONFIG_PLAYLIST_OPTION_NAME": "플레이리스트 이름", "CONFIG_PLAYLIST_OPTION_SEARCH": "플레이리스트 내부에서 검색하여 재생할 트랙 이름 (옵션)", "CONFIG_PLAYLIST_OPTION_URL": "M3U 파일 다운로드 링크", "CONFIG_PLAYLIST_OPTION_INDEX": "삭제할 트랙 번호 (1부터 시작)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ 플레이리스트 매니저가 초기화되지 않았습니다.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ 올바른 하위 명령어를 입력해주세요 (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ 알 수 없는 하위 명령어입니다.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ 현재 재생 중이거나 대기열에 곡이 없습니다.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ 플레이리스트 이름을 입력해주세요.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ 플레이리스트 저장에 실패했습니다.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ 플레이리스트 `{{name}}`을(를) 찾을 수 없거나 비어 있습니다.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ 플레이리스트 `{{name}}` 내에서 `{{searchQuery}}`와(과) 일치하는 트랙을 찾을 수 없습니다.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ 플레이리스트 매니저가 초기화되지 않았습니다", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ 올바른 하위 명령어를 입력해주세요 (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ 알 수 없는 하위 명령어입니다", + "ERROR_PLAYLIST_NO_PLAYING": "❌ 현재 재생 중이거나 대기열에 곡이 없습니다", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ 플레이리스트 이름을 입력해주세요", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ 플레이리스트 저장에 실패했습니다", + "ERROR_PLAYLIST_NOT_FOUND": "❌ 플레이리스트 `{{name}}`을(를) 찾을 수 없거나 비어 있습니다", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ 플레이리스트 `{{name}}` 내에서 `{{searchQuery}}`와(과) 일치하는 트랙을 찾을 수 없습니다", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ M3U 파일을 다운로드하지 못했습니다. (HTTP 상태 코드: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ 보안 정책에 의해 M3U 다운로드가 차단되었습니다. {{timeoutSeconds}}초 이내에 응답하고 {{maxSizeMiB}} MiB 이하인 공개 HTTPS URL을 사용해주세요.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ 플레이리스트에는 최대 {{limit}}개의 트랙만 저장할 수 있습니다.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ 플레이리스트에는 최대 {{limit}}개의 트랙만 저장할 수 있습니다", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ M3U 플레이리스트 파싱 및 저장에 실패했습니다. 올바른 형식인지 확인해주세요.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 곡 삭제에 실패했습니다. 올바른 트랙 번호인지 확인해주세요.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드가 완료되었습니다. (성공: {{added}}곡, 실패/스킵: {{skipped}}곡)", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드 완료: {{added}}곡 추가, {{skipped}}곡 건너뜀", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다", "MESSAGE_PLAYLIST_LIST_TITLE": "서버 플레이리스트 목록", "MESSAGE_PLAYLIST_INFO_TITLE": "플레이리스트 정보: {{name}} (총 {{count}}곡)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*외 {{remaining}}개의 곡이 더 있습니다.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ 플레이리스트 `{{name}}`을(를) 삭제했습니다.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ 플레이리스트 `{{name}}`을(를) 삭제했습니다", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ 외부 M3U 플레이리스트를 성공적으로 가져왔습니다!\n`{{name}}` 플레이리스트에 {{count}}개의 곡이 등록되었습니다.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ 플레이리스트 `{{name}}`에서 {{index}}번 곡을 삭제했습니다.", - "CONFIG_RADIO_DESCRIPTION": "가져온 라디오 플레이리스트에서 특정 장르/채널을 검색해 바로 재생합니다.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ 플레이리스트 `{{name}}`에서 {{index}}번 곡을 삭제했습니다", + "CONFIG_RADIO_DESCRIPTION": "가져온 라디오 플레이리스트에서 특정 장르/채널을 검색해 바로 재생합니다", "CONFIG_RADIO_USAGE": "radio [채널명]", "CONFIG_RADIO_OPTION_PLAYLIST": "검색할 플레이리스트 이름", "CONFIG_RADIO_OPTION_CHANNEL": "재생할 라디오 채널명 또는 장르 (예: workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ 올바른 사용법: `{{prefix}}radio [플레이리스트 이름] [채널명]` (예: `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 플레이리스트 `{{playlistName}}`에서 `{{channelQuery}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", - "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n(예: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n파일은 EXTINF 태그가 포함된 M3U 형식이어야 합니다.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", "ERROR_RADIO_LOAD_FAILED": "❌ 라디오 채널 로딩 중 오류가 발생했습니다: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다.", - "MESSAGE_RADIO_SEARCHING": "🔄 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 검색 중...", + "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다", + "MESSAGE_RADIO_SEARCHING": "🔄 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 검색 중..", "MESSAGE_RADIO_PLAYING": "📻 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 재생을 시작합니다!", "MESSAGE_FOOTER_PAGE": "페이지 {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "명령어를 실행한 사용자만 페이지를 이동할 수 있습니다.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "명령어를 실행한 사용자만 이 작업을 승인할 수 있습니다.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "명령어를 실행한 사용자만 페이지를 이동할 수 있습니다", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "명령어를 실행한 사용자만 이 작업을 승인할 수 있습니다", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ 플레이리스트가 이미 존재합니다", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "**`{{name}}`** 이름의 플레이리스트(총 {{count}}곡)가 이미 존재합니다.\n가져온 플레이리스트로 덮어쓰시겠습니까?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "**`{{name}}`** 이름의 플레이리스트(총 {{count}}곡)가 이미 존재합니다.\n현재 대기열의 트랙으로 덮어쓰시겠습니까?", "LABEL_OVERWRITE": "덮어쓰기", "LABEL_CANCEL": "취소", - "MESSAGE_PLAYLIST_OVERWRITING": "플레이리스트 덮어쓰는 중...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 플레이리스트 가져오기가 취소되었습니다.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", + "MESSAGE_PLAYLIST_OVERWRITING": "플레이리스트 덮어쓰는 중..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 플레이리스트 가져오기가 취소되었습니다", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U 플레이리스트 전체는 재생할 수 없습니다. `/radio` 명령어를 사용하여 원하는 라디오 채널을 선택해 주세요.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U 플레이리스트 가져오기는 관리자 또는 DJ 권한이 있는 사용자만 수행할 수 있습니다." -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/embeds.json b/src/locales/ko-KR/embeds.json index 3929475b..ab2cb737 100644 --- a/src/locales/ko-KR/embeds.json +++ b/src/locales/ko-KR/embeds.json @@ -1,40 +1,40 @@ { "DASHBOARD_TITLE": "대시보드", - "DASHBOARD_FINISH_PLAYING": "모든 음악이 재생되었습니다.", + "DASHBOARD_FINISH_PLAYING": "재생목록 재생이 완료되었습니다", "DASHBOARD_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{duration}}\n────────────────────\n**음량:** `{{volume}}%`\n**반복:** `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**요청자:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성채팅에 성공적으로 연결했습니다.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성 채널에 성공적으로 연결했습니다", "MAINTAIN_DESCRIPTION": "몇 분 후에 봇이 점검을 시작합니다. 일부 기능을 일시적으로 사용하지 못할 수 있습니다.", "MAINTAIN_TITLE": "⚠️ 유지보수 안내 ⚠️", - "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다.", + "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다", "MESSAGE_COMMAND": "명령어 **{{command}}**", - "MESSAGE_FILTER": "`{{effectName}}`(으)로 재생 효과가 변경되었습니다.", - "NODE_ALL_ACTIVE": "모든 노드가 활성화되었습니다.", + "MESSAGE_FILTER": "`{{effectName}}`(으)로 재생 효과가 변경되었습니다", + "NODE_ALL_ACTIVE": "모든 노드가 활성화되었습니다", "NODE_CONNECTED": "지연시간: **{{ping}}ms**", "NODE_DISCONNECTED": "**연결 끊김**", - "NODE_STATUS_ARGS_ERROR": "❌ 유효한 노드 이름을 입력하세요.", + "NODE_STATUS_ARGS_ERROR": "❌ 유효한 노드 이름을 입력하세요", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "**코어:** {{cores}}\n**시스템 로드:** {{systemLoad}}\n**Lavalink 로드:** {{lavalinkLoad}}", "NODE_STATUS_INFO_TITLE": "🏷️ 정보", "NODE_STATUS_INFO_VALUE": "**버전:** {{version}}\n**자바:** {{jvm}}\n**Lavaplayer:** {{lavaplayer}}\n**Git:** {{git}}\n**빌드 시간:** {{buildTime}}", "NODE_STATUS_MEMORY_TITLE": "📑 메모리", - "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당 가능:** {{allocated}}\n**예약 가능:** {{reservable}}\n", + "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당됨:** {{allocated}}\n**예약 가능:** {{reservable}}\n", "NODE_STATUS_PING": "지연시간", "NODE_STATUS_STATS_TITLE": "📊 상태", - "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**음악을 듣는 서버:** {{playerCount}}개\n**음악을 듣는 인원:** {{playingCount}}명", + "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**플레이어:** {{playerCount}}개\n**재생 중:** {{playingCount}}개", "NODE_STATUS_TITLE": "🛰️ 노드 상태", "NODE_STATUS_TITLE_2": "🛰️ '{{nodeName}}'의 상태", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}의 연결이 끊어졌습니다.", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}개 노드의 연결이 끊어졌습니다", "PING_DESCRIPTION": "**봇:** {{botPing}}\n**API:** {{apiPing}}ms", "PING_TITLE": "🛰️ 지연시간", "QUEUE_ADD_PLAYLIST": "재생목록 추가", - "QUEUE_ADD_TRACK": "재생목록 추가", + "QUEUE_ADD_TRACK": "음악 추가", "QUEUE_LIST_LOOP_MODE": "반복: {{repeatMode}}", "QUEUE_LIST_TITLE": "재생목록", "REMOVE_LIST_LOOP_MODE": "반복: {{repeatMode}}", - "REMOVE_LIST_TITLE": "제거된 목록", - "REMOVE_TRACK_TITLE": "제거된 목록", + "REMOVE_LIST_TITLE": "제거 목록", + "REMOVE_TRACK_TITLE": "제거된 음악", "SERVER_TITLE": "{{name}}이(가) 가입된 서버", "SERVER_FIELD_DJ_ROLE": "DJ 역할", "SERVER_FIELD_ADMINS": "관리자", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**재생 중:**", "QUEUE_EMPTY": "*대기열에 노래가 없습니다*", "QUEUE_HEADER": "**대기열:**" -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/events.json b/src/locales/ko-KR/events.json index f0fda299..6fe3ba13 100644 --- a/src/locales/ko-KR/events.json +++ b/src/locales/ko-KR/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ 서버 데이터를 캐시에서 가져올 수 없습니다.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ 봇과 같은 음성 채널에 있지 않습니다.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "지정된 음성 채널 <#{{channelId}}>에 참여 후 요청을 해주세요.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ 음성 채널에 연결되어 있지 않습니다.", - "ERROR_NOT_PLAYING": "❌ 현재 재생 중인 음악이 없습니다.", - "ERROR_REQUIRE_ADMIN": "❌ 해당 명령어는 관리자 권한이 필요합니다.", - "ERROR_REQUIRE_DJ": "❌ 이 명령을 사용하려면 DJ 권한이 필요합니다.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ DM을 보낼 수 없습니다.", - "ERROR_SLASH_NOT_ENABLE": "❌ 슬래시 명령이 활성화되어 있지 않습니다.", - "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다.", - "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다.", - "MESSAGE_MUSIC_SHUFFLE": "✅ 음악을 랜덤으로 재생합니다.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ 서버 데이터를 캐시에서 가져올 수 없습니다", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ 봇과 같은 음성 채널에 있지 않습니다", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "지정된 음성 채널 <#{{channelId}}>에 참여 후 요청을 해주세요", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ 음성 채널에 연결되어 있지 않습니다", + "ERROR_NOT_PLAYING": "❌ 현재 재생 중인 음악이 없습니다", + "ERROR_REQUIRE_ADMIN": "❌ 해당 명령어는 관리자 권한이 필요합니다", + "ERROR_REQUIRE_DJ": "❌ 이 명령을 사용하려면 DJ 권한이 필요합니다", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ DM을 보낼 수 없습니다", + "ERROR_SLASH_NOT_ENABLE": "❌ 슬래시 명령이 활성화되어 있지 않습니다", + "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다", + "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다", + "MESSAGE_MUSIC_SHUFFLE": "✅ 재생목록을 섞었습니다", "MESSAGE_NOW_PLAYING_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{label}}\n", "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 페이지 {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 입력 혹은 선택하세요.", + "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 선택하세요", "LOOP_MODE_OFF": "끄기", "LOOP_MODE_SINGLE": "한 곡 반복", "LOOP_MODE_ALL": "전체 반복", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다.", - "MESSAGE_SET_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다", + "MESSAGE_SET_LOOP_MODE": "✅ 반복 모드를 `{{mode}}`(으)로 설정했습니다", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 이 채널에서는 명령을 실행할 수 없습니다. <#{{channelId}}> 채널을 사용하세요." -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 21019e3e..c44d5cbe 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Удалить пользователя из чёрного списка", "CONFIG_BLACKLIST_OPTION_LIST": "Показать список пользователей в чёрном списке", "CONFIG_BLACKLIST_OPTION_USER": "Пользователь для добавления или удаления из чёрного списка", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже находится в чёрном списке.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> не внесен(-а) в чёрный список.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже в чёрном списке", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> нет в чёрном списке", "MESSAGE_BLACKLIST_LIST_TITLE": "Текущий чёрный список", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст", "CONFIG_CLEAR_DESCRIPTION": "Очистить очередь музыки", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Переместить панель управления вниз", @@ -24,12 +24,12 @@ "CONFIG_FILTER_OPTION_DESCRIPTION": "Выберите музыкальный фильтр для наложения", "CONFIG_HELP_DESCRIPTION": "Получить справку по командам", "CONFIG_HELP_USAGE": "help [команда]", - "CONFIG_HELP_OPTION_DESCRIPTION": "Для какой команды нужна подсказка", + "CONFIG_HELP_OPTION_DESCRIPTION": "Команда, по которой нужна справка", "CONFIG_JOIN_DESCRIPTION": "Подключиться к текущему голосовому каналу", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Изменить язык бота", - "CONFIG_LANG_USAGE": "language <языковой код>", - "CONFIG_LANG_OPTION_DESCRIPTION": "Кодовое обозначение языка", + "CONFIG_LANG_USAGE": "language <доступный язык>", + "CONFIG_LANG_OPTION_DESCRIPTION": "Выберите язык", "CONFIG_LEAVE_DESCRIPTION": "Покинуть текущий голосовой канал", "CONFIG_LEAVE_USAGE": "leave", "CONFIG_LOOP_DESCRIPTION": "Включить или выключить режим повтора музыки", @@ -56,13 +56,13 @@ "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Ссылка или название трека для немедленного воспроизведения", "CONFIG_PLAYLAST_DESCRIPTION": "Повторно воспроизвести текущий или последний трек", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно воспроизводившийся трек не найден.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова добавлен в очередь.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно сыгранный трек не найден", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова в очереди", "CONFIG_QUEUE_DESCRIPTION": "Показать текущий плейлист", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Выбрать трек для удаления из плейлиста", - "CONFIG_REMOVE_USAGE": "remove <номер трека> [начальный индекс конечный индекс]", + "CONFIG_REMOVE_USAGE": "remove <номер трека> [от до]", "CONFIG_REMOVE_OPTION_DESCRIPTION": "Номер трека", "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Диапазон треков", "CONFIG_RESUME_DESCRIPTION": "Продолжить воспроизведение", @@ -70,9 +70,9 @@ "CONFIG_SEARCH_DESCRIPTION": "Введите название трека для поиска", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Название трека", - "CONFIG_SEEK_DESCRIPTION": "Перемотать трек на указанное время", - "CONFIG_SEEK_USAGE": "seek <[чч]мм]сс/[чч:мм]:сс> (пример: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "Целевое время (пример: 3m20s, 1:20:55)", + "CONFIG_SEEK_DESCRIPTION": "Перемотать трек к заданному моменту", + "CONFIG_SEEK_USAGE": "seek <[чч]мм]сс/[чч:мм]:сс> (пример: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Время в треке (пример: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Показать активные серверы", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Перемешать плейлист", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [громкость]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Громкость по умолчанию (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Ошибка команды. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди нет музыки для воспроизведения.", - "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится.", - "ERROR_SKIP_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может её пропустить. Админы и DJ (если включены) тоже могут пропускать.", - "ERROR_SEEK_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может перематывать её. Админы и DJ (если включены) тоже могут перематывать.", - "ERROR_PAUSE_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может поставить её на паузу. Админы и DJ (если включены) тоже могут ставить на паузу.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди сейчас нет треков", + "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится", + "ERROR_SKIP_NOT_REQUESTER": "❌ Пропустить этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_SEEK_NOT_REQUESTER": "❌ Перематывать этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_PAUSE_NOT_REQUESTER": "❌ Ставить этот трек на паузу может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу", "ERROR_PLAY_MUSIC": "❌ Сервис испытывает проблемы, попробуйте позже. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Ничего не найдено. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", - "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена.", - "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев.", - "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев.", - "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя для добавления на роль диджея.", - "MESSAGE_DJ_NO_BOTS": "Нельзя добавлять ботов на роль диджея.", - "MESSAGE_DJ_STATIC_MODE": "Права DJ управляются через конфигурацию в статическом режиме.", - "MESSAGE_DJ_NO_PLAYER": "Активный музыкальный плеер не найден.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем.", - "MESSAGE_DJ_SUCCESS": "Пользователь <@{{userId}}> назначен на роль диджея.", + "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло", + "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена", + "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев", + "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев", + "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя, которого хотите сделать диджеем", + "MESSAGE_DJ_NO_BOTS": "Ботов нельзя назначать диджеями", + "MESSAGE_DJ_STATIC_MODE": "В статическом режиме права DJ настраиваются только через файл конфигурации", + "MESSAGE_DJ_NO_PLAYER": "Нет плеера, воспроизводящего сейчас музыку", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> назначен диджеем", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ покинул канал! Автоматически выбран новый DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Список диджеев", "MESSAGE_DJ_LIST_ADMINS": "Администраторы", "MESSAGE_DJ_LIST_ROLE_DJS": "Назначенные диджеи", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Ситуативные диджеи", - "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались.", + "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались", "MESSAGE_DJ_LIST_ERROR": "❌ Не удалось загрузить список диджеев. Попробуйте снова.", - "MESSAGE_DJ_ROLE_NOT_SET": "Роль DJ не настроена", - "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден.", - "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр для наложения: ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Выберите применяемый эффект", - "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена.", + "MESSAGE_DJ_ROLE_NOT_SET": "Постоянный DJ не назначен", + "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден", + "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Выберите эффект", + "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена", "MESSAGE_HELP_SELECT_LIST": "Выберите команду для получения справки: ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Выберите подсказку", - "MESSAGE_JOIN_SUCCESS": "✅ Бот подключился.", + "MESSAGE_HELP_SELECT_MODE": "Выберите команду", + "MESSAGE_JOIN_SUCCESS": "✅ Подключился к голосовому каналу", "MESSAGE_LANG_ARGS_ERROR": "Неверный языковой код, доступные языки: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Доступные языки: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Язык успешно изменён на `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал.", + "MESSAGE_LANG_SUCCESS": "✅ Язык изменён на `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал", "REPEAT_MODE_OFF": "ВЫКЛ", "REPEAT_MODE_SINGLE": "ОДИНОЧНЫЙ", "REPEAT_MODE_ALL": "ВСЕ", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Все", "LOOP_SELECT_PLACEHOLDER": "Выберите режим повтора", "LOOP_SELECT_DESCRIPTION": "Установить режим повтора: {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Режим повтора установлен: `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена.", + "MESSAGE_LOOP_MODE": "✅ Режим повтора теперь `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена", "MESSAGE_MOVE_WRONG_INDEX": "❌ Укажите позиции в корректном диапазоне. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nи ещё {{length}} треков", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Сохранить трек", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Сейчас играет: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Сейчас играет: {{title}} | Поставил: {{requester}}", "UNKNOWN_USER": "Неизвестно", - "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название искомой музыки.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Музыка добавлена.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено.", + "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название трека или ссылку", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Трек добавлен в очередь", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено", "MESSAGE_PLAY_SELECT_DURATION": "Длительность: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Выберите музыку", - "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено.", - "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить музыку.", - "MESSAGE_REMOVE_INSTRUCTION": "Выберите позицию трека от **1** до **{{length}}** для **удаления** или введите другое значение для отмены. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста.", - "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена.", - "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено.", + "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено", + "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить трек", + "MESSAGE_REMOVE_INSTRUCTION": "Выберите номер трека от **1** до **{{length}}**, чтобы **удалить** его, или введите любой другой номер для отмены. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста", + "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена", + "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено", "MESSAGE_SEEK_ARGS_ERROR": "❌ Неверный формат времени.\n(**`пример: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Позиция перемотки превышает длительность трека. (`{{duration}}`)\nПереход к следующей песне.", - "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан.", - "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку.", - "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен.", - "MESSAGE_STOP_SUCCESS": "✅ Музыка остановлена.", + "MESSAGE_SEEK_SKIP": "✅ Указанная позиция (`{{duration}}`) больше длительности трека\nПерехожу к следующему треку", + "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан", + "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку", + "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен", + "MESSAGE_STOP_SUCCESS": "✅ Воспроизведение остановлено, очередь очищена", "MESSAGE_VOLUME_ARGS_ERROR": "Текущая громкость: **{{volume}}** 🔊\n**Чтобы изменить громкость, введите число от `1` до `{{maxVolume}}`.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Введите число от `1` до `{{maxVolume}}` для изменения громкости.**", - "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\\n**Выберите желаемую громкость:**", - "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена.", + "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\n**Выберите желаемую громкость:**", + "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Громкость по умолчанию для этого сервера изменена на **{{volume}}%**! (Макс: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию.", - "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас у вас **{{current}}** треков в очереди (лимит: **{{limit}}**). Подождите, пока некоторые треки доиграют.", - "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в очереди **{{total}}** треков (глобальный лимит: **{{limit}}**). Подождите, пока некоторые треки доиграют.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен частично: добавлено **{{added}}** треков, пропущено **{{skipped}}** из-за лимита очереди (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен частично: добавлено **{{added}}** треков, пропущено **{{skipped}}** из-за глобального лимита (**{{total}}/{{limit}}**).", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию", + "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас в ней **{{current}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", + "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в ней **{{total}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за лимита очереди (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за глобального лимита (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "ID сервера", "MESSAGE_SERVER_GUILD": "Сервер", "MESSAGE_SERVER_MEMBERS": "Участники", @@ -197,76 +197,76 @@ "UNIT_MB": "МБ", "UNIT_GB": "ГБ", "UNIT_TB": "ТБ", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Менеджер чёрного списка не инициализирован.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Чёрный список ещё не загружен, попробуйте позже", "HELP_SELECT_MUSIC_PLACEHOLDER": "Выберите музыкальную команду", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите утилиту", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите служебную команду", "HELP_COMMAND_ALIASES": "Алиасы: {{aliases}}", "HELP_COMMAND_NONE": "Нет", "LABEL_FILTER_CLEAR": "Очистить фильтры", - "CONFIG_PLAYLIST_DESCRIPTION": "Управление плейлистами: сохранение, загрузка, удаление и импорт из M3U.", + "CONFIG_PLAYLIST_DESCRIPTION": "Управление плейлистами: сохранение, загрузка, удаление и импорт из M3U", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Сохранить текущую очередь в плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Загрузить и воспроизвести сохраненный плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Показать список всех плейлистов на сервере.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Показать список треков в плейлисте.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Удалить плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Сохранить текущую очередь в плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Загрузить и воспроизвести сохраненный плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Показать список всех плейлистов на сервере", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Показать список треков в плейлисте", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Удалить плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста", "CONFIG_PLAYLIST_OPTION_NAME": "Имя плейлиста", - "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (опционально)", + "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (необязательно)", "CONFIG_PLAYLIST_OPTION_URL": "Ссылка на скачивание файла M3U", "CONFIG_PLAYLIST_OPTION_INDEX": "Номер трека для удаления (начиная с 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Менеджер плейлистов не инициализирован.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Пожалуйста, укажите название плейлиста.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U. (Код ошибки: {{status}})", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Плейлисты пока недоступны, попробуйте позже", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Пожалуйста, укажите название плейлиста", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U (код ошибки {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Загрузка M3U заблокирована политикой безопасности. Используйте общедоступный HTTPS URL размером до {{maxSizeMiB}} МиБ, отвечающий в течение {{timeoutSeconds}} секунд.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Ошибка разбора или сохранения M3U плейлиста. Убедитесь, что формат правильный.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Не удалось удалить трек. Убедитесь, что это правильный номер.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Сохранено {{count}} треков из текущей очереди в плейлист `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен. (Добавлено: {{added}}, Пропущено: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Плейлист `{{name}}` сохранён — {{count}} треков из текущей очереди", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен: добавлено {{added}}, пропущено {{skipped}}", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов", "MESSAGE_PLAYLIST_LIST_TITLE": "Плейлисты сервера", "MESSAGE_PLAYLIST_INFO_TITLE": "Информация о плейлисте: {{name}} ({{count}} треков)", - "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и еще {{remaining}} треков.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` успешно удален.", - "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний плейлист M3U успешно импортирован!\nЗарегистрировано {{count}} треков в плейлисте `{{name}}`.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Удален трек №{{index}} из плейлиста `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста.", + "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и ещё {{remaining}} треков.*", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` удалён", + "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний M3U-плейлист импортирован!\nВ плейлист `{{name}}` добавлено {{count}} треков.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Трек №{{index}} удалён из плейлиста `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста", "CONFIG_RADIO_USAGE": "radio [название]", "CONFIG_RADIO_OPTION_PLAYLIST": "Название плейлиста для поиска", "CONFIG_RADIO_OPTION_CHANNEL": "Название канала или жанр для воспроизведения (например, workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Использование: `{{prefix}}radio [название плейлиста] [название канала]` (например, `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ В плейлисте `{{playlistName}}` не найден радиоканал, соответствующий `{{channelQuery}}`.\nДоступные примеры: {{samples}} и др.", - "ERROR_RADIO_NOT_READY": "❌ Плейлист радио не готов.\nСначала импортируйте плейлист радио с помощью `/playlist import {{name}} `.\n(Например: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Радио пока не настроено.\nСначала импортируйте радио-плейлист командой `/playlist import {{name}} `.\nФайл должен быть в формате M3U с тегами EXTINF.", "ERROR_RADIO_NO_MATCH": "❌ Радиоканал по запросу `{{query}}` не найден.\nДоступные варианты: {{samples}} и др.", "ERROR_RADIO_LOAD_FAILED": "❌ Ошибка загрузки радиоканала: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Поиск радиоканала **{{title}}** в плейлисте **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Поиск радиоканала **{{title}}** в плейлисте **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Начало воспроизведения радиоканала **{{title}}** из плейлиста **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Страница {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Только пользователь, вызвавший команду, может переключать страницы.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Только пользователь, вызвавший команду, может подтвердить это действие.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Только пользователь, вызвавший команду, может переключать страницы", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Только пользователь, вызвавший команду, может подтвердить это действие", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Плейлист уже существует", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "Плейлист с именем **`{{name}}`** (всего {{count}} треков) уже существует.\nХотите перезаписать его импортированным плейлистом?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "Плейлист с именем **`{{name}}`** (всего {{count}} треков) уже существует.\nХотите перезаписать его текущей очередью?", "LABEL_OVERWRITE": "Перезаписать", "LABEL_CANCEL": "Отмена", - "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменен.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", + "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменён", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U-плейлисты нельзя добавлять в очередь целиком. Используйте команду `/radio` для выбора радиостанции.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Импорт M3U-плейлистов доступен только администраторам и DJ." -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/embeds.json b/src/locales/ru-RU/embeds.json index 2052c20d..3ec3a2ee 100644 --- a/src/locales/ru-RU/embeds.json +++ b/src/locales/ru-RU/embeds.json @@ -4,22 +4,22 @@ "DASHBOARD_SUBTITLE": "Автор: **{{author}}**\nДлительность: **{{duration}}**\n────────────────────\nГромкость: `{{volume}}%`\nПовтор: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Заказал:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Ситуативный DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу.", - "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время некоторые функции окажутся временно недоступны.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу", + "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время часть функций будет недоступна.", "MAINTAIN_TITLE": "⚠️ Техническое обслуживание ⚠️", - "MESSAGE_BLACKLIST": "В голосовом канале есть пользователь(и) из чёрного списка.", + "MESSAGE_BLACKLIST": "В голосовом канале есть пользователи из чёрного списка", "MESSAGE_COMMAND": "Команда **{{command}}**", "MESSAGE_FILTER": "Установлен фильтр: **{{effectName}}**", "NODE_ALL_ACTIVE": "Все узлы активны", "NODE_CONNECTED": "Задержка: **{{ping}}мс**", "NODE_DISCONNECTED": "**ОТКЛЮЧЕН**", - "NODE_STATUS_ARGS_ERROR": "❌ Укажите корректное имя узла.", + "NODE_STATUS_ARGS_ERROR": "❌ Укажите корректное имя узла", "NODE_STATUS_CPU_TITLE": "⚙️ ЦП", "NODE_STATUS_CPU_VALUE": "Ядра: **{{cores}}**\nНагрузка системы: **{{systemLoad}}**\nНагрузка Lavalink: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ ИНФОРМАЦИЯ", "NODE_STATUS_INFO_VALUE": "Версия: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nВремя сборки: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 ПАМЯТЬ", - "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nРезервируемо: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nДоступно: **{{reservable}}**\n", "NODE_STATUS_PING": "Задержка", "NODE_STATUS_STATS_TITLE": "📊 СТАТИСТИКА", "NODE_STATUS_STATS_VALUE": "Время работы: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} мс**\nПлееров: **{{playerCount}}**\nВоспроизводящих: **{{playingCount}}**", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Сейчас играет:**", "QUEUE_EMPTY": "*Нет треков в очереди*", "QUEUE_HEADER": "**Очередь:**" -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/events.json b/src/locales/ru-RU/events.json index fe14dd73..c69a4dda 100644 --- a/src/locales/ru-RU/events.json +++ b/src/locales/ru-RU/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в указанный голосовой канал <#{{channelId}}> и вызовите меня.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу.", - "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится.", - "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора.", - "ERROR_REQUIRE_DJ": "❌ Для этой команды требуются права DJ.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Я не могу отправить вам личное сообщение.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash-команды не включены.", - "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Бот вышел из канала.", - "MESSAGE_BOT_STOP": "✅ Бот остановлен.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Очередь перемешана.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в голосовой канал <#{{channelId}}> и позовите меня", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу", + "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится", + "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора", + "ERROR_REQUIRE_DJ": "❌ Для этой команды требуются права DJ", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Я не могу отправить вам личное сообщение", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash-команды не включены", + "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Бот вышел из канала", + "MESSAGE_BOT_STOP": "✅ Бот остановлен", + "MESSAGE_MUSIC_SHUFFLE": "✅ Очередь перемешана", "MESSAGE_NOW_PLAYING_SUBTITLE": "Автор: **{{author}}**\nДлительность: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Сейчас играет: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Страница {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Выберите режим повтора трека.", + "MESSAGE_SELECT_LOOP_MODE": "Выберите режим повтора трека", "LOOP_MODE_OFF": "Выкл", "LOOP_MODE_SINGLE": "Один трек", "LOOP_MODE_ALL": "Все", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения.", - "MESSAGE_SET_LOOP_MODE": "✅ Установлен режим повтора: `{{mode}}`", - "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Вы не можете использовать команды в этом канале. Используйте текстовый канал <#{{channelId}}>" -} \ No newline at end of file + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения", + "MESSAGE_SET_LOOP_MODE": "✅ Выбран режим повтора: `{{mode}}`", + "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ В этом канале нельзя использовать команды. Пожалуйста, перейдите в текстовый канал <#{{channelId}}>." +} diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 2408dc25..a2c49cf4 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -1,46 +1,46 @@ { "CONFIG_BLACKLIST_DESCRIPTION": "Upravljaj crnom listom bota", - "CONFIG_BLACKLIST_USAGE": "blacklist [user]", + "CONFIG_BLACKLIST_USAGE": "blacklist [korisnik]", "CONFIG_BLACKLIST_OPTION_SUBCOMMAND": "Radnja koja će se izvršiti", "CONFIG_BLACKLIST_OPTION_ADD": "Dodaj korisnika na crnu listu", "CONFIG_BLACKLIST_OPTION_REMOVE": "Ukloni korisnika sa crne liste", "CONFIG_BLACKLIST_OPTION_LIST": "Prikaži sve korisnike na crnoj listi", "CONFIG_BLACKLIST_OPTION_USER": "Korisnik koji će biti dodat ili uklonjen sa crne liste", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na dinamičnoj crnoj listi.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dinamična crna lista", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Dinamična crna lista je prazna.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na crnoj listi", + "MESSAGE_BLACKLIST_LIST_TITLE": "Crna lista", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Crna lista je trenutno prazna", "CONFIG_CLEAR_DESCRIPTION": "Obriši svu muziku iz reda", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Premesti kontrolnu tablu na dno", "CONFIG_DASHBOARD_USAGE": "dashboard", "CONFIG_DJ_DESCRIPTION": "Dodaj korisnika kao DJ-a ili prikaži listu DJ-eva", - "CONFIG_DJ_USAGE": "dj [@user]", + "CONFIG_DJ_USAGE": "dj [@korisnik]", "CONFIG_DJ_OPTION_DESCRIPTION": "Korisnik koji će biti dodat kao DJ (opciono)", "CONFIG_FILTER_DESCRIPTION": "Podesi muzički filter", - "CONFIG_FILTER_USAGE": "filter [effect name]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "Izaberi režim muzičkog filtera koji želiš da podesiš", + "CONFIG_FILTER_USAGE": "filter [ime efekta]", + "CONFIG_FILTER_OPTION_DESCRIPTION": "Izaberi muzički filter koji želiš da podesiš", "CONFIG_HELP_DESCRIPTION": "Prikaži pomoć za komande", - "CONFIG_HELP_USAGE": "help [command]", - "CONFIG_HELP_OPTION_DESCRIPTION": "za koju komandu trebaš pomoć", + "CONFIG_HELP_USAGE": "help [komanda]", + "CONFIG_HELP_OPTION_DESCRIPTION": "Komanda za koju trebaš pomoć", "CONFIG_JOIN_DESCRIPTION": "Pridruži se trenutnom govornom kanalu", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Promeni jezik bota", "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Naziv jezika", + "CONFIG_LANG_OPTION_DESCRIPTION": "Izaberi jezik", "CONFIG_LEAVE_DESCRIPTION": "Napusti trenutni govorni kanal", "CONFIG_LEAVE_USAGE": "leave", "CONFIG_LOOP_DESCRIPTION": "Uključi ili isključi režim ponavljanja muzike", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Režim ponavljanja muzike", "CONFIG_MOVE_DESCRIPTION": "Zameni pozicije dve pesme u redu", - "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "Pozicija pesme 1 za zamenu", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Pozicija pesme 2 za zamenu", + "CONFIG_MOVE_USAGE": "move ", + "CONFIG_MOVE_OPTION_DESCRIPTION": "Pozicija pesme 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Pozicija pesme 2", "CONFIG_NODE_DESCRIPTION": "Prikaži status aktivnih nodova", - "CONFIG_NODE_USAGE": "node [node name]", + "CONFIG_NODE_USAGE": "node [ime noda]", "CONFIG_NODE_OPTION_DESCRIPTION": "Naziv noda", "CONFIG_NOW_PLAYING_DESCRIPTION": "Prikaži trenutno puštenu muziku", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", @@ -49,30 +49,30 @@ "CONFIG_PING_DESCRIPTION": "Prikaži ping servera", "CONFIG_PING_USAGE": "ping", "CONFIG_PLAY_DESCRIPTION": "Unesi link pesme ili naziv pesme za puštanje", - "CONFIG_PLAY_USAGE": "play ", + "CONFIG_PLAY_USAGE": "play ", "CONFIG_PLAY_OPTION_DESCRIPTION": "Link pesme ili naziv pesme", "CONFIG_PLAYFIRST_DESCRIPTION": "Unesi link pesme ili naziv pesme da bi se pustila prva", - "CONFIG_PLAYFIRST_USAGE": "playfirst ", + "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Link pesme ili naziv pesme koja će se pustiti prva", "CONFIG_PLAYLAST_DESCRIPTION": "Ponovo pusti trenutnu ili poslednju puštenu pesmu", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Ova komanda se može koristiti samo kada je red prazan.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Nije pronađena nedavno puštena pesma.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Pesma je ponovo dodata u red.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Ova komanda se može koristiti samo kada je red prazan", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Nije pronađena nedavno puštena pesma", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Pesma je ponovo dodata u red", "CONFIG_QUEUE_DESCRIPTION": "Prikaži trenutnu listu pesama", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Izaberi pesmu za uklanjanje iz liste pesama", - "CONFIG_REMOVE_USAGE": "remove [from index to index 2 track]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "broj indeksa pesme", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "od indeksa do indeksa 2 pesme", + "CONFIG_REMOVE_USAGE": "remove [od do]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Broj pesme", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Raspon pesama", "CONFIG_RESUME_DESCRIPTION": "Nastavi pauziranu pesmu", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "Unesi naziv pesme za pretragu", - "CONFIG_SEARCH_USAGE": "search ", + "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Naziv pesme", "CONFIG_SEEK_DESCRIPTION": "Premotaj pesmu na određeno vreme", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "ciljno vreme (npr: 3m20s, 1:20:55)", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (npr: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Ciljno vreme (npr: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Prikaži trenutno aktivne servere", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Izmešaj listu pesama", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [jačina]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Podrazumevana jačina zvuka (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Greška u komandi. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Trenutno nema muzike za puštanje u redu.", - "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ U redu trenutno nema pesama", + "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika", "ERROR_SKIP_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je preskoči. Administratori i DJ-evi (ako je omogućeno) takođe mogu da preskoče.", "ERROR_SEEK_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je premotava. Administratori i DJ-evi (ako je omogućeno) takođe mogu da premotavaju.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je pauzira. Administratori i DJ-evi (ako je omogućeno) takođe mogu da pauziraju.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Ne mogu da se pridružim govornom kanalu.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Ne mogu da se pridružim govornom kanalu", "ERROR_PLAY_MUSIC": "❌ Servis ima problema, pokušaj ponovo. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Nema rezultata. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", - "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana.", - "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve.", - "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve.", - "MESSAGE_DJ_MENTION_USER": "Molim te označi korisnika da bi ga dodao kao DJ-a.", - "MESSAGE_DJ_NO_BOTS": "Ne mogu dodati botove kao DJ-eve.", - "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu.", - "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ.", - "MESSAGE_DJ_SUCCESS": "Uspešno dodat <@{{userId}}> kao DJ.", + "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo", + "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana", + "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve", + "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve", + "MESSAGE_DJ_MENTION_USER": "Molim te označi korisnika da bi ga dodao kao DJ-a", + "MESSAGE_DJ_NO_BOTS": "Ne mogu dodati botove kao DJ-eve", + "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu", + "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> je dodat kao DJ", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ je napustio kanal! Automatski izabran novi DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Lista DJ-eva", "MESSAGE_DJ_LIST_ADMINS": "Administratori", - "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi bazirani na ulogama", + "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi po ulozi", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamični DJ-evi", - "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva.", + "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva", "MESSAGE_DJ_LIST_ERROR": "❌ Neuspešno učitavanje liste DJ-eva. Molim te pokušaj ponovo.", "MESSAGE_DJ_ROLE_NOT_SET": "Nije podešena DJ uloga", - "MESSAGE_FILTER_NOT_FOUND": "❌ Naziv efekta nije pronađen.", - "MESSAGE_FILTER_SELECT_LIST": "Izaberi režim muzičkog filtera koji želiš da podesiš. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Izaberi režim filtera", - "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekat nije pronađen", + "MESSAGE_FILTER_SELECT_LIST": "Izaberi muzički filter: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Izaberi efekat", + "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena", "MESSAGE_HELP_SELECT_LIST": "Izaberi komandu da bi dobio pomoć. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Izaberi pomoć", - "MESSAGE_JOIN_SUCCESS": "✅ Bot se pridružio.", - "MESSAGE_LANG_ARGS_ERROR": "Parametar jezika je pogrešan, trenutno dostupni jezici: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Trenutno dostupni jezici: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Uspešno promenjen jezik na `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot je napustio.", + "MESSAGE_HELP_SELECT_MODE": "Izaberi komandu", + "MESSAGE_JOIN_SUCCESS": "✅ Pridružio sam se glasovnom kanalu", + "MESSAGE_LANG_ARGS_ERROR": "Pogrešan jezički kod. Dostupni jezici: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Dostupni jezici: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Jezik promenjen na `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Napustio sam glasovni kanal", "REPEAT_MODE_OFF": "ISKLJUČENO", "REPEAT_MODE_SINGLE": "POJEDINAČNO", "REPEAT_MODE_ALL": "SVE", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Sve", "LOOP_SELECT_PLACEHOLDER": "Izaberi režim ponavljanja", "LOOP_SELECT_DESCRIPTION": "Postavi režim ponavljanja na {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Postavljeno ponavljanje na `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen.", + "MESSAGE_LOOP_MODE": "✅ Režim ponavljanja je sada `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen", "MESSAGE_MOVE_WRONG_INDEX": "❌ Molim te unesi važeći opseg indeksa. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\ni {{length}} drugih pesama", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Sačuvaj pesmu", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Trenutno svira: {{title}} | Zatražio {{requester}}", "UNKNOWN_USER": "Nepoznato", - "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Napiši naziv muzike koju želiš da pretražiš.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Muzika dodata.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja.", + "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Unesi naziv pesme ili link", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Pesma dodata u red", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja", "MESSAGE_PLAY_SELECT_DURATION": "Trajanje: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Izaberi muziku", - "MESSAGE_REMOVE_CANCEL": "✅ Otkazano uklanjanje.", - "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo.", - "MESSAGE_REMOVE_INSTRUCTION": "Izaberi pesmu od **1** do **{{length}}** za **uklanjanje** ili unesi bilo šta drugo za otkazivanje izbora. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Nema muzike u redu nakon trenutne.", - "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ Muzika je nastavljena.", - "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena.", + "MESSAGE_REMOVE_CANCEL": "✅ Uklanjanje otkazano", + "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo", + "MESSAGE_REMOVE_INSTRUCTION": "Unesi broj pesme od **1** do **{{length}}** za **uklanjanje**, ili bilo koji drugi broj za otkazivanje. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Red je prazan nakon trenutne pesme", + "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Muzika je nastavljena", + "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena", "MESSAGE_SEEK_ARGS_ERROR": "❌ Nevažeći format za ciljno vreme.\n(**`npr: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Pozicija premotavanja je izvan trajanja ove pesme. (`{{duration}}`)\nPreskakanje na sledeću pesmu.", - "MESSAGE_SEEK_SUCCESS": "✅ Muzika je premotana na `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Muzika izmešana.", - "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo.", - "MESSAGE_SKIP_SUCCESS": "✅ Muzika preskočena.", - "MESSAGE_STOP_SUCCESS": "✅ Muzika zaustavljena.", - "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Da bi promenio jačinu, unesi broj između `1` i `{{maxVolume}}`.**", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Unesi broj od `1` do `{{maxVolume}}` da bi promenio jačinu.**", - "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\\n**Molim te izaberi jačinu koju želiš da podesiš:**", - "MESSAGE_VOLUME_SAME": "❌ Jačina koju želiš da podesiš je već trenutna jačina.", + "MESSAGE_SEEK_SKIP": "✅ Navedena pozicija (`{{duration}}`) je duža od trajanja pesme.\nPrelazim na sledeću pesmu.", + "MESSAGE_SEEK_SUCCESS": "✅ Premotano na `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Lista pesama izmešana", + "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo", + "MESSAGE_SKIP_SUCCESS": "✅ Pesma preskočena", + "MESSAGE_STOP_SUCCESS": "✅ Reprodukcija zaustavljena, red obrisan", + "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", + "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\n**Molim te izaberi jačinu koju želiš da podesiš:**", + "MESSAGE_VOLUME_SAME": "❌ Ta jačina je već podešena", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Podrazumevana jačina zvuka je promenjena na **{{volume}}%** za ovaj server! (Maks: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nDa bi promenio, unesi broj između `1` i `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Unesi broj od `1` do `{{maxVolume}}` da bi promenio podrazumevanu jačinu.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nZa promenu unesi broj od `1` do `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Za promenu podrazumevane jačine unesi broj od `1` do `{{maxVolume}}`", "ERROR_QUEUE_LIMIT_REACHED": "❌ Dostigao si limit reda! Trenutno imaš **{{current}}** pesama u redu (limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Dostignut je globalni limit reda! Red trenutno ima **{{total}}** pesama (globalni limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog globalnog limita reda (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog globalnog limita reda (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "ID servera", "MESSAGE_SERVER_GUILD": "Server", "MESSAGE_SERVER_MEMBERS": "Članovi", @@ -192,81 +192,81 @@ "UNIT_HOURS": "č", "UNIT_MINUTES": "m", "UNIT_SECONDS": "s", - "UNIT_BYTES": "Bajta", + "UNIT_BYTES": "Bajt", "UNIT_KB": "KB", "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Blacklist manager nije inicijalizovan.", - "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi Muzičku komandu", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberi Utility komandu", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Crna lista još nije učitana. Pokušaj ponovo kasnije.", + "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi muzičku komandu", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberite servisnu komandu", "HELP_COMMAND_ALIASES": "Aliasi: {{aliases}}", "HELP_COMMAND_NONE": "Nijedan", "LABEL_FILTER_CLEAR": "Obriši filtere", - "CONFIG_PLAYLIST_DESCRIPTION": "Sačuvaj, učitaj, uredi plejliste ili ih uvezi iz M3U URL-a.", + "CONFIG_PLAYLIST_DESCRIPTION": "Sačuvaj, učitaj, uredi plejliste ili ih uvezi iz M3U URL-a", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Sačuvaj trenutni red za reprodukciju kao plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Učitaj i pusti sačuvanu plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Prikaži sve plejliste na serveru.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Prikaži pesme u plejlisti.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Obriši plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uvezi plejlistu sa udaljenog M3U fajla.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Ukloni pesmu iz plejliste.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Sačuvaj trenutni red za reprodukciju kao plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Učitaj i pusti sačuvanu plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Prikaži sve plejliste na serveru", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Prikaži pesme u plejlisti", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Obriši plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uvezi plejlistu sa udaljenog M3U fajla", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Ukloni pesmu iz plejliste", "CONFIG_PLAYLIST_OPTION_NAME": "Ime plejliste", "CONFIG_PLAYLIST_OPTION_SEARCH": "Naziv određene pesme za pretragu u plejlisti (Opciono)", "CONFIG_PLAYLIST_OPTION_URL": "Link za preuzimanje M3U fajla", "CONFIG_PLAYLIST_OPTION_INDEX": "Indeks pesme za uklanjanje (počinje od 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Menadžer plejlisti nije inicijalizovan.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Molimo unesite ispravnu potkomandu (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Nepoznata potkomanda.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Muzika se ne pušta ili je red prazan.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Molimo navedite ime plejliste.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Neuspešno čuvanje plejliste.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Plejlista `{{name}}` nije pronađena ili je prazna.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Pesma `{{searchQuery}}` nije pronađena u plejlisti `{{name}}`.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Menadžer plejlisti nije inicijalizovan", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Molimo unesite ispravnu potkomandu (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Nepoznata potkomanda", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Muzika se ne pušta ili je red prazan", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Molimo navedite ime plejliste", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Neuspešno čuvanje plejliste", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Plejlista `{{name}}` nije pronađena ili je prazna", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Pesma `{{searchQuery}}` nije pronađena u plejlisti `{{name}}`", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Preuzimanje M3U fajla nije uspelo. (HTTP status: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Preuzimanje M3U fajla je blokirano bezbednosnim pravilima. Koristite javni HTTPS URL do {{maxSizeMiB}} MiB koji odgovara u roku od {{timeoutSeconds}} sekundi.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Plejlista može sadržati najviše {{limit}} pesama.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Plejlista može sadržati najviše {{limit}} pesama", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Neuspešno parsiranje i čuvanje M3U plejliste. Proverite da li je ispravno formatirana.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Uklanjanje pesme nije uspelo. Proverite da li je indeks ispravan.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana. (Uspešno: {{added}}, Preskočeno: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana: dodato {{added}}, preskočeno {{skipped}}", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru", "MESSAGE_PLAYLIST_LIST_TITLE": "Plejliste na serveru", "MESSAGE_PLAYLIST_INFO_TITLE": "Informacije o plejlisti: {{name}} ({{count}} pesama)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*i još {{remaining}} pesama.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Plejlista `{{name}}` je obrisana.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Plejlista `{{name}}` je obrisana", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Eksterna M3U plejlista je uvezena!\nRegistrovano {{count}} pesama u plejlisti `{{name}}`.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Uklonjena pesma #{{index}} iz plejliste `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Pretraži i pusti određeni žanr/kanal sa uvezene radio plejliste.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Uklonjena pesma #{{index}} iz plejliste `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Pretraži i pusti određeni žanr/kanal sa uvezene radio plejliste", "CONFIG_RADIO_USAGE": "radio [kanal]", "CONFIG_RADIO_OPTION_PLAYLIST": "Naziv plejliste za pretragu", "CONFIG_RADIO_OPTION_CHANNEL": "Radio kanal ili žanr za puštanje (npr. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Upotreba: `{{prefix}}radio [naziv plejliste] [naziv kanala]` (npr. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Radio kanal koji odgovara `{{channelQuery}}` u plejlisti `{{playlistName}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", - "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\n(npr., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\nFajl mora biti u M3U formatu sa EXTINF tagovima.", "ERROR_RADIO_NO_MATCH": "❌ Radio kanal koji odgovara `{{query}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", "ERROR_RADIO_LOAD_FAILED": "❌ Greška pri učitavanju radio kanala: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Pretraživanje radio kanala **{{title}}** u plejlisti **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Pretraživanje radio kanala **{{title}}** u plejlisti **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Započeto puštanje radio kanala **{{title}}** sa plejliste **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Strana {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Samo korisnik koji je pokrenuo komandu može menjati stranice.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Samo korisnik koji je pokrenuo komandu može potvrditi ovu akciju.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Samo korisnik koji je pokrenuo komandu može menjati stranice", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Samo korisnik koji je pokrenuo komandu može potvrditi ovu akciju", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Plejlista već postoji", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "Plejlista sa imenom **`{{name}}`** (ukupno {{count}} pesama) već postoji.\nDa li želite da je zamenite uvezenom plejlistom?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "Plejlista pod nazivom **`{{name}}`** ({{count}} pesama) već postoji.\nDa li želite da je zamenite trenutnim redom pesama?", "LABEL_OVERWRITE": "Zameni", "LABEL_CANCEL": "Otkaži", - "MESSAGE_PLAYLIST_OVERWRITING": "Zamenjivanje plejliste...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Uvoz plejliste je otkazan.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", + "MESSAGE_PLAYLIST_OVERWRITING": "Zamenjivanje plejliste..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Uvoz plejliste je otkazan", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U plejliste se ne mogu staviti u red u celosti. Koristite komandu `/radio` za izbor stanice.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Uvoz M3U plejlisti je ograničen samo na admine i DJ-eve." -} \ No newline at end of file +} diff --git a/src/locales/sr-RS/embeds.json b/src/locales/sr-RS/embeds.json index 8f52cec4..15b7831c 100644 --- a/src/locales/sr-RS/embeds.json +++ b/src/locales/sr-RS/embeds.json @@ -4,25 +4,25 @@ "DASHBOARD_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{duration}}**\n────────────────────\nJačina zvuka: `{{volume}}%`\nPonavljanje: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Zatražio:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Uspešno povezan sa govornim kanalom.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Uspešno povezan sa govornim kanalom", "MAINTAIN_DESCRIPTION": "Bot će uskoro ući u održavanje. Tokom tog vremena, neke funkcije mogu biti privremeno nedostupne.", "MAINTAIN_TITLE": "⚠️ Obaveštenje o održavanju ⚠️", - "MESSAGE_BLACKLIST": "Postoje korisnici na crnoj listi u govornom kanalu.", + "MESSAGE_BLACKLIST": "Postoje korisnici na crnoj listi u govornom kanalu", "MESSAGE_COMMAND": "Komanda **{{command}}**", "MESSAGE_FILTER": "Režim filtera postavljen na **{{effectName}}**", "NODE_ALL_ACTIVE": "Svi nodovi su aktivni", "NODE_CONNECTED": "Kašnjenje: **{{ping}}ms**", "NODE_DISCONNECTED": "**ISKLJUČEN**", - "NODE_STATUS_ARGS_ERROR": "❌ Molim te unesi važeći naziv noda.", + "NODE_STATUS_ARGS_ERROR": "❌ Molim te unesi važeći naziv noda", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Jezgra: **{{cores}}**\nSistemsko opterećenje: **{{systemLoad}}**\nLavalink opterećenje: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ INFO", "NODE_STATUS_INFO_VALUE": "Verzija: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nVreme izgradnje: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 MEMORIJA", - "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nRezervisano: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nDostupno: **{{reservable}}**\n", "NODE_STATUS_PING": "Kašnjenje", "NODE_STATUS_STATS_TITLE": "📊 STATISTIKA", - "NODE_STATUS_STATS_VALUE": "vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "Vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ Status nodova", "NODE_STATUS_TITLE_2": "🛰️ Status noda '{{nodeName}}'", "NODE_UNHEALTHY": "⚠️ Ima {{unhealthValue}} isključenih nodova", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Trenutno svira:**", "QUEUE_EMPTY": "*Nema pesama u redu*", "QUEUE_HEADER": "**Red:**" -} \ No newline at end of file +} diff --git a/src/locales/sr-RS/events.json b/src/locales/sr-RS/events.json index 80e46384..d0b758f7 100644 --- a/src/locales/sr-RS/events.json +++ b/src/locales/sr-RS/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Ne mogu da dobijem podatke servera iz keša.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Nisi na istom govornom kanalu kao ja.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Molim te pridruži se određenom govornom kanalu <#{{channelId}}> i pozovi me.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Nisi povezan na govorni kanal.", - "ERROR_NOT_PLAYING": "❌ Trenutno se ne pušta muzika.", - "ERROR_REQUIRE_ADMIN": "❌ Ova komanda zahteva administratorske privilegije.", - "ERROR_REQUIRE_DJ": "❌ Ova komanda zahteva DJ privilegije.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Ne mogu da ti pošaljem privatnu poruku.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash komanda nije omogućena.", - "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal.", - "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Muzika izmešana.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Ne mogu da dobijem podatke servera iz keša", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Nisi na istom govornom kanalu kao ja", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Molim te pridruži se određenom govornom kanalu <#{{channelId}}> i pozovi me", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Nisi povezan na govorni kanal", + "ERROR_NOT_PLAYING": "❌ Trenutno se ne pušta muzika", + "ERROR_REQUIRE_ADMIN": "❌ Ova komanda zahteva administratorske privilegije", + "ERROR_REQUIRE_DJ": "❌ Ova komanda zahteva DJ privilegije", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Ne mogu da ti pošaljem privatnu poruku", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash komanda nije omogućena", + "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal", + "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen", + "MESSAGE_MUSIC_SHUFFLE": "✅ Red je izmešan", "MESSAGE_NOW_PLAYING_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Strana {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Izaberi režim ponavljanja pesme.", + "MESSAGE_SELECT_LOOP_MODE": "Izaberi režim ponavljanja pesme", "LOOP_MODE_OFF": "Isključeno", "LOOP_MODE_SINGLE": "Jedna", "LOOP_MODE_ALL": "Sve", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci.", - "MESSAGE_SET_LOOP_MODE": "✅ Ponavljanje postavljeno na `{{mode}}`", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci", + "MESSAGE_SET_LOOP_MODE": "✅ Izabran režim ponavljanja: `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Ne možeš izvršavati komande u ovom kanalu. Molim te koristi <#{{channelId}}> tekstualni kanal." -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 8de4a7e9..b3a98e7b 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Kullanıcıyı kara listeden çıkar", "CONFIG_BLACKLIST_OPTION_LIST": "Kara listedeki tüm kullanıcıları listele", "CONFIG_BLACKLIST_OPTION_USER": "Kara listeye eklenecek veya çıkarılacak kullanıcı", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> dinamik kara listede değil.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dinamik Kara Liste", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Dinamik kara liste boş.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> kara listede değil", + "MESSAGE_BLACKLIST_LIST_TITLE": "Kara Liste", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Kara liste şu anda boş", "CONFIG_CLEAR_DESCRIPTION": "Kuyruktaki tüm parçaları temizle", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Dashboard'u alta al", @@ -22,112 +22,112 @@ "CONFIG_FILTER_DESCRIPTION": "Müzik filtresini belirleme", "CONFIG_FILTER_USAGE": "filter [efekt adı]", "CONFIG_FILTER_OPTION_DESCRIPTION": "Belirlemek istediğiniz filtreyi seçin", - "CONFIG_HELP_DESCRIPTION": "Yardım komutunu kullanma", + "CONFIG_HELP_DESCRIPTION": "Komutlar hakkında yardım al", "CONFIG_HELP_USAGE": "help [komut]", - "CONFIG_HELP_OPTION_DESCRIPTION": "yardıma ihtiyaç duyduğunuz komut", + "CONFIG_HELP_OPTION_DESCRIPTION": "Hakkında yardım almak istediğiniz komut", "CONFIG_JOIN_DESCRIPTION": "Seçilen ses kanalına bağlan", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Bot'un dilini değiştir", "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Yerelleştirme kodu", + "CONFIG_LANG_OPTION_DESCRIPTION": "Bir dil seçin", "CONFIG_LEAVE_DESCRIPTION": "Ses kanalını terket", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "Döngü modunu açma", + "CONFIG_LOOP_DESCRIPTION": "Döngü modunu aç veya kapat", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Döngü modu", "CONFIG_MOVE_DESCRIPTION": "Kuyruktaki 2 parçanın sırasını değiştir", "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "1. sıraya alındı", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "2. sıraya alındı", - "CONFIG_NODE_DESCRIPTION": "Aktif bağlantı noktalarının adını göster", - "CONFIG_NODE_USAGE": "node [bağlantı noktası adı]", - "CONFIG_NODE_OPTION_DESCRIPTION": "Bağlantı noktası adı", + "CONFIG_MOVE_OPTION_DESCRIPTION": "1. parçanın konumu", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "2. parçanın konumu", + "CONFIG_NODE_DESCRIPTION": "Aktif düğümlerin durumunu göster", + "CONFIG_NODE_USAGE": "node [düğüm adı]", + "CONFIG_NODE_OPTION_DESCRIPTION": "Düğüm adı", "CONFIG_NOW_PLAYING_DESCRIPTION": "Şuan çalan parçayı göster", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", "CONFIG_PAUSE_DESCRIPTION": "Şuan çalan parçayı durdur", "CONFIG_PAUSE_USAGE": "pause", - "CONFIG_PING_DESCRIPTION": "Sunucu gecikmesi", + "CONFIG_PING_DESCRIPTION": "Sunucu gecikmesini öğren", "CONFIG_PING_USAGE": "ping", "CONFIG_PLAY_DESCRIPTION": "Çalınacak parçanın adını ya da linkini girin", "CONFIG_PLAY_USAGE": "play ", - "CONFIG_PLAY_OPTION_DESCRIPTION": "Parçanın linki ya da adını girin", + "CONFIG_PLAY_OPTION_DESCRIPTION": "Parçanın linki ya da adı", "CONFIG_PLAYFIRST_DESCRIPTION": "Kuyrukta başa eklenecek parçanın adını ya da linkini girin", "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Kuyrukta başa alınacak parçanın linki ya da adı", "CONFIG_PLAYLAST_DESCRIPTION": "Mevcut veya en son çalınan parçayı tekrar çal", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Bu komut yalnızca kuyruk boşken kullanılabilir.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Son çalınan parça bulunamadı.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Parça kuyruğa tekrar eklendi.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Bu komut yalnızca kuyruk boşken kullanılabilir", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Son çalınan parça bulunamadı", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Parça kuyruğa tekrar eklendi", "CONFIG_QUEUE_DESCRIPTION": "Şuanki oynatma listesini göster", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Oynatma kuyruğundan silmek için bir parça seçin", - "CONFIG_REMOVE_USAGE": "remove [baştan sona kadar 2 parça]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "parça sırası", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "baştan sona kadar 2 parça", + "CONFIG_REMOVE_USAGE": "remove [başlangıç bitiş]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Parça numarası", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Parça aralığı", "CONFIG_RESUME_DESCRIPTION": "Duraklatılan parçayı devam ettir", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "Aramak istediğiniz parçanın adını girin", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Parça adı", "CONFIG_SEEK_DESCRIPTION": "Parçada belirlenen bir zamana atlama", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (örn: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "hedeflenen zaman (örn: 3m20s, 1:20:55)", - "CONFIG_SERVER_DESCRIPTION": "Aktif olan sunucular", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (örn: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Hedef zaman (örn: 165, 3m20s, 1:20:55)", + "CONFIG_SERVER_DESCRIPTION": "Aktif sunucuları göster", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Oynatma listesini karıştır", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "Parçayı atlama", "CONFIG_SKIP_USAGE": "skip", - "CONFIG_STATUS_DESCRIPTION": "Bot'un durumunu öğrenme", + "CONFIG_STATUS_DESCRIPTION": "Bot'un durumunu göster", "CONFIG_STATUS_USAGE": "status", "CONFIG_STOP_DESCRIPTION": "Şu an çalan müziği ve kuyruğu temizle ancak kanaldan ayrılma", "CONFIG_STOP_USAGE": "stop", - "CONFIG_VOLUME_DESCRIPTION": "Bot'un ses sesviyesini belirleme", + "CONFIG_VOLUME_DESCRIPTION": "Bot'un ses seviyesini değiştir", "CONFIG_VOLUME_USAGE": "v <0-100>", "CONFIG_VOLUME_OPTION_DESCRIPTION": "Ses seviyesi", "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "Bu sunucu için varsayılan ses seviyesini değiştir", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [ses-seviyesi]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Belirlenecek varsayılan ses seviyesi (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Komut hatası. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta çalınacak başka bir parça bulunmuyor.", - "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta şu anda parça yok", + "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor", "ERROR_SKIP_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi atlayabilir. Yöneticiler ve DJ'ler (etkinse) de atlayabilir.", "ERROR_SEEK_NOT_REQUESTER": "❌ Bu şarkıda yalnızca isteyen kişi zaman atlayabilir. Yöneticiler ve DJ'ler (etkinse) de zaman atlayabilir.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi duraklatabilir. Yöneticiler ve DJ'ler (etkinse) de duraklatabilir.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Ses kanalına giriş yapamıyorum.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Ses kanalına giriş yapamıyorum", "ERROR_PLAY_MUSIC": "❌ Sistemimiz şuan sorun yaşıyor. Lütfen birazdan tekrar deneyiniz. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Herhangi bir sonuç bulunamadı. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Süre doldu.", - "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi.", - "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var.", - "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir.", - "MESSAGE_DJ_MENTION_USER": "DJ olarak eklemek için bir kullanıcıdan bahsedin.", - "MESSAGE_DJ_NO_BOTS": "Botlar DJ olarak eklenemez.", - "MESSAGE_DJ_STATIC_MODE": "DJ izinleri STATIC modda konfigürasyon üzerinden yönetilir.", - "MESSAGE_DJ_NO_PLAYER": "Aktif müzik çalar bulunamadı.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> zaten bir DJ.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}> başarıyla DJ olarak eklendi.", + "ERROR_TIME_EXPIRED": "❌ Süre doldu", + "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi", + "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var", + "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir", + "MESSAGE_DJ_MENTION_USER": "DJ olarak eklemek için bir kullanıcıdan bahsedin", + "MESSAGE_DJ_NO_BOTS": "Botlar DJ olarak eklenemez", + "MESSAGE_DJ_STATIC_MODE": "DJ izinleri STATIC modda konfigürasyon üzerinden yönetilir", + "MESSAGE_DJ_NO_PLAYER": "Aktif müzik çalar bulunamadı", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> zaten bir DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> başarıyla DJ olarak eklendi", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ kanaldan ayrıldı! Yeni DJ otomatik olarak seçildi: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ Listesi", "MESSAGE_DJ_LIST_ADMINS": "Yöneticiler", "MESSAGE_DJ_LIST_ROLE_DJS": "Rol tabanlı DJ'ler", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamik DJ'ler", - "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok.", + "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok", "MESSAGE_DJ_LIST_ERROR": "❌ DJ listesi yüklenemedi. Lütfen tekrar deneyin.", "MESSAGE_DJ_ROLE_NOT_SET": "DJ rolü yapılandırılmamış", - "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt adı bulunamadı.", - "MESSAGE_FILTER_SELECT_LIST": "Belirlemek istediğiniz filtreyi seçin. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Filtre modunu seçin", - "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt bulunamadı", + "MESSAGE_FILTER_SELECT_LIST": "Bir müzik filtresi seçin: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Bir efekt seçin", + "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı", "MESSAGE_HELP_SELECT_LIST": "Yardım almak için bir komut seçin. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Yardımı seçin", - "MESSAGE_JOIN_SUCCESS": "✅ Bot Katıldı.", - "MESSAGE_LANG_ARGS_ERROR": "Dil parametresi yanlış, şu anda mevcut diller: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Şu anda mevcut diller: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Dil başarıyla `{{locale}}` olarak değiştirildi", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot ayrıldı.", + "MESSAGE_HELP_SELECT_MODE": "Bir komut seçin", + "MESSAGE_JOIN_SUCCESS": "✅ Ses kanalına katıldım", + "MESSAGE_LANG_ARGS_ERROR": "Yanlış dil kodu. Mevcut diller: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Mevcut diller: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Dil `{{locale}}` olarak değiştirildi", + "MESSAGE_LEAVE_SUCCESS": "✅ Ses kanalından ayrıldım", "REPEAT_MODE_OFF": "KAPALI", "REPEAT_MODE_SINGLE": "TEK", "REPEAT_MODE_ALL": "HEPSİ", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Hepsi", "LOOP_SELECT_PLACEHOLDER": "Döngü modunu seçin", "LOOP_SELECT_DESCRIPTION": "Döngü modunu {{mode}} olarak ayarla", - "MESSAGE_LOOP_MODE": "✅ Döngü `{{mode}}` olarak ayarlandı.", - "MESSAGE_MOVE_SUCCESS": "✅ Şarkı sırası başarıyla değiştirildi.", + "MESSAGE_LOOP_MODE": "✅ Döngü `{{mode}}` olarak ayarlandı", + "MESSAGE_MOVE_SUCCESS": "✅ Şarkı sırası başarıyla değiştirildi", "MESSAGE_MOVE_WRONG_INDEX": "❌ Lütfen geçerli bir dizin aralığı girin. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nve {{length}} diğer şarkı", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Şarkıyı Kaydet", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Şu An Çalıyor: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Şu An Çalıyor: {{title}} | {{requester}} tarafından istendi", "UNKNOWN_USER": "Bilinmiyor", - "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Aramak istediğiniz müziğin adını yazın.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Müzik eklendi.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok.", + "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Parça adı veya linki girin", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Parça kuyruğa eklendi", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok", "MESSAGE_PLAY_SELECT_DURATION": "Süre: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Müziği seçin", - "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi.", - "MESSAGE_REMOVE_FAIL": "❌ Müzik kaldırılamadı.", - "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir şarkı seçerek **kaldırın** veya seçimi iptal etmek için başka bir şey girin. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcuttan sonra kuyrukta müzik yok.", - "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ Müzik devam ettirildi.", - "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi.", + "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi", + "MESSAGE_REMOVE_FAIL": "❌ Parça kaldırılamadı", + "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir parça numarası seçerek **kaldırın** veya iptal için başka bir numara girin. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcut parçadan sonra kuyruk boş", + "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Müzik devam ettirildi", + "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi", "MESSAGE_SEEK_ARGS_ERROR": "❌ Hedef zaman için geçersiz format.\n(**`örn: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Arama konumu bu parçanın süresinin ötesinde. (`{{duration}}`)\nSıradaki şarkıya geçiliyor.", - "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Müzik karıştırıldı.", - "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı.", - "MESSAGE_SKIP_SUCCESS": "✅ Müzik atlandı.", - "MESSAGE_STOP_SUCCESS": "✅ Müzik durduruldu.", + "MESSAGE_SEEK_SKIP": "✅ Belirtilen konum (`{{duration}}`), parçanın süresinden uzun.\nSıradaki parçaya geçiyorum.", + "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Çalma listesi karıştırıldı", + "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı", + "MESSAGE_SKIP_SUCCESS": "✅ Parça atlandı", + "MESSAGE_STOP_SUCCESS": "✅ Çalma durduruldu, kuyruk temizlendi", "MESSAGE_VOLUME_ARGS_ERROR": "Mevcut ses seviyesi: **{{volume}}** 🔊\n**Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", - "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", - "MESSAGE_VOLUME_SAME": "❌ Değiştirmek istediğiniz ses seviyesi zaten mevcut ses seviyesiyle aynı.", + "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", + "MESSAGE_VOLUME_SAME": "❌ Bu ses seviyesi zaten ayarlı", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Bu sunucu için varsayılan ses seviyesi **{{volume}}%** olarak değiştirildi! (En fazla: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Varsayılan ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Varsayılan ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin", "ERROR_QUEUE_LIMIT_REACHED": "❌ Kuyruk sınırınıza ulaştınız! Şu anda kuyrukta **{{current}}** şarkı var (sınır: **{{limit}}**). Daha fazla eklemek için lütfen bazı şarkıların bitmesini bekleyin.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Küresel kuyruk sınırına ulaşıldı! Kuyrukta şu anda **{{total}}** şarkı var (küresel sınır: **{{limit}}**). Daha fazla eklemek için lütfen bazı şarkıların bitmesini bekleyin.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, kuyruk sınırınız nedeniyle **{{skipped}}** şarkı atlandı (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, küresel sıra sınırı nedeniyle **{{skipped}}** şarkı atlandı (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, kuyruk sınırınız nedeniyle **{{skipped}}** şarkı atlandı (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, küresel sıra sınırı nedeniyle **{{skipped}}** şarkı atlandı (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "Sunucu Kimliği", "MESSAGE_SERVER_GUILD": "Sunucu", "MESSAGE_SERVER_MEMBERS": "Üyeler", @@ -197,76 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste yöneticisi başlatılmadı.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste henüz yüklenmedi, sonra tekrar deneyin", "HELP_SELECT_MUSIC_PLACEHOLDER": "Bir Müzik komutu seçin", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Yardımcı komut seçin", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Hizmet komutu seçin", "HELP_COMMAND_ALIASES": "Takma Adlar: {{aliases}}", "HELP_COMMAND_NONE": "Yok", "LABEL_FILTER_CLEAR": "Filtreleri temizle", - "CONFIG_PLAYLIST_DESCRIPTION": "Çalma listelerini kaydedin, yükleyin, düzenleyin veya M3U bağlantısından içe aktarın.", + "CONFIG_PLAYLIST_DESCRIPTION": "Çalma listelerini kaydedin, yükleyin, düzenleyin veya M3U bağlantısından içe aktarın", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Mevcut sırayı bir çalma listesi olarak kaydeder.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Kaydedilmiş çalma listesini yükler ve oynatır.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Sunucudaki tüm çalma listelerini listeler.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Çalma listesindeki parçaları gösterir.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Çalma listesini siler.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uzak M3U dosyasından çalma listesini içe aktarır.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Çalma listesinden bir parçayı kaldırır.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Mevcut sırayı bir çalma listesi olarak kaydeder", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Kaydedilmiş çalma listesini yükler ve oynatır", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Sunucudaki tüm çalma listelerini listeler", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Çalma listesindeki parçaları gösterir", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Çalma listesini siler", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uzak M3U dosyasından çalma listesini içe aktarır", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Çalma listesinden bir parçayı kaldırır", "CONFIG_PLAYLIST_OPTION_NAME": "Çalma listesi adı", "CONFIG_PLAYLIST_OPTION_SEARCH": "Çalma listesinde aranacak parça adı (İsteğe bağlı)", "CONFIG_PLAYLIST_OPTION_URL": "M3U dosyası indirme bağlantısı", "CONFIG_PLAYLIST_OPTION_INDEX": "Kaldırılacak parça sırası (1'den başlar)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Çalma listesi yöneticisi başlatılamadı.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Lütfen geçerli bir alt komut girin (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Bilinmeyen alt komut.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Şu anda hiçbir şey çalmıyor veya sıra boş.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Lütfen bir çalma listesi adı belirtin.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Çalma listesi kaydedilemedi.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ `{{name}}` adlı çalma listesi bulunamadı veya boş.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ `{{name}}` çalma listesinde `{{searchQuery}}` parçası bulunamadı.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Çalma listesi yöneticisi başlatılamadı", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Lütfen geçerli bir alt komut girin (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Bilinmeyen alt komut", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Şu anda hiçbir şey çalmıyor veya sıra boş", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Lütfen bir çalma listesi adı belirtin", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Çalma listesi kaydedilemedi", + "ERROR_PLAYLIST_NOT_FOUND": "❌ `{{name}}` adlı çalma listesi bulunamadı veya boş", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ `{{name}}` çalma listesinde `{{searchQuery}}` parçası bulunamadı", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ M3U dosyası indirilemedi. (HTTP durumu: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ M3U indirmesi güvenlik politikası tarafından engellendi. {{timeoutSeconds}} saniye içinde yanıt veren ve {{maxSizeMiB}} MiB boyutunu aşmayan herkese açık bir HTTPS URL kullanın.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Bir çalma listesi en fazla {{limit}} parça içerebilir.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Bir çalma listesi en fazla {{limit}} parça içerebilir", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ M3U çalma listesi ayrıştırılamadı ve kaydedilemedi. Formatın doğru olduğundan emin olun.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Parça kaldırılamadı. Geçerli bir indeks girdiğinizden emin olun.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi. (Başarılı: {{added}}, Atlanan: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi: {{added}} eklendi, {{skipped}} atlandı", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok", "MESSAGE_PLAYLIST_LIST_TITLE": "Sunucu Çalma Listeleri", "MESSAGE_PLAYLIST_INFO_TITLE": "Çalma Listesi Bilgisi: {{name}} ({{count}} parça)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*ve {{remaining}} parça daha.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ `{{name}}` çalma listesi silindi.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ `{{name}}` çalma listesi silindi", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Harici M3U çalma listesi içe aktarıldı!\n`{{name}}` çalma listesine {{count}} parça eklendi.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ `{{name}}` çalma listesinden #{{index}} parça kaldırıldı.", - "CONFIG_RADIO_DESCRIPTION": "İçe aktarılan radyo çalma listesinden belirli bir türü/kanalı arayın ve oynatın.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ `{{name}}` çalma listesinden #{{index}} parça kaldırıldı", + "CONFIG_RADIO_DESCRIPTION": "İçe aktarılan radyo çalma listesinden belirli bir türü/kanalı arayın ve oynatın", "CONFIG_RADIO_USAGE": "radio [kanal]", "CONFIG_RADIO_OPTION_PLAYLIST": "Aranacak çalma listesi adı", "CONFIG_RADIO_OPTION_CHANNEL": "Oynatılacak radyo kanalı veya türü (örn. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Kullanım: `{{prefix}}radio [çalma listesi adı] [kanal adı]` (örn. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ `{{playlistName}}` çalma listesinde `{{channelQuery}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", - "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\n(örn., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\nDosya, EXTINF etiketleri içeren bir M3U formatında olmalı.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", "ERROR_RADIO_LOAD_FAILED": "❌ Radyo kanalı yüklenirken hata oluştu: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 **{{playlist}}** çalma listesindeki **{{title}}** radyo kanalı aranıyor...", + "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 **{{playlist}}** çalma listesindeki **{{title}}** radyo kanalı aranıyor..", "MESSAGE_RADIO_PLAYING": "📻 **{{playlist}}** çalma listesinden **{{title}}** radyo kanalı oynatılmaya başlandı!", "MESSAGE_FOOTER_PAGE": "Sayfa {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Sadece komutu çalıştıran kullanıcı sayfaları değiştirebilir.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Sadece komutu çalıştıran kullanıcı bu işlemi onaylayabilir.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Sadece komutu çalıştıran kullanıcı sayfaları değiştirebilir", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Sadece komutu çalıştıran kullanıcı bu işlemi onaylayabilir", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Çalma Listesi Zaten Var", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "**`{{name}}`** adında bir çalma listesi (toplam {{count}} parça) zaten mevcut.\nİçe aktarılan çalma listesiyle değiştirmek istiyor musunuz?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "**`{{name}}`** isimli oynatma listesi (toplam {{count}} şarkı) zaten mevcut.\nMevcut sıradaki şarkılarla değiştirmek istiyor musunuz?", "LABEL_OVERWRITE": "Üzerine Yaz", "LABEL_CANCEL": "İptal", - "MESSAGE_PLAYLIST_OVERWRITING": "Çalma listesinin üzerine yazılıyor...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Çalma listesi içe aktarma iptal edildi.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", + "MESSAGE_PLAYLIST_OVERWRITING": "Çalma listesinin üzerine yazılıyor..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Çalma listesi içe aktarma iptal edildi", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U çalma listeleri tamamen sıraya eklenemez. Lütfen belirli bir istasyon seçmek için `/radio` komutunu kullanın.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U çalma listesi içe aktarma yalnızca Yöneticiler ve DJ'ler ile sınırlıdır." -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/embeds.json b/src/locales/tr-TR/embeds.json index 9a4dc7d3..06d2e007 100644 --- a/src/locales/tr-TR/embeds.json +++ b/src/locales/tr-TR/embeds.json @@ -1,41 +1,41 @@ { "DASHBOARD_TITLE": "Panel", - "DASHBOARD_FINISH_PLAYING": "Kuyruk sonlandı.", + "DASHBOARD_FINISH_PLAYING": "Kuyruğun çalınması tamamlandı", "DASHBOARD_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{duration}}**\n────────────────────\nSes Yüksekliği: `{{volume}}%`\nDöngü: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Talep Eden:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı", "MAINTAIN_DESCRIPTION": "Bot birkaç dakika içinde bakım moduna alınacaktır. Bu süre boyunca bazı özellikler geçici olarak kullanılamayabilir.", "MAINTAIN_TITLE": "⚠️ Bakım Notu ⚠️", - "MESSAGE_BLACKLIST": "Ses kanalında kara listeye alınmış kullanıcı(lar) bulunuyor.", + "MESSAGE_BLACKLIST": "Ses kanalında kara listedeki kullanıcılar var", "MESSAGE_COMMAND": "Komut **{{command}}**", - "MESSAGE_FILTER": "Filtre modu seçiliyor **{{effectName}}**", - "NODE_ALL_ACTIVE": "Tüm bağlantı noktaları etkin", + "MESSAGE_FILTER": "Filtre **{{effectName}}** olarak ayarlandı", + "NODE_ALL_ACTIVE": "Tüm düğümler etkin", "NODE_CONNECTED": "Gecikme: **{{ping}}ms**", "NODE_DISCONNECTED": "**BAĞLANTI KESİLDİ**", - "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir bağlantı noktası seçin.", + "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir düğüm adı girin", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Çekirdekler: **{{cores}}**\nSistem yükü: **{{systemLoad}}**\nLavalink yükü: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ Bilgi", - "NODE_STATUS_INFO_VALUE": "Versiyon: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nBuild time: **{{buildTime}}**", + "NODE_STATUS_INFO_VALUE": "Versiyon: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nDerleme süresi: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 Hafıza", - "NODE_STATUS_MEMORY_VALUE": "Kullanılan: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Kullanılan: **{{used}}**\nBoş: **{{free}}**\nAyrılan: **{{allocated}}**\nRezerve: **{{reservable}}**\n", "NODE_STATUS_PING": "Gecikme", "NODE_STATUS_STATS_TITLE": "📊 İstatistikler", - "NODE_STATUS_STATS_VALUE": "Bağlantı Süresi: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", - "NODE_STATUS_TITLE": "🛰️ Bağlantı Noktası", - "NODE_STATUS_TITLE_2": "🛰️ Node '{{nodeName}}' durumu", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}} tane bağlantı noktasının bağlantısı kesildi", + "NODE_STATUS_STATS_VALUE": "Bağlantı Süresi: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nOynatıcı: **{{playerCount}}**\nÇalan oynatıcılar: **{{playingCount}}**", + "NODE_STATUS_TITLE": "🛰️ Düğüm Durumu", + "NODE_STATUS_TITLE_2": "🛰️ Düğüm '{{nodeName}}' durumu", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}} düğümün bağlantısı kesildi", "PING_DESCRIPTION": "Bot: **{{botPing}}**\nAPI: **{{apiPing}}ms**", - "PING_TITLE": "🛰️ Gecikme'", + "PING_TITLE": "🛰️ Gecikme", "QUEUE_ADD_PLAYLIST": "Oynatma listesi eklendi", "QUEUE_ADD_TRACK": "Parça eklendi", "QUEUE_LIST_LOOP_MODE": "Döngü: {{repeatMode}}", - "QUEUE_LIST_TITLE": "Kuyruktaki Liste", + "QUEUE_LIST_TITLE": "Kuyruk Listesi", "REMOVE_LIST_LOOP_MODE": "Döngü: {{repeatMode}}", - "REMOVE_LIST_TITLE": "Liste kaldırıldı.", - "REMOVE_TRACK_TITLE": "Parça kaldırıldı.", - "SERVER_TITLE": "**{{name}}** bulunan sunucular", + "REMOVE_LIST_TITLE": "Kaldırma Listesi", + "REMOVE_TRACK_TITLE": "Kaldırılan Parça", + "SERVER_TITLE": "**{{name}}** olan sunucular", "SERVER_FIELD_DJ_ROLE": "DJ Rolü", "SERVER_FIELD_ADMINS": "Yöneticiler", "SERVER_FIELD_DJ_USERS": "DJ Kullanıcıları", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Şu An Çalıyor:**", "QUEUE_EMPTY": "*Kuyrukta şarkı yok*", "QUEUE_HEADER": "**Kuyruk:**" -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/events.json b/src/locales/tr-TR/events.json index ed921011..daf7f0c1 100644 --- a/src/locales/tr-TR/events.json +++ b/src/locales/tr-TR/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ ⚠ Sunucu verileri önbelleğe alınamadı!", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz.", - "ERROR_NOT_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", - "ERROR_REQUIRE_ADMIN": "❌ Bu komut yönetici izinleri gerektirmektedir.", - "ERROR_REQUIRE_DJ": "❌ Bu komut DJ izinleri gerektirmektedir.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Size özel mesaj gönderemiyorum.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash ile çalışan komutlar aktif değil.", - "ERROR_TIME_EXPIRED": "❌ Süre doldu.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot ayrıldı.", - "MESSAGE_BOT_STOP": "✅ Bot durduruldu.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Kuyruk karıştırıldı.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Sunucu verileri önbellekten alınamadı", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz", + "ERROR_NOT_PLAYING": "❌ Şuan çalan bir parça bulunmuyor", + "ERROR_REQUIRE_ADMIN": "❌ Bu komut yönetici izinleri gerektirmektedir", + "ERROR_REQUIRE_DJ": "❌ Bu komut DJ izinleri gerektirmektedir", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Size özel mesaj gönderemiyorum", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash ile çalışan komutlar aktif değil", + "ERROR_TIME_EXPIRED": "❌ Süre doldu", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot ayrıldı", + "MESSAGE_BOT_STOP": "✅ Bot durduruldu", + "MESSAGE_MUSIC_SHUFFLE": "✅ Kuyruk karıştırıldı", "MESSAGE_NOW_PLAYING_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Şuan Çalıyor: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Sayfa {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz.", - "LOOP_MODE_OFF": "Off", - "LOOP_MODE_SINGLE": "Single", - "LOOP_MODE_ALL": "All", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim.", - "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}`", + "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz", + "LOOP_MODE_OFF": "Kapalı", + "LOOP_MODE_SINGLE": "Tek", + "LOOP_MODE_ALL": "Tümü", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim", + "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}` olarak ayarlandı", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Bu kanalda komut kullanamazsınız! Lütfen <#{{channelId}}> metin kanalını kullanın." -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index a7cc0cd6..7f5f2e2c 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -9,12 +9,12 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> 已被加入黑名單。", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> 已從黑名單中移除。", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> 已在黑名單中。", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> 不在動態黑名單中。", - "MESSAGE_BLACKLIST_LIST_TITLE": "動態黑名單", - "MESSAGE_BLACKLIST_LIST_EMPTY": "動態黑名單為空。", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> 不在黑名單中。", + "MESSAGE_BLACKLIST_LIST_TITLE": "黑名單", + "MESSAGE_BLACKLIST_LIST_EMPTY": "黑名單目前是空的。", "CONFIG_CLEAR_DESCRIPTION": "清除隊列中的所有音樂", "CONFIG_CLEAR_USAGE": "clear", - "CONFIG_DASHBOARD_DESCRIPTION": "將儀表板更新到底部", + "CONFIG_DASHBOARD_DESCRIPTION": "將儀表板移至底部", "CONFIG_DASHBOARD_USAGE": "dashboard", "CONFIG_DJ_DESCRIPTION": "將使用者添加為 DJ 或顯示 DJ 列表", "CONFIG_DJ_USAGE": "dj [@使用者]", @@ -37,8 +37,8 @@ "CONFIG_LOOP_OPTION_DESCRIPTION": "音樂循環模式", "CONFIG_MOVE_DESCRIPTION": "交換隊列中兩首歌曲的位置", "CONFIG_MOVE_USAGE": "move <位置1> <位置2>", - "CONFIG_MOVE_OPTION_DESCRIPTION": "交換歌曲位置1", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "交換歌曲位置2", + "CONFIG_MOVE_OPTION_DESCRIPTION": "歌曲位置 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "歌曲位置 2", "CONFIG_NODE_DESCRIPTION": "顯示節點的運作狀態", "CONFIG_NODE_USAGE": "node [節點名稱]", "CONFIG_NODE_OPTION_DESCRIPTION": "節點名稱", @@ -62,9 +62,9 @@ "CONFIG_QUEUE_DESCRIPTION": "顯示當前播放列表", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "從播放列表中選擇要移除的歌曲", - "CONFIG_REMOVE_USAGE": "remove <曲目索引號> [從索引到索引2的曲目]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "曲目索引號", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "從索引到索引2的曲目", + "CONFIG_REMOVE_USAGE": "remove <曲目編號> [起始到結束]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "曲目編號", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "曲目範圍", "CONFIG_RESUME_DESCRIPTION": "恢復播放已暫停的曲目", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "輸入歌曲名稱以搜尋", @@ -75,7 +75,7 @@ "CONFIG_SEEK_OPTION_DESCRIPTION": "目標時間 (例如: 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "顯示當前活躍的伺服器", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "隨機播放列表", + "CONFIG_SHUFFLE_DESCRIPTION": "隨機打亂播放清單", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "跳過當前曲目", "CONFIG_SKIP_USAGE": "skip", @@ -98,7 +98,7 @@ "ERROR_PLAY_JOIN_CHANNEL": "❌ 我無法加入語音頻道。", "ERROR_PLAY_MUSIC": "❌ 服務遇到一些問題,請稍後再試。({{reason}})", "ERROR_PLAY_SEARCH": "❌ 找不到結果。({{reason}})", - "ERROR_TIME_EXPIRED": "❌ 時間已過期。", + "ERROR_TIME_EXPIRED": "❌ 等待時間已逾時。", "MESSAGE_CLEAR_SUCCESS": "✅ 播放清單已清空。", "MESSAGE_DASHBOARD_SUCCESS": "✅ 控制面板已更新。", "MESSAGE_DJ_NO_PERMISSION": "您需要 DJ 或管理員權限才能添加 DJ。", @@ -156,20 +156,20 @@ "MESSAGE_REMOVE_CANCEL": "✅ 已取消移除。", "MESSAGE_REMOVE_FAIL": "❌ 音樂移除失敗。", "MESSAGE_REMOVE_INSTRUCTION": "從 **1** 到 **{{length}}** 中選擇一首歌曲來 **移除**,或輸入其他內容以取消選擇。 ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 當前播放之後沒有音樂。", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 目前曲目之後的佇列是空的。", "MESSAGE_RESUME_FAIL": "❌ 音樂恢復失敗。", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ 音樂已恢復播放。", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 音樂已恢復播放。", "MESSAGE_RESUME_SUCCESS": "▶️ 音樂已恢復播放。", "MESSAGE_SEEK_ARGS_ERROR": "❌ 目標時間格式無效。\n(**`例如: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ 搜尋位置超出了此曲目的時長。(`{{duration}}`)\n正在跳到下一首歌曲。", + "MESSAGE_SEEK_SKIP": "✅ 指定的位置 (`{{duration}}`) 超出此曲目的時長。\n正在跳到下一首歌曲。", "MESSAGE_SEEK_SUCCESS": "✅ 音樂已跳轉至 `{{duration}}`。", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 音樂已隨機播放。", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 播放清單已打亂。", "MESSAGE_SKIP_FAIL": "❌ 音樂跳過失敗。", "MESSAGE_SKIP_SUCCESS": "✅ 音樂已跳過。", - "MESSAGE_STOP_SUCCESS": "✅ 音樂已停止。", + "MESSAGE_STOP_SUCCESS": "✅ 已停止播放並清空佇列。", "MESSAGE_VOLUME_ARGS_ERROR": "當前音量:**{{volume}}** 🔊\n**要更改音量,請輸入 `1` 到 `{{maxVolume}}` 之間的數字。**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **輸入 `1` 到 `{{maxVolume}}` 之間的數字以更改音量。**", - "MESSAGE_VOLUME_SELECT": "當前音量:**{{volume}}%** 🔊\\n**請選擇您要設置的音量:**", + "MESSAGE_VOLUME_SELECT": "當前音量:**{{volume}}%** 🔊\n**請選擇您要設置的音量:**", "MESSAGE_VOLUME_SAME": "❌ 您要更改的音量已經是當前音量。", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 此伺服器的預設音量已更改為 **{{volume}}%**! (最大值: {{maxVolume}}%)", @@ -177,7 +177,7 @@ "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 輸入 `1` 到 `{{maxVolume}}` 之間的數字以更改預設音量。", "ERROR_QUEUE_LIMIT_REACHED": "❌ 您已達到隊列限制!您目前在隊列中有 **{{current}}** 首歌曲(限制:**{{limit}}**)。請等待一些歌曲播放完畢後再添加。", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ 已達到全域隊列限制!隊列目前有 **{{total}}** 首歌曲(全域限制:**{{limit}}**)。請等待一些歌曲播放完畢後再添加。", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於您的隊列限制跳過 **{{skipped}}** 首歌曲 (**{{current}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於您的隊列限制跳過 **{{skipped}}** 首歌曲 (**{{current}}/{{limit}}**)", "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於全域隊列限制,跳過 **{{skipped}}** 首歌曲 (**{{total}}/{{limit}}**)。", "MESSAGE_SERVER_GUILD_ID": "伺服器 ID", "MESSAGE_SERVER_GUILD": "伺服器", @@ -192,14 +192,14 @@ "UNIT_HOURS": "小時", "UNIT_MINUTES": "分", "UNIT_SECONDS": "秒", - "UNIT_BYTES": "Bytes", + "UNIT_BYTES": "位元組", "UNIT_KB": "KB", "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", "ERROR_BLACKLIST_NOT_INITIALIZED": "黑名單管理器尚未初始化。", "HELP_SELECT_MUSIC_PLACEHOLDER": "選擇音樂指令", - "HELP_SELECT_UTILITY_PLACEHOLDER": "選擇工具指令", + "HELP_SELECT_UTILITY_PLACEHOLDER": "選擇服務指令", "HELP_COMMAND_ALIASES": "別名: {{aliases}}", "HELP_COMMAND_NONE": "無", "LABEL_FILTER_CLEAR": "清除效果", @@ -230,7 +230,7 @@ "ERROR_PLAYLIST_IMPORT_FAILED": "❌ 解析並儲存 M3U 播放清單失敗。請確認格式是否正確。", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 刪除歌曲失敗。請確認編號是否正確。", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 已將目前佇列中的 {{count}} 首歌曲儲存至播放清單 `{{name}}`。", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 正在從播放清單 `{{name}}` 搜尋並新增 {{count}} 首歌曲...", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 正在從播放清單 `{{name}}` 搜尋並新增 {{count}} 首歌曲..", "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 播放清單 `{{name}}` 載入完成。 (成功: {{added}}首, 略過/失敗: {{skipped}}首)", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 目前伺服器沒有已儲存的播放清單。", "MESSAGE_PLAYLIST_LIST_TITLE": "伺服器播放清單列表", @@ -246,11 +246,11 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ 正確用法: `{{prefix}}radio [播放清單名稱] [電台名稱]` (例如: `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ 請同時輸入播放清單名稱與電台頻道名稱。", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 在播放清單 `{{playlistName}}` 中找不到與 `{{channelQuery}}` 符合的電台頻道。\n可用範例: {{samples}} 等", - "ERROR_RADIO_NOT_READY": "❌ 電台播放清單尚未就緒。\n請先使用 `/playlist import {{name}} ` 指令匯入電台網址清單。\n(例如: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ 電台播放清單尚未就緒。\n請先使用 `/playlist import {{name}} ` 指令匯入電台網址清單。\n檔案必須是包含 EXTINF 標籤的 M3U 格式。", "ERROR_RADIO_NO_MATCH": "❌ 找不到與 `{{query}}` 符合的電台頻道。\n可用範例: {{samples}} 等", "ERROR_RADIO_LOAD_FAILED": "❌ 載入電台頻道時發生錯誤: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ 無法載入或找不到電台串流 (`{{title}}`)。", - "MESSAGE_RADIO_SEARCHING": "🔄 正在搜尋播放清單 **{{playlist}}** 中的電台頻道 **{{title}}**...", + "MESSAGE_RADIO_SEARCHING": "🔄 正在搜尋播放清單 **{{playlist}}** 中的電台頻道 **{{title}}**..", "MESSAGE_RADIO_PLAYING": "📻 已開始播放來自播放清單 **{{playlist}}** 的電台頻道 **{{title}}**!", "MESSAGE_FOOTER_PAGE": "第 {{page}} / {{totalPages}} 頁", "ERROR_ONLY_COMMAND_USER_PAGINATE": "只有執行該指令的使用者才能切換頁面。", @@ -260,7 +260,7 @@ "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "名為 **`{{name}}`** 的播放清單(共 {{count}} 首歌曲)已經存在。\n您是否要使用當前佇列覆蓋它?", "LABEL_OVERWRITE": "覆蓋", "LABEL_CANCEL": "取消", - "MESSAGE_PLAYLIST_OVERWRITING": "正在覆蓋播放清單...", + "MESSAGE_PLAYLIST_OVERWRITING": "正在覆蓋播放清單..", "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 已取消匯入播放清單。", "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", @@ -269,4 +269,4 @@ "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ 無法將 M3U 播放清單整本加入佇列。請使用 `/radio` 指令選擇特定電台頻道。", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ 匯入 M3U 播放清單僅限管理員及 DJ 權限使用者使用。" -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/embeds.json b/src/locales/zh-TW/embeds.json index 60f295d2..74dc8489 100644 --- a/src/locales/zh-TW/embeds.json +++ b/src/locales/zh-TW/embeds.json @@ -1,6 +1,6 @@ { "DASHBOARD_TITLE": "控制面板", - "DASHBOARD_FINISH_PLAYING": "音樂播放已結束", + "DASHBOARD_FINISH_PLAYING": "播放清單播放已結束", "DASHBOARD_SUBTITLE": "作者: **{{author}}**\n時長: **{{duration}}**\n────────────────────\n音量: `{{volume}}%`\n循環模式: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**請求者:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", @@ -9,7 +9,7 @@ "MAINTAIN_TITLE": "⚠️ 維護通知 ⚠️", "MESSAGE_BLACKLIST": "語音頻道中有被列入黑名單的用戶。", "MESSAGE_COMMAND": "指令 **{{command}}**", - "MESSAGE_FILTER": "將效果模式設置為 **{{effectName}}**", + "MESSAGE_FILTER": "效果已設置為:**{{effectName}}**", "NODE_ALL_ACTIVE": "所有節點均已啟用", "NODE_CONNECTED": "延遲: **{{ping}}ms**", "NODE_DISCONNECTED": "**已斷線**", @@ -22,7 +22,7 @@ "NODE_STATUS_MEMORY_VALUE": "已使用: **{{used}}**\n可用: **{{free}}**\n分配: **{{allocated}}**\n可保留: **{{reservable}}**\n", "NODE_STATUS_PING": "延遲", "NODE_STATUS_STATS_TITLE": "📊 統計", - "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n正在播放的播放器: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n播放中: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ 節點狀態", "NODE_STATUS_TITLE_2": "🛰️ 節點 '{{nodeName}}' 狀態", "NODE_UNHEALTHY": "⚠️ 有 {{unhealthValue}} 個節點已斷線", @@ -42,11 +42,11 @@ "STATUS_DESCRIPTION": "**• 服務於 {{serverCount}} 個伺服器**\n**• 正在 {{playingCount}} 個伺服器上播放**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "STATUS_LATENCY_TITLE": "🛰️ 延遲", "STATUS_LATENCY_VALUE": "機器人: **{{botPing}}**\nAPI: **{{apiPing}} 毫秒**", - "STATUS_SYSTEM_TITLE": "⚙️ SYSTEM", + "STATUS_SYSTEM_TITLE": "⚙️ 系統", "STATUS_SYSTEM_VALUE": "作業系統: **{{os_version}}**\nNode.js: **{{node_version}}**\nDiscord.js: **{{dc_version}}**\nLavaShark: **{{shark_version}}**\nCPU: **{{cpu}}**\n運行時間: **{{uptime}}**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "STATUS_USAGE_TITLE": "📊 資源使用", "STATUS_USAGE_VALUE": "CPU: **{{cpuUsage}}**\nRam: **{{ramUsage}}**\nHeap: **{{heapUsage}}**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "QUEUE_NOW_PLAYING": "**正在播放:**", "QUEUE_EMPTY": "*播放清單中沒有歌曲*", "QUEUE_HEADER": "**播放清單:**" -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/events.json b/src/locales/zh-TW/events.json index 79a6ca21..a9179a00 100644 --- a/src/locales/zh-TW/events.json +++ b/src/locales/zh-TW/events.json @@ -11,15 +11,15 @@ "ERROR_TIME_EXPIRED": "❌ 時間已過期。", "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 機器人已離開頻道。", "MESSAGE_BOT_STOP": "✅ 機器人已停止。", - "MESSAGE_MUSIC_SHUFFLE": "✅ 音樂已隨機播放。", + "MESSAGE_MUSIC_SHUFFLE": "✅ 播放清單已打亂。", "MESSAGE_NOW_PLAYING_SUBTITLE": "作者: **{{author}}**\n時長: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "正在播放: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 第 {{curPage}}/{{maxPage}} 頁 -----", "MESSAGE_SELECT_LOOP_MODE": "選擇歌曲循環模式。", - "LOOP_MODE_OFF": "Off", - "LOOP_MODE_SINGLE": "Single", - "LOOP_MODE_ALL": "All", + "LOOP_MODE_OFF": "關閉", + "LOOP_MODE_SINGLE": "單曲", + "LOOP_MODE_ALL": "全部", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ 我已將歌曲名稱發送至您的私人訊息中。", - "MESSAGE_SET_LOOP_MODE": "✅ 循環模式設置為 `{{mode}}`", - "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。\n" -} \ No newline at end of file + "MESSAGE_SET_LOOP_MODE": "✅ 已選擇循環模式:`{{mode}}`", + "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。" +} diff --git a/src/utils/languageUtils.ts b/src/utils/languageUtils.ts index 4caaf54c..174849a4 100644 --- a/src/utils/languageUtils.ts +++ b/src/utils/languageUtils.ts @@ -2,7 +2,7 @@ const LANGUAGE_NAMES: Record = { 'en-US': 'English (en-US)', 'ko-KR': '한국어 (ko-KR)', 'ru-RU': 'Русский (ru-RU)', - 'sr-RS': 'Српски (sr-RS)', + 'sr-RS': 'Srpski (sr-RS)', 'tr-TR': 'Türkçe (tr-TR)', 'zh-TW': '繁體中文 (zh-TW)', 'ar': 'العربية (ar)', From 181e0b3477063952b427f2c3510771be8391cf0f Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:12:16 +0300 Subject: [PATCH 39/49] chore: refactor and dependencies - generateRandomKey relocation, dependency bumps Co-authored-by: hoehoetvhss --- package-lock.json | 157 +++++++++++------------ package.json | 10 +- src/lib/PlaylistManager.ts | 4 +- src/lib/api/routes/AuthRouter.ts | 8 +- src/lib/hashGenerator.ts | 30 ----- src/utils/functions/generateRandomKey.ts | 5 + 6 files changed, 94 insertions(+), 120 deletions(-) delete mode 100644 src/lib/hashGenerator.ts create mode 100644 src/utils/functions/generateRandomKey.ts diff --git a/package-lock.json b/package-lock.json index 410b848c..93ad6fee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "bcryptjs": "^3.0.3", - "better-sqlite3": "^13.0.2", + "better-sqlite3": "^13.0.3", "cookie": "^2.0.1", "discord.js": "~14.27.0", "dotenv": "^17.4.2", @@ -25,12 +25,12 @@ "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.6", - "@types/node": "^26.1.2", - "eslint": "^10.8.0", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", "globals": "^17.9.0", - "tsx": "^4.23.5", + "tsx": "^4.23.12", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0" + "typescript-eslint": "^8.67.0" }, "engines": { "node": ">=22.23.2" @@ -941,9 +941,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -994,17 +994,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1017,7 +1017,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1033,16 +1033,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1058,14 +1058,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1080,14 +1080,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1098,9 +1098,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -1115,15 +1115,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1140,9 +1140,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -1154,16 +1154,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1182,16 +1182,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1206,13 +1206,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1306,10 +1306,9 @@ } }, "node_modules/better-sqlite3": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.2.tgz", - "integrity": "sha512-jW6oufeDhXZaiX9Lw5A+oerVClx4iFrI6uDj1zu7SqUAjak9vbJvA0NEcKLNxHiQHb6kYCoFzzXYV0YOauhV3g==", - "hasInstallScript": true, + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", "license": "MIT", "dependencies": { "node-addon-api": "^8.0.0" @@ -1676,9 +1675,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -2939,9 +2938,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz", - "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3016,16 +3015,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", - "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.66.0", - "@typescript-eslint/parser": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0" + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index cf627ef7..d78a2829 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "bcryptjs": "^3.0.3", - "better-sqlite3": "^13.0.2", + "better-sqlite3": "^13.0.3", "cookie": "^2.0.1", "discord.js": "~14.27.0", "dotenv": "^17.4.2", @@ -48,12 +48,12 @@ "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.6", - "@types/node": "^26.1.2", - "eslint": "^10.8.0", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", "globals": "^17.9.0", - "tsx": "^4.23.5", + "tsx": "^4.23.12", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0" + "typescript-eslint": "^8.67.0" }, "engines": { "node": ">=22.23.2" diff --git a/src/lib/PlaylistManager.ts b/src/lib/PlaylistManager.ts index 6cc4b3a4..f8e59541 100644 --- a/src/lib/PlaylistManager.ts +++ b/src/lib/PlaylistManager.ts @@ -66,7 +66,7 @@ export class PlaylistManager { const playlistRow = db.prepare(` SELECT id, guild_id as guildId, name, created_at as createdAt, is_m3u as isM3u FROM playlists - WHERE guild_id = ? AND name = ? + WHERE guild_id = ? AND name = ? COLLATE NOCASE `).get(guildId, name) as any; if (!playlistRow) return null; @@ -156,7 +156,7 @@ export class PlaylistManager { `); const replacePlaylist = db.transaction(() => { - db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ?').run(guildId, name); + db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ? COLLATE NOCASE').run(guildId, name); const playlistInfo = db.prepare(` INSERT INTO playlists (guild_id, name, created_at, is_m3u) diff --git a/src/lib/api/routes/AuthRouter.ts b/src/lib/api/routes/AuthRouter.ts index c56667b0..77f5e343 100644 --- a/src/lib/api/routes/AuthRouter.ts +++ b/src/lib/api/routes/AuthRouter.ts @@ -5,7 +5,7 @@ import { parseCookie } from 'cookie'; import undici from 'undici'; import { LoginTypeEnum } from '../../../@types/index.js'; -import { hashGenerator } from '../../hashGenerator.js'; +import { generateRandomKey } from '../../../utils/functions/generateRandomKey.js'; import { loginSchema } from '../validators/schemas.js'; import { noContent, ok, problem } from '../http.js'; import { validateBody } from '../validators/validate.js'; @@ -126,7 +126,7 @@ export class AuthRouter extends BaseRouter { if (username === siteConfig.user.username && passwordMatch) { this.sessionManager.unblockIP(userIP); - const sessionId = hashGenerator.generateRandomKey(); + const sessionId = generateRandomKey(); this.sessionManager.createSession(sessionId); res.cookie('sessionId', sessionId, SESSION_COOKIE_OPTIONS); @@ -245,7 +245,7 @@ export class AuthRouter extends BaseRouter { }); } - const state = hashGenerator.generateRandomKey(); + const state = generateRandomKey(); this.#oauthStates.set(state, { expiresAt: Date.now() + OAUTH2_STATE_TTL_MS, requesterIp, @@ -344,7 +344,7 @@ export class AuthRouter extends BaseRouter { if (this.bot.config.bot.admin.includes(user.id)) { this.sessionManager.unblockIP(userIP); - const sessionId = hashGenerator.generateRandomKey(); + const sessionId = generateRandomKey(); this.sessionManager.createSession(sessionId); res.cookie('sessionId', sessionId, SESSION_COOKIE_OPTIONS); diff --git a/src/lib/hashGenerator.ts b/src/lib/hashGenerator.ts deleted file mode 100644 index 1385744d..00000000 --- a/src/lib/hashGenerator.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createHash, randomBytes } from 'crypto'; - - -const hashGenerator = { - /** - * Generates a random 32-byte key in hexadecimal format. - * @returns {string} - A randomly generated key. - */ - generateRandomKey: (): string => { - return randomBytes(32).toString('hex'); - }, - - /** - * Generates a hash using the SHA-512 algorithm - * and a provided password along with a pre-generated salt. - * @param {string} password - The password to be hashed. - * @returns {string} - The hashed password. - */ - generateHash: (password: string): string => { - return createHash('sha512') - .update(password + salt, 'utf-8') - .digest('hex'); - } -}; - -export { hashGenerator }; - - -// pre-generated salt -const salt = hashGenerator.generateRandomKey(); diff --git a/src/utils/functions/generateRandomKey.ts b/src/utils/functions/generateRandomKey.ts new file mode 100644 index 00000000..12b577da --- /dev/null +++ b/src/utils/functions/generateRandomKey.ts @@ -0,0 +1,5 @@ +import { randomBytes } from 'crypto'; + +export const generateRandomKey = (): string => { + return randomBytes(32).toString('hex'); +}; From 5a507bdbf3f2c809b291bbc18c343428ce55ff46 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:55:28 +0300 Subject: [PATCH 40/49] fix: clear single-track repeat before starting radio playback Co-authored-by: hoehoetvhss --- src/commands/RadioCommand.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 071b6240..6976d262 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -1,5 +1,6 @@ import i18next from 'i18next'; import { ApplicationCommandOptionType } from 'discord.js'; +import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum } from '../@types/index.js'; @@ -180,6 +181,10 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; (track as any).isRadio = true; if (isAlreadyPlaying) { + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + await player.skip(); } else { player.filters.setVolume(curVolume); From d9235fffd32a319db632853398111a84fbfae629 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 02:55:48 +0300 Subject: [PATCH 41/49] fix: resolve radio tracks strictly from saved playlist data Co-authored-by: hoehoetvhss --- src/commands/RadioCommand.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 6976d262..8eb80191 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -5,6 +5,7 @@ import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum } from '../@types/index.js'; import { DJManager } from '../lib/DjManager.js'; +import { decodeTrackWithRetry } from '../utils/functions/lavasharkRequest.js'; import type { Client, GuildMember } from 'discord.js'; import type { CommandContext } from './base/CommandContext.js'; @@ -156,21 +157,24 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; await context.replySuccess(bot, context.t('commands:MESSAGE_RADIO_SEARCHING', { playlist: playlistName, title: targetTrack.title })); try { - let searchResult = null; - if (targetTrack.url) { + let radioTrack = null; + if (targetTrack.encoded) { try { - searchResult = await client.lavashark.search(targetTrack.url); + radioTrack = await decodeTrackWithRetry(client.lavashark, targetTrack.encoded); } catch (_) {} } - if (!searchResult || !searchResult.tracks || searchResult.tracks.length === 0) { + if (!radioTrack && targetTrack.url) { try { - searchResult = await client.lavashark.search(`ytsearch:${targetTrack.title}`); + const result = await client.lavashark.search(targetTrack.url); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + radioTrack = result.tracks[0]; + } } catch (_) {} } - if (searchResult && searchResult.tracks && searchResult.tracks.length > 0) { - const track = searchResult.tracks[0]; + if (radioTrack) { + const track = radioTrack; (track as any).requester = requester; const isAlreadyPlaying = player.playing; From 33bd821649963d7a581abdadec39a64a329fdccb Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 03:32:46 +0300 Subject: [PATCH 42/49] fix: stop radio session when playing a saved playlist Co-authored-by: hoehoetvhss --- .../playlist/PlayPlaylistSubcommand.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 15b673d1..241b0ac9 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -12,6 +12,7 @@ import { import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; import type { GuildMember, VoiceBasedChannel } from 'discord.js'; +import { RepeatMode } from 'lavashark'; import type { Player } from 'lavashark'; import type { Playlist, PlaylistTrack } from '../../lib/PlaylistManager.js'; import type { @@ -83,6 +84,13 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const player = await this.initializePlayer(context, voiceChannel); if (!player) return; + // A radio session is replaced by the playlist: drop its queued tracks + // so the playlist content plays next instead of the radio station + const isRadioPlaying = Boolean(player.current && (player.current as any).isRadio); + if (isRadioPlaying) { + player.queue.tracks = []; + } + const tracks = playlist.tracks ?? []; // Reuse one progress message for the final load result const progressMessage = await context.command.reply({ @@ -96,7 +104,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const result = await this.loadTracks(context, player, playlist, member); if (result.added > 0) { - await this.startPlayback(context, player); + await this.startPlayback(context, player, isRadioPlaying); if (context.bot.config.queuePersistence.enabled && context.client.queuePersistence) { await context.client.queuePersistence.saveQueue(player); @@ -372,6 +380,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { private async startPlayback( context: PlaylistSubcommandContext, player: Player, + stopRadio = false, ): Promise { if (!player.playing) { const volume = player.setting.volume ?? @@ -406,6 +415,15 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { return; } + // Stop the active radio and let the playlist take over: any repeat + // would otherwise keep the radio track in the queue or replay it + if (stopRadio) { + if (player.repeatMode !== RepeatMode.OFF) { + player.setRepeatMode(RepeatMode.OFF); + } + await player.skip(); + } + if (player.current) { await context.client.dashboard.update(player, player.current); } From 9294cf9d53b23b9e4d627964bab92a4ebf62aac6 Mon Sep 17 00:00:00 2001 From: Dum4G Date: Sat, 15 Aug 2026 03:32:47 +0300 Subject: [PATCH 43/49] fix: serialize dashboard lifecycle and always edit in place Co-authored-by: hoehoetvhss --- src/events/lavashark/PlayerDestroyEvent.ts | 6 ++- src/lib/DashboardManager.ts | 62 +++++++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/events/lavashark/PlayerDestroyEvent.ts b/src/events/lavashark/PlayerDestroyEvent.ts index 9c2e4186..de54e8f5 100644 --- a/src/events/lavashark/PlayerDestroyEvent.ts +++ b/src/events/lavashark/PlayerDestroyEvent.ts @@ -38,9 +38,11 @@ export class PlayerDestroyEvent extends BaseLavaSharkEvent<'playerDestroy'> { await setVoiceChannelStatus(bot, client, player.voiceChannelId, null); } - // Clean up dashboard message + // Clean up dashboard message: the session is over and nothing will + // update it anymore, so delete it instead of leaving a stale message if (player.dashboardMsg) { - await client.dashboard.destroy(player); + await player.dashboardMsg.delete().catch(() => {}); + player.dashboardMsg = null; } } } diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index de16159f..e7b1b57c 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -25,11 +25,23 @@ export class DashboardManager { } /** - * Initialize dashboard in a channel + * Initialize dashboard in a channel. + * Serialized with updates and destroys so no two dashboard messages + * can ever be created for the same player at the same time. */ public async initialize( target: ChatInputCommandInteraction | Message | any, player: Player + ): Promise { + this.#updatePromise = this.#updatePromise + .then(() => this.#performInitialize(target, player)) + .catch(() => {}); + return this.#updatePromise; + } + + async #performInitialize( + target: ChatInputCommandInteraction | Message | any, + player: Player ): Promise { let channel; @@ -55,6 +67,19 @@ export class DashboardManager { const lng = this.#bot.guildLanguageManager?.get(player.guildId); + // Remove stale dashboards from earlier sessions (e.g. after a bot + // restart the in-memory reference is gone) so only the new one stays + const dashboardTitle = this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }); + try { + const messages = await (channel as any).messages.fetch({ limit: 30 }); + for (const message of messages.values()) { + if (message.author?.id === this.#client.user?.id && + message.embeds.some((embed: any) => embed.title === dashboardTitle)) { + await message.delete().catch(() => {}); + } + } + } catch (_) {} + player.dashboardMsg = await (channel as any).send({ embeds: [embeds.connected(this.#bot, lng)], components: [] @@ -111,10 +136,28 @@ export class DashboardManager { } } catch (_) {} + // The dashboard is no longer the last message: move it to the + // bottom. A replacement is only sent after the old one was + // actually deleted, otherwise the message is refreshed in place + // and no duplicate can ever appear. + let deleted = false; try { await player.dashboardMsg.delete(); + deleted = true; } catch (_) {} - player.dashboardMsg = null; + + if (!deleted) { + try { + await player.dashboardMsg.edit(embedPayload); + return; + } catch (_) { + // Message is gone: drop the reference and send a new one + player.dashboardMsg = null; + } + } + else { + player.dashboardMsg = null; + } } try { @@ -125,9 +168,19 @@ export class DashboardManager { } /** - * Destroy dashboard and show disconnect message + * Destroy dashboard and show disconnect message. + * The message reference is kept so the next initialize() deletes it and + * the next update() reuses it instead of leaving a stale duplicate. + * Serialized with updates so a queued update can never race the destroy. */ public async destroy(player: Player): Promise { + this.#updatePromise = this.#updatePromise + .then(() => this.#performDestroy(player)) + .catch(() => {}); + return this.#updatePromise; + } + + async #performDestroy(player: Player): Promise { if (!player.dashboardMsg) { return; } @@ -142,9 +195,6 @@ export class DashboardManager { } catch (error) { this.#bot.logger.error( this.#bot.shardId, 'Dashboard destroy error: ' + error); } - finally { - player.dashboardMsg = null; - } } /** From 99d16203a08cc82cac331f836119fc56076ac13c Mon Sep 17 00:00:00 2001 From: hoehoetvhss Date: Sat, 15 Aug 2026 22:10:33 +0900 Subject: [PATCH 44/49] fix: resolve playfirst player connection, query parsing, and dashboard update issues --- src/commands/PlayCommand.ts | 53 ++++++---- src/commands/PlayFirstCommand.ts | 128 +++++++++++++++--------- src/commands/SearchCommand.ts | 49 +++++---- src/utils/functions/cleanSearchQuery.ts | 15 +++ 4 files changed, 164 insertions(+), 81 deletions(-) create mode 100644 src/utils/functions/cleanSearchQuery.ts diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index 71221da5..2ef5e103 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -1,9 +1,11 @@ import i18next from 'i18next'; +import { ConnectionState } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType } from '../@types/index.js'; import { embeds } from '../embeds/index.js'; import { isUserInBlacklist } from '../utils/functions/isUserInBlacklist.js'; +import { cleanSearchQuery } from '../utils/functions/cleanSearchQuery.js'; import { DJManager } from '../lib/DjManager.js'; import { QueueLimitManager } from '../lib/QueueLimitManager.js'; @@ -37,17 +39,19 @@ export class PlayCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { // Get search query - const str = context.isMessage() + const rawStr = context.isMessage() ? context.args.join(' ') : context.getStringOption('play'); + const str = cleanSearchQuery(rawStr || ''); + if (!str) { await context.replyEphemeralError(bot, context.t('commands:MESSAGE_PLAY_ARGS_ERROR')); return; } - const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str.trim()); - const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str.trim()); + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str); if (isM3uUrl || existingPlaylist?.isM3u) { await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); return; @@ -117,20 +121,28 @@ export class PlayCommand extends BaseCommand { } /** - * Create and initialize player + * Create and initialize player (or reuse existing player) * @private */ async #createPlayer(bot: Bot, client: Client, context: CommandContext): Promise { + const guildId = String(context.guild?.id); const voiceChannelId = context.isMessage() ? String(context.getMessage().member?.voice.channelId) : String(context.getInteraction().guild!.members.cache.get(context.user.id)?.voice.channelId); - const player = client.lavashark.createPlayer({ - guildId: String(context.guild?.id), - voiceChannelId: voiceChannelId, - textChannelId: context.channel!.id, - selfDeaf: true - }); + let player = client.lavashark.getPlayer(guildId); + + if (!player) { + player = client.lavashark.createPlayer({ + guildId: guildId, + voiceChannelId: voiceChannelId, + textChannelId: context.channel!.id, + selfDeaf: true + }); + } else { + player.voiceChannelId = voiceChannelId; + player.textChannelId = context.channel!.id; + } if (!player.setting) { player.setting = { @@ -141,15 +153,17 @@ export class PlayCommand extends BaseCommand { } const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - - try { - await player.connect(); - player.metadata = metadata; - } catch (error) { - bot.logger.error( bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - await player.destroy(); - return null; + player.metadata = metadata; + + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + } catch (error) { + bot.logger.error( bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return null; + } } try { @@ -266,3 +280,4 @@ export class PlayCommand extends BaseCommand { } } } + diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 0781f6b2..2d49e826 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -1,10 +1,11 @@ import i18next from 'i18next'; -import { RepeatMode } from 'lavashark'; +import { ConnectionState, RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType } from '../@types/index.js'; import { embeds } from '../embeds/index.js'; import { isUserInBlacklist } from '../utils/functions/isUserInBlacklist.js'; +import { cleanSearchQuery } from '../utils/functions/cleanSearchQuery.js'; import { DJManager } from '../lib/DjManager.js'; import { QueueLimitManager } from '../lib/QueueLimitManager.js'; @@ -38,17 +39,19 @@ export class PlayFirstCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { // Get search query - const str = context.isMessage() + const rawStr = context.isMessage() ? context.args.join(' ') : context.getStringOption('playfirst'); + const str = cleanSearchQuery(rawStr || ''); + if (!str) { await context.replyEphemeralError(bot, context.t('commands:MESSAGE_PLAY_ARGS_ERROR')); return; } - const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str.trim()); - const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str.trim()); + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str); if (isM3uUrl || existingPlaylist?.isM3u) { await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); return; @@ -60,7 +63,7 @@ export class PlayFirstCommand extends BaseCommand { res = await client.lavashark.search(str); } catch (error) { console.error(error); - bot.logger.error( bot.shardId, `Search Error: ${error}`); + bot.logger.error(bot.shardId, `Search Error: ${error}`); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_SEARCH', { reason: error instanceof Error ? error.message : String(error) })); @@ -69,7 +72,7 @@ export class PlayFirstCommand extends BaseCommand { // Handle search results if (res.loadType === LoadType.ERROR) { - bot.logger.error( bot.shardId, `Search Error: ${JSON.stringify(res)}`); + bot.logger.error(bot.shardId, `Search Error: ${JSON.stringify(res)}`); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_SEARCH', { reason: (res as any).data?.message })); @@ -117,20 +120,28 @@ export class PlayFirstCommand extends BaseCommand { } /** - * Create and initialize player + * Create and initialize player (or reuse existing player) * @private */ async #createPlayer(bot: Bot, client: Client, context: CommandContext): Promise { + const guildId = String(context.guild?.id); const voiceChannelId = context.isMessage() ? String(context.getMessage().member?.voice.channelId) : String(context.getInteraction().guild!.members.cache.get(context.user.id)?.voice.channelId); - const player = client.lavashark.createPlayer({ - guildId: String(context.guild?.id), - voiceChannelId: voiceChannelId, - textChannelId: context.channel!.id, - selfDeaf: true - }); + let player = client.lavashark.getPlayer(guildId); + + if (!player) { + player = client.lavashark.createPlayer({ + guildId: guildId, + voiceChannelId: voiceChannelId, + textChannelId: context.channel!.id, + selfDeaf: true + }); + } else { + player.voiceChannelId = voiceChannelId; + player.textChannelId = context.channel!.id; + } if (!player.setting) { player.setting = { @@ -140,17 +151,18 @@ export class PlayFirstCommand extends BaseCommand { }; } - const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - - try { - await player.connect(); - player.metadata = metadata; - } catch (error) { - bot.logger.error( bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - await player.destroy(); - return null; + player.metadata = metadata; + + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + } catch (error) { + bot.logger.error(bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return null; + } } try { @@ -257,33 +269,59 @@ export class PlayFirstCommand extends BaseCommand { (t as any).requester = requester; } - const firstTrack = tracks[0]; - const restTracks = tracks.slice(1); - - if (restTracks.length > 0) { - player.queue.tracks.unshift(...restTracks); - } - - player.filters.setVolume(curVolume); - await player.prioritizePlay(firstTrack, requester as any) - .catch(async (error) => { - bot.logger.error(bot.shardId, 'Error playing track: ' + error); - await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); - return player.destroy(); - }); + if (!player.playing) { + player.addTracks(tracks, requester as any); + player.filters.setVolume(curVolume); + await player.play() + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + } else { + const firstTrack = tracks[0]; + const restTracks = tracks.slice(1); + + if (restTracks.length > 0) { + player.queue.tracks.unshift(...restTracks); + } - if (player.current && client.dashboard) { - await client.dashboard.update(player, player.current); + player.filters.setVolume(curVolume); + await player.prioritizePlay(firstTrack, requester as any) + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); } } else { const track = res.tracks[0]; - await player.prioritizePlay(track, requester as any) - .catch(async (error) => { - bot.logger.error(bot.shardId, 'Error playing track: ' + error); - await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); - return player.destroy(); - }); + (track as any).requester = requester; + + if (!player.playing) { + player.addTracks(track, requester as any); + player.filters.setVolume(curVolume); + await player.play() + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + } else { + player.filters.setVolume(curVolume); + await player.prioritizePlay(track, requester as any) + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + } + } + + if (player.current && client.dashboard) { + await client.dashboard.update(player, player.current); } } } + diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index 5cd484a4..cc33009e 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -6,11 +6,13 @@ import { StringSelectMenuInteraction, } from 'discord.js'; import i18next from 'i18next'; +import { ConnectionState } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType, SelectButtonId } from '../@types/index.js'; import { embeds } from '../embeds/index.js'; import { isUserInBlacklist } from '../utils/functions/isUserInBlacklist.js'; +import { cleanSearchQuery } from '../utils/functions/cleanSearchQuery.js'; import { DJManager } from '../lib/DjManager.js'; import { QueueLimitManager } from '../lib/QueueLimitManager.js'; @@ -44,10 +46,12 @@ export class SearchCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { // Get search query - const str = context.isMessage() + const rawStr = context.isMessage() ? context.args.join(' ') : context.getStringOption('search'); + const str = cleanSearchQuery(rawStr || ''); + if (!str) { await context.replyEphemeralError(bot, context.t('commands:MESSAGE_PLAY_ARGS_ERROR')); return; @@ -119,20 +123,28 @@ export class SearchCommand extends BaseCommand { } /** - * Create and initialize player + * Create and initialize player (or reuse existing player) * @private */ async #createPlayer(bot: Bot, client: Client, context: CommandContext): Promise { + const guildId = String(context.guild?.id); const voiceChannelId = context.isMessage() ? String(context.getMessage().member?.voice.channelId) : String(context.getInteraction().guild!.members.cache.get(context.user.id)?.voice.channelId); - const player = client.lavashark.createPlayer({ - guildId: String(context.guild?.id), - voiceChannelId: voiceChannelId, - textChannelId: context.channel!.id, - selfDeaf: true - }); + let player = client.lavashark.getPlayer(guildId); + + if (!player) { + player = client.lavashark.createPlayer({ + guildId: guildId, + voiceChannelId: voiceChannelId, + textChannelId: context.channel!.id, + selfDeaf: true + }); + } else { + player.voiceChannelId = voiceChannelId; + player.textChannelId = context.channel!.id; + } if (!player.setting) { player.setting = { @@ -143,15 +155,17 @@ export class SearchCommand extends BaseCommand { } const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - - try { - await player.connect(); - player.metadata = metadata; - } catch (error) { - bot.logger.error( bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - await player.destroy(); - return null; + player.metadata = metadata; + + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + } catch (error) { + bot.logger.error( bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return null; + } } try { @@ -358,3 +372,4 @@ export class SearchCommand extends BaseCommand { }); } } + diff --git a/src/utils/functions/cleanSearchQuery.ts b/src/utils/functions/cleanSearchQuery.ts new file mode 100644 index 00000000..16376b3b --- /dev/null +++ b/src/utils/functions/cleanSearchQuery.ts @@ -0,0 +1,15 @@ +/** + * Clean and normalize search query for Lavashark / Lavalink + * Handles Discord link wrappers like ``, trims whitespace, + * and extracts clean URLs or search terms. + */ +export function cleanSearchQuery(query: string): string { + if (!query) return ''; + + let cleaned = query.trim(); + + // Remove Discord link wrapper brackets: -> https://... + cleaned = cleaned.replace(/^<(.+)>$/, '$1').trim(); + + return cleaned; +} From 0fa1fc50e60a59989a539ec8eb23ceacb4cf70e8 Mon Sep 17 00:00:00 2001 From: hoehoetvhss Date: Sat, 15 Aug 2026 22:18:29 +0900 Subject: [PATCH 45/49] fix: enforce connection state check and align with pr-126-clean updates --- src/commands/RadioCommand.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 071b6240..5313b2e0 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -1,5 +1,6 @@ import i18next from 'i18next'; import { ApplicationCommandOptionType } from 'discord.js'; +import { ConnectionState } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum } from '../@types/index.js'; @@ -122,14 +123,18 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - try { - await player.connect(); + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + player.metadata = metadata; + } catch (error) { + bot.logger.error(bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return; + } + } else { player.metadata = metadata; - } catch (error) { - bot.logger.error(bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - await player.destroy(); - return; } try { From 5d2333ef181b5589b4ec9e6cdaba8282cf1f653d Mon Sep 17 00:00:00 2001 From: hoehoetvhss Date: Sat, 15 Aug 2026 22:34:21 +0900 Subject: [PATCH 46/49] feat: integrate pr-126-clean dashboard lifecycle, radio playback fixes, queue restore retry, and i18n polish --- src/commands/RadioCommand.ts | 32 ++- .../playlist/PlayPlaylistSubcommand.ts | 20 +- src/events/lavashark/PlayerDestroyEvent.ts | 6 +- src/lib/DashboardManager.ts | 62 ++++- src/lib/QueuePersistence.ts | 193 +++++++++---- src/locales/en-US/commands.json | 180 ++++++------ src/locales/en-US/embeds.json | 6 +- src/locales/en-US/events.json | 30 +- src/locales/ko-KR/commands.json | 258 +++++++++--------- src/locales/ko-KR/embeds.json | 16 +- src/locales/ko-KR/events.json | 34 +-- src/locales/ru-RU/commands.json | 180 ++++++------ src/locales/ru-RU/embeds.json | 6 +- src/locales/ru-RU/events.json | 30 +- src/locales/sr-RS/commands.json | 176 ++++++------ src/locales/sr-RS/embeds.json | 6 +- src/locales/sr-RS/events.json | 32 +-- src/locales/tr-TR/commands.json | 178 ++++++------ src/locales/tr-TR/embeds.json | 10 +- src/locales/tr-TR/events.json | 32 +-- src/locales/zh-TW/commands.json | 12 +- src/locales/zh-TW/embeds.json | 2 +- src/locales/zh-TW/events.json | 2 +- src/utils/functions/lavasharkRequest.ts | 79 ++++++ 24 files changed, 914 insertions(+), 668 deletions(-) create mode 100644 src/utils/functions/lavasharkRequest.ts diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index 5313b2e0..a0a61864 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -1,10 +1,11 @@ import i18next from 'i18next'; import { ApplicationCommandOptionType } from 'discord.js'; -import { ConnectionState } from 'lavashark'; +import { ConnectionState, RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum } from '../@types/index.js'; import { DJManager } from '../lib/DjManager.js'; +import { decodeTrackWithRetry } from '../utils/functions/lavasharkRequest.js'; import type { Client, GuildMember } from 'discord.js'; import type { CommandContext } from './base/CommandContext.js'; @@ -160,21 +161,33 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; await context.replySuccess(bot, context.t('commands:MESSAGE_RADIO_SEARCHING', { playlist: playlistName, title: targetTrack.title })); try { - let searchResult = null; - if (targetTrack.url) { + let radioTrack = null; + if (targetTrack.encoded) { try { - searchResult = await client.lavashark.search(targetTrack.url); + radioTrack = await decodeTrackWithRetry(client.lavashark, targetTrack.encoded); } catch (_) {} } - if (!searchResult || !searchResult.tracks || searchResult.tracks.length === 0) { + if (!radioTrack && targetTrack.url) { try { - searchResult = await client.lavashark.search(`ytsearch:${targetTrack.title}`); + const result = await client.lavashark.search(targetTrack.url); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + radioTrack = result.tracks[0]; + } } catch (_) {} } - if (searchResult && searchResult.tracks && searchResult.tracks.length > 0) { - const track = searchResult.tracks[0]; + if (!radioTrack && targetTrack.title) { + try { + const result = await client.lavashark.search(`ytsearch:${targetTrack.title}`); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + radioTrack = result.tracks[0]; + } + } catch (_) {} + } + + if (radioTrack) { + const track = radioTrack; (track as any).requester = requester; const isAlreadyPlaying = player.playing; @@ -185,6 +198,9 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; (track as any).isRadio = true; if (isAlreadyPlaying) { + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } await player.skip(); } else { player.filters.setVolume(curVolume); diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 14d395e0..9efeec86 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -6,6 +6,7 @@ import { isUserInBlacklist } from '../../utils/functions/isUserInBlacklist.js'; import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; import type { GuildMember, VoiceBasedChannel } from 'discord.js'; +import { RepeatMode } from 'lavashark'; import type { Player } from 'lavashark'; import type { Playlist, PlaylistTrack } from '../../lib/PlaylistManager.js'; import type { @@ -77,6 +78,13 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const player = await this.initializePlayer(context, voiceChannel); if (!player) return; + // A radio session is replaced by the playlist: drop its queued tracks + // so the playlist content plays next instead of the radio station + const isRadioPlaying = Boolean(player.current && (player.current as any).isRadio); + if (isRadioPlaying) { + player.queue.tracks = []; + } + const tracks = playlist.tracks ?? []; // Reuse one progress message for the final load result const progressMessage = await context.command.reply({ @@ -90,7 +98,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const result = await this.loadTracks(context, player, playlist, member); if (result.added > 0) { - await this.startPlayback(context, player); + await this.startPlayback(context, player, isRadioPlaying); if (context.bot.config.queuePersistence.enabled && context.client.queuePersistence) { await context.client.queuePersistence.saveQueue(player); @@ -340,6 +348,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { private async startPlayback( context: PlaylistSubcommandContext, player: Player, + stopRadio = false, ): Promise { if (!player.playing) { const volume = player.setting.volume ?? @@ -359,6 +368,15 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { return; } + // Stop the active radio and let the playlist take over: any repeat + // would otherwise keep the radio track in the queue or replay it + if (stopRadio) { + if (player.repeatMode !== RepeatMode.OFF) { + player.setRepeatMode(RepeatMode.OFF); + } + await player.skip(); + } + if (player.current) { await context.client.dashboard.update(player, player.current); } diff --git a/src/events/lavashark/PlayerDestroyEvent.ts b/src/events/lavashark/PlayerDestroyEvent.ts index 9c2e4186..de54e8f5 100644 --- a/src/events/lavashark/PlayerDestroyEvent.ts +++ b/src/events/lavashark/PlayerDestroyEvent.ts @@ -38,9 +38,11 @@ export class PlayerDestroyEvent extends BaseLavaSharkEvent<'playerDestroy'> { await setVoiceChannelStatus(bot, client, player.voiceChannelId, null); } - // Clean up dashboard message + // Clean up dashboard message: the session is over and nothing will + // update it anymore, so delete it instead of leaving a stale message if (player.dashboardMsg) { - await client.dashboard.destroy(player); + await player.dashboardMsg.delete().catch(() => {}); + player.dashboardMsg = null; } } } diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index de16159f..e7b1b57c 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -25,11 +25,23 @@ export class DashboardManager { } /** - * Initialize dashboard in a channel + * Initialize dashboard in a channel. + * Serialized with updates and destroys so no two dashboard messages + * can ever be created for the same player at the same time. */ public async initialize( target: ChatInputCommandInteraction | Message | any, player: Player + ): Promise { + this.#updatePromise = this.#updatePromise + .then(() => this.#performInitialize(target, player)) + .catch(() => {}); + return this.#updatePromise; + } + + async #performInitialize( + target: ChatInputCommandInteraction | Message | any, + player: Player ): Promise { let channel; @@ -55,6 +67,19 @@ export class DashboardManager { const lng = this.#bot.guildLanguageManager?.get(player.guildId); + // Remove stale dashboards from earlier sessions (e.g. after a bot + // restart the in-memory reference is gone) so only the new one stays + const dashboardTitle = this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }); + try { + const messages = await (channel as any).messages.fetch({ limit: 30 }); + for (const message of messages.values()) { + if (message.author?.id === this.#client.user?.id && + message.embeds.some((embed: any) => embed.title === dashboardTitle)) { + await message.delete().catch(() => {}); + } + } + } catch (_) {} + player.dashboardMsg = await (channel as any).send({ embeds: [embeds.connected(this.#bot, lng)], components: [] @@ -111,10 +136,28 @@ export class DashboardManager { } } catch (_) {} + // The dashboard is no longer the last message: move it to the + // bottom. A replacement is only sent after the old one was + // actually deleted, otherwise the message is refreshed in place + // and no duplicate can ever appear. + let deleted = false; try { await player.dashboardMsg.delete(); + deleted = true; } catch (_) {} - player.dashboardMsg = null; + + if (!deleted) { + try { + await player.dashboardMsg.edit(embedPayload); + return; + } catch (_) { + // Message is gone: drop the reference and send a new one + player.dashboardMsg = null; + } + } + else { + player.dashboardMsg = null; + } } try { @@ -125,9 +168,19 @@ export class DashboardManager { } /** - * Destroy dashboard and show disconnect message + * Destroy dashboard and show disconnect message. + * The message reference is kept so the next initialize() deletes it and + * the next update() reuses it instead of leaving a stale duplicate. + * Serialized with updates so a queued update can never race the destroy. */ public async destroy(player: Player): Promise { + this.#updatePromise = this.#updatePromise + .then(() => this.#performDestroy(player)) + .catch(() => {}); + return this.#updatePromise; + } + + async #performDestroy(player: Player): Promise { if (!player.dashboardMsg) { return; } @@ -142,9 +195,6 @@ export class DashboardManager { } catch (error) { this.#bot.logger.error( this.#bot.shardId, 'Dashboard destroy error: ' + error); } - finally { - player.dashboardMsg = null; - } } /** diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 3b8bb6fe..ea2c0ef3 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -3,6 +3,12 @@ import { ChannelType } from 'discord.js'; import type { Client } from 'discord.js'; import type { Player, Track } from 'lavashark'; import type { Bot, QueueTableRow } from '../@types/index.js'; +import { + decodeTracksWithRetry, + decodeTrackWithRetry, + searchWithRetry, + sleep, +} from '../utils/functions/lavasharkRequest.js'; /** @@ -40,6 +46,27 @@ interface SerializedTrack { requesterTag: string; } +/** + * Format milliseconds as a duration label (mirrors lavashark's formatTime) + */ +const formatDurationLabel = (milliseconds: number): string => { + const seconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let timeString = ''; + if (days > 0) { + timeString += `${days}d `; + } + if (hours % 24 > 0) { + timeString += `${(hours % 24).toString().padStart(2, '0')}:`; + } + timeString += `${(minutes % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`; + + return timeString; +}; + /** * Manager for persisting queue state to SQLite database */ @@ -291,67 +318,47 @@ export class QueuePersistence { } } - // Restore tracks in batch chunks to prevent performance bottlenecks - const BATCH_SIZE = 5; - const allRestoredTracks: any[] = []; - - for (let i = 0; i < queueData.tracks.length; i += BATCH_SIZE) { - const chunk = queueData.tracks.slice(i, i + BATCH_SIZE); - const restoredChunk = await Promise.all(chunk.map(async (serializedTrack) => { - try { - let result = null; - - // Try loading by encoded track string first - if (serializedTrack.track && serializedTrack.track.trim() !== '') { - try { - result = await client.lavashark.search(serializedTrack.track); - } catch (_) { - // Encoded string failed, will try fallback - } - } - - // Fallback: search by URI - if ((!result || !result.tracks || result.tracks.length === 0) && serializedTrack.info?.uri && serializedTrack.info.uri.trim() !== '') { - try { - result = await client.lavashark.search(serializedTrack.info.uri); - } catch (_) { - // URI search failed, will try title search - } - } - - // Final fallback: search by title + author - if ((!result || !result.tracks || result.tracks.length === 0) && serializedTrack.info?.title && serializedTrack.info.title.trim() !== '') { - try { - const query = serializedTrack.info.author - ? `${serializedTrack.info.title} ${serializedTrack.info.author}`.trim() - : serializedTrack.info.title.trim(); - result = await client.lavashark.search(`ytsearch:${query}`); - } catch (_) { - // All methods failed - } - } + // Restore tracks in batch chunks with bulk decode and retry + const TRACK_REQUEST_DELAY_MS = 250; + const serializedEntries = queueData.tracks.map((track, index) => ({ track, index })); + const encodedEntries = serializedEntries.filter( + ({ track }) => track.track && track.track.trim() !== '', + ); - if (result && result.tracks && result.tracks.length > 0) { - const track = result.tracks[0]; - track.requester = { - id: serializedTrack.requesterId, - tag: serializedTrack.requesterTag - } as any; - return track; - } else { - this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); + const restoredByIndex = new Map(); + + if (encodedEntries.length > 0) { + const bulkDecoded = await decodeTracksWithRetry( + client.lavashark, + encodedEntries.map(({ track }) => track.track), + ); + + if (bulkDecoded && bulkDecoded.length === encodedEntries.length) { + for (let i = 0; i < encodedEntries.length; i++) { + const track = bulkDecoded[i]; + if (track) { + restoredByIndex.set( + encodedEntries[i].index, + this.applySerializedMetadata(track, encodedEntries[i].track), + ); } - } catch (error) { - this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); } - return null; - })); + } + } - for (const track of restoredChunk) { - if (track) { - allRestoredTracks.push(track); - } + const allRestoredTracks: any[] = []; + for (const { track, index } of serializedEntries) { + const restored = restoredByIndex.get(index); + if (restored) { + allRestoredTracks.push(restored); + continue; + } + + const fallback = await this.restoreTrack(client, track); + if (fallback) { + allRestoredTracks.push(fallback); } + await sleep(TRACK_REQUEST_DELAY_MS); } for (const track of allRestoredTracks) { @@ -400,6 +407,80 @@ export class QueuePersistence { } } + /** + * Resolve a single serialized track back into a playable track. + * Decodes the saved encoded string first (exact restore), then falls back + * to searching by URI, then by title + author. Requests are retried once + * each because Lavalink nodes rate-limit REST calls (HTTP 429). + */ + private async restoreTrack( + client: Client, + serializedTrack: SerializedTrack, + ): Promise { + try { + let resolvedTrack: any = null; + + // 1) Decode the saved encoded track exactly (preserves the original track data) + if (serializedTrack.track && serializedTrack.track.trim() !== '') { + resolvedTrack = await decodeTrackWithRetry( + client.lavashark, + serializedTrack.track, + ); + } + + // 2) Fallback: search by URI + if (!resolvedTrack && serializedTrack.info?.uri && serializedTrack.info.uri.trim() !== '') { + const result = await searchWithRetry(client.lavashark, serializedTrack.info.uri); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + resolvedTrack = result.tracks[0]; + } + } + + // 3) Final fallback: search by title + author + if (!resolvedTrack && serializedTrack.info?.title && serializedTrack.info.title.trim() !== '') { + const query = serializedTrack.info.author + ? `${serializedTrack.info.title} ${serializedTrack.info.author}`.trim() + : serializedTrack.info.title.trim(); + const result = await searchWithRetry(client.lavashark, `ytsearch:${query}`); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + resolvedTrack = result.tracks[0]; + } + } + + if (resolvedTrack) { + return this.applySerializedMetadata(resolvedTrack, serializedTrack); + } + + this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); + } catch (error) { + this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); + } + return null; + } + + /** + * Restore the originally saved metadata so names are never replaced by + * whatever the fallback search returned + */ + private applySerializedMetadata(track: any, serializedTrack: SerializedTrack): any { + const info = serializedTrack.info ?? {}; + if (info.title) track.title = info.title; + if (info.author) track.author = info.author; + if (info.uri) track.uri = info.uri; + if (info.identifier) track.identifier = info.identifier; + if (typeof info.length === 'number' && info.length > 0) { + track.duration = { + label: formatDurationLabel(info.length), + value: info.length, + }; + } + track.requester = { + id: serializedTrack.requesterId, + tag: serializedTrack.requesterTag + } as any; + return track; + } + /** * Check if any persisted queues exist for a specific voice channel * @param voiceChannelId - Voice channel ID to check diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index fed39f15..4eeff197 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Remove a user from the blacklist", "CONFIG_BLACKLIST_OPTION_LIST": "List all blacklisted users", "CONFIG_BLACKLIST_OPTION_USER": "User to add or remove from blacklist", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not on the blacklist.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not on the blacklist", "MESSAGE_BLACKLIST_LIST_TITLE": "Current Blacklist", - "MESSAGE_BLACKLIST_LIST_EMPTY": "The blacklist is currently empty.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "The blacklist is currently empty", "CONFIG_CLEAR_DESCRIPTION": "Clear the music queue", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Move the dashboard to the bottom", @@ -56,9 +56,9 @@ "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "The song link or song name to play first", "CONFIG_PLAYLAST_DESCRIPTION": "Replay the current or last played song", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ The track is back in the queue.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found", + "MESSAGE_PLAYLAST_SUCCESS": "✅ The track is back in the queue", "CONFIG_QUEUE_DESCRIPTION": "Show current playlist", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Select a song to remove from the playlist", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [volume]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "The default volume to set (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Command error. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ There are no tracks in the queue right now.", - "ERROR_NO_PLAYING": "❌ There is no music currently playing.", - "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if enabled) can also skip.", - "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if enabled) can also seek.", - "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if enabled) can also pause.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join the voice channel.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ There are no tracks in the queue right now", + "ERROR_NO_PLAYING": "❌ There is no music currently playing", + "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if present) can also skip.", + "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if present) can also seek.", + "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if present) can also pause.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join the voice channel", "ERROR_PLAY_MUSIC": "❌ The service is experiencing some problems, please try again. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ No results found. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ The wait time has expired.", - "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated.", - "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs.", - "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs.", - "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ.", - "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ.", - "MESSAGE_DJ_STATIC_MODE": "In STATIC mode, DJ permissions can only be set through the config file.", - "MESSAGE_DJ_NO_PLAYER": "There's no player currently playing music.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}> has been added as a DJ.", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired", + "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated", + "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs", + "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs", + "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ", + "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ", + "MESSAGE_DJ_STATIC_MODE": "In STATIC mode, DJ permissions can only be set through the config file", + "MESSAGE_DJ_NO_PLAYER": "There's no player currently playing music", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> has been added as a DJ", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ left the channel! Auto-selected new DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ List", "MESSAGE_DJ_LIST_ADMINS": "Admins", "MESSAGE_DJ_LIST_ROLE_DJS": "Role-based DJs", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dynamic DJs", - "MESSAGE_DJ_LIST_NONE": "No DJs configured.", + "MESSAGE_DJ_LIST_NONE": "No DJs configured", "MESSAGE_DJ_LIST_ERROR": "❌ Failed to load DJ list. Please try again.", - "MESSAGE_DJ_ROLE_NOT_SET": "No one has been assigneed to a permanent DJ role.", - "MESSAGE_FILTER_NOT_FOUND": "❌ Effect not found.", + "MESSAGE_DJ_ROLE_NOT_SET": "No one has been assigneed to a permanent DJ role", + "MESSAGE_FILTER_NOT_FOUND": "❌ Effect not found", "MESSAGE_FILTER_SELECT_LIST": "Select a music filter: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "Select an effect", - "MESSAGE_HELP_NOT_FOUND": "❌ Command not found.", + "MESSAGE_HELP_NOT_FOUND": "❌ Command not found", "MESSAGE_HELP_SELECT_LIST": "Choose a command to get help. ⬇️", "MESSAGE_HELP_SELECT_MODE": "Select a command", - "MESSAGE_JOIN_SUCCESS": "✅ Joined the voice channel.", + "MESSAGE_JOIN_SUCCESS": "✅ Joined the voice channel", "MESSAGE_LANG_ARGS_ERROR": "Invalid locale code. Available languages: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Available languages: {{langList}}", "MESSAGE_LANG_SUCCESS": "✅ Locale changed to `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Left the voice channel.", + "MESSAGE_LEAVE_SUCCESS": "✅ Left the voice channel", "REPEAT_MODE_OFF": "OFF", "REPEAT_MODE_SINGLE": "SINGLE", "REPEAT_MODE_ALL": "ALL", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "All", "LOOP_SELECT_PLACEHOLDER": "Select the loop mode", "LOOP_SELECT_DESCRIPTION": "Set loop mode to {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Loop mode is now `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Track order changed.", + "MESSAGE_LOOP_MODE": "✅ Loop mode is now `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Track order changed", "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter positions in a valid range. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nand {{length}} other songs", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Save Song", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Now Playing: {{title}} | Requested by {{requester}}", "UNKNOWN_USER": "Unknown", - "MESSAGE_PAUSE_FAIL": "❌ Failed to pause the music.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Please provide a song name or link.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Track added to the queue.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches found.", + "MESSAGE_PAUSE_FAIL": "❌ Failed to pause the music", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Please provide a song name or link", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Track added to the queue", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches found", "MESSAGE_PLAY_SELECT_DURATION": "Duration: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Select the music", - "MESSAGE_REMOVE_CANCEL": "✅ Removal cancelled.", - "MESSAGE_REMOVE_FAIL": "❌ Failed to remove the track.", + "MESSAGE_REMOVE_CANCEL": "✅ Removal cancelled", + "MESSAGE_REMOVE_FAIL": "❌ Failed to remove the track", "MESSAGE_REMOVE_INSTRUCTION": "Choose a track number from **1** to **{{length}}** to **remove** it, or enter any other number to cancel. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ The queue is empty after the current track.", - "MESSAGE_RESUME_FAIL": "❌ Failed to resume the music.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ The music has been resumed.", - "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ The queue is empty after the current track", + "MESSAGE_RESUME_FAIL": "❌ Failed to resume the music", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ The music has been resumed", + "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed", "MESSAGE_SEEK_ARGS_ERROR": "❌ Invalid format for the target time.\n(**`ex: 3m20s, 1m 50s, 1:20:55, 5:20`**)", "MESSAGE_SEEK_SKIP": "✅ The requested time (`{{duration}}`) is beyond the track's duration.\nSkipping to the next track.", - "MESSAGE_SEEK_SUCCESS": "✅ Seeked to `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Playlist shuffled.", - "MESSAGE_SKIP_FAIL": "❌ Failed to skip the track.", - "MESSAGE_SKIP_SUCCESS": "✅ Track skipped.", - "MESSAGE_STOP_SUCCESS": "✅ Playback stopped and the queue cleared.", + "MESSAGE_SEEK_SUCCESS": "✅ Seeked to `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Playlist shuffled", + "MESSAGE_SKIP_FAIL": "❌ Failed to skip the track", + "MESSAGE_SKIP_SUCCESS": "✅ Track skipped", + "MESSAGE_STOP_SUCCESS": "✅ Playback stopped and the queue cleared", "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, enter a number from `1` to `{{maxVolume}}`.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Enter a number from `1` to `{{maxVolume}}` to change the volume.**", "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\n**Select the volume you want:**", - "MESSAGE_VOLUME_SAME": "❌ That volume is already set.", + "MESSAGE_VOLUME_SAME": "❌ That volume is already set", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume for this server changed to **{{volume}}%**! (Max: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume", "ERROR_QUEUE_LIMIT_REACHED": "❌ You have reached your queue limit! You currently have **{{current}}** songs in the queue (limit: **{{limit}}**). Please wait for some songs to finish before adding more.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ The global queue limit has been reached! The queue currently has **{{total}}** songs (global limit: **{{limit}}**). Please wait for some songs to finish before adding more.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to your queue limit (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to the global queue limit (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to your queue limit (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to the global queue limit (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "Guild ID", "MESSAGE_SERVER_GUILD": "Guild", "MESSAGE_SERVER_MEMBERS": "Members", @@ -199,72 +199,72 @@ "UNIT_TB": "TB", "ERROR_BLACKLIST_NOT_INITIALIZED": "The blacklist isn't loaded yet. Please try again later.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Select a Music command", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Utility command", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Service command", "HELP_COMMAND_ALIASES": "Aliases: {{aliases}}", "HELP_COMMAND_NONE": "None", "LABEL_FILTER_CLEAR": "Clear filters", - "CONFIG_PLAYLIST_DESCRIPTION": "Manage playlists: save, load, delete, and import from M3U.", + "CONFIG_PLAYLIST_DESCRIPTION": "Manage playlists: save, load, delete, and import from M3U", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "List all playlists in the server.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Show the tracks in a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Delete a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Import a playlist from a remote M3U file.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Remove a track from a playlist.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "List all playlists in the server", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Show the tracks in a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Delete a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Import a playlist from a remote M3U file", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Remove a track from a playlist", "CONFIG_PLAYLIST_OPTION_NAME": "Playlist name", "CONFIG_PLAYLIST_OPTION_SEARCH": "Specific track title to search in the playlist (Optional)", "CONFIG_PLAYLIST_OPTION_URL": "M3U file download link", "CONFIG_PLAYLIST_OPTION_INDEX": "Track index to remove (starts from 1)", "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlists aren't available yet. Please try again later.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Please specify a playlist name.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download the M3U file (error code {{status}}).", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand", + "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Please specify a playlist name", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download the M3U file (error code {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ The M3U download was blocked by the security policy. Use a public HTTPS URL up to {{maxSizeMiB}} MiB that responds within {{timeoutSeconds}} seconds.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Failed to parse and save M3U playlist. Check if it's formatted correctly.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Failed to remove track. Check if it is a valid index.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded: {{added}} added, {{skipped}} skipped.", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded: {{added}} added, {{skipped}} skipped", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server", "MESSAGE_PLAYLIST_LIST_TITLE": "Server Playlists", "MESSAGE_PLAYLIST_INFO_TITLE": "Playlist Info: {{name}} ({{count}} tracks)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*and {{remaining}} more tracks.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Deleted playlist `{{name}}`.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Deleted playlist `{{name}}`", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ External M3U playlist imported!\nRegistered {{count}} tracks in `{{name}}` playlist.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Removed track #{{index}} from playlist `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Search and play a specific genre/channel from imported radio playlist.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Removed track #{{index}} from playlist `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Search and play a specific genre/channel from imported radio playlist", "CONFIG_RADIO_USAGE": "radio [channel]", "CONFIG_RADIO_OPTION_PLAYLIST": "Playlist name to search", "CONFIG_RADIO_OPTION_CHANNEL": "Radio channel or genre to play (e.g. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Usage: `{{prefix}}radio [playlist name] [channel name]` (e.g. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Could not find a radio channel matching `{{channelQuery}}` in playlist `{{playlistName}}`.\nAvailable examples: {{samples}} etc.", "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\nThe file must be an M3U playlist with EXTINF tags.", "ERROR_RADIO_NO_MATCH": "❌ Radio channel matching `{{query}}` not found.\nAvailable examples: {{samples}} etc.", "ERROR_RADIO_LOAD_FAILED": "❌ Error occurred during loading radio channel: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Searching radio channel **{{title}}** in playlist **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Searching radio channel **{{title}}** in playlist **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Started playing radio channel **{{title}}** from playlist **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Page {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Only the command user can navigate pages.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Only the command user can confirm this action.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Only the command user can navigate pages", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Only the command user can confirm this action", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Playlist Exists", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "A playlist named **`{{name}}`** ({{count}} tracks) already exists.\nDo you want to overwrite it with the imported playlist?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "A playlist named **`{{name}}`** ({{count}} tracks) already exists.\nDo you want to overwrite it with the current queue?", "LABEL_OVERWRITE": "Overwrite", "LABEL_CANCEL": "Cancel", - "MESSAGE_PLAYLIST_OVERWRITING": "Overwriting playlist...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Playlist import cancelled.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", + "MESSAGE_PLAYLIST_OVERWRITING": "Overwriting playlist..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Playlist import cancelled", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", "ERROR_PLAYLIST_DISABLED": "❌ Playlist feature is disabled.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U playlists cannot be queued entirely. Please use the `/radio` command to select a specific station.", diff --git a/src/locales/en-US/embeds.json b/src/locales/en-US/embeds.json index 95560f74..0027ed9f 100644 --- a/src/locales/en-US/embeds.json +++ b/src/locales/en-US/embeds.json @@ -4,16 +4,16 @@ "DASHBOARD_SUBTITLE": "Author: **{{author}}**\nDuration: **{{duration}}**\n────────────────────\nVolume: `{{volume}}%`\nLoop: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Requester:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Dynamic DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Connected to the voice channel.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Connected to the voice channel", "MAINTAIN_DESCRIPTION": "The bot will begin maintenance in a few minutes. During this time, some features may be temporarily unavailable.", "MAINTAIN_TITLE": "⚠️ Maintenance Notice ⚠️", - "MESSAGE_BLACKLIST": "There are blacklisted users in the voice channel.", + "MESSAGE_BLACKLIST": "There are blacklisted users in the voice channel", "MESSAGE_COMMAND": "Command **{{command}}**", "MESSAGE_FILTER": "Filter set to: **{{effectName}}**", "NODE_ALL_ACTIVE": "All nodes are active", "NODE_CONNECTED": "Latency: **{{ping}}ms**", "NODE_DISCONNECTED": "**DISCONNECTED**", - "NODE_STATUS_ARGS_ERROR": "❌ Please enter a valid node name.", + "NODE_STATUS_ARGS_ERROR": "❌ Please enter a valid node name", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Cores: **{{cores}}**\nSystem load: **{{systemLoad}}**\nLavalink load: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ INFO", diff --git a/src/locales/en-US/events.json b/src/locales/en-US/events.json index 9be0f4cc..89ab1c11 100644 --- a/src/locales/en-US/events.json +++ b/src/locales/en-US/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Unable to get guild data in cache.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ You are not on the same audio channel as me.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Please join the specified voice channel <#{{channelId}}> and call me.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ You are not connected to an audio channel.", - "ERROR_NOT_PLAYING": "❌ There is no music currently playing.", - "ERROR_REQUIRE_ADMIN": "❌ This command requires administrator privileges.", - "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message.", - "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled.", - "ERROR_TIME_EXPIRED": "❌ The wait time has expired.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left.", - "MESSAGE_BOT_STOP": "✅ Bot stopped.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Unable to get guild data in cache", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ You are not on the same audio channel as me", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Please join the specified voice channel <#{{channelId}}> and call me", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ You are not connected to an audio channel", + "ERROR_NOT_PLAYING": "❌ There is no music currently playing", + "ERROR_REQUIRE_ADMIN": "❌ This command requires administrator privileges", + "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message", + "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left", + "MESSAGE_BOT_STOP": "✅ Bot stopped", + "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled", "MESSAGE_NOW_PLAYING_SUBTITLE": "Author: **{{author}}**\nDuration: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Page {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Select song loop mode.", + "MESSAGE_SELECT_LOOP_MODE": "Select song loop mode", "LOOP_MODE_OFF": "Off", "LOOP_MODE_SINGLE": "Single", "LOOP_MODE_ALL": "All", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I've sent you the track name in a private message.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I've sent you the track name in a private message", "MESSAGE_SET_LOOP_MODE": "✅ Loop mode set to `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ You can not execute commands in this channel. Please use the <#{{channelId}}> text channel." } diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index 6496dc82..7e891761 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -6,138 +6,138 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "사용자를 블랙리스트에서 제거합니다", "CONFIG_BLACKLIST_OPTION_LIST": "블랙리스트 사용자 목록을 조회합니다", "CONFIG_BLACKLIST_OPTION_USER": "블랙리스트에 추가하거나 제거할 사용자", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 블랙리스트에 없습니다.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 블랙리스트에 없습니다", "MESSAGE_BLACKLIST_LIST_TITLE": "블랙리스트", - "MESSAGE_BLACKLIST_LIST_EMPTY": "블랙리스트가 현재 비어 있습니다.", - "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "블랙리스트가 현재 비어 있습니다", + "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다", "CONFIG_CLEAR_USAGE": "clear", - "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다.", + "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다", "CONFIG_DASHBOARD_USAGE": "dashboard", - "CONFIG_DJ_DESCRIPTION": "사용자를 DJ로 추가하거나 DJ 목록을 표시합니다.", + "CONFIG_DJ_DESCRIPTION": "사용자를 DJ로 추가하거나 DJ 목록을 표시합니다", "CONFIG_DJ_USAGE": "dj [@사용자]", "CONFIG_DJ_OPTION_DESCRIPTION": "DJ로 추가할 사용자 (선택사항)", - "CONFIG_FILTER_DESCRIPTION": "재생 효과를 변경합니다.", + "CONFIG_FILTER_DESCRIPTION": "재생 효과를 변경합니다", "CONFIG_FILTER_USAGE": "filter [효과명]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "원하는 음악 효과 모드를 입력 및 선택하세요.", - "CONFIG_HELP_DESCRIPTION": "명령어 도움말을 확인합니다.", + "CONFIG_FILTER_OPTION_DESCRIPTION": "원하는 음악 효과 모드를 입력 및 선택하세요", + "CONFIG_HELP_DESCRIPTION": "명령어 도움말을 확인합니다", "CONFIG_HELP_USAGE": "help [명령어]", - "CONFIG_HELP_OPTION_DESCRIPTION": "도움이 필요한 명령어를 입력하세요.", - "CONFIG_JOIN_DESCRIPTION": "현재 음성 채널에 봇을 참여시킵니다.", + "CONFIG_HELP_OPTION_DESCRIPTION": "도움이 필요한 명령어를 입력하세요", + "CONFIG_JOIN_DESCRIPTION": "현재 음성 채널에 봇을 참여시킵니다", "CONFIG_JOIN_USAGE": "join", - "CONFIG_LANG_DESCRIPTION": "봇 언어를 변경합니다.", + "CONFIG_LANG_DESCRIPTION": "봇 언어를 변경합니다", "CONFIG_LANG_USAGE": "language [언어]", "CONFIG_LANG_OPTION_DESCRIPTION": "언어", - "CONFIG_LEAVE_DESCRIPTION": "현재 음성 채널에서 봇을 내보냅니다.", + "CONFIG_LEAVE_DESCRIPTION": "현재 음성 채널에서 봇을 내보냅니다", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "반복 모드를 켜거나 끕니다.", + "CONFIG_LOOP_DESCRIPTION": "반복 모드를 켜거나 끕니다", "CONFIG_LOOP_USAGE": "loop [끄기/한 곡 반복/전체 반복]", - "CONFIG_LOOP_OPTION_DESCRIPTION": "반복 모드를 선택하세요.", - "CONFIG_MOVE_DESCRIPTION": "재생목록에 있는 두 곡의 위치를 바꿉니다.", + "CONFIG_LOOP_OPTION_DESCRIPTION": "반복 모드를 선택하세요", + "CONFIG_MOVE_DESCRIPTION": "재생목록에 있는 두 곡의 위치를 바꿉니다", "CONFIG_MOVE_USAGE": "move [번호 1] [번호 2]", "CONFIG_MOVE_OPTION_DESCRIPTION": "바뀔 재생목록 번호", "CONFIG_MOVE_OPTION_DESCRIPTION_2": "바꾸고자 하는 재생목록 번호", - "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다.", + "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다", "CONFIG_NODE_USAGE": "node [노드명]", - "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요.", - "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악을 표시합니다.", + "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요", + "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악을 표시합니다", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", - "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다.", + "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다", "CONFIG_PAUSE_USAGE": "pause", - "CONFIG_PING_DESCRIPTION": "서버와 봇의 지연시간을 확인합니다.", + "CONFIG_PING_DESCRIPTION": "서버와 봇의 지연시간을 확인합니다", "CONFIG_PING_USAGE": "ping", - "CONFIG_PLAY_DESCRIPTION": "재생할 음악 링크 또는 음악 이름을 입력하세요.", + "CONFIG_PLAY_DESCRIPTION": "재생할 음악 링크 또는 음악 이름을 입력하세요", "CONFIG_PLAY_USAGE": "play [음악 링크/음악 이름]", - "CONFIG_PLAY_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요.", - "CONFIG_PLAYFIRST_DESCRIPTION": "먼저 재생할 음악 링크 혹은 음악 이름을 입력하세요.", + "CONFIG_PLAY_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요", + "CONFIG_PLAYFIRST_DESCRIPTION": "먼저 재생할 음악 링크 혹은 음악 이름을 입력하세요", "CONFIG_PLAYFIRST_USAGE": "playfirst [음악 링크/음악 이름]", - "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요.", + "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요", "CONFIG_PLAYLAST_DESCRIPTION": "현재 또는 마지막으로 재생된 곡을 다시 재생합니다", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ 이 명령은 대기열이 비어있을 때만 사용할 수 있습니다.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ 최근 재생된 곡을 찾을 수 없습니다.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ 곡이 대기열에 다시 추가되었습니다.", - "CONFIG_QUEUE_DESCRIPTION": "현재 재생중인 음악을 확인합니다.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ 이 명령은 대기열이 비어있을 때만 사용할 수 있습니다", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ 최근 재생된 곡을 찾을 수 없습니다", + "MESSAGE_PLAYLAST_SUCCESS": "✅ 곡이 대기열에 다시 추가되었습니다", + "CONFIG_QUEUE_DESCRIPTION": "현재 재생중인 음악을 확인합니다", "CONFIG_QUEUE_USAGE": "queue", - "CONFIG_REMOVE_DESCRIPTION": "재생목록에서 제거할 음악을 선택합니다.", + "CONFIG_REMOVE_DESCRIPTION": "재생목록에서 제거할 음악을 선택합니다", "CONFIG_REMOVE_USAGE": "remove [재생목록 번호]", "CONFIG_REMOVE_OPTION_DESCRIPTION": "재생목록 번호", "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "재생목록 번호", - "CONFIG_RESUME_DESCRIPTION": "일시중지된 음악을 재생합니다.", + "CONFIG_RESUME_DESCRIPTION": "일시중지된 음악을 재생합니다", "CONFIG_RESUME_USAGE": "resume", - "CONFIG_SEARCH_DESCRIPTION": "검색할 음악 이름 혹은 링크를 입력하세요.", + "CONFIG_SEARCH_DESCRIPTION": "검색할 음악 이름 혹은 링크를 입력하세요", "CONFIG_SEARCH_USAGE": "search [음악 이름/음악 링크]", - "CONFIG_SEARCH_OPTION_DESCRIPTION": "음악 이름 혹은 음악 링크를 입력하세요.", - "CONFIG_SEEK_DESCRIPTION": "음악을 특정 시간으로 이동합니다.", + "CONFIG_SEARCH_OPTION_DESCRIPTION": "음악 이름 혹은 음악 링크를 입력하세요", + "CONFIG_SEEK_DESCRIPTION": "음악을 특정 시간으로 이동합니다", "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (예시: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요.", - "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다.", + "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요", + "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "재생목록을 섞습니다.", + "CONFIG_SHUFFLE_DESCRIPTION": "재생목록을 섞습니다", "CONFIG_SHUFFLE_USAGE": "shuffle", - "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다.", + "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다", "CONFIG_SKIP_USAGE": "skip", - "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다.", + "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다", "CONFIG_STATUS_USAGE": "status", "CONFIG_STOP_DESCRIPTION": "재생을 멈추고 재생목록을 비웁니다. 채널은 떠나지 않습니다.", "CONFIG_STOP_USAGE": "stop", - "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다.", + "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다", "CONFIG_VOLUME_USAGE": "v [0-200]", "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정할 음량", - "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다.", + "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [음량]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "설정할 기본 음량 (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ 명령에 오류가 발생했습니다. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ 현재 대기열에 재생할 음악이 없습니다.", - "ERROR_NO_PLAYING": "❌ 현재 재생 중인 음악이 없습니다.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ 현재 대기열에 재생할 음악이 없습니다", + "ERROR_NO_PLAYING": "❌ 현재 재생 중인 음악이 없습니다", "ERROR_SKIP_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 건너뛸 수 있습니다. 관리자 및 DJ(활성화된 경우)도 건너뛸 수 있습니다.", "ERROR_SEEK_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 탐색할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 탐색할 수 있습니다.", "ERROR_PAUSE_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 일시정지할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 일시정지할 수 있습니다.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다", "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ 검색결과가 없습니다. 이유: ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", - "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다.", - "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다.", - "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다.", - "MESSAGE_DJ_MENTION_USER": "DJ로 추가할 사용자를 멘션해주세요.", - "MESSAGE_DJ_NO_BOTS": "봇을 DJ로 추가할 수 없습니다.", - "MESSAGE_DJ_STATIC_MODE": "고정 모드에서는 설정을 통해 DJ 권한이 관리됩니다.", - "MESSAGE_DJ_NO_PLAYER": "활성화된 음악 플레이어를 찾을 수 없습니다.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}>는 이미 DJ입니다.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}>를 DJ로 성공적으로 추가했습니다.", + "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다", + "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다", + "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다", + "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다", + "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다", + "MESSAGE_DJ_MENTION_USER": "DJ로 추가할 사용자를 멘션해주세요", + "MESSAGE_DJ_NO_BOTS": "봇을 DJ로 추가할 수 없습니다", + "MESSAGE_DJ_STATIC_MODE": "고정 모드에서는 설정을 통해 DJ 권한이 관리됩니다", + "MESSAGE_DJ_NO_PLAYER": "활성화된 음악 플레이어를 찾을 수 없습니다", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}>는 이미 DJ입니다", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}>를 DJ로 성공적으로 추가했습니다", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ가 채널을 떠났습니다! 새로운 DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ 목록", "MESSAGE_DJ_LIST_ADMINS": "관리자", "MESSAGE_DJ_LIST_ROLE_DJS": "역할 기반 DJ", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "동적 DJ", - "MESSAGE_DJ_LIST_NONE": "구성된 DJ가 없습니다.", + "MESSAGE_DJ_LIST_NONE": "구성된 DJ가 없습니다", "MESSAGE_DJ_LIST_ERROR": "❌ DJ 목록을 불러오지 못했습니다. 다시 시도해 주세요.", "MESSAGE_DJ_ROLE_NOT_SET": "DJ 역할이 구성되지 않음", - "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다.", + "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다", "MESSAGE_FILTER_SELECT_LIST": "설정할 재생 효과를 선택하세요: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "필터 모드 선택", - "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다.", + "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다", "MESSAGE_HELP_SELECT_LIST": "도움말을 받으려면 명령어를 선택하세요. ⬇️", "MESSAGE_HELP_SELECT_MODE": "도움말 선택하기", - "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다.", + "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다", "MESSAGE_LANG_ARGS_ERROR": "언어 변수가 잘못되었습니다. (현재 사용 가능한 언어: {{langList}})", "MESSAGE_LANG_AVAILABLE_LIST": "현재 사용 가능한 언어: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ 봇의 언어가 `{{locale}}`(으)로 성공적으로 변경되었습니다.", - "MESSAGE_LEAVE_SUCCESS": "✅ 봇이 떠났습니다.", + "MESSAGE_LANG_SUCCESS": "✅ 봇의 언어가 `{{locale}}`(으)로 성공적으로 변경되었습니다", + "MESSAGE_LEAVE_SUCCESS": "✅ 봇이 떠났습니다", "REPEAT_MODE_OFF": "끄기", "REPEAT_MODE_SINGLE": "한 곡 반복", "REPEAT_MODE_ALL": "전체 반복", "LOOP_CHOICE_OFF": "끄기", "LOOP_CHOICE_ONE": "한 곡 반복", "LOOP_CHOICE_ALL": "전체 반복", - "LOOP_SELECT_PLACEHOLDER": "반복 모드를 선택하세요.", - "LOOP_SELECT_DESCRIPTION": "반복 모드를 {{mode}}(으)로 설정합니다.", - "MESSAGE_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다.", - "MESSAGE_MOVE_SUCCESS": "✅ 음악 순서가 성공적으로 변경되었습니다.", + "LOOP_SELECT_PLACEHOLDER": "반복 모드를 선택하세요", + "LOOP_SELECT_DESCRIPTION": "반복 모드를 {{mode}}(으)로 설정합니다", + "MESSAGE_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다", + "MESSAGE_MOVE_SUCCESS": "✅ 음악 순서가 성공적으로 변경되었습니다", "MESSAGE_MOVE_WRONG_INDEX": "❌ 유효한 범위를 입력하세요. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\n그 외 {{length}}개의 곡", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "음악 저장", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 현재 재생 중: {{title}} | {{requester}} 요청", "UNKNOWN_USER": "알 수 없음", - "MESSAGE_PAUSE_FAIL": "❌ 음악 일시 중지에 실패했습니다.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ 음악이 일시정지되었습니다.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ 음악이 일시정지되었습니다.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ 검색하려는 음악의 이름을 입력하세요.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ 음악이 추가되었습니다.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ 검색결과가 없습니다.", + "MESSAGE_PAUSE_FAIL": "❌ 음악 일시 중지에 실패했습니다", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ 음악이 일시정지되었습니다", + "MESSAGE_PAUSE_SUCCESS": "⏸️ 음악이 일시정지되었습니다", + "MESSAGE_PLAY_ARGS_ERROR": "❌ 검색하려는 음악의 이름을 입력하세요", + "MESSAGE_PLAY_MUSIC_ADD": "✅ 음악이 추가되었습니다", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ 검색결과가 없습니다", "MESSAGE_PLAY_SELECT_DURATION": "**길이:** {{label}}", - "MESSAGE_PLAY_SELECT_TITLE": "음악을 선택하세요.", - "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다.", - "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다.", + "MESSAGE_PLAY_SELECT_TITLE": "음악을 선택하세요", + "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다", + "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다", "MESSAGE_REMOVE_INSTRUCTION": "⬇️ **1 ~ {{length}}** 중에서 노래를 선택하여 **제거**하거나 다른 문자를 입력하여 선택을 취소합니다. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 곡 이후 대기열이 비어 있습니다.", - "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 음악이 다시 재생되었습니다.", - "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 곡 이후 대기열이 비어 있습니다", + "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 음악이 다시 재생되었습니다", + "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다", "MESSAGE_SEEK_ARGS_ERROR": "❌ 목표 시간이 잘못된 형식입니다. (예: 3m20s, 1m 50s, 1:20:55, 5:20)", "MESSAGE_SEEK_SKIP": "✅ 해당 위치는 {{duration}}을 초과합니다.\n다음 곡으로 건너뜁니다.", - "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 재생목록을 섞었습니다.", - "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다.", - "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다.", - "MESSAGE_STOP_SUCCESS": "✅ 재생을 멈추고 재생목록을 비웠습니다.", - "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", + "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 재생목록을 섞었습니다", + "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다", + "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다", + "MESSAGE_STOP_SUCCESS": "✅ 재생을 멈추고 재생목록을 비웠습니다", + "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다", "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\n**설정할 음량을 선택하세요:**", - "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다.", + "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 이 서버의 기본 음량이 **{{volume}}%**로 변경되었습니다! (최대: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요", "ERROR_QUEUE_LIMIT_REACHED": "❌ 대기열 한도에 도달했습니다! 현재 대기열에 **{{current}}**개의 음악이 있습니다 (한도: **{{limit}}**). 더 추가하려면 일부 음악이 끝날 때까지 기다려 주세요.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ 전역 대기열 한도에 도달했습니다! 대기열에 현재 **{{total}}**개의 음악이 있습니다 (전역 한도: **{{limit}}**). 더 추가하려면 일부 음악이 끝날 때까지 기다려 주세요.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (대기열 한도: **{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (전역 대기열 한도: **{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (대기열 한도: **{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (전역 대기열 한도: **{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "서버 ID", "MESSAGE_SERVER_GUILD": "서버", "MESSAGE_SERVER_MEMBERS": "멤버", @@ -197,76 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "블랙리스트 관리자가 초기화되지 않았습니다.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "블랙리스트 관리자가 초기화되지 않았습니다", "HELP_SELECT_MUSIC_PLACEHOLDER": "음악 명령어 선택", - "HELP_SELECT_UTILITY_PLACEHOLDER": "유틸리티 명령어 선택", + "HELP_SELECT_UTILITY_PLACEHOLDER": "서비스 명령어를 선택하세요", "HELP_COMMAND_ALIASES": "별칭: {{aliases}}", "HELP_COMMAND_NONE": "없음", "LABEL_FILTER_CLEAR": "효과 초기화", - "CONFIG_PLAYLIST_DESCRIPTION": "플레이리스트를 저장, 로드, 편집 또는 외부 M3U에서 가져옵니다.", + "CONFIG_PLAYLIST_DESCRIPTION": "플레이리스트를 저장, 로드, 편집 또는 외부 M3U에서 가져옵니다", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "현재 대기열을 플레이리스트로 저장합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "저장된 플레이리스트를 대기열에 추가하고 재생합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "서버의 플레이리스트 목록을 조회합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "플레이리스트에 저장된 곡 목록을 확인합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "플레이리스트를 삭제합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "외부 M3U 파일 주소에서 플레이리스트를 가져옵니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "플레이리스트에서 특정 트랙을 삭제합니다.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "현재 대기열을 플레이리스트로 저장합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "저장된 플레이리스트를 대기열에 추가하고 재생합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "서버의 플레이리스트 목록을 조회합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "플레이리스트에 저장된 곡 목록을 확인합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "플레이리스트를 삭제합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "외부 M3U 파일 주소에서 플레이리스트를 가져옵니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "플레이리스트에서 특정 트랙을 삭제합니다", "CONFIG_PLAYLIST_OPTION_NAME": "플레이리스트 이름", "CONFIG_PLAYLIST_OPTION_SEARCH": "플레이리스트 내부에서 검색하여 재생할 트랙 이름 (옵션)", "CONFIG_PLAYLIST_OPTION_URL": "M3U 파일 다운로드 링크", "CONFIG_PLAYLIST_OPTION_INDEX": "삭제할 트랙 번호 (1부터 시작)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ 플레이리스트 매니저가 초기화되지 않았습니다.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ 올바른 하위 명령어를 입력해주세요 (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ 알 수 없는 하위 명령어입니다.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ 현재 재생 중이거나 대기열에 곡이 없습니다.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ 플레이리스트 이름을 입력해주세요.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ 플레이리스트 저장에 실패했습니다.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ 플레이리스트 `{{name}}`을(를) 찾을 수 없거나 비어 있습니다.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ 플레이리스트 `{{name}}` 내에서 `{{searchQuery}}`와(과) 일치하는 트랙을 찾을 수 없습니다.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ 플레이리스트 매니저가 초기화되지 않았습니다", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ 올바른 하위 명령어를 입력해주세요 (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ 알 수 없는 하위 명령어입니다", + "ERROR_PLAYLIST_NO_PLAYING": "❌ 현재 재생 중이거나 대기열에 곡이 없습니다", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ 플레이리스트 이름을 입력해주세요", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ 플레이리스트 저장에 실패했습니다", + "ERROR_PLAYLIST_NOT_FOUND": "❌ 플레이리스트 `{{name}}`을(를) 찾을 수 없거나 비어 있습니다", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ 플레이리스트 `{{name}}` 내에서 `{{searchQuery}}`와(과) 일치하는 트랙을 찾을 수 없습니다", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ M3U 파일을 다운로드하지 못했습니다. (HTTP 상태 코드: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ 보안 정책에 의해 M3U 다운로드가 차단되었습니다. {{timeoutSeconds}}초 이내에 응답하고 {{maxSizeMiB}} MiB 이하인 공개 HTTPS URL을 사용해주세요.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ 플레이리스트에는 최대 {{limit}}개의 트랙만 저장할 수 있습니다.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ 플레이리스트에는 최대 {{limit}}개의 트랙만 저장할 수 있습니다", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ M3U 플레이리스트 파싱 및 저장에 실패했습니다. 올바른 형식인지 확인해주세요.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 곡 삭제에 실패했습니다. 올바른 트랙 번호인지 확인해주세요.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드 완료: {{added}}곡 추가, {{skipped}}곡 건너뜀.", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드 완료: {{added}}곡 추가, {{skipped}}곡 건너뜀", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다", "MESSAGE_PLAYLIST_LIST_TITLE": "서버 플레이리스트 목록", "MESSAGE_PLAYLIST_INFO_TITLE": "플레이리스트 정보: {{name}} (총 {{count}}곡)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*외 {{remaining}}개의 곡이 더 있습니다.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ 플레이리스트 `{{name}}`을(를) 삭제했습니다.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ 플레이리스트 `{{name}}`을(를) 삭제했습니다", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ 외부 M3U 플레이리스트를 성공적으로 가져왔습니다!\n`{{name}}` 플레이리스트에 {{count}}개의 곡이 등록되었습니다.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ 플레이리스트 `{{name}}`에서 {{index}}번 곡을 삭제했습니다.", - "CONFIG_RADIO_DESCRIPTION": "가져온 라디오 플레이리스트에서 특정 장르/채널을 검색해 바로 재생합니다.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ 플레이리스트 `{{name}}`에서 {{index}}번 곡을 삭제했습니다", + "CONFIG_RADIO_DESCRIPTION": "가져온 라디오 플레이리스트에서 특정 장르/채널을 검색해 바로 재생합니다", "CONFIG_RADIO_USAGE": "radio [채널명]", "CONFIG_RADIO_OPTION_PLAYLIST": "검색할 플레이리스트 이름", "CONFIG_RADIO_OPTION_CHANNEL": "재생할 라디오 채널명 또는 장르 (예: workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ 올바른 사용법: `{{prefix}}radio [플레이리스트 이름] [채널명]` (예: `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 플레이리스트 `{{playlistName}}`에서 `{{channelQuery}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n파일은 EXTINF 태그가 포함된 M3U 형식이어야 합니다.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", "ERROR_RADIO_LOAD_FAILED": "❌ 라디오 채널 로딩 중 오류가 발생했습니다: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다.", - "MESSAGE_RADIO_SEARCHING": "🔄 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 검색 중...", + "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다", + "MESSAGE_RADIO_SEARCHING": "🔄 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 검색 중..", "MESSAGE_RADIO_PLAYING": "📻 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 재생을 시작합니다!", "MESSAGE_FOOTER_PAGE": "페이지 {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "명령어를 실행한 사용자만 페이지를 이동할 수 있습니다.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "명령어를 실행한 사용자만 이 작업을 승인할 수 있습니다.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "명령어를 실행한 사용자만 페이지를 이동할 수 있습니다", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "명령어를 실행한 사용자만 이 작업을 승인할 수 있습니다", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ 플레이리스트가 이미 존재합니다", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "**`{{name}}`** 이름의 플레이리스트(총 {{count}}곡)가 이미 존재합니다.\n가져온 플레이리스트로 덮어쓰시겠습니까?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "**`{{name}}`** 이름의 플레이리스트(총 {{count}}곡)가 이미 존재합니다.\n현재 대기열의 트랙으로 덮어쓰시겠습니까?", "LABEL_OVERWRITE": "덮어쓰기", "LABEL_CANCEL": "취소", - "MESSAGE_PLAYLIST_OVERWRITING": "플레이리스트 덮어쓰는 중...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 플레이리스트 가져오기가 취소되었습니다.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", + "MESSAGE_PLAYLIST_OVERWRITING": "플레이리스트 덮어쓰는 중..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 플레이리스트 가져오기가 취소되었습니다", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U 플레이리스트 전체는 재생할 수 없습니다. `/radio` 명령어를 사용하여 원하는 라디오 채널을 선택해 주세요.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U 플레이리스트 가져오기는 관리자 또는 DJ 권한이 있는 사용자만 수행할 수 있습니다." -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/embeds.json b/src/locales/ko-KR/embeds.json index 126bae26..ab2cb737 100644 --- a/src/locales/ko-KR/embeds.json +++ b/src/locales/ko-KR/embeds.json @@ -1,19 +1,19 @@ { "DASHBOARD_TITLE": "대시보드", - "DASHBOARD_FINISH_PLAYING": "재생목록 재생이 완료되었습니다.", + "DASHBOARD_FINISH_PLAYING": "재생목록 재생이 완료되었습니다", "DASHBOARD_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{duration}}\n────────────────────\n**음량:** `{{volume}}%`\n**반복:** `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**요청자:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성 채널에 성공적으로 연결했습니다.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성 채널에 성공적으로 연결했습니다", "MAINTAIN_DESCRIPTION": "몇 분 후에 봇이 점검을 시작합니다. 일부 기능을 일시적으로 사용하지 못할 수 있습니다.", "MAINTAIN_TITLE": "⚠️ 유지보수 안내 ⚠️", - "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다.", + "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다", "MESSAGE_COMMAND": "명령어 **{{command}}**", - "MESSAGE_FILTER": "`{{effectName}}`(으)로 재생 효과가 변경되었습니다.", - "NODE_ALL_ACTIVE": "모든 노드가 활성화되었습니다.", + "MESSAGE_FILTER": "`{{effectName}}`(으)로 재생 효과가 변경되었습니다", + "NODE_ALL_ACTIVE": "모든 노드가 활성화되었습니다", "NODE_CONNECTED": "지연시간: **{{ping}}ms**", "NODE_DISCONNECTED": "**연결 끊김**", - "NODE_STATUS_ARGS_ERROR": "❌ 유효한 노드 이름을 입력하세요.", + "NODE_STATUS_ARGS_ERROR": "❌ 유효한 노드 이름을 입력하세요", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "**코어:** {{cores}}\n**시스템 로드:** {{systemLoad}}\n**Lavalink 로드:** {{lavalinkLoad}}", "NODE_STATUS_INFO_TITLE": "🏷️ 정보", @@ -25,7 +25,7 @@ "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**플레이어:** {{playerCount}}개\n**재생 중:** {{playingCount}}개", "NODE_STATUS_TITLE": "🛰️ 노드 상태", "NODE_STATUS_TITLE_2": "🛰️ '{{nodeName}}'의 상태", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}개 노드의 연결이 끊어졌습니다.", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}개 노드의 연결이 끊어졌습니다", "PING_DESCRIPTION": "**봇:** {{botPing}}\n**API:** {{apiPing}}ms", "PING_TITLE": "🛰️ 지연시간", "QUEUE_ADD_PLAYLIST": "재생목록 추가", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**재생 중:**", "QUEUE_EMPTY": "*대기열에 노래가 없습니다*", "QUEUE_HEADER": "**대기열:**" -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/events.json b/src/locales/ko-KR/events.json index ba936214..6fe3ba13 100644 --- a/src/locales/ko-KR/events.json +++ b/src/locales/ko-KR/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ 서버 데이터를 캐시에서 가져올 수 없습니다.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ 봇과 같은 음성 채널에 있지 않습니다.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "지정된 음성 채널 <#{{channelId}}>에 참여 후 요청을 해주세요.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ 음성 채널에 연결되어 있지 않습니다.", - "ERROR_NOT_PLAYING": "❌ 현재 재생 중인 음악이 없습니다.", - "ERROR_REQUIRE_ADMIN": "❌ 해당 명령어는 관리자 권한이 필요합니다.", - "ERROR_REQUIRE_DJ": "❌ 이 명령을 사용하려면 DJ 권한이 필요합니다.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ DM을 보낼 수 없습니다.", - "ERROR_SLASH_NOT_ENABLE": "❌ 슬래시 명령이 활성화되어 있지 않습니다.", - "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다.", - "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다.", - "MESSAGE_MUSIC_SHUFFLE": "✅ 재생목록을 섞었습니다.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ 서버 데이터를 캐시에서 가져올 수 없습니다", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ 봇과 같은 음성 채널에 있지 않습니다", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "지정된 음성 채널 <#{{channelId}}>에 참여 후 요청을 해주세요", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ 음성 채널에 연결되어 있지 않습니다", + "ERROR_NOT_PLAYING": "❌ 현재 재생 중인 음악이 없습니다", + "ERROR_REQUIRE_ADMIN": "❌ 해당 명령어는 관리자 권한이 필요합니다", + "ERROR_REQUIRE_DJ": "❌ 이 명령을 사용하려면 DJ 권한이 필요합니다", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ DM을 보낼 수 없습니다", + "ERROR_SLASH_NOT_ENABLE": "❌ 슬래시 명령이 활성화되어 있지 않습니다", + "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다", + "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다", + "MESSAGE_MUSIC_SHUFFLE": "✅ 재생목록을 섞었습니다", "MESSAGE_NOW_PLAYING_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{label}}\n", "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 페이지 {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 선택하세요.", + "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 선택하세요", "LOOP_MODE_OFF": "끄기", "LOOP_MODE_SINGLE": "한 곡 반복", "LOOP_MODE_ALL": "전체 반복", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다.", - "MESSAGE_SET_LOOP_MODE": "✅ 반복 모드를 `{{mode}}`(으)로 설정했습니다.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다", + "MESSAGE_SET_LOOP_MODE": "✅ 반복 모드를 `{{mode}}`(으)로 설정했습니다", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 이 채널에서는 명령을 실행할 수 없습니다. <#{{channelId}}> 채널을 사용하세요." -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 07805056..c44d5cbe 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Удалить пользователя из чёрного списка", "CONFIG_BLACKLIST_OPTION_LIST": "Показать список пользователей в чёрном списке", "CONFIG_BLACKLIST_OPTION_USER": "Пользователь для добавления или удаления из чёрного списка", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже в чёрном списке.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> нет в чёрном списке.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже в чёрном списке", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> нет в чёрном списке", "MESSAGE_BLACKLIST_LIST_TITLE": "Текущий чёрный список", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст", "CONFIG_CLEAR_DESCRIPTION": "Очистить очередь музыки", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Переместить панель управления вниз", @@ -56,9 +56,9 @@ "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Ссылка или название трека для немедленного воспроизведения", "CONFIG_PLAYLAST_DESCRIPTION": "Повторно воспроизвести текущий или последний трек", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно сыгранный трек не найден.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова в очереди.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно сыгранный трек не найден", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова в очереди", "CONFIG_QUEUE_DESCRIPTION": "Показать текущий плейлист", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Выбрать трек для удаления из плейлиста", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [громкость]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Громкость по умолчанию (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Ошибка команды. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди сейчас нет треков.", - "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди сейчас нет треков", + "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится", "ERROR_SKIP_NOT_REQUESTER": "❌ Пропустить этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", "ERROR_SEEK_NOT_REQUESTER": "❌ Перематывать этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Ставить этот трек на паузу может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу", "ERROR_PLAY_MUSIC": "❌ Сервис испытывает проблемы, попробуйте позже. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Ничего не найдено. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", - "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена.", - "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев.", - "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев.", - "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя, которого хотите сделать диджеем.", - "MESSAGE_DJ_NO_BOTS": "Ботов нельзя назначать диджеями.", - "MESSAGE_DJ_STATIC_MODE": "В статическом режиме права DJ настраиваются только через файл конфигурации.", - "MESSAGE_DJ_NO_PLAYER": "Нет плеера, воспроизводящего сейчас музыку.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}> назначен диджеем.", + "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло", + "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена", + "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев", + "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев", + "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя, которого хотите сделать диджеем", + "MESSAGE_DJ_NO_BOTS": "Ботов нельзя назначать диджеями", + "MESSAGE_DJ_STATIC_MODE": "В статическом режиме права DJ настраиваются только через файл конфигурации", + "MESSAGE_DJ_NO_PLAYER": "Нет плеера, воспроизводящего сейчас музыку", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> назначен диджеем", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ покинул канал! Автоматически выбран новый DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Список диджеев", "MESSAGE_DJ_LIST_ADMINS": "Администраторы", "MESSAGE_DJ_LIST_ROLE_DJS": "Назначенные диджеи", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Ситуативные диджеи", - "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались.", + "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались", "MESSAGE_DJ_LIST_ERROR": "❌ Не удалось загрузить список диджеев. Попробуйте снова.", - "MESSAGE_DJ_ROLE_NOT_SET": "Постоянный DJ не назначен.", - "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден.", + "MESSAGE_DJ_ROLE_NOT_SET": "Постоянный DJ не назначен", + "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден", "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "Выберите эффект", - "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена.", + "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена", "MESSAGE_HELP_SELECT_LIST": "Выберите команду для получения справки: ⬇️", "MESSAGE_HELP_SELECT_MODE": "Выберите команду", - "MESSAGE_JOIN_SUCCESS": "✅ Подключился к голосовому каналу.", + "MESSAGE_JOIN_SUCCESS": "✅ Подключился к голосовому каналу", "MESSAGE_LANG_ARGS_ERROR": "Неверный языковой код, доступные языки: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Доступные языки: {{langList}}", "MESSAGE_LANG_SUCCESS": "✅ Язык изменён на `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал.", + "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал", "REPEAT_MODE_OFF": "ВЫКЛ", "REPEAT_MODE_SINGLE": "ОДИНОЧНЫЙ", "REPEAT_MODE_ALL": "ВСЕ", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Все", "LOOP_SELECT_PLACEHOLDER": "Выберите режим повтора", "LOOP_SELECT_DESCRIPTION": "Установить режим повтора: {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Режим повтора теперь `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена.", + "MESSAGE_LOOP_MODE": "✅ Режим повтора теперь `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена", "MESSAGE_MOVE_WRONG_INDEX": "❌ Укажите позиции в корректном диапазоне. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nи ещё {{length}} треков", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Сохранить трек", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Сейчас играет: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Сейчас играет: {{title}} | Поставил: {{requester}}", "UNKNOWN_USER": "Неизвестно", - "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название трека или ссылку.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Трек добавлен в очередь.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено.", + "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название трека или ссылку", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Трек добавлен в очередь", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено", "MESSAGE_PLAY_SELECT_DURATION": "Длительность: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Выберите музыку", - "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено.", - "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить трек.", + "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено", + "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить трек", "MESSAGE_REMOVE_INSTRUCTION": "Выберите номер трека от **1** до **{{length}}**, чтобы **удалить** его, или введите любой другой номер для отмены. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста.", - "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена.", - "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста", + "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена", + "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено", "MESSAGE_SEEK_ARGS_ERROR": "❌ Неверный формат времени.\n(**`пример: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Указанная позиция (`{{duration}}`) больше длительности трека\nПерехожу к следующему треку.", - "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан.", - "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку.", - "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен.", - "MESSAGE_STOP_SUCCESS": "✅ Воспроизведение остановлено, очередь очищена.", + "MESSAGE_SEEK_SKIP": "✅ Указанная позиция (`{{duration}}`) больше длительности трека\nПерехожу к следующему треку", + "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан", + "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку", + "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен", + "MESSAGE_STOP_SUCCESS": "✅ Воспроизведение остановлено, очередь очищена", "MESSAGE_VOLUME_ARGS_ERROR": "Текущая громкость: **{{volume}}** 🔊\n**Чтобы изменить громкость, введите число от `1` до `{{maxVolume}}`.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Введите число от `1` до `{{maxVolume}}` для изменения громкости.**", "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\n**Выберите желаемую громкость:**", - "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена.", + "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Громкость по умолчанию для этого сервера изменена на **{{volume}}%**! (Макс: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию", "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас в ней **{{current}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в ней **{{total}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за лимита очереди (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за глобального лимита (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за лимита очереди (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за глобального лимита (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "ID сервера", "MESSAGE_SERVER_GUILD": "Сервер", "MESSAGE_SERVER_MEMBERS": "Участники", @@ -197,74 +197,74 @@ "UNIT_MB": "МБ", "UNIT_GB": "ГБ", "UNIT_TB": "ТБ", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Чёрный список ещё не загружен, попробуйте позже.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Чёрный список ещё не загружен, попробуйте позже", "HELP_SELECT_MUSIC_PLACEHOLDER": "Выберите музыкальную команду", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите утилиту", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите служебную команду", "HELP_COMMAND_ALIASES": "Алиасы: {{aliases}}", "HELP_COMMAND_NONE": "Нет", "LABEL_FILTER_CLEAR": "Очистить фильтры", - "CONFIG_PLAYLIST_DESCRIPTION": "Управление плейлистами: сохранение, загрузка, удаление и импорт из M3U.", + "CONFIG_PLAYLIST_DESCRIPTION": "Управление плейлистами: сохранение, загрузка, удаление и импорт из M3U", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Сохранить текущую очередь в плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Загрузить и воспроизвести сохраненный плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Показать список всех плейлистов на сервере.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Показать список треков в плейлисте.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Удалить плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Сохранить текущую очередь в плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Загрузить и воспроизвести сохраненный плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Показать список всех плейлистов на сервере", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Показать список треков в плейлисте", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Удалить плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста", "CONFIG_PLAYLIST_OPTION_NAME": "Имя плейлиста", "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (необязательно)", "CONFIG_PLAYLIST_OPTION_URL": "Ссылка на скачивание файла M3U", "CONFIG_PLAYLIST_OPTION_INDEX": "Номер трека для удаления (начиная с 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Плейлисты пока недоступны, попробуйте позже.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Пожалуйста, укажите название плейлиста.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U (код ошибки {{status}}).", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Плейлисты пока недоступны, попробуйте позже", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Пожалуйста, укажите название плейлиста", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U (код ошибки {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Загрузка M3U заблокирована политикой безопасности. Используйте общедоступный HTTPS URL размером до {{maxSizeMiB}} МиБ, отвечающий в течение {{timeoutSeconds}} секунд.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Ошибка разбора или сохранения M3U плейлиста. Убедитесь, что формат правильный.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Не удалось удалить трек. Убедитесь, что это правильный номер.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Плейлист `{{name}}` сохранён — {{count}} треков из текущей очереди.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен: добавлено {{added}}, пропущено {{skipped}}.", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Плейлист `{{name}}` сохранён — {{count}} треков из текущей очереди", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен: добавлено {{added}}, пропущено {{skipped}}", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов", "MESSAGE_PLAYLIST_LIST_TITLE": "Плейлисты сервера", "MESSAGE_PLAYLIST_INFO_TITLE": "Информация о плейлисте: {{name}} ({{count}} треков)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и ещё {{remaining}} треков.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` удалён.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` удалён", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний M3U-плейлист импортирован!\nВ плейлист `{{name}}` добавлено {{count}} треков.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Трек №{{index}} удалён из плейлиста `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Трек №{{index}} удалён из плейлиста `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста", "CONFIG_RADIO_USAGE": "radio [название]", "CONFIG_RADIO_OPTION_PLAYLIST": "Название плейлиста для поиска", "CONFIG_RADIO_OPTION_CHANNEL": "Название канала или жанр для воспроизведения (например, workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Использование: `{{prefix}}radio [название плейлиста] [название канала]` (например, `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ В плейлисте `{{playlistName}}` не найден радиоканал, соответствующий `{{channelQuery}}`.\nДоступные примеры: {{samples}} и др.", "ERROR_RADIO_NOT_READY": "❌ Радио пока не настроено.\nСначала импортируйте радио-плейлист командой `/playlist import {{name}} `.\nФайл должен быть в формате M3U с тегами EXTINF.", "ERROR_RADIO_NO_MATCH": "❌ Радиоканал по запросу `{{query}}` не найден.\nДоступные варианты: {{samples}} и др.", "ERROR_RADIO_LOAD_FAILED": "❌ Ошибка загрузки радиоканала: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Поиск радиоканала **{{title}}** в плейлисте **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Поиск радиоканала **{{title}}** в плейлисте **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Начало воспроизведения радиоканала **{{title}}** из плейлиста **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Страница {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Только пользователь, вызвавший команду, может переключать страницы.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Только пользователь, вызвавший команду, может подтвердить это действие.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Только пользователь, вызвавший команду, может переключать страницы", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Только пользователь, вызвавший команду, может подтвердить это действие", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Плейлист уже существует", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "Плейлист с именем **`{{name}}`** (всего {{count}} треков) уже существует.\nХотите перезаписать его импортированным плейлистом?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "Плейлист с именем **`{{name}}`** (всего {{count}} треков) уже существует.\nХотите перезаписать его текущей очередью?", "LABEL_OVERWRITE": "Перезаписать", "LABEL_CANCEL": "Отмена", - "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменён.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", + "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменён", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U-плейлисты нельзя добавлять в очередь целиком. Используйте команду `/radio` для выбора радиостанции.", diff --git a/src/locales/ru-RU/embeds.json b/src/locales/ru-RU/embeds.json index 5b78f87f..3ec3a2ee 100644 --- a/src/locales/ru-RU/embeds.json +++ b/src/locales/ru-RU/embeds.json @@ -4,16 +4,16 @@ "DASHBOARD_SUBTITLE": "Автор: **{{author}}**\nДлительность: **{{duration}}**\n────────────────────\nГромкость: `{{volume}}%`\nПовтор: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Заказал:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Ситуативный DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу", "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время часть функций будет недоступна.", "MAINTAIN_TITLE": "⚠️ Техническое обслуживание ⚠️", - "MESSAGE_BLACKLIST": "В голосовом канале есть пользователи из чёрного списка.", + "MESSAGE_BLACKLIST": "В голосовом канале есть пользователи из чёрного списка", "MESSAGE_COMMAND": "Команда **{{command}}**", "MESSAGE_FILTER": "Установлен фильтр: **{{effectName}}**", "NODE_ALL_ACTIVE": "Все узлы активны", "NODE_CONNECTED": "Задержка: **{{ping}}мс**", "NODE_DISCONNECTED": "**ОТКЛЮЧЕН**", - "NODE_STATUS_ARGS_ERROR": "❌ Укажите корректное имя узла.", + "NODE_STATUS_ARGS_ERROR": "❌ Укажите корректное имя узла", "NODE_STATUS_CPU_TITLE": "⚙️ ЦП", "NODE_STATUS_CPU_VALUE": "Ядра: **{{cores}}**\nНагрузка системы: **{{systemLoad}}**\nНагрузка Lavalink: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ ИНФОРМАЦИЯ", diff --git a/src/locales/ru-RU/events.json b/src/locales/ru-RU/events.json index 7d9a5a61..c69a4dda 100644 --- a/src/locales/ru-RU/events.json +++ b/src/locales/ru-RU/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в голосовой канал <#{{channelId}}> и позовите меня.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу.", - "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится.", - "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора.", - "ERROR_REQUIRE_DJ": "❌ Для этой команды требуются права DJ.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Я не могу отправить вам личное сообщение.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash-команды не включены.", - "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Бот вышел из канала.", - "MESSAGE_BOT_STOP": "✅ Бот остановлен.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Очередь перемешана.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в голосовой канал <#{{channelId}}> и позовите меня", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу", + "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится", + "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора", + "ERROR_REQUIRE_DJ": "❌ Для этой команды требуются права DJ", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Я не могу отправить вам личное сообщение", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash-команды не включены", + "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Бот вышел из канала", + "MESSAGE_BOT_STOP": "✅ Бот остановлен", + "MESSAGE_MUSIC_SHUFFLE": "✅ Очередь перемешана", "MESSAGE_NOW_PLAYING_SUBTITLE": "Автор: **{{author}}**\nДлительность: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Сейчас играет: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Страница {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Выберите режим повтора трека.", + "MESSAGE_SELECT_LOOP_MODE": "Выберите режим повтора трека", "LOOP_MODE_OFF": "Выкл", "LOOP_MODE_SINGLE": "Один трек", "LOOP_MODE_ALL": "Все", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения", "MESSAGE_SET_LOOP_MODE": "✅ Выбран режим повтора: `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ В этом канале нельзя использовать команды. Пожалуйста, перейдите в текстовый канал <#{{channelId}}>." } diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 098ea7a3..a2c49cf4 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Ukloni korisnika sa crne liste", "CONFIG_BLACKLIST_OPTION_LIST": "Prikaži sve korisnike na crnoj listi", "CONFIG_BLACKLIST_OPTION_USER": "Korisnik koji će biti dodat ili uklonjen sa crne liste", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na crnoj listi.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na crnoj listi", "MESSAGE_BLACKLIST_LIST_TITLE": "Crna lista", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Crna lista je trenutno prazna.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Crna lista je trenutno prazna", "CONFIG_CLEAR_DESCRIPTION": "Obriši svu muziku iz reda", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Premesti kontrolnu tablu na dno", @@ -56,9 +56,9 @@ "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Link pesme ili naziv pesme koja će se pustiti prva", "CONFIG_PLAYLAST_DESCRIPTION": "Ponovo pusti trenutnu ili poslednju puštenu pesmu", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Ova komanda se može koristiti samo kada je red prazan.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Nije pronađena nedavno puštena pesma.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Pesma je ponovo dodata u red.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Ova komanda se može koristiti samo kada je red prazan", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Nije pronađena nedavno puštena pesma", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Pesma je ponovo dodata u red", "CONFIG_QUEUE_DESCRIPTION": "Prikaži trenutnu listu pesama", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Izaberi pesmu za uklanjanje iz liste pesama", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [jačina]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Podrazumevana jačina zvuka (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Greška u komandi. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ U redu trenutno nema pesama.", - "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ U redu trenutno nema pesama", + "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika", "ERROR_SKIP_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je preskoči. Administratori i DJ-evi (ako je omogućeno) takođe mogu da preskoče.", "ERROR_SEEK_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je premotava. Administratori i DJ-evi (ako je omogućeno) takođe mogu da premotavaju.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je pauzira. Administratori i DJ-evi (ako je omogućeno) takođe mogu da pauziraju.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Ne mogu da se pridružim govornom kanalu.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Ne mogu da se pridružim govornom kanalu", "ERROR_PLAY_MUSIC": "❌ Servis ima problema, pokušaj ponovo. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Nema rezultata. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", - "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana.", - "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve.", - "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve.", - "MESSAGE_DJ_MENTION_USER": "Molim te označi korisnika da bi ga dodao kao DJ-a.", - "MESSAGE_DJ_NO_BOTS": "Ne mogu dodati botove kao DJ-eve.", - "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu.", - "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}> je dodat kao DJ.", + "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo", + "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana", + "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve", + "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve", + "MESSAGE_DJ_MENTION_USER": "Molim te označi korisnika da bi ga dodao kao DJ-a", + "MESSAGE_DJ_NO_BOTS": "Ne mogu dodati botove kao DJ-eve", + "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu", + "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> je dodat kao DJ", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ je napustio kanal! Automatski izabran novi DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Lista DJ-eva", "MESSAGE_DJ_LIST_ADMINS": "Administratori", "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi po ulozi", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamični DJ-evi", - "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva.", + "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva", "MESSAGE_DJ_LIST_ERROR": "❌ Neuspešno učitavanje liste DJ-eva. Molim te pokušaj ponovo.", - "MESSAGE_DJ_ROLE_NOT_SET": "Nije podešena DJ uloga.", - "MESSAGE_FILTER_NOT_FOUND": "❌ Efekat nije pronađen.", + "MESSAGE_DJ_ROLE_NOT_SET": "Nije podešena DJ uloga", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekat nije pronađen", "MESSAGE_FILTER_SELECT_LIST": "Izaberi muzički filter: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "Izaberi efekat", - "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena.", + "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena", "MESSAGE_HELP_SELECT_LIST": "Izaberi komandu da bi dobio pomoć. ⬇️", "MESSAGE_HELP_SELECT_MODE": "Izaberi komandu", - "MESSAGE_JOIN_SUCCESS": "✅ Pridružio sam se glasovnom kanalu.", + "MESSAGE_JOIN_SUCCESS": "✅ Pridružio sam se glasovnom kanalu", "MESSAGE_LANG_ARGS_ERROR": "Pogrešan jezički kod. Dostupni jezici: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Dostupni jezici: {{langList}}", "MESSAGE_LANG_SUCCESS": "✅ Jezik promenjen na `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Napustio sam glasovni kanal.", + "MESSAGE_LEAVE_SUCCESS": "✅ Napustio sam glasovni kanal", "REPEAT_MODE_OFF": "ISKLJUČENO", "REPEAT_MODE_SINGLE": "POJEDINAČNO", "REPEAT_MODE_ALL": "SVE", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Sve", "LOOP_SELECT_PLACEHOLDER": "Izaberi režim ponavljanja", "LOOP_SELECT_DESCRIPTION": "Postavi režim ponavljanja na {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Režim ponavljanja je sada `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen.", + "MESSAGE_LOOP_MODE": "✅ Režim ponavljanja je sada `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen", "MESSAGE_MOVE_WRONG_INDEX": "❌ Molim te unesi važeći opseg indeksa. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\ni {{length}} drugih pesama", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Sačuvaj pesmu", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Trenutno svira: {{title}} | Zatražio {{requester}}", "UNKNOWN_USER": "Nepoznato", - "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Unesi naziv pesme ili link.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Pesma dodata u red.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja.", + "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Unesi naziv pesme ili link", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Pesma dodata u red", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja", "MESSAGE_PLAY_SELECT_DURATION": "Trajanje: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Izaberi muziku", - "MESSAGE_REMOVE_CANCEL": "✅ Uklanjanje otkazano.", - "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo.", + "MESSAGE_REMOVE_CANCEL": "✅ Uklanjanje otkazano", + "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo", "MESSAGE_REMOVE_INSTRUCTION": "Unesi broj pesme od **1** do **{{length}}** za **uklanjanje**, ili bilo koji drugi broj za otkazivanje. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Red je prazan nakon trenutne pesme.", - "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Muzika je nastavljena.", - "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Red je prazan nakon trenutne pesme", + "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Muzika je nastavljena", + "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena", "MESSAGE_SEEK_ARGS_ERROR": "❌ Nevažeći format za ciljno vreme.\n(**`npr: 3m20s, 1m 50s, 1:20:55, 5:20`**)", "MESSAGE_SEEK_SKIP": "✅ Navedena pozicija (`{{duration}}`) je duža od trajanja pesme.\nPrelazim na sledeću pesmu.", - "MESSAGE_SEEK_SUCCESS": "✅ Premotano na `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Lista pesama izmešana.", - "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo.", - "MESSAGE_SKIP_SUCCESS": "✅ Pesma preskočena.", - "MESSAGE_STOP_SUCCESS": "✅ Reprodukcija zaustavljena, red obrisan.", + "MESSAGE_SEEK_SUCCESS": "✅ Premotano na `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Lista pesama izmešana", + "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo", + "MESSAGE_SKIP_SUCCESS": "✅ Pesma preskočena", + "MESSAGE_STOP_SUCCESS": "✅ Reprodukcija zaustavljena, red obrisan", "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\n**Molim te izaberi jačinu koju želiš da podesiš:**", - "MESSAGE_VOLUME_SAME": "❌ Ta jačina je već podešena.", + "MESSAGE_VOLUME_SAME": "❌ Ta jačina je već podešena", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Podrazumevana jačina zvuka je promenjena na **{{volume}}%** za ovaj server! (Maks: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nZa promenu unesi broj od `1` do `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Za promenu podrazumevane jačine unesi broj od `1` do `{{maxVolume}}`.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nZa promenu unesi broj od `1` do `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Za promenu podrazumevane jačine unesi broj od `1` do `{{maxVolume}}`", "ERROR_QUEUE_LIMIT_REACHED": "❌ Dostigao si limit reda! Trenutno imaš **{{current}}** pesama u redu (limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Dostignut je globalni limit reda! Red trenutno ima **{{total}}** pesama (globalni limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog globalnog limita reda (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog globalnog limita reda (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "ID servera", "MESSAGE_SERVER_GUILD": "Server", "MESSAGE_SERVER_MEMBERS": "Članovi", @@ -199,74 +199,74 @@ "UNIT_TB": "TB", "ERROR_BLACKLIST_NOT_INITIALIZED": "Crna lista još nije učitana. Pokušaj ponovo kasnije.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi muzičku komandu", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberi utility komandu", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberite servisnu komandu", "HELP_COMMAND_ALIASES": "Aliasi: {{aliases}}", "HELP_COMMAND_NONE": "Nijedan", "LABEL_FILTER_CLEAR": "Obriši filtere", - "CONFIG_PLAYLIST_DESCRIPTION": "Sačuvaj, učitaj, uredi plejliste ili ih uvezi iz M3U URL-a.", + "CONFIG_PLAYLIST_DESCRIPTION": "Sačuvaj, učitaj, uredi plejliste ili ih uvezi iz M3U URL-a", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Sačuvaj trenutni red za reprodukciju kao plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Učitaj i pusti sačuvanu plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Prikaži sve plejliste na serveru.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Prikaži pesme u plejlisti.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Obriši plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uvezi plejlistu sa udaljenog M3U fajla.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Ukloni pesmu iz plejliste.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Sačuvaj trenutni red za reprodukciju kao plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Učitaj i pusti sačuvanu plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Prikaži sve plejliste na serveru", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Prikaži pesme u plejlisti", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Obriši plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uvezi plejlistu sa udaljenog M3U fajla", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Ukloni pesmu iz plejliste", "CONFIG_PLAYLIST_OPTION_NAME": "Ime plejliste", "CONFIG_PLAYLIST_OPTION_SEARCH": "Naziv određene pesme za pretragu u plejlisti (Opciono)", "CONFIG_PLAYLIST_OPTION_URL": "Link za preuzimanje M3U fajla", "CONFIG_PLAYLIST_OPTION_INDEX": "Indeks pesme za uklanjanje (počinje od 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Menadžer plejlisti nije inicijalizovan.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Molimo unesite ispravnu potkomandu (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Nepoznata potkomanda.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Muzika se ne pušta ili je red prazan.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Molimo navedite ime plejliste.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Neuspešno čuvanje plejliste.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Plejlista `{{name}}` nije pronađena ili je prazna.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Pesma `{{searchQuery}}` nije pronađena u plejlisti `{{name}}`.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Menadžer plejlisti nije inicijalizovan", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Molimo unesite ispravnu potkomandu (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Nepoznata potkomanda", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Muzika se ne pušta ili je red prazan", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Molimo navedite ime plejliste", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Neuspešno čuvanje plejliste", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Plejlista `{{name}}` nije pronađena ili je prazna", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Pesma `{{searchQuery}}` nije pronađena u plejlisti `{{name}}`", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Preuzimanje M3U fajla nije uspelo. (HTTP status: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Preuzimanje M3U fajla je blokirano bezbednosnim pravilima. Koristite javni HTTPS URL do {{maxSizeMiB}} MiB koji odgovara u roku od {{timeoutSeconds}} sekundi.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Plejlista može sadržati najviše {{limit}} pesama.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Plejlista može sadržati najviše {{limit}} pesama", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Neuspešno parsiranje i čuvanje M3U plejliste. Proverite da li je ispravno formatirana.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Uklanjanje pesme nije uspelo. Proverite da li je indeks ispravan.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana: dodato {{added}}, preskočeno {{skipped}}.", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana: dodato {{added}}, preskočeno {{skipped}}", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru", "MESSAGE_PLAYLIST_LIST_TITLE": "Plejliste na serveru", "MESSAGE_PLAYLIST_INFO_TITLE": "Informacije o plejlisti: {{name}} ({{count}} pesama)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*i još {{remaining}} pesama.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Plejlista `{{name}}` je obrisana.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Plejlista `{{name}}` je obrisana", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Eksterna M3U plejlista je uvezena!\nRegistrovano {{count}} pesama u plejlisti `{{name}}`.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Uklonjena pesma #{{index}} iz plejliste `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Pretraži i pusti određeni žanr/kanal sa uvezene radio plejliste.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Uklonjena pesma #{{index}} iz plejliste `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Pretraži i pusti određeni žanr/kanal sa uvezene radio plejliste", "CONFIG_RADIO_USAGE": "radio [kanal]", "CONFIG_RADIO_OPTION_PLAYLIST": "Naziv plejliste za pretragu", "CONFIG_RADIO_OPTION_CHANNEL": "Radio kanal ili žanr za puštanje (npr. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Upotreba: `{{prefix}}radio [naziv plejliste] [naziv kanala]` (npr. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Radio kanal koji odgovara `{{channelQuery}}` u plejlisti `{{playlistName}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\nFajl mora biti u M3U formatu sa EXTINF tagovima.", "ERROR_RADIO_NO_MATCH": "❌ Radio kanal koji odgovara `{{query}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", "ERROR_RADIO_LOAD_FAILED": "❌ Greška pri učitavanju radio kanala: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Pretraživanje radio kanala **{{title}}** u plejlisti **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Pretraživanje radio kanala **{{title}}** u plejlisti **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Započeto puštanje radio kanala **{{title}}** sa plejliste **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Strana {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Samo korisnik koji je pokrenuo komandu može menjati stranice.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Samo korisnik koji je pokrenuo komandu može potvrditi ovu akciju.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Samo korisnik koji je pokrenuo komandu može menjati stranice", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Samo korisnik koji je pokrenuo komandu može potvrditi ovu akciju", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Plejlista već postoji", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "Plejlista sa imenom **`{{name}}`** (ukupno {{count}} pesama) već postoji.\nDa li želite da je zamenite uvezenom plejlistom?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "Plejlista pod nazivom **`{{name}}`** ({{count}} pesama) već postoji.\nDa li želite da je zamenite trenutnim redom pesama?", "LABEL_OVERWRITE": "Zameni", "LABEL_CANCEL": "Otkaži", - "MESSAGE_PLAYLIST_OVERWRITING": "Zamenjivanje plejliste...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Uvoz plejliste je otkazan.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", + "MESSAGE_PLAYLIST_OVERWRITING": "Zamenjivanje plejliste..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Uvoz plejliste je otkazan", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U plejliste se ne mogu staviti u red u celosti. Koristite komandu `/radio` za izbor stanice.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Uvoz M3U plejlisti je ograničen samo na admine i DJ-eve." -} \ No newline at end of file +} diff --git a/src/locales/sr-RS/embeds.json b/src/locales/sr-RS/embeds.json index d27edc74..15b7831c 100644 --- a/src/locales/sr-RS/embeds.json +++ b/src/locales/sr-RS/embeds.json @@ -4,16 +4,16 @@ "DASHBOARD_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{duration}}**\n────────────────────\nJačina zvuka: `{{volume}}%`\nPonavljanje: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Zatražio:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Uspešno povezan sa govornim kanalom.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Uspešno povezan sa govornim kanalom", "MAINTAIN_DESCRIPTION": "Bot će uskoro ući u održavanje. Tokom tog vremena, neke funkcije mogu biti privremeno nedostupne.", "MAINTAIN_TITLE": "⚠️ Obaveštenje o održavanju ⚠️", - "MESSAGE_BLACKLIST": "Postoje korisnici na crnoj listi u govornom kanalu.", + "MESSAGE_BLACKLIST": "Postoje korisnici na crnoj listi u govornom kanalu", "MESSAGE_COMMAND": "Komanda **{{command}}**", "MESSAGE_FILTER": "Režim filtera postavljen na **{{effectName}}**", "NODE_ALL_ACTIVE": "Svi nodovi su aktivni", "NODE_CONNECTED": "Kašnjenje: **{{ping}}ms**", "NODE_DISCONNECTED": "**ISKLJUČEN**", - "NODE_STATUS_ARGS_ERROR": "❌ Molim te unesi važeći naziv noda.", + "NODE_STATUS_ARGS_ERROR": "❌ Molim te unesi važeći naziv noda", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Jezgra: **{{cores}}**\nSistemsko opterećenje: **{{systemLoad}}**\nLavalink opterećenje: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ INFO", diff --git a/src/locales/sr-RS/events.json b/src/locales/sr-RS/events.json index 04f26327..d0b758f7 100644 --- a/src/locales/sr-RS/events.json +++ b/src/locales/sr-RS/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Ne mogu da dobijem podatke servera iz keša.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Nisi na istom govornom kanalu kao ja.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Molim te pridruži se određenom govornom kanalu <#{{channelId}}> i pozovi me.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Nisi povezan na govorni kanal.", - "ERROR_NOT_PLAYING": "❌ Trenutno se ne pušta muzika.", - "ERROR_REQUIRE_ADMIN": "❌ Ova komanda zahteva administratorske privilegije.", - "ERROR_REQUIRE_DJ": "❌ Ova komanda zahteva DJ privilegije.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Ne mogu da ti pošaljem privatnu poruku.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash komanda nije omogućena.", - "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal.", - "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Red je izmešan.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Ne mogu da dobijem podatke servera iz keša", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Nisi na istom govornom kanalu kao ja", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Molim te pridruži se određenom govornom kanalu <#{{channelId}}> i pozovi me", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Nisi povezan na govorni kanal", + "ERROR_NOT_PLAYING": "❌ Trenutno se ne pušta muzika", + "ERROR_REQUIRE_ADMIN": "❌ Ova komanda zahteva administratorske privilegije", + "ERROR_REQUIRE_DJ": "❌ Ova komanda zahteva DJ privilegije", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Ne mogu da ti pošaljem privatnu poruku", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash komanda nije omogućena", + "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal", + "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen", + "MESSAGE_MUSIC_SHUFFLE": "✅ Red je izmešan", "MESSAGE_NOW_PLAYING_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Strana {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Izaberi režim ponavljanja pesme.", + "MESSAGE_SELECT_LOOP_MODE": "Izaberi režim ponavljanja pesme", "LOOP_MODE_OFF": "Isključeno", "LOOP_MODE_SINGLE": "Jedna", "LOOP_MODE_ALL": "Sve", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci", "MESSAGE_SET_LOOP_MODE": "✅ Izabran režim ponavljanja: `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Ne možeš izvršavati komande u ovom kanalu. Molim te koristi <#{{channelId}}> tekstualni kanal." -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 3d21ae7f..b3a98e7b 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Kullanıcıyı kara listeden çıkar", "CONFIG_BLACKLIST_OPTION_LIST": "Kara listedeki tüm kullanıcıları listele", "CONFIG_BLACKLIST_OPTION_USER": "Kara listeye eklenecek veya çıkarılacak kullanıcı", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> kara listede değil.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> kara listede değil", "MESSAGE_BLACKLIST_LIST_TITLE": "Kara Liste", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Kara liste şu anda boş.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Kara liste şu anda boş", "CONFIG_CLEAR_DESCRIPTION": "Kuyruktaki tüm parçaları temizle", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Dashboard'u alta al", @@ -56,9 +56,9 @@ "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Kuyrukta başa alınacak parçanın linki ya da adı", "CONFIG_PLAYLAST_DESCRIPTION": "Mevcut veya en son çalınan parçayı tekrar çal", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Bu komut yalnızca kuyruk boşken kullanılabilir.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Son çalınan parça bulunamadı.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Parça kuyruğa tekrar eklendi.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Bu komut yalnızca kuyruk boşken kullanılabilir", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Son çalınan parça bulunamadı", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Parça kuyruğa tekrar eklendi", "CONFIG_QUEUE_DESCRIPTION": "Şuanki oynatma listesini göster", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Oynatma kuyruğundan silmek için bir parça seçin", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [ses-seviyesi]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Belirlenecek varsayılan ses seviyesi (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Komut hatası. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta şu anda parça yok.", - "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta şu anda parça yok", + "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor", "ERROR_SKIP_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi atlayabilir. Yöneticiler ve DJ'ler (etkinse) de atlayabilir.", "ERROR_SEEK_NOT_REQUESTER": "❌ Bu şarkıda yalnızca isteyen kişi zaman atlayabilir. Yöneticiler ve DJ'ler (etkinse) de zaman atlayabilir.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi duraklatabilir. Yöneticiler ve DJ'ler (etkinse) de duraklatabilir.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Ses kanalına giriş yapamıyorum.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Ses kanalına giriş yapamıyorum", "ERROR_PLAY_MUSIC": "❌ Sistemimiz şuan sorun yaşıyor. Lütfen birazdan tekrar deneyiniz. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Herhangi bir sonuç bulunamadı. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Süre doldu.", - "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi.", - "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var.", - "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir.", - "MESSAGE_DJ_MENTION_USER": "DJ olarak eklemek için bir kullanıcıdan bahsedin.", - "MESSAGE_DJ_NO_BOTS": "Botlar DJ olarak eklenemez.", - "MESSAGE_DJ_STATIC_MODE": "DJ izinleri STATIC modda konfigürasyon üzerinden yönetilir.", - "MESSAGE_DJ_NO_PLAYER": "Aktif müzik çalar bulunamadı.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> zaten bir DJ.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}> başarıyla DJ olarak eklendi.", + "ERROR_TIME_EXPIRED": "❌ Süre doldu", + "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi", + "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var", + "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir", + "MESSAGE_DJ_MENTION_USER": "DJ olarak eklemek için bir kullanıcıdan bahsedin", + "MESSAGE_DJ_NO_BOTS": "Botlar DJ olarak eklenemez", + "MESSAGE_DJ_STATIC_MODE": "DJ izinleri STATIC modda konfigürasyon üzerinden yönetilir", + "MESSAGE_DJ_NO_PLAYER": "Aktif müzik çalar bulunamadı", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> zaten bir DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> başarıyla DJ olarak eklendi", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ kanaldan ayrıldı! Yeni DJ otomatik olarak seçildi: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ Listesi", "MESSAGE_DJ_LIST_ADMINS": "Yöneticiler", "MESSAGE_DJ_LIST_ROLE_DJS": "Rol tabanlı DJ'ler", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamik DJ'ler", - "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok.", + "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok", "MESSAGE_DJ_LIST_ERROR": "❌ DJ listesi yüklenemedi. Lütfen tekrar deneyin.", - "MESSAGE_DJ_ROLE_NOT_SET": "DJ rolü yapılandırılmamış.", - "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt bulunamadı.", + "MESSAGE_DJ_ROLE_NOT_SET": "DJ rolü yapılandırılmamış", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt bulunamadı", "MESSAGE_FILTER_SELECT_LIST": "Bir müzik filtresi seçin: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "Bir efekt seçin", - "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı.", + "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı", "MESSAGE_HELP_SELECT_LIST": "Yardım almak için bir komut seçin. ⬇️", "MESSAGE_HELP_SELECT_MODE": "Bir komut seçin", - "MESSAGE_JOIN_SUCCESS": "✅ Ses kanalına katıldım.", + "MESSAGE_JOIN_SUCCESS": "✅ Ses kanalına katıldım", "MESSAGE_LANG_ARGS_ERROR": "Yanlış dil kodu. Mevcut diller: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Mevcut diller: {{langList}}", "MESSAGE_LANG_SUCCESS": "✅ Dil `{{locale}}` olarak değiştirildi", - "MESSAGE_LEAVE_SUCCESS": "✅ Ses kanalından ayrıldım.", + "MESSAGE_LEAVE_SUCCESS": "✅ Ses kanalından ayrıldım", "REPEAT_MODE_OFF": "KAPALI", "REPEAT_MODE_SINGLE": "TEK", "REPEAT_MODE_ALL": "HEPSİ", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Hepsi", "LOOP_SELECT_PLACEHOLDER": "Döngü modunu seçin", "LOOP_SELECT_DESCRIPTION": "Döngü modunu {{mode}} olarak ayarla", - "MESSAGE_LOOP_MODE": "✅ Döngü `{{mode}}` olarak ayarlandı.", - "MESSAGE_MOVE_SUCCESS": "✅ Şarkı sırası başarıyla değiştirildi.", + "MESSAGE_LOOP_MODE": "✅ Döngü `{{mode}}` olarak ayarlandı", + "MESSAGE_MOVE_SUCCESS": "✅ Şarkı sırası başarıyla değiştirildi", "MESSAGE_MOVE_WRONG_INDEX": "❌ Lütfen geçerli bir dizin aralığı girin. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nve {{length}} diğer şarkı", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Şarkıyı Kaydet", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Şu An Çalıyor: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Şu An Çalıyor: {{title}} | {{requester}} tarafından istendi", "UNKNOWN_USER": "Bilinmiyor", - "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Parça adı veya linki girin.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Parça kuyruğa eklendi.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok.", + "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Parça adı veya linki girin", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Parça kuyruğa eklendi", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok", "MESSAGE_PLAY_SELECT_DURATION": "Süre: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Müziği seçin", - "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi.", - "MESSAGE_REMOVE_FAIL": "❌ Parça kaldırılamadı.", + "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi", + "MESSAGE_REMOVE_FAIL": "❌ Parça kaldırılamadı", "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir parça numarası seçerek **kaldırın** veya iptal için başka bir numara girin. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcut parçadan sonra kuyruk boş.", - "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Müzik devam ettirildi.", - "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcut parçadan sonra kuyruk boş", + "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Müzik devam ettirildi", + "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi", "MESSAGE_SEEK_ARGS_ERROR": "❌ Hedef zaman için geçersiz format.\n(**`örn: 3m20s, 1m 50s, 1:20:55, 5:20`**)", "MESSAGE_SEEK_SKIP": "✅ Belirtilen konum (`{{duration}}`), parçanın süresinden uzun.\nSıradaki parçaya geçiyorum.", - "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Çalma listesi karıştırıldı.", - "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı.", - "MESSAGE_SKIP_SUCCESS": "✅ Parça atlandı.", - "MESSAGE_STOP_SUCCESS": "✅ Çalma durduruldu, kuyruk temizlendi.", + "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Çalma listesi karıştırıldı", + "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı", + "MESSAGE_SKIP_SUCCESS": "✅ Parça atlandı", + "MESSAGE_STOP_SUCCESS": "✅ Çalma durduruldu, kuyruk temizlendi", "MESSAGE_VOLUME_ARGS_ERROR": "Mevcut ses seviyesi: **{{volume}}** 🔊\n**Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", - "MESSAGE_VOLUME_SAME": "❌ Bu ses seviyesi zaten ayarlı.", + "MESSAGE_VOLUME_SAME": "❌ Bu ses seviyesi zaten ayarlı", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Bu sunucu için varsayılan ses seviyesi **{{volume}}%** olarak değiştirildi! (En fazla: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Varsayılan ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Varsayılan ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin", "ERROR_QUEUE_LIMIT_REACHED": "❌ Kuyruk sınırınıza ulaştınız! Şu anda kuyrukta **{{current}}** şarkı var (sınır: **{{limit}}**). Daha fazla eklemek için lütfen bazı şarkıların bitmesini bekleyin.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Küresel kuyruk sınırına ulaşıldı! Kuyrukta şu anda **{{total}}** şarkı var (küresel sınır: **{{limit}}**). Daha fazla eklemek için lütfen bazı şarkıların bitmesini bekleyin.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, kuyruk sınırınız nedeniyle **{{skipped}}** şarkı atlandı (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, küresel sıra sınırı nedeniyle **{{skipped}}** şarkı atlandı (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, kuyruk sınırınız nedeniyle **{{skipped}}** şarkı atlandı (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, küresel sıra sınırı nedeniyle **{{skipped}}** şarkı atlandı (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "Sunucu Kimliği", "MESSAGE_SERVER_GUILD": "Sunucu", "MESSAGE_SERVER_MEMBERS": "Üyeler", @@ -197,76 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste henüz yüklenmedi, sonra tekrar deneyin.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste henüz yüklenmedi, sonra tekrar deneyin", "HELP_SELECT_MUSIC_PLACEHOLDER": "Bir Müzik komutu seçin", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Yardımcı komut seçin", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Hizmet komutu seçin", "HELP_COMMAND_ALIASES": "Takma Adlar: {{aliases}}", "HELP_COMMAND_NONE": "Yok", "LABEL_FILTER_CLEAR": "Filtreleri temizle", - "CONFIG_PLAYLIST_DESCRIPTION": "Çalma listelerini kaydedin, yükleyin, düzenleyin veya M3U bağlantısından içe aktarın.", + "CONFIG_PLAYLIST_DESCRIPTION": "Çalma listelerini kaydedin, yükleyin, düzenleyin veya M3U bağlantısından içe aktarın", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Mevcut sırayı bir çalma listesi olarak kaydeder.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Kaydedilmiş çalma listesini yükler ve oynatır.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Sunucudaki tüm çalma listelerini listeler.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Çalma listesindeki parçaları gösterir.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Çalma listesini siler.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uzak M3U dosyasından çalma listesini içe aktarır.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Çalma listesinden bir parçayı kaldırır.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Mevcut sırayı bir çalma listesi olarak kaydeder", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Kaydedilmiş çalma listesini yükler ve oynatır", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Sunucudaki tüm çalma listelerini listeler", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Çalma listesindeki parçaları gösterir", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Çalma listesini siler", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uzak M3U dosyasından çalma listesini içe aktarır", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Çalma listesinden bir parçayı kaldırır", "CONFIG_PLAYLIST_OPTION_NAME": "Çalma listesi adı", "CONFIG_PLAYLIST_OPTION_SEARCH": "Çalma listesinde aranacak parça adı (İsteğe bağlı)", "CONFIG_PLAYLIST_OPTION_URL": "M3U dosyası indirme bağlantısı", "CONFIG_PLAYLIST_OPTION_INDEX": "Kaldırılacak parça sırası (1'den başlar)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Çalma listesi yöneticisi başlatılamadı.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Lütfen geçerli bir alt komut girin (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Bilinmeyen alt komut.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Şu anda hiçbir şey çalmıyor veya sıra boş.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Lütfen bir çalma listesi adı belirtin.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Çalma listesi kaydedilemedi.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ `{{name}}` adlı çalma listesi bulunamadı veya boş.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ `{{name}}` çalma listesinde `{{searchQuery}}` parçası bulunamadı.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Çalma listesi yöneticisi başlatılamadı", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Lütfen geçerli bir alt komut girin (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Bilinmeyen alt komut", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Şu anda hiçbir şey çalmıyor veya sıra boş", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Lütfen bir çalma listesi adı belirtin", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Çalma listesi kaydedilemedi", + "ERROR_PLAYLIST_NOT_FOUND": "❌ `{{name}}` adlı çalma listesi bulunamadı veya boş", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ `{{name}}` çalma listesinde `{{searchQuery}}` parçası bulunamadı", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ M3U dosyası indirilemedi. (HTTP durumu: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ M3U indirmesi güvenlik politikası tarafından engellendi. {{timeoutSeconds}} saniye içinde yanıt veren ve {{maxSizeMiB}} MiB boyutunu aşmayan herkese açık bir HTTPS URL kullanın.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Bir çalma listesi en fazla {{limit}} parça içerebilir.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Bir çalma listesi en fazla {{limit}} parça içerebilir", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ M3U çalma listesi ayrıştırılamadı ve kaydedilemedi. Formatın doğru olduğundan emin olun.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Parça kaldırılamadı. Geçerli bir indeks girdiğinizden emin olun.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi: {{added}} eklendi, {{skipped}} atlandı.", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi: {{added}} eklendi, {{skipped}} atlandı", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok", "MESSAGE_PLAYLIST_LIST_TITLE": "Sunucu Çalma Listeleri", "MESSAGE_PLAYLIST_INFO_TITLE": "Çalma Listesi Bilgisi: {{name}} ({{count}} parça)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*ve {{remaining}} parça daha.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ `{{name}}` çalma listesi silindi.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ `{{name}}` çalma listesi silindi", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Harici M3U çalma listesi içe aktarıldı!\n`{{name}}` çalma listesine {{count}} parça eklendi.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ `{{name}}` çalma listesinden #{{index}} parça kaldırıldı.", - "CONFIG_RADIO_DESCRIPTION": "İçe aktarılan radyo çalma listesinden belirli bir türü/kanalı arayın ve oynatın.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ `{{name}}` çalma listesinden #{{index}} parça kaldırıldı", + "CONFIG_RADIO_DESCRIPTION": "İçe aktarılan radyo çalma listesinden belirli bir türü/kanalı arayın ve oynatın", "CONFIG_RADIO_USAGE": "radio [kanal]", "CONFIG_RADIO_OPTION_PLAYLIST": "Aranacak çalma listesi adı", "CONFIG_RADIO_OPTION_CHANNEL": "Oynatılacak radyo kanalı veya türü (örn. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Kullanım: `{{prefix}}radio [çalma listesi adı] [kanal adı]` (örn. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ `{{playlistName}}` çalma listesinde `{{channelQuery}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\nDosya, EXTINF etiketleri içeren bir M3U formatında olmalı.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", "ERROR_RADIO_LOAD_FAILED": "❌ Radyo kanalı yüklenirken hata oluştu: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 **{{playlist}}** çalma listesindeki **{{title}}** radyo kanalı aranıyor...", + "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 **{{playlist}}** çalma listesindeki **{{title}}** radyo kanalı aranıyor..", "MESSAGE_RADIO_PLAYING": "📻 **{{playlist}}** çalma listesinden **{{title}}** radyo kanalı oynatılmaya başlandı!", "MESSAGE_FOOTER_PAGE": "Sayfa {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Sadece komutu çalıştıran kullanıcı sayfaları değiştirebilir.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Sadece komutu çalıştıran kullanıcı bu işlemi onaylayabilir.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Sadece komutu çalıştıran kullanıcı sayfaları değiştirebilir", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Sadece komutu çalıştıran kullanıcı bu işlemi onaylayabilir", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Çalma Listesi Zaten Var", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "**`{{name}}`** adında bir çalma listesi (toplam {{count}} parça) zaten mevcut.\nİçe aktarılan çalma listesiyle değiştirmek istiyor musunuz?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "**`{{name}}`** isimli oynatma listesi (toplam {{count}} şarkı) zaten mevcut.\nMevcut sıradaki şarkılarla değiştirmek istiyor musunuz?", "LABEL_OVERWRITE": "Üzerine Yaz", "LABEL_CANCEL": "İptal", - "MESSAGE_PLAYLIST_OVERWRITING": "Çalma listesinin üzerine yazılıyor...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Çalma listesi içe aktarma iptal edildi.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", + "MESSAGE_PLAYLIST_OVERWRITING": "Çalma listesinin üzerine yazılıyor..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Çalma listesi içe aktarma iptal edildi", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu", "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U çalma listeleri tamamen sıraya eklenemez. Lütfen belirli bir istasyon seçmek için `/radio` komutunu kullanın.", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U çalma listesi içe aktarma yalnızca Yöneticiler ve DJ'ler ile sınırlıdır." -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/embeds.json b/src/locales/tr-TR/embeds.json index c9402ff8..06d2e007 100644 --- a/src/locales/tr-TR/embeds.json +++ b/src/locales/tr-TR/embeds.json @@ -1,19 +1,19 @@ { "DASHBOARD_TITLE": "Panel", - "DASHBOARD_FINISH_PLAYING": "Kuyruğun çalınması tamamlandı.", + "DASHBOARD_FINISH_PLAYING": "Kuyruğun çalınması tamamlandı", "DASHBOARD_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{duration}}**\n────────────────────\nSes Yüksekliği: `{{volume}}%`\nDöngü: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Talep Eden:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı", "MAINTAIN_DESCRIPTION": "Bot birkaç dakika içinde bakım moduna alınacaktır. Bu süre boyunca bazı özellikler geçici olarak kullanılamayabilir.", "MAINTAIN_TITLE": "⚠️ Bakım Notu ⚠️", - "MESSAGE_BLACKLIST": "Ses kanalında kara listedeki kullanıcılar var.", + "MESSAGE_BLACKLIST": "Ses kanalında kara listedeki kullanıcılar var", "MESSAGE_COMMAND": "Komut **{{command}}**", "MESSAGE_FILTER": "Filtre **{{effectName}}** olarak ayarlandı", "NODE_ALL_ACTIVE": "Tüm düğümler etkin", "NODE_CONNECTED": "Gecikme: **{{ping}}ms**", "NODE_DISCONNECTED": "**BAĞLANTI KESİLDİ**", - "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir düğüm adı girin.", + "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir düğüm adı girin", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Çekirdekler: **{{cores}}**\nSistem yükü: **{{systemLoad}}**\nLavalink yükü: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ Bilgi", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Şu An Çalıyor:**", "QUEUE_EMPTY": "*Kuyrukta şarkı yok*", "QUEUE_HEADER": "**Kuyruk:**" -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/events.json b/src/locales/tr-TR/events.json index e7dca23c..daf7f0c1 100644 --- a/src/locales/tr-TR/events.json +++ b/src/locales/tr-TR/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Sunucu verileri önbellekten alınamadı.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz.", - "ERROR_NOT_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", - "ERROR_REQUIRE_ADMIN": "❌ Bu komut yönetici izinleri gerektirmektedir.", - "ERROR_REQUIRE_DJ": "❌ Bu komut DJ izinleri gerektirmektedir.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Size özel mesaj gönderemiyorum.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash ile çalışan komutlar aktif değil.", - "ERROR_TIME_EXPIRED": "❌ Süre doldu.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot ayrıldı.", - "MESSAGE_BOT_STOP": "✅ Bot durduruldu.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Kuyruk karıştırıldı.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Sunucu verileri önbellekten alınamadı", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz", + "ERROR_NOT_PLAYING": "❌ Şuan çalan bir parça bulunmuyor", + "ERROR_REQUIRE_ADMIN": "❌ Bu komut yönetici izinleri gerektirmektedir", + "ERROR_REQUIRE_DJ": "❌ Bu komut DJ izinleri gerektirmektedir", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Size özel mesaj gönderemiyorum", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash ile çalışan komutlar aktif değil", + "ERROR_TIME_EXPIRED": "❌ Süre doldu", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot ayrıldı", + "MESSAGE_BOT_STOP": "✅ Bot durduruldu", + "MESSAGE_MUSIC_SHUFFLE": "✅ Kuyruk karıştırıldı", "MESSAGE_NOW_PLAYING_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Şuan Çalıyor: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Sayfa {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz.", + "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz", "LOOP_MODE_OFF": "Kapalı", "LOOP_MODE_SINGLE": "Tek", "LOOP_MODE_ALL": "Tümü", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim", "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}` olarak ayarlandı", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Bu kanalda komut kullanamazsınız! Lütfen <#{{channelId}}> metin kanalını kullanın." -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index c6f9d936..7f5f2e2c 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -177,7 +177,7 @@ "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 輸入 `1` 到 `{{maxVolume}}` 之間的數字以更改預設音量。", "ERROR_QUEUE_LIMIT_REACHED": "❌ 您已達到隊列限制!您目前在隊列中有 **{{current}}** 首歌曲(限制:**{{limit}}**)。請等待一些歌曲播放完畢後再添加。", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ 已達到全域隊列限制!隊列目前有 **{{total}}** 首歌曲(全域限制:**{{limit}}**)。請等待一些歌曲播放完畢後再添加。", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於您的隊列限制跳過 **{{skipped}}** 首歌曲 (**{{current}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於您的隊列限制跳過 **{{skipped}}** 首歌曲 (**{{current}}/{{limit}}**)", "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於全域隊列限制,跳過 **{{skipped}}** 首歌曲 (**{{total}}/{{limit}}**)。", "MESSAGE_SERVER_GUILD_ID": "伺服器 ID", "MESSAGE_SERVER_GUILD": "伺服器", @@ -199,7 +199,7 @@ "UNIT_TB": "TB", "ERROR_BLACKLIST_NOT_INITIALIZED": "黑名單管理器尚未初始化。", "HELP_SELECT_MUSIC_PLACEHOLDER": "選擇音樂指令", - "HELP_SELECT_UTILITY_PLACEHOLDER": "選擇工具指令", + "HELP_SELECT_UTILITY_PLACEHOLDER": "選擇服務指令", "HELP_COMMAND_ALIASES": "別名: {{aliases}}", "HELP_COMMAND_NONE": "無", "LABEL_FILTER_CLEAR": "清除效果", @@ -230,7 +230,7 @@ "ERROR_PLAYLIST_IMPORT_FAILED": "❌ 解析並儲存 M3U 播放清單失敗。請確認格式是否正確。", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 刪除歌曲失敗。請確認編號是否正確。", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 已將目前佇列中的 {{count}} 首歌曲儲存至播放清單 `{{name}}`。", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 正在從播放清單 `{{name}}` 搜尋並新增 {{count}} 首歌曲...", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 正在從播放清單 `{{name}}` 搜尋並新增 {{count}} 首歌曲..", "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 播放清單 `{{name}}` 載入完成。 (成功: {{added}}首, 略過/失敗: {{skipped}}首)", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 目前伺服器沒有已儲存的播放清單。", "MESSAGE_PLAYLIST_LIST_TITLE": "伺服器播放清單列表", @@ -250,7 +250,7 @@ "ERROR_RADIO_NO_MATCH": "❌ 找不到與 `{{query}}` 符合的電台頻道。\n可用範例: {{samples}} 等", "ERROR_RADIO_LOAD_FAILED": "❌ 載入電台頻道時發生錯誤: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ 無法載入或找不到電台串流 (`{{title}}`)。", - "MESSAGE_RADIO_SEARCHING": "🔄 正在搜尋播放清單 **{{playlist}}** 中的電台頻道 **{{title}}**...", + "MESSAGE_RADIO_SEARCHING": "🔄 正在搜尋播放清單 **{{playlist}}** 中的電台頻道 **{{title}}**..", "MESSAGE_RADIO_PLAYING": "📻 已開始播放來自播放清單 **{{playlist}}** 的電台頻道 **{{title}}**!", "MESSAGE_FOOTER_PAGE": "第 {{page}} / {{totalPages}} 頁", "ERROR_ONLY_COMMAND_USER_PAGINATE": "只有執行該指令的使用者才能切換頁面。", @@ -260,7 +260,7 @@ "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "名為 **`{{name}}`** 的播放清單(共 {{count}} 首歌曲)已經存在。\n您是否要使用當前佇列覆蓋它?", "LABEL_OVERWRITE": "覆蓋", "LABEL_CANCEL": "取消", - "MESSAGE_PLAYLIST_OVERWRITING": "正在覆蓋播放清單...", + "MESSAGE_PLAYLIST_OVERWRITING": "正在覆蓋播放清單..", "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 已取消匯入播放清單。", "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", @@ -269,4 +269,4 @@ "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ 無法將 M3U 播放清單整本加入佇列。請使用 `/radio` 指令選擇特定電台頻道。", "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ 匯入 M3U 播放清單僅限管理員及 DJ 權限使用者使用。" -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/embeds.json b/src/locales/zh-TW/embeds.json index a1fe62eb..74dc8489 100644 --- a/src/locales/zh-TW/embeds.json +++ b/src/locales/zh-TW/embeds.json @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**正在播放:**", "QUEUE_EMPTY": "*播放清單中沒有歌曲*", "QUEUE_HEADER": "**播放清單:**" -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/events.json b/src/locales/zh-TW/events.json index 3e987074..a9179a00 100644 --- a/src/locales/zh-TW/events.json +++ b/src/locales/zh-TW/events.json @@ -22,4 +22,4 @@ "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ 我已將歌曲名稱發送至您的私人訊息中。", "MESSAGE_SET_LOOP_MODE": "✅ 已選擇循環模式:`{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。" -} \ No newline at end of file +} diff --git a/src/utils/functions/lavasharkRequest.ts b/src/utils/functions/lavasharkRequest.ts new file mode 100644 index 00000000..e5fef773 --- /dev/null +++ b/src/utils/functions/lavasharkRequest.ts @@ -0,0 +1,79 @@ +import type { LavaShark } from 'lavashark'; + + +const DEFAULT_RETRY_ATTEMPTS = 3; +const DEFAULT_RETRY_DELAY_MS = 2000; + +/** + * Wait for the given number of milliseconds + */ +export const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Search a track with retries. + * Lavalink nodes enforce REST rate limits (HTTP 429); a single retry after a + * short delay lets transient rate limiting resolve without skipping tracks. + */ +export async function searchWithRetry( + lavashark: LavaShark, + query: string, + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.search(query); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} + +/** + * Decode a track from its encoded string with retries + */ +export async function decodeTrackWithRetry( + lavashark: LavaShark, + encoded: string, + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.decodeTrack(encoded); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} + +/** + * Decode many tracks in a single request. + * Lavalink rate limits REST calls (HTTP 429) aggressively; bulk-decoding a + * whole saved queue with one `/decodetracks` request avoids exhausting the + * node's request budget entirely. + */ +export async function decodeTracksWithRetry( + lavashark: LavaShark, + encodedTracks: string[], + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.decodeTracks(encodedTracks); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} From a269a6e2e68f2fd84d01930f06bef442f064f67e Mon Sep 17 00:00:00 2001 From: hoehoetvhss Date: Sat, 15 Aug 2026 22:37:22 +0900 Subject: [PATCH 47/49] fix(playlist): align bulk decode and error logging with pr-126-clean --- .../playlist/PlayPlaylistSubcommand.ts | 191 +++++++++++------- src/events/lavashark/ErrorEvent.ts | 7 +- 2 files changed, 123 insertions(+), 75 deletions(-) diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 9efeec86..475ebbaa 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -1,8 +1,14 @@ -import { DJModeEnum } from '../../@types/index.js'; +import { DJModeEnum, LoadType } from '../../@types/index.js'; import { embeds } from '../../embeds/index.js'; import { DJManager } from '../../lib/DjManager.js'; import { QueueLimitManager } from '../../lib/QueueLimitManager.js'; import { isUserInBlacklist } from '../../utils/functions/isUserInBlacklist.js'; +import { + decodeTracksWithRetry, + decodeTrackWithRetry, + searchWithRetry, + sleep, +} from '../../utils/functions/lavasharkRequest.js'; import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; import type { GuildMember, VoiceBasedChannel } from 'discord.js'; @@ -240,54 +246,53 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { } /** - * Resolve a stored track using encoded data, URL, then title search + * Resolve a stored track using encoded data, URL, then title search. + * YouTube mix/playlist URLs (`watch?v=X&list=...`) resolve as a + * PLAYLIST result, so the whole playlist is expanded into the queue. */ private async findTrack( context: PlaylistSubcommandContext, playlistTrack: PlaylistTrack, - ) { + ): Promise { if (playlistTrack.encoded) { - try { - const result = await context.client.lavashark.search(playlistTrack.encoded); - if (result.tracks.length > 0) return result.tracks[0]; - } catch { - // Try next query - } + const track = await decodeTrackWithRetry( + context.client.lavashark, + playlistTrack.encoded, + ); + if (track) return [track]; } - if (playlistTrack.url && (playlistTrack.url.startsWith('http://') || playlistTrack.url.startsWith('https://'))) { - try { - const result = await context.client.lavashark.search(playlistTrack.url); - if (result.tracks.length > 0) return result.tracks[0]; - } catch { - // Try next query + const queries = [ + playlistTrack.url, + `ytsearch:${playlistTrack.title}`, + ].filter((query): query is string => Boolean(query)); + + for (const query of new Set(queries)) { + const result = await searchWithRetry(context.client.lavashark, query); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + if (result.loadType === LoadType.PLAYLIST) { + // Mix/playlist expansion keeps the real track names + return result.tracks; + } + const track = result.tracks[0]; + // Keep the name exactly as it was saved, even if the fallback + // search resolved to a different video + if (playlistTrack.title) track.title = playlistTrack.title; + if (playlistTrack.author) track.author = playlistTrack.author; + if (playlistTrack.url) track.uri = playlistTrack.url; + return [track]; } } - const cleanTitle = playlistTrack.title - ?.replace(/\.(mp3|flac|wav|aac|ogg|m4a|webm)$/i, '') - .trim(); - - if (!cleanTitle || cleanTitle.length < 3) { - return null; - } - - const searchQuery = playlistTrack.author - ? `ytsearch:${playlistTrack.author} - ${cleanTitle}` - : `ytsearch:${cleanTitle}`; - - try { - const result = await context.client.lavashark.search(searchQuery); - if (result.tracks.length > 0) return result.tracks[0]; - } catch { - // Track search failed - } - return null; } /** - * Resolve and enqueue tracks while respecting queue limits + * Resolve and enqueue tracks while respecting queue limits. + * Tracks carrying a saved encoded string are bulk-decoded in a single + * request (`/decodetracks`) — Lavalink nodes rate-limit REST calls + * (HTTP 429), and bursting per-track requests makes tracks get skipped. + * Entries without an encoded string fall back to paced per-track lookup. */ private async loadTracks( context: PlaylistSubcommandContext, @@ -298,42 +303,65 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { let added = 0; let skipped = 0; const tracks = playlist.tracks ?? []; - const BATCH_SIZE = 5; - - const resolvedTracks: any[] = []; - - for (let i = 0; i < tracks.length; i += BATCH_SIZE) { - const chunk = tracks.slice(i, i + BATCH_SIZE); - const promises = chunk.map(async (playlistTrack) => { - const limit = QueueLimitManager.canAddSongs( - context.bot, - player, - context.command.user.id, - member, - 1, - ); - if (!limit.canAdd) { - return null; - } + const TRACK_REQUEST_DELAY_MS = 400; + + const canAdd = (): boolean => QueueLimitManager.canAddSongs( + context.bot, + player, + context.command.user.id, + member, + 1, + ).canAdd; + + const entries = tracks.map((track, index) => ({ track, index })); + const resolvedByIndex: Map = new Map(); + + // Bulk-decode entries that carry a saved encoded track string + const encodedEntries = entries.filter(({ track }) => + track.encoded && track.encoded.trim() !== '', + ); + if (encodedEntries.length > 0) { + const decoded = await decodeTracksWithRetry( + context.client.lavashark, + encodedEntries.map(({ track }) => track.encoded!.trim()), + ); + if (decoded && decoded.length === encodedEntries.length) { + encodedEntries.forEach(({ track, index }, i) => { + if (decoded[i] && canAdd()) { + // Keep the names exactly as they were saved + if (track.title) decoded[i].title = track.title; + if (track.author) decoded[i].author = track.author; + if (track.url) decoded[i].uri = track.url; + resolvedByIndex.set(index, [decoded[i]]); + added++; + } + }); + } + } - try { - return await this.findTrack(context, playlistTrack); - } catch { - return null; - } - }); + // Per-track fallback for entries not covered by the bulk decode + for (const { track, index } of entries) { + if (resolvedByIndex.has(index)) { + continue; + } - const results = await Promise.all(promises); - for (const track of results) { - if (track) { - resolvedTracks.push(track); - added++; - } else { - skipped++; - } + if (!canAdd()) { + skipped++; + await sleep(TRACK_REQUEST_DELAY_MS); + continue; + } + + const result = await this.findTrack(context, track); + if (result && result.length > 0) { + resolvedByIndex.set(index, result); + added += result.length; + } else { + skipped++; } + await sleep(TRACK_REQUEST_DELAY_MS); } + const resolvedTracks = entries.flatMap(({ index }) => resolvedByIndex.get(index) ?? []); if (resolvedTracks.length > 0) { const requester = context.command.user as unknown as PlayerRequester; player.addTracks(resolvedTracks, requester); @@ -356,14 +384,29 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { context.bot.config.bot.volume.default; player.filters.setVolume(volume); - try { - await player.play(); - } catch (error) { - context.bot.logger.error( - context.bot.shardId, - `[PlaylistCommand] Error playing track: ${error}`, - ); - await player.destroy(); + // Retry playback a few times so a transient rate limit (HTTP 429) + // does not destroy the player right after loading a playlist + const PLAY_RETRY_ATTEMPTS = 3; + const PLAY_RETRY_DELAY_MS = 2000; + for (let attempt = 0; attempt < PLAY_RETRY_ATTEMPTS; attempt++) { + try { + await player.play(); + return; + } catch (error) { + if (attempt < PLAY_RETRY_ATTEMPTS - 1) { + context.bot.logger.log( + context.bot.shardId, + `[PlaylistCommand] Error playing track (attempt ${attempt + 1}/${PLAY_RETRY_ATTEMPTS}): ${error}`, + ); + await sleep(PLAY_RETRY_DELAY_MS); + continue; + } + context.bot.logger.error( + context.bot.shardId, + `[PlaylistCommand] Error playing track: ${error}`, + ); + await player.destroy(); + } } return; } diff --git a/src/events/lavashark/ErrorEvent.ts b/src/events/lavashark/ErrorEvent.ts index 1612b342..4bdc6ac1 100644 --- a/src/events/lavashark/ErrorEvent.ts +++ b/src/events/lavashark/ErrorEvent.ts @@ -15,6 +15,11 @@ export class ErrorEvent extends BaseLavaSharkEvent<'error'> { } public execute(bot: Bot, _client: Client, node: Node, error: any): void { - bot.logger.error( bot.shardId, `[LavaShark] ${node.identifier} error: ${error.message}`); + // `node` can be null at runtime: lavashark may emit 'error' after the + // node was detached from a destroyed player + bot.logger.error( + bot.shardId, + `[LavaShark] ${node?.identifier ?? 'unknown'} error: ${error?.message ?? error}`, + ); } } From 6251336095395bcc7ee1153a73a705a749f5924d Mon Sep 17 00:00:00 2001 From: hoehoetvhss Date: Sat, 15 Aug 2026 23:06:52 +0900 Subject: [PATCH 48/49] feat(config): restrict playback control commands to DJ and Admin --- config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js b/config.js index 782c1d0d..4227e91f 100644 --- a/config.js +++ b/config.js @@ -160,7 +160,7 @@ const config = { command: { disableCommand: [], // Disabled commands, all enabled by default adminCommand: ['blacklist', 'language', 'server', 'status', 'volume-default'], // Admin commands, only Admin role user can use - djCommand: ['dj', 'filter'], // DJ commands, only DJ role user can use + djCommand: ['dj', 'filter', 'stop', 'pause', 'resume', 'skip', 'clear', 'leave', 'shuffle', 'loop', 'volume'], // DJ commands, only DJ role user can use // Supported commands: 'skip', 'seek', 'pause' // When a command name is listed here, only the requester of the currently playing song may use it. // Admins can always bypass this restriction regardless. From e5a448a0d0010aba0a80fa1bef7fa21571f1bd12 Mon Sep 17 00:00:00 2001 From: hoehoetvhss Date: Sat, 15 Aug 2026 23:16:00 +0900 Subject: [PATCH 49/49] chore: update globals and clean up package overrides --- package-lock.json | 8 ++++---- package.json | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93ad6fee..664ecd68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@types/express": "^5.0.6", "@types/node": "^26.2.0", "eslint": "^10.8.1", - "globals": "^17.9.0", + "globals": "^17.11.0", "tsx": "^4.23.12", "typescript": "~6.0.3", "typescript-eslint": "^8.67.0" @@ -2093,9 +2093,9 @@ } }, "node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index d78a2829..9055fb2d 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,6 @@ "url": "https://github.com/hmes98318/Music-Disc/issues" }, "homepage": "https://github.com/hmes98318/Music-Disc#readme", - "overrides": { - "body-parser": "^2.2.3", - "brace-expansion": "^5.0.8" - }, "dependencies": { "bcryptjs": "^3.0.3", "better-sqlite3": "^13.0.3", @@ -50,7 +46,7 @@ "@types/express": "^5.0.6", "@types/node": "^26.2.0", "eslint": "^10.8.1", - "globals": "^17.9.0", + "globals": "^17.11.0", "tsx": "^4.23.12", "typescript": "~6.0.3", "typescript-eslint": "^8.67.0"