From fadd4f37cdf0407e584b56d4657e453efc0c980c Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 20 Jul 2026 17:35:25 +0000 Subject: [PATCH] Localize residual notices: logout, channel release, password change, founder-language expiry mail, and DiceServ prompts and errors --- lang/de.json | 21 ++++++++++++++++++++- lang/es-ar.json | 21 ++++++++++++++++++++- lang/es.json | 21 ++++++++++++++++++++- lang/fr.json | 21 ++++++++++++++++++++- lang/pt-br.json | 21 ++++++++++++++++++++- lang/pt.json | 21 ++++++++++++++++++++- modules/diceserv/src/roll.rs | 7 +++++-- src/engine/db/account.rs | 11 +++++++---- src/engine/mod.rs | 20 ++++++++------------ src/engine/register.rs | 5 +++-- 10 files changed, 143 insertions(+), 26 deletions(-) diff --git a/lang/de.json b/lang/de.json index 188a18d..4f0d175 100644 --- a/lang/de.json +++ b/lang/de.json @@ -1334,5 +1334,24 @@ "You didn't identify in time; you've been renamed.": "Du hast dich nicht rechtzeitig identifiziert; du wurdest umbenannt.", "Please mind the channel rules — {reason} Next time you'll be kicked.": "Bitte beachte die Kanalregeln — {reason} Beim nächsten Mal wirst du gekickt.", "Access denied — {what} is for services operators.": "Zugriff verweigert — {what} ist für Services-Operatoren.", - "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 ist kein gültiger Nickname." + "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 ist kein gültiger Nickname.", + "Channels you had registered as \u0002{account}\u0002 were released: {list}.": "Die Kanäle, die du als \u0002{account}\u0002 registriert hattest, wurden freigegeben: {list}.", + "Your account \u0002{account}\u0002 {reason}. You have been logged out.": "Dein Konto \u0002{account}\u0002 {reason}. Du wurdest abgemeldet.", + "was dropped": "wurde gelöscht", + "collided with another network and no longer belongs to you": "kollidierte mit einem anderen Netzwerk und gehört dir nicht mehr", + "was suspended": "wurde gesperrt", + "expired after a long period of inactivity": "ist nach langer Inaktivität abgelaufen", + "Your password for \u0002{account}\u0002 has been changed.": "Dein Passwort für \u0002{account}\u0002 wurde geändert.", + "That's a lot of dice, stopping the repeats here.": "Das sind viele Würfel, ich stoppe die Wiederholungen hier.", + "trailing characters in expression": "überzählige Zeichen im Ausdruck", + "missing closing parenthesis": "schließende Klammer fehlt", + "expected a value": "Wert erwartet", + "nothing to roll": "nichts zum Würfeln", + "that expression is too long": "dieser Ausdruck ist zu lang", + "that doesn't come out to a real number": "das ergibt keine reelle Zahl", + "division by zero": "Division durch null", + "modulo by zero": "Modulo durch null", + "that expression rolls too many dice": "dieser Ausdruck würfelt zu viele Würfel", + "hypot() takes two arguments": "hypot() nimmt zwei Argumente", + "pow() takes two arguments": "pow() nimmt zwei Argumente" } \ No newline at end of file diff --git a/lang/es-ar.json b/lang/es-ar.json index e623a36..184e923 100644 --- a/lang/es-ar.json +++ b/lang/es-ar.json @@ -1334,5 +1334,24 @@ "You didn't identify in time; you've been renamed.": "No te identificaste a tiempo; se te cambió el nombre.", "Please mind the channel rules — {reason} Next time you'll be kicked.": "Prestá atención a las reglas del canal — {reason} La próxima vez te van a expulsar.", "Access denied — {what} is for services operators.": "Acceso denegado — {what} es solo para operadores de los servicios.", - "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 no es un nick válido." + "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 no es un nick válido.", + "Channels you had registered as \u0002{account}\u0002 were released: {list}.": "Los canales que habías registrado como \u0002{account}\u0002 fueron liberados: {list}.", + "Your account \u0002{account}\u0002 {reason}. You have been logged out.": "Tu cuenta \u0002{account}\u0002 {reason}. Se cerró tu sesión.", + "was dropped": "fue eliminada", + "collided with another network and no longer belongs to you": "entró en conflicto con otra red y ya no te pertenece", + "was suspended": "fue suspendida", + "expired after a long period of inactivity": "caducó tras un largo período de inactividad", + "Your password for \u0002{account}\u0002 has been changed.": "Tu contraseña para \u0002{account}\u0002 fue cambiada.", + "That's a lot of dice, stopping the repeats here.": "Son un montón de dados, corto las repeticiones acá.", + "trailing characters in expression": "caracteres de más en la expresión", + "missing closing parenthesis": "falta el paréntesis de cierre", + "expected a value": "se esperaba un valor", + "nothing to roll": "nada para tirar", + "that expression is too long": "esa expresión es demasiado larga", + "that doesn't come out to a real number": "eso no da un número real", + "division by zero": "división por cero", + "modulo by zero": "módulo por cero", + "that expression rolls too many dice": "esa expresión tira demasiados dados", + "hypot() takes two arguments": "hypot() toma dos argumentos", + "pow() takes two arguments": "pow() toma dos argumentos" } \ No newline at end of file diff --git a/lang/es.json b/lang/es.json index 4352991..58b77b2 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1334,5 +1334,24 @@ "You didn't identify in time; you've been renamed.": "No te identificaste a tiempo; se te ha cambiado el nombre.", "Please mind the channel rules — {reason} Next time you'll be kicked.": "Ten en cuenta las reglas del canal — {reason} La próxima vez se te expulsará.", "Access denied — {what} is for services operators.": "Acceso denegado — {what} es solo para operadores de servicios.", - "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 no es un nick válido." + "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 no es un nick válido.", + "Channels you had registered as \u0002{account}\u0002 were released: {list}.": "Los canales que habías registrado como \u0002{account}\u0002 han sido liberados: {list}.", + "Your account \u0002{account}\u0002 {reason}. You have been logged out.": "Tu cuenta \u0002{account}\u0002 {reason}. Se ha cerrado tu sesión.", + "was dropped": "ha sido eliminada", + "collided with another network and no longer belongs to you": "entró en conflicto con otra red y ya no te pertenece", + "was suspended": "ha sido suspendida", + "expired after a long period of inactivity": "ha caducado tras un largo periodo de inactividad", + "Your password for \u0002{account}\u0002 has been changed.": "Tu contraseña para \u0002{account}\u0002 ha sido cambiada.", + "That's a lot of dice, stopping the repeats here.": "Son muchos dados, detengo las repeticiones aquí.", + "trailing characters in expression": "caracteres sobrantes en la expresión", + "missing closing parenthesis": "falta el paréntesis de cierre", + "expected a value": "se esperaba un valor", + "nothing to roll": "nada que lanzar", + "that expression is too long": "esa expresión es demasiado larga", + "that doesn't come out to a real number": "eso no da un número real", + "division by zero": "división por cero", + "modulo by zero": "módulo por cero", + "that expression rolls too many dice": "esa expresión lanza demasiados dados", + "hypot() takes two arguments": "hypot() toma dos argumentos", + "pow() takes two arguments": "pow() toma dos argumentos" } \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json index 26eaec3..39b2f79 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1334,5 +1334,24 @@ "You didn't identify in time; you've been renamed.": "Vous ne vous êtes pas identifié à temps ; vous avez été renommé.", "Please mind the channel rules — {reason} Next time you'll be kicked.": "Veuillez respecter les règles du canal — {reason} La prochaine fois, vous serez expulsé.", "Access denied — {what} is for services operators.": "Accès refusé — {what} est réservé aux opérateurs des services.", - "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 n'est pas un pseudo valide." + "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 n'est pas un pseudo valide.", + "Channels you had registered as \u0002{account}\u0002 were released: {list}.": "Les salons que vous aviez enregistrés en tant que \u0002{account}\u0002 ont été libérés : {list}.", + "Your account \u0002{account}\u0002 {reason}. You have been logged out.": "Votre compte \u0002{account}\u0002 {reason}. Vous avez été déconnecté.", + "was dropped": "a été supprimé", + "collided with another network and no longer belongs to you": "est entré en collision avec un autre réseau et ne vous appartient plus", + "was suspended": "a été suspendu", + "expired after a long period of inactivity": "a expiré après une longue période d'inactivité", + "Your password for \u0002{account}\u0002 has been changed.": "Votre mot de passe pour \u0002{account}\u0002 a été modifié.", + "That's a lot of dice, stopping the repeats here.": "Ça fait beaucoup de dés, j'arrête les répétitions ici.", + "trailing characters in expression": "caractères en trop dans l'expression", + "missing closing parenthesis": "parenthèse fermante manquante", + "expected a value": "valeur attendue", + "nothing to roll": "rien à lancer", + "that expression is too long": "cette expression est trop longue", + "that doesn't come out to a real number": "cela ne donne pas un nombre réel", + "division by zero": "division par zéro", + "modulo by zero": "modulo par zéro", + "that expression rolls too many dice": "cette expression lance trop de dés", + "hypot() takes two arguments": "hypot() prend deux arguments", + "pow() takes two arguments": "pow() prend deux arguments" } \ No newline at end of file diff --git a/lang/pt-br.json b/lang/pt-br.json index 1a815da..2368d0f 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -1334,5 +1334,24 @@ "You didn't identify in time; you've been renamed.": "Você não se identificou a tempo; seu apelido foi alterado.", "Please mind the channel rules — {reason} Next time you'll be kicked.": "Respeite as regras do canal — {reason} Da próxima vez você será expulso.", "Access denied — {what} is for services operators.": "Acesso negado — {what} é para operadores de serviços.", - "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 não é um apelido válido." + "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 não é um apelido válido.", + "Channels you had registered as \u0002{account}\u0002 were released: {list}.": "Os canais que você registrou como \u0002{account}\u0002 foram liberados: {list}.", + "Your account \u0002{account}\u0002 {reason}. You have been logged out.": "Sua conta \u0002{account}\u0002 {reason}. Sua sessão foi encerrada.", + "was dropped": "foi removida", + "collided with another network and no longer belongs to you": "entrou em conflito com outra rede e não pertence mais a você", + "was suspended": "foi suspensa", + "expired after a long period of inactivity": "expirou após um longo período de inatividade", + "Your password for \u0002{account}\u0002 has been changed.": "Sua senha para \u0002{account}\u0002 foi alterada.", + "That's a lot of dice, stopping the repeats here.": "São muitos dados, parando as repetições aqui.", + "trailing characters in expression": "caracteres a mais na expressão", + "missing closing parenthesis": "falta o parêntese de fechamento", + "expected a value": "esperava um valor", + "nothing to roll": "nada para rolar", + "that expression is too long": "essa expressão é longa demais", + "that doesn't come out to a real number": "isso não resulta em um número real", + "division by zero": "divisão por zero", + "modulo by zero": "módulo por zero", + "that expression rolls too many dice": "essa expressão rola dados demais", + "hypot() takes two arguments": "hypot() recebe dois argumentos", + "pow() takes two arguments": "pow() recebe dois argumentos" } \ No newline at end of file diff --git a/lang/pt.json b/lang/pt.json index 1082806..43a88f4 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -1334,5 +1334,24 @@ "You didn't identify in time; you've been renamed.": "Não se identificou a tempo; o seu nome foi alterado.", "Please mind the channel rules — {reason} Next time you'll be kicked.": "Respeite as regras do canal — {reason} Da próxima vez será expulso.", "Access denied — {what} is for services operators.": "Acesso negado — {what} é para operadores de serviços.", - "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 não é uma alcunha válida." + "\u0002{nick}\u0002 isn't a valid nickname.": "\u0002{nick}\u0002 não é uma alcunha válida.", + "Channels you had registered as \u0002{account}\u0002 were released: {list}.": "Os canais que tinha registado como \u0002{account}\u0002 foram libertados: {list}.", + "Your account \u0002{account}\u0002 {reason}. You have been logged out.": "A sua conta \u0002{account}\u0002 {reason}. A sua sessão foi terminada.", + "was dropped": "foi removida", + "collided with another network and no longer belongs to you": "entrou em conflito com outra rede e já não lhe pertence", + "was suspended": "foi suspensa", + "expired after a long period of inactivity": "expirou após um longo período de inatividade", + "Your password for \u0002{account}\u0002 has been changed.": "A sua palavra-passe para \u0002{account}\u0002 foi alterada.", + "That's a lot of dice, stopping the repeats here.": "São muitos dados, a parar as repetições aqui.", + "trailing characters in expression": "caracteres a mais na expressão", + "missing closing parenthesis": "falta o parêntese de fecho", + "expected a value": "esperava um valor", + "nothing to roll": "nada para lançar", + "that expression is too long": "essa expressão é demasiado longa", + "that doesn't come out to a real number": "isso não dá um número real", + "division by zero": "divisão por zero", + "modulo by zero": "módulo por zero", + "that expression rolls too many dice": "essa expressão lança demasiados dados", + "hypot() takes two arguments": "hypot() recebe dois argumentos", + "pow() takes two arguments": "pow() recebe dois argumentos" } \ No newline at end of file diff --git a/modules/diceserv/src/roll.rs b/modules/diceserv/src/roll.rs index e0a3e69..0adfd3b 100644 --- a/modules/diceserv/src/roll.rs +++ b/modules/diceserv/src/roll.rs @@ -11,7 +11,7 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, exte let input = rest.join(" "); let input = input.trim(); if input.is_empty() { - ctx.notice(me, from.uid, "Give me something to roll, e.g. \x022d6+3\x02."); + ctx.notice(me, from.uid, t!(ctx, "Give me something to roll, e.g. \x022d6+3\x02.")); return; } // `N~expr` repeats the roll N times. @@ -44,12 +44,15 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, exte // `25~99999d1+…` can't multiply the per-roll cap into tens of millions. if spent >= expr::MAX_TOTAL { if i + 1 < times { - ctx.notice(me, from.uid, "That's a lot of dice — stopping the repeats here."); + ctx.notice(me, from.uid, t!(ctx, "That's a lot of dice, stopping the repeats here.")); } break; } } Err(why) => { + // The parser returns its diagnostic in English; localize the static + // fragments through the catalog (interpolated ones fall back verbatim). + let why = echo_api::render(ctx.lang(), &why, &[]); ctx.notice(me, from.uid, t!(ctx, "I couldn't roll \x02{body}\x02: {why}.", body = body, why = why)); return; // the whole batch shares the expression, so it'll fail the same way } diff --git a/src/engine/db/account.rs b/src/engine/db/account.rs index 461006c..5ae7994 100644 --- a/src/engine/db/account.rs +++ b/src/engine/db/account.rs @@ -842,8 +842,9 @@ impl Db { } /// Channels entering the warning window, paired with their founder's email - /// (skipped when the founder has none). Returns (channel, email, seconds left). - pub fn channels_to_warn(&self, now: u64, ttl: u64, lead: u64) -> Vec<(String, String, u64)> { + /// (skipped when the founder has none) and preferred language. Returns + /// (channel, email, seconds left, founder language). + pub fn channels_to_warn(&self, now: u64, ttl: u64, lead: u64) -> Vec<(String, String, u64, String)> { let floor = ttl.saturating_sub(lead); self.channels .values() @@ -853,8 +854,10 @@ impl Db { if idle <= floor || idle > ttl { return None; } - let email = self.accounts.get(&key(&c.founder)).and_then(|a| a.email.clone())?; - Some((c.name.clone(), email, ttl - idle)) + let acc = self.accounts.get(&key(&c.founder))?; + let email = acc.email.clone()?; + let lang = acc.language.clone().unwrap_or_else(|| self.default_language()); + Some((c.name.clone(), email, ttl - idle, lang)) }) .collect() } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 9d21cfc..e9d2cb9 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -727,11 +727,8 @@ impl Engine { self.logout_uid(&uid, account, reason); if !orphaned.is_empty() { if let Some(ns) = &ns { - self.emit_irc(NetAction::Notice { - from: ns.clone(), - to: uid, - text: format!("Channels you had registered as \x02{account}\x02 were released: {}.", orphaned.join(", ")), - }); + let text = echo_api::render(&self.lang_for_uid(&uid), "Channels you had registered as \x02{account}\x02 were released: {list}.", &[("account", account.to_string()), ("list", orphaned.join(", "))]); + self.emit_irc(NetAction::Notice { from: ns.clone(), to: uid.clone(), text }); } } } @@ -773,11 +770,10 @@ impl Engine { self.network.clear_account(uid); self.emit_irc(NetAction::Metadata { target: uid.to_string(), key: "accountname".to_string(), value: String::new() }); if let Some(ns) = self.nick_service.clone() { - self.emit_irc(NetAction::Notice { - from: ns, - to: uid.to_string(), - text: format!("Your account \x02{account}\x02 {reason}. You have been logged out."), - }); + let lang = self.lang_for_uid(uid); + let reason = echo_api::render(&lang, reason, &[]); + let text = echo_api::render(&lang, "Your account \x02{account}\x02 {reason}. You have been logged out.", &[("account", account.to_string()), ("reason", reason)]); + self.emit_irc(NetAction::Notice { from: ns, to: uid.to_string(), text }); } } @@ -859,8 +855,8 @@ impl Engine { } } if let Some(ttl) = self.channel_ttl { - for (channel, email, left) in self.db.channels_to_warn(now, ttl, lead) { - let mail = echo_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), echo_api::email::ExpiryTarget::Channel, &channel, &human_duration(left), &self.db.default_language()); + for (channel, email, left, lang) in self.db.channels_to_warn(now, ttl, lead) { + let mail = echo_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), echo_api::email::ExpiryTarget::Channel, &channel, &human_duration(left), &lang); self.emit_irc(NetAction::SendEmail { to: email, subject: mail.subject, text: mail.text, html: Some(mail.html) }); self.db.mark_channel_warned(&channel); } diff --git a/src/engine/register.rs b/src/engine/register.rs index 9105ca1..c8090ca 100644 --- a/src/engine/register.rs +++ b/src/engine/register.rs @@ -356,9 +356,10 @@ impl Engine { // Commit a password change the link layer derived off-thread, then notice the user. pub fn complete_password_change(&mut self, account: &str, creds: Option, agent: &str, uid: &str) -> Vec { + let lang = self.lang_for_account(account); let text = match creds.and_then(|c| self.db.set_credentials(account, c).ok()) { - Some(()) => format!("Your password for \x02{account}\x02 has been changed."), - None => "Sorry, that didn't work. Please try again in a moment.".to_string(), + Some(()) => echo_api::render(&lang, "Your password for \x02{account}\x02 has been changed.", &[("account", account.to_string())]), + None => echo_api::render(&lang, "Sorry, that didn't work. Please try again in a moment.", &[]), }; vec![NetAction::Notice { from: agent.to_string(), to: uid.to_string(), text }] }