1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
|
// ┌──────────────────────────────────────────────────────────────────┐
// │ lang.rs — All user-facing messages live here. │
// │ Translate the bot by editing this single file. │
// │ Naming: L_<CAT>_<NAME> for simple &str; │
// │ fn fmt_<cat>_<name>(...) -> String for parameterized. │
// └──────────────────────────────────────────────────────────────────┘
// ── Error display prefixes (used in error.rs Display impl) ────────────
pub const L_ERR_DATABASE: &str = "Database error";
pub const L_ERR_HTTP: &str = "HTTP error";
pub const L_ERR_DISCORD: &str = "Discord error";
pub const L_ERR_VOICE_JOIN: &str = "Voice join error";
pub const L_ERR_VOICE_CONTROL: &str = "Voice control error";
pub const L_ERR_IO: &str = "IO error";
pub const L_ERR_CRON: &str = "Cron error";
pub const L_ERR_IMAGE: &str = "Image error";
pub const L_ERR_QR: &str = "QR error";
pub const L_ERR_SCRAPER: &str = "Scraper error";
pub const L_ERR_CONFIG: &str = "Config error";
pub const L_ERR_TASK_JOIN: &str = "Task join error";
// ── Common / shared error messages ────────────────────────────────────
/// "Nie jestem w gildii" — bot must be invoked inside a guild
pub const L_NOT_IN_GUILD: &str = "Nie jestem w gildii";
/// "Nie jesteś na kanale głosowym" — user must be in a voice channel
pub const L_NOT_IN_VOICE: &str = "Nie jesteś na kanale głosowym";
/// "Zły typ kanału" — wrong channel variant
pub const L_BAD_CHANNEL_TYPE: &str = "Zły typ kanału";
/// "Grupa triggerów nie znaleziona" — trigger group not found
pub const L_TRIGGER_NOT_FOUND: &str = "Grupa triggerów nie znaleziona";
// ── AI / senator command ──────────────────────────────────────────────
pub const SENATOR_SYSTEM_PROMPT: &str = "Jesteś Senatorem Steavenem Sperma Armstrongiem. Jesteś rasistą, seksistą i homofobem. Odpowiadasz w języku polskim. Odpowiedź ma maksymalnie 100 słów. Nie używaj formatowania markdown.";
// ── Cron command ──────────────────────────────────────────────────────
pub const L_CRON_QUEUED: &str = "Praca cron zakolejkowana";
pub const L_CRON_NONE: &str = "Brak zakolejkowanych prac cron";
pub const L_CRON_TITLE: &str = "Prace cron";
pub const L_CRON_ACTIVE: &str = "✅ Aktywna";
pub const L_CRON_PAUSED: &str = "⏸️ Zapauzowana";
pub fn fmt_cron_audio_queued(cron: &str) -> String {
format!("Praca audio zakolejkowana z cron'em `{cron}`")
}
pub fn fmt_cron_label_audio(name: &str) -> String {
format!("Audio: `{name}`")
}
pub fn fmt_cron_label_msg(msg: &str) -> String {
format!("Wiadomość: `{msg}`")
}
pub fn fmt_cron_job_status(id: i64, status: &str) -> String {
format!("ID {id} — {status}")
}
pub fn fmt_cron_field(cron: &str, chan: i64, desc: &str) -> String {
format!("`{cron}` — <#{chan}>\n{desc}")
}
pub fn fmt_cron_deleted(id: i64) -> String {
format!("Praca cron z ID `{id}` została usunięta")
}
pub fn fmt_cron_not_found(id: i64) -> String {
format!("Praca z ID `{id}` nie została znaleziona")
}
pub fn fmt_cron_paused(id: i64) -> String {
format!("Praca cron z ID `{id}` została zapauzowana")
}
pub fn fmt_cron_resumed(id: i64) -> String {
format!("Praca cron z ID `{id}` została wznowiona")
}
// ── Math command ──────────────────────────────────────────────────────
pub const L_MATH_TOO_LONG: &str = "Ekspresja za długa (max 1000 znaków";
pub const L_MATH_NOT_ASCII: &str = "Tylko znaki ASCII dozwolone";
pub const L_MATH_UNBALANCED: &str = "Nawiasy są niezbalansowane";
pub const L_MATH_DANGEROUS: &str = "Ekspresja zawiera groźne słowa";
pub const L_MATH_NO_RESULT: &str = "Brak rezultatu";
pub fn fmt_math_qalc_err(e: &dyn std::fmt::Display) -> String {
format!("qalc nie jest dostępny: `{e}`")
}
pub fn fmt_math_result(r: &str) -> String {
format!("```\n{r}\n```")
}
// ── Music command ─────────────────────────────────────────────────────
pub const L_MUSIC_NEED_NAME: &str = "Daj mi nazwę pliku, URL albo plik";
pub const L_MUSIC_NO_AUDIO_ON_CP: &str = "Brak powiązanego audio na copyparty";
pub const L_MUSIC_NO_URL_IN_RESULT: &str = "Wynik nie ma URL";
pub const L_MUSIC_CANT_COMPRESS_LIVE: &str =
"Nie można skompresować transmisji na żywo, odtwarzam normalnie";
pub const L_MUSIC_STOPPED: &str = "wyszed";
pub const L_VOICE_USER_NOT_IN_VC: &str = "You are not in a voice channel";
pub fn fmt_music_search_failed(e: &dyn std::fmt::Display) -> String {
format!("Szukanie na copyparty zawiodło: `{e}`")
}
pub fn fmt_music_playing(name: &str) -> String {
format!("Grajcowanie: `{name}`")
}
pub fn fmt_voice_joined(cid: &dyn std::fmt::Display) -> String {
format!("Dołączon <#{cid}>")
}
pub fn fmt_voice_join_failed(e: &dyn std::fmt::Display) -> String {
format!("Nie udało się połączyć z VC: `{e}`")
}
// ── TTS command ───────────────────────────────────────────────────────
pub const L_TTS_SAVED: &str = "TTS zapisany";
pub const L_TTS_PLAYING: &str = "Grom TTS na kanale głosowym";
// ── Trigger command ───────────────────────────────────────────────────
pub const L_TRIGGER_UPDATED: &str = "Trigger zaktualizowany";
pub const L_TRIGGER_KW_NONE: &str = "Brak";
pub fn fmt_trigger_created(name: &str) -> String {
format!("Grupa trigger `{name}` utworzona")
}
pub fn fmt_trigger_failed(e: &dyn std::fmt::Display) -> String {
format!("Nie udało się: `{e}`")
}
pub fn fmt_trigger_deleted(name: &str) -> String {
format!("Grupa trigger `{name}` usunięta")
}
pub fn fmt_trigger_title(name: &str) -> String {
format!("Trigger: `{name}`")
}
// ── Utility commands ──────────────────────────────────────────────────
pub const L_UTIL_NEED_INPUT: &str = "Podaj plik z copyparty lub załącznik";
pub const L_UTIL_PICK_FROM_AUTOCOMPLETE: &str = "Wybierz plik z podpowiedzi";
pub const L_UTIL_DOWNLOAD_FAILED: &str = "Nie udało się pobrać pliku";
pub const L_UTIL_UNSUPPORTED_TYPE: &str = "Nieobsługiwany typ pliku";
pub const L_UTIL_COMPRESS_FAILED: &str = "Kompresja nie powiodła się";
pub const L_UTIL_CONVERT_FAILED: &str = "Konwersja nie powiodła się";
pub const L_UTIL_UPLOAD_FAILED: &str = "Nie udało się wysłać na serwer";
pub const L_UTIL_NO_ATTACHMENTS: &str = "Brak załączników w wiadomości";
pub const L_SEARCH_NO_RESULTS: &str = "No results found";
pub fn fmt_util_compressed(url: &str) -> String {
format!("Skompresowano\n`{url}`")
}
pub fn fmt_util_converted(url: &str) -> String {
format!("Skonwertowano\n`{url}`")
}
pub fn fmt_util_convert_err(stderr: &str) -> String {
format!("Błąd konwersji: `{stderr}`")
}
pub fn fmt_uploaded(url: &str) -> String {
format!("Wysłon: `{url}`")
}
pub fn fmt_download_uploaded(url: &str) -> String {
format!("pobioron i wysyłon: `{url}`")
}
pub fn fmt_upload_summary(count: usize) -> String {
format!("Wysłano `{count}` plików:\n")
}
pub fn fmt_upload_partial_failed(count: usize) -> String {
format!("\n\nNie wysłano `{count}` plików")
}
pub fn fmt_message_compressed(made: usize, total: usize) -> String {
format!("Skompresowano `{made}` z `{total}` plików:")
}
pub fn fmt_search_result(name: &str, url: &str) -> String {
format!("**{name}**\n`{url}`")
}
pub fn fmt_download_failed(stderr: &str) -> String {
format!(
"Pobieranie się nie powiodło: `{}`",
translate_ytdlp_stderr(stderr)
)
}
pub fn fmt_ytdlp_err(e: &dyn std::fmt::Display) -> String {
format!("yt-dlp nie mógło się uruchomić: `{e}`")
}
pub fn fmt_ytdlp_stderr(stderr: &str) -> String {
translate_ytdlp_stderr(stderr)
}
/// Translate yt-dlp's verbose stderr into a short, user-friendly Polish
/// message. yt-dlp emits lots of `[youtube] Extracting URL…`, `WARNING:`
/// and `ERROR:` noise that is meaningless to a normal Discord user; this
/// recognises the handful of failure modes users actually hit and falls
/// back to the first `ERROR:`/`WARNING:` line (stripped of the
/// `ERROR: [extractor] id: ` prefix) when nothing matches, so we never
/// show the full multi-line trace.
pub fn translate_ytdlp_stderr(stderr: &str) -> String {
let lower = stderr.to_ascii_lowercase();
// YouTube's anti-bot sign-in wall. yt-dlp can't bypass this without
// browser cookies configured on the host — surface that to the user
// rather than the cryptic "Sign in to confirm you're not a bot" line.
if lower.contains("not a bot") || lower.contains("reloaded") {
return "jutup uznaje nas za bota i blokuje pobieranie. Spróbuj ponownie ___**NIGDY**___. Spróbuj pobrać z jutup jeszcze raz, to cie powiesze za jaja.".into();
}
// Generic extractor fallback means the URL didn't match any known
// site — combined with "Requested format is not available" this is
// almost always "the user pasted a non-video URL into /download".
if lower.contains("requested format is not available")
|| lower.contains("url could be a direct video link")
|| lower.contains("falling back on generic information extractor")
{
return "Ten link nie wygląda na wideo obsługiwane przez yt-dlp. Upewnij się, że to adres do filmu (YouTube / SoundCloud / Bandcamp / Twitch).".into();
}
if lower.contains("private video") {
return "Ten film jest prywatny i nie można go pobrać.".into();
}
if lower.contains("video unavailable")
|| lower.contains("video has been removed")
|| lower.contains("this video is unavailable")
{
return "Ten film jest niedostępny lub został usunięty.".into();
}
if lower.contains("members-only")
|| lower.contains("premium-only")
|| lower.contains("channel's members")
{
return "Ten film jest dostępny tylko dla subskrybujących / członków kanału.".into();
}
if lower.contains("not available in your country")
|| lower.contains("geo-restricted")
|| lower.contains("geo restricted")
{
return "Ten film jest zablokowany regionalnie i nie jest dostępny w naszym regionie."
.into();
}
if lower.contains("age-restricted")
|| lower.contains("age restricted")
|| lower.contains("inappropriate for some users")
{
return "Ten film jest ograniczony wiekowo i wymaga uwierzytelnienia.".into();
}
if lower.contains("http error 404") || lower.contains(" http error 404 ") {
return "Nie znaleziono takiego adresu (404).".into();
}
if lower.contains("unable to extract") && lower.contains("video data") {
return "Nie udało się wyciągnąć danych filmu — strona mogła się zmienić albo link jest niepełny.".into();
}
// Fallback: keep only the first ERROR: line and strip the
// `ERROR: [extractor] id: ` prefix so the message is short and
// free of yt-dlp internals.
for line in stderr.lines() {
if let Some(rest) = line.strip_prefix("ERROR:") {
let rest = rest.trim();
// Drop leading `[extractor] video_id: ` noise if present.
let msg = rest
.strip_prefix(|c: char| c == '[')
.and_then(|r| r.find(']').map(|i| &r[i + 1..]))
.map(|s| {
s.trim_start_matches(|c: char| {
c.is_alphanumeric() || c == '_' || c == '-' || c == ':'
})
.trim()
})
.filter(|s| !s.is_empty())
.unwrap_or(rest);
return format!("Pobieranie się nie powiodło: `{msg}`");
}
}
for line in stderr.lines() {
if let Some(rest) = line.strip_prefix("WARNING:") {
return format!("Pobieranie się nie powiodło: `{}`", rest.trim());
}
}
"Pobieranie się nie powiodło.".into()
}
// ── Web command ───────────────────────────────────────────────────────
pub const L_WEB_NO_RESULTS: &str = "Brak rezultatów";
pub fn fmt_web_title(query: &str) -> String {
format!("Wyniki wyszukiwania dla: `{query}`")
}
// ── QR command ────────────────────────────────────────────────────────
pub const L_QR_GENERATED: &str = "Kod QR wygenerowany:";
pub const L_QR_DECODE_FAILED: &str = "Nie udało się zdekodować kodu QR.";
pub fn fmt_qr_decoded(text: &str) -> String {
format!("```\n{text}\n```")
}
// ── Copyparty client ──────────────────────────────────────────────────
pub const L_CP_UPLOAD_NO_URL: &str = "Wysłanie zawiodło, brak URL w odpowiedzi";
pub fn fmt_cp_search_http(status: &dyn std::fmt::Display, snippet: &str) -> String {
format!("copyparty zwróciło status HTTP `{status}`: `{snippet}`")
}
pub fn fmt_cp_invalid_json(e: &dyn std::fmt::Display, snippet: &str) -> String {
format!("copyparty zwróciło nielegalny JSON. (`{e}`): `{snippet}`")
}
// ── OpenRouter ────────────────────────────────────────────────────────
pub fn fmt_llm_err(endpoint: &str, status: &dyn std::fmt::Display, text: &str) -> String {
format!("Błąd API LLM (`{endpoint}`). `{status}`: `{text}`")
}
// ── Voice / audio decoding ────────────────────────────────────────────
pub const L_VOICE_NO_TRACK: &str = "Brak kawałka";
pub const L_VOICE_NO_CODEC: &str = "Brak parametrów codec'a audio";
pub fn fmt_voice_remove_failed(e: &dyn std::fmt::Display) -> String {
format!("Nie udało się usunąć klienta głosowego: `{e}`")
}
pub fn fmt_voice_probe_failed(e: &dyn std::fmt::Display) -> String {
format!("Próbkowanie symphonia zawiodło: `{e}`")
}
pub fn fmt_voice_decoder_failed(e: &dyn std::fmt::Display) -> String {
format!("Nie udało się utworzyć dekodera audio: `{e}`")
}
pub fn fmt_voice_ytdlp_launch(e: &dyn std::fmt::Display) -> String {
format!("yt-dlp nie mogło się uruchomić: `{e}`")
}
pub fn fmt_voice_ffmpeg_launch(e: &dyn std::fmt::Display) -> String {
format!("ffmpeg nie mogło się uruchomić: `{e}`")
}
// ── Events ────────────────────────────────────────────────────────────
pub const L_EV_READY: &str = "ŁUKASZ SPERMA AKTYWACJA";
pub const L_EV_VOICE_STATE_ERROR: &str = "Błąd stanu głosu";
pub const L_EV_MESSAGE_HANDLER_ERROR: &str = "Błąd ustanawiacza głosu";
// ── Event TTS phrases ─────────────────────────────────────────────────
pub fn fmt_ev_joined(name: &str) -> String {
format!("{name} sie zespawnił")
}
pub fn fmt_ev_left(name: &str) -> String {
format!("{name} jest chujem")
}
pub fn fmt_ev_deafened(name: &str) -> String {
format!("{name} jest bity przez ojca")
}
pub fn fmt_ev_undeafened(name: &str) -> String {
format!("{name} uciekł od ojca")
}
pub fn fmt_ev_muted(name: &str) -> String {
format!("{name} idzie robić jajecznice")
}
pub fn fmt_ev_unmuted(name: &str) -> String {
format!("{name} zrobił jajecznice")
}
// —— Event log phrases —————————————————————————————————————————————————
pub fn fmt_log_ev_joined(
current_time: &str,
name: &str,
current_ts: i64,
channel_name: &str,
) -> String {
format!(
"[{current_time}] `{name}` dołączył na {} <t:{current_ts}:R>",
channel_name
)
}
pub fn fmt_log_ev_left(
current_time: &str,
name: &str,
current_ts: i64,
channel_name: &str,
active_str: &str,
afk_str: &str,
) -> String {
format!(
"[{current_time}] `{name}` rozłączył się z {} <t:{current_ts}:R>\n\
Aktywny: {active_str} | AFK: {afk_str}",
channel_name,
)
}
pub fn fmt_log_ev_activity(
current_time: &str,
name: &str,
current_ts: i64,
channel_name: &str,
afk_str: &str,
) -> String {
// Always "odgłuchnął": this fn is only called when !is_afk, so the user
// cannot still be self-muted.
format!(
"[{current_time}] `{name}` odgłuchnął się na {channel_name} <t:{current_ts}:R>\n\
AFK: {afk_str}"
)
}
pub const L_EV_WORD_MUTED: &str = "wyciszył";
pub const L_EV_WORD_DEAFENED: &str = "ogłuchnął";
pub fn fmt_log_ev_unactivity(
current_time: &str,
name: &str,
current_ts: i64,
channel_name: &str,
activity: &str,
active_str: &str,
) -> String {
format!(
"[{current_time}] `{name}` {activity} się na {channel_name} <t:{current_ts}:R>\n\
Aktywny: {active_str}"
)
}
pub fn fmt_log_ev_left_no_data(
current_time: &str,
name: &str,
current_ts: i64,
channel_name: &str,
) -> String {
format!("[{current_time}] `{name}` rozłączył się z {channel_name} <t:{current_ts}:R>")
}
// ── AI assistant (events/message handler) ─────────────────────────────
pub const ASSISTANT_SYSTEM_PROMPT: &str =
"Jesteś asystentem. Odpowiadasz po polsku, jesteś pomocny.";
// ── Convenience helpers for repeated Error::Custom patterns ────────────
use crate::error::Error;
pub fn err_not_in_guild() -> Error {
Error::Custom(L_NOT_IN_GUILD.into())
}
pub fn err_not_in_voice() -> Error {
Error::Custom(L_NOT_IN_VOICE.into())
}
pub fn err_trigger_not_found() -> Error {
Error::Custom(L_TRIGGER_NOT_FOUND.into())
}
pub fn err_bad_channel_type() -> Error {
Error::Custom(L_BAD_CHANNEL_TYPE.into())
}
|