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