diff --git a/lang/es.conf b/lang/es.conf index fa016fb..8abd2b7 100644 --- a/lang/es.conf +++ b/lang/es.conf @@ -31,6 +31,63 @@ "{0} created channel {1}" = "{0} creó el canal {1}" "{0} used OJOIN to enter {1}" = "{0} usó OJOIN para entrar a {1}" +# --- comandos de operador: SA*/CHG/XLINE/FILTER/RLINE/etc. (prosa dinámica) --- +"GLOBOPS from {0}: {1}" = "GLOBOPS de {0}: {1}" +"{0}-line not found: {1}" = "línea {0} no encontrada: {1}" +"R-line not found: {0}" = "línea R no encontrada: {0}" +"Invalid RLINE regex: {0}" = "expresión regular RLINE no válida: {0}" +"{0} used NICKLOCK on {1}" = "{0} usó NICKLOCK en {1}" +"{0} used NICKUNLOCK on {1}" = "{0} usó NICKUNLOCK en {1}" +"{0} used SQUIT on {1}" = "{0} usó SQUIT en {1}" +"{0} used SAMODE: {1}" = "{0} usó SAMODE: {1}" +"{0} used SATOPIC on {1}" = "{0} usó SATOPIC en {1}" +"{0} used SAKICK on {1} in {2}" = "{0} usó SAKICK en {1} en {2}" +"{0} used SAQUIT on {1}: {2}" = "{0} usó SAQUIT en {1}: {2}" +"{0} used CHGNAME on {1}: {2}" = "{0} usó CHGNAME en {1}: {2}" +"{0} used CLEARCHAN on {1}" = "{0} usó CLEARCHAN en {1}" +"{0} used SWHOIS on {1}: {2}" = "{0} usó SWHOIS en {1}: {2}" +"{0} is already banned on {1}" = "{0} ya está vetado en {1}" +"[Knock] {0} is knocking: {1}" = "[Knock] {0} está llamando: {1}" +"Channel invite list is full" = "La lista de invitaciones del canal está llena" +"Your invite to {0} was revoked" = "Tu invitación a {0} fue revocada" +"{0} renamed to {1} by {2}" = "{0} renombrado a {1} por {2}" +"SSLINFO {0}: TLS={1} certfp={2}" = "SSLINFO {0}: TLS={1} certfp={2}" +"{0} did a /WHOIS on you" = "{0} te hizo un /WHOIS" +"Cannot message {0}: a TLS connection is required (+z)" = "No se puede enviar mensaje a {0}: se requiere una conexión TLS (+z)" +"{0} IRC network" = "Red IRC {0}" +"Your vhost is now {0}" = "Tu vhost ahora es {0}" +"Your SIGNORE list is full ({0} max)." = "Tu lista SIGNORE está llena ({0} máx.)." +"SIGNORE \x02{0}\x02 added — you and they can no longer see each other's messages." = "SIGNORE \x02{0}\x02 añadido — tú y esa persona ya no podéis ver los mensajes del otro." +"SIGNORE \x02{0}\x02 removed." = "SIGNORE \x02{0}\x02 eliminado." +"{0} (Flagged by the spam filter; network operators have been notified.)" = "{0} (Detectado por el filtro antispam; se ha notificado a los operadores de la red.)" +"User {0} had an /amsg or /ame blocked" = "Al usuario {0} se le bloqueó un /amsg o /ame" +"Cloudflare Challenge: {0}" = "Cloudflare Challenge: {0}" +"connect class {0} is full ({1})" = "la clase de conexión {0} está llena ({1})" +"connect class {0} refused {1}: {2}" = "la clase de conexión {0} rechazó {1}: {2}" +"TITLE: you are now known as \"{0}\"." = "TITLE: ahora se le conoce como \"{0}\"." +"Your DCC to {0} was blocked; they must /DCCALLOW +{1} first." = "Su DCC a {0} fue bloqueado; primero debe hacer /DCCALLOW +{1}." +"{0} tried to send you {1} — blocked. /DCCALLOW +{2} to allow it, then ask them to resend." = "{0} intentó enviarle {1} — bloqueado. Haga /DCCALLOW +{2} para permitirlo, y luego pídale que lo reenvíe." +"oper type '{0}' is not defined — granting full access" = "el tipo de operador '{0}' no está definido — otorgando acceso completo" +"{0} hashed password: {1}" = "contraseña cifrada con {0}: {1}" +"Unknown hash '{0}' (try md5, sha1, sha256, sha512, pbkdf2, bcrypt)" = "hash desconocido '{0}' (pruebe md5, sha1, sha256, sha512, pbkdf2, bcrypt)" +"REPUTATION {0} ({1}) set to {2}" = "REPUTATION {0} ({1}) establecido en {2}" +"RMODE: removed {0} +{1} entr{2} from {3}" = "RMODE: eliminó {0} entr{2} +{1} de {3}" +"{0} is in security groups: {1}" = "{0} está en los grupos de seguridad: {1}" +"{0}: file not available." = "{0}: archivo no disponible." +"To cut spam, answer this to send your message — what is {0} {1} {2} ?" = "Para reducir el spam, responda esto para enviar su mensaje — ¿cuánto es {0} {1} {2} ?" +"TLINE: {0} matches {1} of {2} local users ({3}%)" = "TLINE: {0} coincide con {1} de {2} usuarios locales ({3}%)" +"RPC initiated a link to {0}" = "RPC inició un enlace hacia {0}" +"slow event: a command took {0}ms on the core thread" = "evento lento: un comando tardó {0}ms en el hilo principal" +"Could not hash with '{0}'" = "No se pudo cifrar con '{0}'" +"Link denied: {0}" = "Enlace denegado: {0}" +"End of channel {0}" = "Fin de {0} del canal" +"{0} is now known as {1}" = "{0} ahora se conoce como {1}" +"FILTER rejected ({0})" = "FILTER rechazado ({0})" +"{0} added FILTER {1} (engine={2} action={3})" = "{0} añadió FILTER {1} (engine={2} action={3})" +"XLINE: {0} added a {1}: {2}" = "XLINE: {0} añadió un {1}: {2}" +"XLINE: {0} removed a {1}" = "XLINE: {0} eliminó un {1}" +"XLINE: {0}-line on {1} expired" = "XLINE: la {0}-line sobre {1} ha expirado" + # --- avisos / desafíos --- "*** Cloudflare Challenge: verification successful — you may continue." = "*** Desafío de Cloudflare: verificación exitosa — puedes continuar." "*** Correct — you may now message freely; please resend your message." = "*** Correcto — ya puedes enviar mensajes libremente; por favor reenvía tu mensaje." diff --git a/lang/fr.conf b/lang/fr.conf index 5acd99e..a37ba79 100644 --- a/lang/fr.conf +++ b/lang/fr.conf @@ -31,6 +31,63 @@ "{0} created channel {1}" = "{0} a créé le salon {1}" "{0} used OJOIN to enter {1}" = "{0} a utilisé OJOIN pour entrer dans {1}" +# --- commandes oper : SA*/CHG/XLINE/FILTER/RLINE/etc. (prose dynamique) --- +"GLOBOPS from {0}: {1}" = "GLOBOPS de {0} : {1}" +"{0}-line not found: {1}" = "ligne {0} introuvable : {1}" +"R-line not found: {0}" = "ligne R introuvable : {0}" +"Invalid RLINE regex: {0}" = "expression régulière RLINE invalide : {0}" +"{0} used NICKLOCK on {1}" = "{0} a utilisé NICKLOCK sur {1}" +"{0} used NICKUNLOCK on {1}" = "{0} a utilisé NICKUNLOCK sur {1}" +"{0} used SQUIT on {1}" = "{0} a utilisé SQUIT sur {1}" +"{0} used SAMODE: {1}" = "{0} a utilisé SAMODE : {1}" +"{0} used SATOPIC on {1}" = "{0} a utilisé SATOPIC sur {1}" +"{0} used SAKICK on {1} in {2}" = "{0} a utilisé SAKICK sur {1} dans {2}" +"{0} used SAQUIT on {1}: {2}" = "{0} a utilisé SAQUIT sur {1} : {2}" +"{0} used CHGNAME on {1}: {2}" = "{0} a utilisé CHGNAME sur {1} : {2}" +"{0} used CLEARCHAN on {1}" = "{0} a utilisé CLEARCHAN sur {1}" +"{0} used SWHOIS on {1}: {2}" = "{0} a utilisé SWHOIS sur {1} : {2}" +"{0} is already banned on {1}" = "{0} est déjà banni sur {1}" +"[Knock] {0} is knocking: {1}" = "[Knock] {0} frappe à la porte : {1}" +"Channel invite list is full" = "La liste d'invitations du salon est pleine" +"Your invite to {0} was revoked" = "Votre invitation à {0} a été révoquée" +"{0} renamed to {1} by {2}" = "{0} renommé en {1} par {2}" +"SSLINFO {0}: TLS={1} certfp={2}" = "SSLINFO {0} : TLS={1} certfp={2}" +"{0} did a /WHOIS on you" = "{0} a fait un /WHOIS sur vous" +"Cannot message {0}: a TLS connection is required (+z)" = "Impossible d'envoyer un message à {0} : une connexion TLS est requise (+z)" +"{0} IRC network" = "Réseau IRC {0}" +"Your vhost is now {0}" = "Votre vhost est maintenant {0}" +"Your SIGNORE list is full ({0} max)." = "Votre liste SIGNORE est pleine ({0} max)." +"SIGNORE \x02{0}\x02 added — you and they can no longer see each other's messages." = "SIGNORE \x02{0}\x02 ajouté — vous et cette personne ne pouvez plus voir vos messages respectifs." +"SIGNORE \x02{0}\x02 removed." = "SIGNORE \x02{0}\x02 retiré." +"{0} (Flagged by the spam filter; network operators have been notified.)" = "{0} (Signalé par le filtre anti-spam ; les opérateurs du réseau ont été avertis.)" +"User {0} had an /amsg or /ame blocked" = "L'utilisateur {0} a eu un /amsg ou /ame bloqué" +"Cloudflare Challenge: {0}" = "Cloudflare Challenge : {0}" +"connect class {0} is full ({1})" = "la classe de connexion {0} est pleine ({1})" +"connect class {0} refused {1}: {2}" = "la classe de connexion {0} a refusé {1} : {2}" +"TITLE: you are now known as \"{0}\"." = "TITLE : vous êtes désormais connu sous le nom de « {0} »." +"Your DCC to {0} was blocked; they must /DCCALLOW +{1} first." = "Votre DCC vers {0} a été bloqué ; il doit d'abord faire /DCCALLOW +{1}." +"{0} tried to send you {1} — blocked. /DCCALLOW +{2} to allow it, then ask them to resend." = "{0} a tenté de vous envoyer {1} — bloqué. Faites /DCCALLOW +{2} pour l'autoriser, puis demandez-lui de renvoyer." +"oper type '{0}' is not defined — granting full access" = "le type d'opérateur '{0}' n'est pas défini — octroi d'un accès complet" +"{0} hashed password: {1}" = "mot de passe haché en {0} : {1}" +"Unknown hash '{0}' (try md5, sha1, sha256, sha512, pbkdf2, bcrypt)" = "hachage inconnu '{0}' (essayez md5, sha1, sha256, sha512, pbkdf2, bcrypt)" +"REPUTATION {0} ({1}) set to {2}" = "REPUTATION {0} ({1}) réglé sur {2}" +"RMODE: removed {0} +{1} entr{2} from {3}" = "RMODE : a supprimé {0} entr{2} +{1} de {3}" +"{0} is in security groups: {1}" = "{0} est dans les groupes de sécurité : {1}" +"{0}: file not available." = "{0} : fichier non disponible." +"To cut spam, answer this to send your message — what is {0} {1} {2} ?" = "Pour limiter le spam, répondez à ceci pour envoyer votre message — combien font {0} {1} {2} ?" +"TLINE: {0} matches {1} of {2} local users ({3}%)" = "TLINE : {0} correspond à {1} des {2} utilisateurs locaux ({3}%)" +"RPC initiated a link to {0}" = "RPC a initié une liaison vers {0}" +"slow event: a command took {0}ms on the core thread" = "événement lent : une commande a pris {0}ms sur le thread principal" +"Could not hash with '{0}'" = "Impossible de hacher avec '{0}'" +"Link denied: {0}" = "Lien refusé : {0}" +"End of channel {0}" = "Fin de {0} du salon" +"{0} is now known as {1}" = "{0} est maintenant connu sous le nom de {1}" +"FILTER rejected ({0})" = "FILTER rejeté ({0})" +"{0} added FILTER {1} (engine={2} action={3})" = "{0} a ajouté FILTER {1} (engine={2} action={3})" +"XLINE: {0} added a {1}: {2}" = "XLINE : {0} a ajouté un {1} : {2}" +"XLINE: {0} removed a {1}" = "XLINE : {0} a supprimé un {1}" +"XLINE: {0}-line on {1} expired" = "XLINE : la {0}-line sur {1} a expiré" + # --- notices / défis --- "*** Cloudflare Challenge: verification successful — you may continue." = "*** Défi Cloudflare : vérification réussie — vous pouvez continuer." "*** Correct — you may now message freely; please resend your message." = "*** Correct — vous pouvez maintenant envoyer des messages librement ; veuillez renvoyer votre message." diff --git a/src/coremods/core_channel.rs b/src/coremods/core_channel.rs index 65891ba..c36082f 100644 --- a/src/coremods/core_channel.rs +++ b/src/coremods/core_channel.rs @@ -66,12 +66,10 @@ impl Command for Tban { (u.nick.clone(), u.prefix()) }; if s.channels[&key].bans.iter().any(|b| b.mask == mask) { + let m = s.trf("{0} is already banned on {1}", &[mask.as_str(), chan.as_str()]); s.send( uid, - format!( - ":{} NOTICE {nick} :{mask} is already banned on {chan}", - s.name - ), + format!(":{} NOTICE {nick} :{m}", s.name), ); return CmdResult::Fail; } @@ -127,12 +125,10 @@ impl Command for Knock { .cloned() .unwrap_or_else(|| "requesting an invite".to_string()); let who = s.users[&uid].prefix(); + let m = s.trf("[Knock] {0} is knocking: {1}", &[who.as_str(), reason.as_str()]); s.to_channel( &key, - &format!( - ":{} NOTICE {chan} :[Knock] {who} is knocking: {reason}", - s.name - ), + &format!(":{} NOTICE {chan} :{m}", s.name), None, ); s.numeric( @@ -314,9 +310,10 @@ impl Command for Invite { let maxinv = s.conf_num("maxinvites", 100usize); if s.channels[&key].invites.len() >= maxinv && !s.channels[&key].invites.contains(&tuid) { let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default(); + let m = s.trf("Channel invite list is full", &[]); s.send( uid, - format!(":{} NOTICE {nick} :{chan} :Channel invite list is full", s.name), + format!(":{} NOTICE {nick} :{chan} :{m}", s.name), ); return CmdResult::Fail; } @@ -408,12 +405,10 @@ impl Command for Uninvite { format!(":{} NOTICE {nick} :{who} {word} {chan}", s.name), ); if removed { + let m = s.trf("Your invite to {0} was revoked", &[chan.as_str()]); s.send( tuid, - format!( - ":{} NOTICE {who} :Your invite to {chan} was revoked", - s.name - ), + format!(":{} NOTICE {who} :{m}", s.name), ); } CmdResult::Ok @@ -563,7 +558,8 @@ impl Command for Rename { s.fail(uid, "RENAME", "CANNOT_RENAME", "The channel cannot be renamed."); return CmdResult::Fail; } - s.snotice_c('a', &format!("{oldname} renamed to {new} by {prefix}")); + let m = s.trf("{0} renamed to {1} by {2}", &[oldname.as_str(), new.as_str(), prefix.as_str()]); + s.snotice_c('a', &m); s.propagate_rename(&uuid, &oldname, new, &reason, None); CmdResult::Ok } diff --git a/src/coremods/core_extra.rs b/src/coremods/core_extra.rs index 43dae82..00472e1 100644 --- a/src/coremods/core_extra.rs +++ b/src/coremods/core_extra.rs @@ -245,7 +245,9 @@ impl Command for Admin { RPL_ADMINME, &format!("{} :Administrative info", s.name), ); - s.numeric(uid, RPL_ADMINLOC1, &format!(":{} IRC network", s.network)); + let network = s.network.clone(); + let m = s.trf("{0} IRC network", &[network.as_str()]); + s.numeric(uid, RPL_ADMINLOC1, &format!(":{m}")); s.numeric( uid, RPL_ADMINLOC2, diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index b66d146..91d347e 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -53,12 +53,10 @@ impl Command for SslInfo { }; let tls = if secure { "yes" } else { "no" }; let fp = certfp.unwrap_or_else(|| "none".to_string()); + let m = s.trf("SSLINFO {0}: TLS={1} certfp={2}", &[nick.as_str(), tls, fp.as_str()]); s.send( uid, - format!( - ":{} NOTICE {asker} :SSLINFO {nick}: TLS={tls} certfp={fp}", - s.name - ), + format!(":{} NOTICE {asker} :{m}", s.name), ); CmdResult::Ok } @@ -437,9 +435,10 @@ impl Command for Whois { // +W showwhois — tell the target that someone looked them up if showwhois && !is_self { let by = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); + let m = s.trf("{0} did a /WHOIS on you", &[by.as_str()]); s.send( tuid, - format!(":{} NOTICE {nick} :*** {by} did a /WHOIS on you", s.name), + format!(":{} NOTICE {nick} :*** {m}", s.name), ); } s.numeric(uid, RPL_ENDOFWHOIS, &format!("{nick} :End of /WHOIS list")); diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index 854f69a..6df0868 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -543,12 +543,10 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) .map(|u| u.nick.clone()) .unwrap_or_default(), ); + let m = s.trf("Cannot message {0}: a TLS connection is required (+z)", &[tn.as_str()]); s.send( uid, - format!( - ":{} NOTICE {sn} :Cannot message {tn}: a TLS connection is required (+z)", - s.name - ), + format!(":{} NOTICE {sn} :{m}", s.name), ); } return CmdResult::Fail; diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index f2ada87..b430be5 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -398,14 +398,12 @@ impl Command for GlobOps { .filter(|(_, u)| u.flags.oper) .map(|(&u, _)| u) .collect(); + let m = s.trf("GLOBOPS from {0}: {1}", &[from.as_str(), params[0].as_str()]); for o in opers { let nick = s.users.get(&o).map(|u| u.nick.clone()).unwrap_or_default(); s.send( o, - format!( - ":{} NOTICE {nick} :*** GLOBOPS from {from}: {}", - s.name, params[0] - ), + format!(":{} NOTICE {nick} :*** {m}", s.name), ); } CmdResult::Ok @@ -731,9 +729,10 @@ fn do_xline(s: &mut Server, uid: Uid, params: &[String], kind: XKind) -> CmdResu if s.remove_xline(kind, &mask, &nick) { s.propagate_delline(kind.tag(), &mask); } else { + let m = s.trf("{0}-line not found: {1}", &[kind.tag(), mask.as_str()]); s.send( uid, - format!(":{} NOTICE {nick} :{}-line not found: {mask}", s.name, kind.tag()), + format!(":{} NOTICE {nick} :{m}", s.name), ); } return CmdResult::Ok; @@ -878,17 +877,20 @@ impl Command for Rline { if s.remove_xline(XKind::Rline, &pattern, &nick) { s.propagate_delline("R", &pattern); } else { + let m = s.trf("R-line not found: {0}", &[pattern.as_str()]); s.send( uid, - format!(":{} NOTICE {nick} :R-line not found: {pattern}", s.name), + format!(":{} NOTICE {nick} :{m}", s.name), ); } return CmdResult::Ok; } if let Err(e) = crate::regex::Regex::new(&pattern) { + let e = e.to_string(); + let m = s.trf("Invalid RLINE regex: {0}", &[e.as_str()]); s.send( uid, - format!(":{} NOTICE {nick} :Invalid RLINE regex: {e}", s.name), + format!(":{} NOTICE {nick} :{m}", s.name), ); return CmdResult::Fail; } @@ -952,7 +954,8 @@ impl Command for NickLock { u.flags.nick_locked = true; } let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used NICKLOCK on {newnick}")); + let m = s.trf("{0} used NICKLOCK on {1}", &[by.as_str(), newnick.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -977,7 +980,8 @@ impl Command for NickUnlock { u.flags.nick_locked = false; } let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used NICKUNLOCK on {}", params[0])); + let m = s.trf("{0} used NICKUNLOCK on {1}", &[by.as_str(), params[0].as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -1049,7 +1053,8 @@ impl Command for Squit { return CmdResult::Fail; }; let by = oper_nick(s, uid); - s.snotice_c('l', &format!("{by} used SQUIT on {name}")); + let m = s.trf("{0} used SQUIT on {1}", &[by.as_str(), name.as_str()]); + s.snotice_c('l', &m); s.close_link(via, &format!("SQUIT from {by}")); CmdResult::Ok } @@ -1209,7 +1214,9 @@ impl Command for SaMode { let r = apply_mode(s, uid, params); s.mode_sudo = false; let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used SAMODE: {}", params.join(" "))); + let joined = params.join(" "); + let m = s.trf("{0} used SAMODE: {1}", &[by.as_str(), joined.as_str()]); + s.snotice_c('v', &m); r } } @@ -1248,7 +1255,8 @@ impl Command for SaTopic { s.to_channel(&key, &format!(":{prefix} TOPIC {chan} :{text}"), None); s.propagate_topic(uid, chan, &text); let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used SATOPIC on {chan}")); + let m = s.trf("{0} used SATOPIC on {1}", &[by.as_str(), chan.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -1313,7 +1321,8 @@ impl Command for SaKick { s.events .push_back(Hook::Part(tuid, key, "kicked".to_string())); let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used SAKICK on {victim} in {chan}")); + let m = s.trf("{0} used SAKICK on {1} in {2}", &[by.as_str(), victim.as_str(), chan.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -1343,7 +1352,8 @@ impl Command for SaQuit { s.send(tuid, format!("ERROR :Closing link: (SAQUIT: {reason})")); s.remove_user(tuid, &format!("Quit: {reason}")); let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used SAQUIT on {}: {reason}", params[0])); + let m = s.trf("{0} used SAQUIT on {1}: {2}", &[by.as_str(), params[0].as_str(), reason.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -1379,7 +1389,8 @@ impl Command for ChgName { } s.notify_peers(t, &line, |c| c.setname); let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used CHGNAME on {}: {realname}", params[0])); + let m = s.trf("{0} used CHGNAME on {1}: {2}", &[by.as_str(), params[0].as_str(), realname.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -1436,7 +1447,8 @@ impl Command for ClearChan { } s.channels.retain(|_, c| c.keep_alive()); let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used CLEARCHAN on {chan}")); + let m = s.trf("{0} used CLEARCHAN on {1}", &[by.as_str(), chan.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } @@ -1553,7 +1565,8 @@ impl Command for SwhoisCmd { } } let by = oper_nick(s, uid); - s.snotice_c('v', &format!("{by} used SWHOIS on {}: {text}", params[0])); + let m = s.trf("{0} used SWHOIS on {1}: {2}", &[by.as_str(), params[0].as_str(), text.as_str()]); + s.snotice_c('v', &m); CmdResult::Ok } } diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index c7ab02b..55375f2 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -60,9 +60,10 @@ impl Command for Vhost { match host { Some(h) => { s.change_host_ident(uid, None, Some(&h)); + let m = s.trf("Your vhost is now {0}", &[h.as_str()]); s.send( uid, - format!(":{} NOTICE {nick} :Your vhost is now {h}", s.name), + format!(":{} NOTICE {nick} :{m}", s.name), ); } None => { diff --git a/src/coremods/core_watch.rs b/src/coremods/core_watch.rs index dff4a73..ed60836 100644 --- a/src/coremods/core_watch.rs +++ b/src/coremods/core_watch.rs @@ -351,7 +351,9 @@ impl Command for Signore { .map(|u| u.signore.len() >= max && !u.signore.contains(&mask)) .unwrap_or(true); if full { - s.send(uid, format!(":{sn} NOTICE {nick} :Your SIGNORE list is full ({max} max).")); + let maxs = max.to_string(); + let m = s.trf("Your SIGNORE list is full ({0} max).", &[maxs.as_str()]); + s.send(uid, format!(":{sn} NOTICE {nick} :{m}")); return CmdResult::Fail; } if let Some(u) = s.users.get_mut(&uid) { @@ -359,12 +361,14 @@ impl Command for Signore { u.signore.push(mask.clone()); } } - s.send(uid, format!(":{sn} NOTICE {nick} :SIGNORE \x02{mask}\x02 added — you and they can no longer see each other's messages.")); + let m = s.trf("SIGNORE \x02{0}\x02 added — you and they can no longer see each other's messages.", &[mask.as_str()]); + s.send(uid, format!(":{sn} NOTICE {nick} :{m}")); } else { if let Some(u) = s.users.get_mut(&uid) { u.signore.retain(|x| x != &mask); } - s.send(uid, format!(":{sn} NOTICE {nick} :SIGNORE \x02{mask}\x02 removed.")); + let m = s.trf("SIGNORE \x02{0}\x02 removed.", &[mask.as_str()]); + s.send(uid, format!(":{sn} NOTICE {nick} :{m}")); } // Persist the change to the user's services account (no-op if not logged in). s.push_signore_to_services(uid); diff --git a/src/i18n.rs b/src/i18n.rs index e5753db..e3088ad 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -231,6 +231,20 @@ fn read_quoted(s: &str) -> Option<(String, &str)> { 't' => out.push('\t'), '"' => out.push('"'), '\\' => out.push('\\'), + // \xNN — a raw byte, so IRC formatting controls (e.g. \x02 bold) in a + // msgid match the literal byte the source `"\x02"` compiles to. + 'x' => { + let (h1, h2) = (chars.next()?.1, chars.next()?.1); + match u8::from_str_radix(&format!("{h1}{h2}"), 16) { + Ok(b) => out.push(b as char), + Err(_) => { + out.push('\\'); + out.push('x'); + out.push(h1); + out.push(h2); + } + } + } other => { out.push('\\'); out.push(other); diff --git a/src/ircd.rs b/src/ircd.rs index edf21da..2815428 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -175,8 +175,11 @@ impl Ircd { busy.store(0, Ordering::Relaxed); let ms = start.elapsed().as_millis() as u64; if slow_ms != 0 && ms >= slow_ms { - self.server - .snotice(&format!("slow event: a command took {ms}ms on the core thread")); + let ms_s = ms.to_string(); + let m = self + .server + .trf("slow event: a command took {0}ms on the core thread", &[ms_s.as_str()]); + self.server.snotice(&m); } } } @@ -260,11 +263,16 @@ impl Ircd { .map(|u| u.nick.clone()) .unwrap_or_default(); let line = match hash { - Some(h) => format!( - ":{} NOTICE {nick} :{algo} hashed password: {h}", - self.server.name - ), - None => format!(":{} NOTICE {nick} :Could not hash with '{algo}'", self.server.name), + Some(h) => { + let m = self + .server + .trf("{0} hashed password: {1}", &[algo.as_str(), h.as_str()]); + format!(":{} NOTICE {nick} :{m}", self.server.name) + } + None => { + let m = self.server.trf("Could not hash with '{0}'", &[algo.as_str()]); + format!(":{} NOTICE {nick} :{m}", self.server.name) + } }; self.server.send(uid, line); } diff --git a/src/link.rs b/src/link.rs index 21703f7..ad7e7c4 100644 --- a/src/link.rs +++ b/src/link.rs @@ -340,7 +340,8 @@ impl Server { } fn reject_link(&mut self, uid: Uid, why: &str) { - self.link_out(uid, format!("ERROR :Link denied: {why}")); + let m = self.trf("Link denied: {0}", &[why]); + self.link_out(uid, format!("ERROR :{m}")); eprintln!("[link] rejected {uid}: {why}"); self.close_link(uid, why); } diff --git a/src/mode.rs b/src/mode.rs index a306683..78c1650 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -726,7 +726,8 @@ impl ChanMode for ListMode { for (m, setter, ts) in rows { s.numeric(uid, entry_num, &format!("{chan} {m} {setter} {ts}")); } - s.numeric(uid, end_num, &format!("{chan} :End of channel {noun}")); + let m = s.trf("End of channel {0}", &[noun]); + s.numeric(uid, end_num, &format!("{chan} :{m}")); return Applied::No; }; // autoop (+w) embeds a status prefix to grant on join, applied under server diff --git a/src/modules/antimixedutf8.rs b/src/modules/antimixedutf8.rs index b2957db..febd288 100644 --- a/src/modules/antimixedutf8.rs +++ b/src/modules/antimixedutf8.rs @@ -285,12 +285,14 @@ impl Module for AntiMixedUtf8 { // Notify the sender even for punitive actions: the writer flushes queued // lines before a disconnect. + let block_msg = srv.amu.block_msg.clone(); + let m = srv.trf( + "{0} (Flagged by the spam filter; network operators have been notified.)", + &[block_msg.as_str()], + ); srv.send( uid, - format!( - ":{} NOTICE {nick} :*** {} (Flagged by the spam filter; network operators have been notified.)", - srv.name, srv.amu.block_msg - ), + format!(":{} NOTICE {nick} :*** {m}", srv.name), ); let action = srv.amu.action.to_ascii_lowercase(); diff --git a/src/modules/blockamsg.rs b/src/modules/blockamsg.rs index 6fe5093..11f80b1 100644 --- a/src/modules/blockamsg.rs +++ b/src/modules/blockamsg.rs @@ -90,7 +90,8 @@ impl Module for BlockAmsg { let notify_opers = matches!(action.as_str(), "killopers" | "noticeopers" | "zlineopers"); if notify_opers { let mask = srv.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); - srv.snotice(&format!("User {mask} had an /amsg or /ame blocked")); + let m = srv.trf("User {0} had an /amsg or /ame blocked", &[mask.as_str()]); + srv.snotice(&m); } let reason = "Attempted to global message (/amsg or /ame)"; match action.as_str() { diff --git a/src/modules/cloudflare_challenge.rs b/src/modules/cloudflare_challenge.rs index 8aab8c6..4beb182 100644 --- a/src/modules/cloudflare_challenge.rs +++ b/src/modules/cloudflare_challenge.rs @@ -158,9 +158,10 @@ impl Command for VerifyChallenge { .unwrap_or_default(); let fail = |s: &mut Server, why: &str| { + let m = s.trf("Cloudflare Challenge: {0}", &[why]); s.send( uid, - format!(":{} NOTICE {nick} :*** Cloudflare Challenge: {why}", s.name), + format!(":{} NOTICE {nick} :*** {m}", s.name), ); CmdResult::Fail }; diff --git a/src/modules/connclass.rs b/src/modules/connclass.rs index e773cb2..f36de1d 100644 --- a/src/modules/connclass.rs +++ b/src/modules/connclass.rs @@ -289,7 +289,9 @@ fn pick( if let Some(max) = c.limit { if class_count(s, &c.name, uid) >= max { if c.maxconnwarn { - s.snotice_c('c', &format!("connect class {} is full ({max})", c.name)); + let max_s = max.to_string(); + let m = s.trf("connect class {0} is full ({1})", &[c.name.as_str(), max_s.as_str()]); + s.snotice_c('c', &m); } continue; // full — try the next class } @@ -355,7 +357,11 @@ pub fn assign(s: &mut Server, uid: Uid) -> Option { }; let warn = |s: &Server, why: &str| { if class.maxconnwarn { - s.snotice_c('c', &format!("connect class {} refused {ip}: {why}", class.name)); + let m = s.trf( + "connect class {0} refused {1}: {2}", + &[class.name.as_str(), ip.as_str(), why], + ); + s.snotice_c('c', &m); } }; if let Some(max) = class.localmax { diff --git a/src/modules/customtitle.rs b/src/modules/customtitle.rs index 0881df4..222b99a 100644 --- a/src/modules/customtitle.rs +++ b/src/modules/customtitle.rs @@ -45,9 +45,10 @@ pub fn grant(s: &mut Server, uid: Uid, title: &str, vhost: &str) { s.change_host_ident(uid, None, Some(vhost)); } let nick = nick(s, uid); + let m = s.trf("TITLE: you are now known as \"{0}\".", &[title]); s.send( uid, - format!(":{} NOTICE {nick} :*** TITLE: you are now known as \"{title}\".", s.name), + format!(":{} NOTICE {nick} :*** {m}", s.name), ); } diff --git a/src/modules/dccallow.rs b/src/modules/dccallow.rs index 9582606..d90b291 100644 --- a/src/modules/dccallow.rs +++ b/src/modules/dccallow.rs @@ -117,19 +117,21 @@ impl Module for DccAllow { return ModResult::Passthru; } let tnick = s.users.get(&tuid).map(|u| u.nick.clone()).unwrap_or_default(); + let m1 = s.trf( + "Your DCC to {0} was blocked; they must /DCCALLOW +{1} first.", + &[tnick.as_str(), sender.as_str()], + ); s.send( uid, - format!( - ":{} NOTICE {sender} :*** Your DCC to {tnick} was blocked; they must /DCCALLOW +{sender} first.", - s.name - ), + format!(":{} NOTICE {sender} :*** {m1}", s.name), + ); + let m2 = s.trf( + "{0} tried to send you {1} — blocked. /DCCALLOW +{2} to allow it, then ask them to resend.", + &[sender.as_str(), what.as_str(), sender.as_str()], ); s.send( tuid, - format!( - ":{} NOTICE {tnick} :*** {sender} tried to send you {what} — blocked. /DCCALLOW +{sender} to allow it, then ask them to resend.", - s.name - ), + format!(":{} NOTICE {tnick} :*** {m2}", s.name), ); ModResult::Deny } diff --git a/src/modules/filter.rs b/src/modules/filter.rs index 1bb4bde..f1e05a9 100644 --- a/src/modules/filter.rs +++ b/src/modules/filter.rs @@ -185,14 +185,20 @@ impl Command for FilterCmd { let filter = match SpamFilter::new(pattern.clone(), engine.clone(), action.clone(), duration, reason) { Ok(f) => f, Err(e) => { - s.send(uid, format!(":{} NOTICE {nick} :FILTER rejected ({e})", s.name)); + let e_s = e.to_string(); + let m = s.trf("FILTER rejected ({0})", &[e_s.as_str()]); + s.send(uid, format!(":{} NOTICE {nick} :{m}", s.name)); return CmdResult::Fail; } }; let f = s.ext.get_or_insert_with::(Filters::default); f.0.retain(|r| r.pattern != pattern); f.0.push(filter); - s.snotice_c('f', &format!("{nick} added FILTER {pattern} (engine={engine} action={action})")); + let m = s.trf( + "{0} added FILTER {1} (engine={2} action={3})", + &[nick.as_str(), pattern.as_str(), engine.as_str(), action.as_str()], + ); + s.snotice_c('f', &m); } } } diff --git a/src/modules/globops.rs b/src/modules/globops.rs index b74b65b..bd3d0bc 100644 --- a/src/modules/globops.rs +++ b/src/modules/globops.rs @@ -28,7 +28,9 @@ impl Command for GlobopsCmd { return CmdResult::Fail; } let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default(); - s.snotice_c('g', &format!("GLOBOPS from {nick}: {}", params.join(" "))); + let body = params.join(" "); + let m = s.trf("GLOBOPS from {0}: {1}", &[nick.as_str(), body.as_str()]); + s.snotice_c('g', &m); CmdResult::Ok } } diff --git a/src/modules/opertypes.rs b/src/modules/opertypes.rs index 77e3667..ca2b4c2 100644 --- a/src/modules/opertypes.rs +++ b/src/modules/opertypes.rs @@ -102,7 +102,8 @@ pub fn apply(s: &mut Server, uid: Uid, type_id: Option<&str>) { return; }; let Some(r) = with_resolved(s, |m| m.get(&id).cloned()) else { - s.snotice_c('o', &format!("oper type '{id}' is not defined — granting full access")); + let m = s.trf("oper type '{0}' is not defined — granting full access", &[id.as_str()]); + s.snotice_c('o', &m); return; }; if !r.modes.is_empty() { diff --git a/src/modules/password_hash.rs b/src/modules/password_hash.rs index 7f75f89..03a9726 100644 --- a/src/modules/password_hash.rs +++ b/src/modules/password_hash.rs @@ -199,23 +199,16 @@ impl Command for MkPasswd { } match make(&algo, &pass) { Some(hashed) => { - s.send( - uid, - format!( - ":{} NOTICE {nick} :{algo} hashed password: {hashed}", - s.name - ), - ); + let m = s.trf("{0} hashed password: {1}", &[algo.as_str(), hashed.as_str()]); + s.send(uid, format!(":{} NOTICE {nick} :{m}", s.name)); CmdResult::Ok } None => { - s.send( - uid, - format!( - ":{} NOTICE {nick} :Unknown hash '{algo}' (try md5, sha1, sha256, sha512, pbkdf2, bcrypt)", - s.name - ), + let m = s.trf( + "Unknown hash '{0}' (try md5, sha1, sha256, sha512, pbkdf2, bcrypt)", + &[algo.as_str()], ); + s.send(uid, format!(":{} NOTICE {nick} :{m}", s.name)); CmdResult::Fail } } diff --git a/src/modules/reputation.rs b/src/modules/reputation.rs index 6fd51ab..f766a2e 100644 --- a/src/modules/reputation.rs +++ b/src/modules/reputation.rs @@ -289,13 +289,13 @@ impl Command for ReputationCmd { e.score = val.min(cap); e.last_seen = n; save(s); - s.send( - uid, - format!( - ":{} NOTICE {anick} :REPUTATION {nick} ({k}) set to {val}", - s.name - ), + let ks = k.to_string(); + let vals = val.to_string(); + let m = s.trf( + "REPUTATION {0} ({1}) set to {2}", + &[nick.as_str(), ks.as_str(), vals.as_str()], ); + s.send(uid, format!(":{} NOTICE {anick} :{m}", s.name)); } else { let score = score_of(s, tuid); s.send( diff --git a/src/modules/rmode.rs b/src/modules/rmode.rs index 8bfb40b..222e53e 100644 --- a/src/modules/rmode.rs +++ b/src/modules/rmode.rs @@ -99,13 +99,13 @@ impl Command for RMode { .map(|u| u.nick.clone()) .unwrap_or_default(); let plural = if removed == 1 { "y" } else { "ies" }; - s.send( - uid, - format!( - ":{} NOTICE {nick} :RMODE: removed {removed} +{mode} entr{plural} from {chan}", - s.name - ), + let removeds = removed.to_string(); + let modes = mode.to_string(); + let m = s.trf( + "RMODE: removed {0} +{1} entr{2} from {3}", + &[removeds.as_str(), modes.as_str(), plural, chan.as_str()], ); + s.send(uid, format!(":{} NOTICE {nick} :{m}", s.name)); CmdResult::Ok } } diff --git a/src/modules/rpc/server.rs b/src/modules/rpc/server.rs index 304e713..683a264 100644 --- a/src/modules/rpc/server.rs +++ b/src/modules/rpc/server.rs @@ -61,7 +61,8 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result { diff --git a/src/modules/securitygroups.rs b/src/modules/securitygroups.rs index fec0e00..39cd54f 100644 --- a/src/modules/securitygroups.rs +++ b/src/modules/securitygroups.rs @@ -221,13 +221,8 @@ impl Command for SecGroupsCmd { .map(|u| u.nick.clone()) .unwrap_or_default(), ); - s.send( - uid, - format!( - ":{} NOTICE {anick} :{tnick} is in security groups: {list}", - s.name - ), - ); + let m = s.trf("{0} is in security groups: {1}", &[tnick.as_str(), list.as_str()]); + s.send(uid, format!(":{} NOTICE {anick} :{m}", s.name)); CmdResult::Ok } } diff --git a/src/modules/showfile.rs b/src/modules/showfile.rs index c50cb17..11cc52b 100644 --- a/src/modules/showfile.rs +++ b/src/modules/showfile.rs @@ -36,10 +36,10 @@ pub fn maybe_show(s: &mut Server, uid: Uid, cmd: &str) -> bool { s.send(uid, format!(":{} NOTICE {nick} :{line}", s.name)); } } - None => s.send( - uid, - format!(":{} NOTICE {nick} :*** {cmd}: file not available.", s.name), - ), + None => { + let m = s.trf("{0}: file not available.", &[cmd]); + s.send(uid, format!(":{} NOTICE {nick} :*** {m}", s.name)); + } } true } diff --git a/src/modules/solvemsg.rs b/src/modules/solvemsg.rs index a7eb140..6e95187 100644 --- a/src/modules/solvemsg.rs +++ b/src/modules/solvemsg.rs @@ -97,13 +97,13 @@ impl Module for SolveMsg { answer: Some(ans), }); } - s.send( - uid, - format!( - ":{} NOTICE {nick} :*** To cut spam, answer this to send your message — what is {a} {sym} {b} ?", - s.name - ), + let as_ = a.to_string(); + let bs = b.to_string(); + let m = s.trf( + "To cut spam, answer this to send your message — what is {0} {1} {2} ?", + &[as_.as_str(), sym, bs.as_str()], ); + s.send(uid, format!(":{} NOTICE {nick} :*** {m}", s.name)); ModResult::Deny } } diff --git a/src/modules/tline.rs b/src/modules/tline.rs index 5f638cf..50e36cc 100644 --- a/src/modules/tline.rs +++ b/src/modules/tline.rs @@ -48,13 +48,14 @@ impl Command for TLine { .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); - s.send( - uid, - format!( - ":{} NOTICE {nick} :*** TLINE: {mask} matches {matched} of {total} local users ({pct}%)", - s.name - ), + let matcheds = matched.to_string(); + let totals = total.to_string(); + let pcts = pct.to_string(); + let m = s.trf( + "TLINE: {0} matches {1} of {2} local users ({3}%)", + &[mask.as_str(), matcheds.as_str(), totals.as_str(), pcts.as_str()], ); + s.send(uid, format!(":{} NOTICE {nick} :*** {m}", s.name)); CmdResult::Ok } } diff --git a/src/users.rs b/src/users.rs index 50520fd..7f79035 100644 --- a/src/users.rs +++ b/src/users.rs @@ -438,7 +438,8 @@ impl Server { self.send(t, line.clone()); } if self.conf_bool("seenicks", false) { - self.snotice_c('n', &format!("{old} is now known as {newnick}")); + let m = self.trf("{0} is now known as {1}", &[old.as_str(), newnick]); + self.snotice_c('n', &m); } // WATCH/MONITOR: the old nick is now gone, the new one is here self.watch_notify_offline(&old); diff --git a/src/xline.rs b/src/xline.rs index ab0d9ff..e1fa768 100644 --- a/src/xline.rs +++ b/src/xline.rs @@ -304,7 +304,8 @@ impl Server { crate::server::long_date(expires) ) }; - self.snotice_c('x', &format!("XLINE: {setter} added a {detail}: {reason}")); + let m = self.trf("XLINE: {0} added a {1}: {2}", &[setter, detail.as_str(), reason]); + self.snotice_c('x', &m); self.save_xlines(); self.enforce_xlines(); } @@ -335,7 +336,8 @@ impl Server { Some(e) if e != 0 => format!("timed {tag}-line on {mask} (already expired)"), _ => format!("permanent {tag}-line on {mask}"), }; - self.snotice_c('x', &format!("XLINE: {remover} removed a {detail}")); + let m = self.trf("XLINE: {0} removed a {1}", &[remover, detail.as_str()]); + self.snotice_c('x', &m); self.save_xlines(); } removed @@ -383,7 +385,8 @@ impl Server { } self.xlines.retain(|x| x.expires == 0 || x.expires > n); for (kind, mask) in &expired { - self.snotice_c('x', &format!("XLINE: {}-line on {mask} expired", kind.tag())); + let m = self.trf("XLINE: {0}-line on {1} expired", &[kind.tag(), mask.as_str()]); + self.snotice_c('x', &m); } self.save_xlines(); // an expiry changed the set — persist it }