Localize engine notices, transactional emails, and operator-access denials in all six languages
All checks were successful
CI / check (push) Successful in 5m18s
All checks were successful
CI / check (push) Successful in 5m18s
This commit is contained in:
parent
ecdead8278
commit
e4a14b9813
13 changed files with 299 additions and 55 deletions
|
|
@ -87,7 +87,8 @@ impl Engine {
|
|||
};
|
||||
if !already {
|
||||
self.bump("botserv.warn");
|
||||
return vec![NetAction::Notice { from: botuid, to: from.to_string(), text: format!("Please mind the channel rules — {reason} Next time you'll be kicked.") }];
|
||||
let text = echo_api::render(&self.lang_for_uid(from), "Please mind the channel rules — {reason} Next time you'll be kicked.", &[("reason", reason.to_string())]);
|
||||
return vec![NetAction::Notice { from: botuid, to: from.to_string(), text }];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -620,7 +620,7 @@ impl Engine {
|
|||
out.push(NetAction::Notice {
|
||||
from: ns.clone(),
|
||||
to: uid.to_string(),
|
||||
text: format!("You have \x02{unread}\x02 new memo(s). Read them with \x02/msg MemoServ READ NEW\x02."),
|
||||
text: echo_api::render_plural(&self.lang_for_account(account), unread as u64, "You have \x02{unread}\x02 new memo. Read it with \x02/msg MemoServ READ NEW\x02.", "You have \x02{unread}\x02 new memos. Read them with \x02/msg MemoServ READ NEW\x02.", &[("unread", unread.to_string())]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -718,7 +718,8 @@ impl Engine {
|
|||
// Tell any online successor they inherited a channel.
|
||||
for (chan, s) in &inherited {
|
||||
if let (Some(ns), Some(uid)) = (&ns, self.network.uids_logged_into(s).into_iter().next()) {
|
||||
self.emit_irc(NetAction::Notice { from: ns.clone(), to: uid, text: format!("You are now the founder of \x02{chan}\x02 (inherited from \x02{account}\x02).") });
|
||||
let text = echo_api::render(&self.lang_for_account(s), "You are now the founder of \x02{chan}\x02 (inherited from \x02{account}\x02).", &[("chan", chan.to_string()), ("account", account.to_string())]);
|
||||
self.emit_irc(NetAction::Notice { from: ns.clone(), to: uid, text });
|
||||
}
|
||||
}
|
||||
// Log out and inform each local session that held the name.
|
||||
|
|
@ -809,6 +810,21 @@ impl Engine {
|
|||
// no per-record timer. Opers, accounts with a live session, and occupied
|
||||
// channels are spared; each expiry is announced to the audit channel. Emits
|
||||
// cleanup directly over the outbound path (like a takeover cleanup).
|
||||
// The language to address `account`'s owner in: their stored preference, or
|
||||
// the network default.
|
||||
fn lang_for_account(&self, account: &str) -> String {
|
||||
self.db.language_of(account).unwrap_or_else(|| self.db.default_language())
|
||||
}
|
||||
|
||||
// The language to address the user on `uid` in: their logged-in account's
|
||||
// preference, or the network default (e.g. an unidentified user).
|
||||
fn lang_for_uid(&self, uid: &str) -> String {
|
||||
self.network
|
||||
.account_of(uid)
|
||||
.and_then(|a| self.db.language_of(a))
|
||||
.unwrap_or_else(|| self.db.default_language())
|
||||
}
|
||||
|
||||
pub fn expire_sweep(&mut self) {
|
||||
let now = self.now_secs();
|
||||
// First, warn owners whose account/channel is approaching expiry and for
|
||||
|
|
@ -816,14 +832,14 @@ impl Engine {
|
|||
if let Some(lead) = self.expire_warn.filter(|_| self.db.email_enabled()) {
|
||||
if let Some(ttl) = self.account_ttl {
|
||||
for (account, email, left) in self.db.accounts_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::Account, &account, &human_duration(left));
|
||||
let mail = echo_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), echo_api::email::ExpiryTarget::Account, &account, &human_duration(left), &self.lang_for_account(&account));
|
||||
self.emit_irc(NetAction::SendEmail { to: email, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||
self.db.mark_account_warned(&account);
|
||||
}
|
||||
}
|
||||
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));
|
||||
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());
|
||||
self.emit_irc(NetAction::SendEmail { to: email, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||
self.db.mark_channel_warned(&channel);
|
||||
}
|
||||
|
|
@ -1032,7 +1048,8 @@ impl Engine {
|
|||
for uid in fire {
|
||||
let guest = echo_api::next_guest_nick(&self.guest_nick, &mut self.enforce_seq, &self.network, &self.db);
|
||||
if let Some(ns) = self.nick_service.clone() {
|
||||
self.emit_irc(NetAction::Notice { from: ns, to: uid.clone(), text: "You didn't identify in time; you've been renamed.".to_string() });
|
||||
let text = echo_api::render(&self.db.default_language(), "You didn't identify in time; you've been renamed.", &[]);
|
||||
self.emit_irc(NetAction::Notice { from: ns, to: uid.clone(), text });
|
||||
}
|
||||
self.emit_irc(NetAction::ForceNick { uid, nick: guest });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ impl Engine {
|
|||
if status == AuthorityStatus::Ok && !self.db.is_verified(name) {
|
||||
if let Some(addr) = addr {
|
||||
let code = self.db.issue_code(name, db::CodeKind::Confirm);
|
||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code);
|
||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code, &self.lang_for_account(name));
|
||||
self.emit_irc(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||
}
|
||||
}
|
||||
|
|
@ -191,7 +191,8 @@ impl Engine {
|
|||
self.network.clear_account(&uid);
|
||||
self.emit_irc(NetAction::Metadata { target: uid.clone(), key: "accountname".to_string(), value: String::new() });
|
||||
if let Some(ns) = &ns {
|
||||
self.emit_irc(NetAction::Notice { from: ns.clone(), to: uid, text: format!("You have been logged out of \x02{account}\x02.") });
|
||||
let text = echo_api::render(&self.lang_for_account(account), "You have been logged out of \x02{account}\x02.", &[("account", account.to_string())]);
|
||||
self.emit_irc(NetAction::Notice { from: ns.clone(), to: uid, text });
|
||||
}
|
||||
}
|
||||
n
|
||||
|
|
@ -273,7 +274,7 @@ impl Engine {
|
|||
}
|
||||
Some((false, Some(addr))) => {
|
||||
let code = self.db.issue_code(&account, db::CodeKind::Confirm);
|
||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), &account, &code);
|
||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), &account, &code, &self.lang_for_account(&account));
|
||||
let mut out = resp("verification_required", "VERIFICATION_REQUIRED", "A new confirmation code has been emailed.");
|
||||
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||
out
|
||||
|
|
@ -338,10 +339,10 @@ impl Engine {
|
|||
if needs_verify {
|
||||
if let Some(addr) = addr {
|
||||
let code = self.db.issue_code(account, db::CodeKind::Confirm);
|
||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code);
|
||||
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code, &self.lang_for_account(account));
|
||||
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
|
||||
if let RegReply::NickServ { agent, uid, .. } = &reply {
|
||||
out.push(NetAction::Notice { from: agent.clone(), to: uid.clone(), text: "A confirmation code has been emailed to you. Confirm with \x02CONFIRM <code>\x02.".to_string() });
|
||||
out.push(NetAction::Notice { from: agent.clone(), to: uid.clone(), text: echo_api::render(&self.lang_for_account(account), "A confirmation code has been emailed to you. Confirm with \x02CONFIRM <code>\x02.", &[]) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -366,14 +367,15 @@ impl Engine {
|
|||
let actions = match then {
|
||||
AuthThen::Identify { uid, agent, name, account } => {
|
||||
self.db.note_auth(&name, ok);
|
||||
let mut ctx = ServiceCtx::default();
|
||||
let lang = self.lang_for_account(&account);
|
||||
let mut ctx = ServiceCtx { lang: lang.clone(), ..Default::default() };
|
||||
if !ok {
|
||||
ctx.count("nickserv.identify_fail");
|
||||
ctx.fail(&agent, &uid, "IDENTIFY", "INVALID_CREDENTIALS", "Invalid password. Please try again.");
|
||||
} else {
|
||||
ctx.login(&uid, &account);
|
||||
ctx.count("nickserv.identify");
|
||||
ctx.notice(&agent, &uid, format!("You're now identified as \x02{account}\x02. Welcome back!"));
|
||||
ctx.notice(&agent, &uid, echo_api::render(&lang, "You're now identified as \x02{account}\x02. Welcome back!", &[("account", account.clone())]));
|
||||
for entry in self.db.ajoin_list(&account) {
|
||||
ctx.force_join(&uid, &entry.channel, &entry.key);
|
||||
}
|
||||
|
|
@ -386,7 +388,7 @@ impl Engine {
|
|||
}
|
||||
let unread = self.db.unread_memos(&account);
|
||||
if unread > 0 && self.db.memo_notify_on(&account) {
|
||||
ctx.notice(&agent, &uid, format!("You have \x02{unread}\x02 new memo(s). Read them with \x02/msg MemoServ READ NEW\x02."));
|
||||
ctx.notice(&agent, &uid, echo_api::render_plural(&lang, unread as u64, "You have \x02{unread}\x02 new memo. Read it with \x02/msg MemoServ READ NEW\x02.", "You have \x02{unread}\x02 new memos. Read them with \x02/msg MemoServ READ NEW\x02.", &[("unread", unread.to_string())]));
|
||||
}
|
||||
}
|
||||
for key in std::mem::take(&mut ctx.stats) {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,50 @@ fn every_template_is_translated() {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Also guard the engine- and email-layer strings, which localise via
|
||||
// echo_api::render / crate::render (with a literal msgid) rather than the
|
||||
// module macros. Scoped to those call prefixes so email.rs's own private
|
||||
// `render` HTML helper and test literals aren't picked up.
|
||||
let render_re =
|
||||
Regex::new(&format!(r"(?:echo_api|crate)::render\s*\(\s*[^,]+,\s*{str_lit}")).unwrap();
|
||||
let render_plural_re = Regex::new(&format!(
|
||||
r"(?:echo_api|crate)::render_plural\s*\(\s*[^,]+,\s*[^,]+,\s*{str_lit}\s*,\s*{str_lit}"
|
||||
))
|
||||
.unwrap();
|
||||
let mut extra = vec![PathBuf::from(ROOT).join("api/src/email.rs")];
|
||||
let mut stack = vec![PathBuf::from(ROOT).join("src/engine")];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let Ok(rd) = fs::read_dir(&dir) else { continue };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
stack.push(p);
|
||||
} else if p.extension().and_then(|s| s.to_str()) == Some("rs")
|
||||
&& p.file_name().and_then(|s| s.to_str()) != Some("tests.rs")
|
||||
{
|
||||
extra.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
for path in extra {
|
||||
let Ok(src) = fs::read_to_string(&path) else { continue };
|
||||
let rel = path.strip_prefix(ROOT).unwrap_or(&path).display().to_string();
|
||||
for cap in render_re.captures_iter(&src) {
|
||||
let id = unescape(&cap[1]);
|
||||
if !reference.contains_key(&id) {
|
||||
missing.push(format!("{rel}: render {id:?}"));
|
||||
}
|
||||
}
|
||||
for cap in render_plural_re.captures_iter(&src) {
|
||||
for g in [1usize, 2] {
|
||||
let id = unescape(&cap[g]);
|
||||
if !reference.contains_key(&id) {
|
||||
missing.push(format!("{rel}: render_plural {id:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"these templates have no catalog entry:\n{}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue