Localize engine notices, transactional emails, and operator-access denials in all six languages
All checks were successful
CI / check (push) Successful in 5m18s

This commit is contained in:
Jean Chevronnet 2026-07-20 02:25:54 +00:00
parent ecdead8278
commit e4a14b9813
No known key found for this signature in database
13 changed files with 299 additions and 55 deletions

View file

@ -33,38 +33,44 @@ fn render(brand: &str, accent: &str, logo: &str, title: &str, message: &str, cod
.replace("{{note}}", &escape(note))
}
pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail {
pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str, lang: &str) -> Mail {
let acc = || vec![("account", account.to_string())];
Mail {
subject: format!("Password reset for {account}"),
text: format!(
"Your password reset code for {account} is: {code}\nIt expires in 15 minutes.\nReset with:\n /msg NickServ RESETPASS {account} {code} <newpassword>\n"
subject: crate::render(lang, "Password reset for {account}", &acc()),
text: crate::render(
lang,
"Your password reset code for {account} is: {code}\nIt expires in 15 minutes.\nReset with:\n /msg NickServ RESETPASS {account} {code} <newpassword>\n",
&[("account", account.to_string()), ("code", code.to_string())],
),
html: render(
brand,
accent,
logo,
"Password reset",
&format!("Use this code to reset the password for your account {account}."),
&crate::render(lang, "Password reset", &[]),
&crate::render(lang, "Use this code to reset the password for your account {account}.", &acc()),
code,
"This code expires in 15 minutes. If you didn't ask to reset it, ignore this email.",
&crate::render(lang, "This code expires in 15 minutes. If you didn't ask to reset it, ignore this email.", &[]),
),
}
}
pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail {
pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str, lang: &str) -> Mail {
let acc = || vec![("account", account.to_string())];
Mail {
subject: format!("Confirm your {account} registration"),
text: format!(
"Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes.\n"
subject: crate::render(lang, "Confirm your {account} registration", &acc()),
text: crate::render(
lang,
"Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes.\n",
&[("account", account.to_string()), ("code", code.to_string())],
),
html: render(
brand,
accent,
logo,
"Confirm your account",
&format!("Welcome! Confirm the email for your account {account} with the code below."),
&crate::render(lang, "Confirm your account", &[]),
&crate::render(lang, "Welcome! Confirm the email for your account {account} with the code below.", &acc()),
code,
"This code expires in 15 minutes.",
&crate::render(lang, "This code expires in 15 minutes.", &[]),
),
}
}
@ -87,26 +93,39 @@ impl ExpiryTarget {
// Warn the owner of an account or channel that inactivity will soon expire it.
// `remaining` is a human span ("7 days") and takes the prominent code slot.
pub fn expiry_warning(brand: &str, accent: &str, logo: &str, kind: ExpiryTarget, name: &str, remaining: &str) -> Mail {
let word = kind.word();
let keep = if kind == ExpiryTarget::Channel {
"To keep it, have a member join the channel before then. Otherwise it will be removed."
} else {
"To keep it, just identify to it before then. Otherwise it will be removed."
pub fn expiry_warning(brand: &str, accent: &str, logo: &str, kind: ExpiryTarget, name: &str, remaining: &str, lang: &str) -> Mail {
let word = crate::render(lang, kind.word(), &[]);
let keep = crate::render(
lang,
if kind == ExpiryTarget::Channel {
"To keep it, have a member join the channel before then. Otherwise it will be removed."
} else {
"To keep it, just identify to it before then. Otherwise it will be removed."
},
&[],
);
let base = || {
vec![
("word", word.clone()),
("name", name.to_string()),
("remaining", remaining.to_string()),
]
};
Mail {
subject: format!("Your {word} {name} is about to expire"),
text: format!(
"Your {word} {name} has been inactive and will expire in {remaining}.\n{keep}\n"
subject: crate::render(lang, "Your {word} {name} is about to expire", &[("word", word.clone()), ("name", name.to_string())]),
text: crate::render(
lang,
"Your {word} {name} has been inactive and will expire in {remaining}.\n{keep}\n",
&[("word", word.clone()), ("name", name.to_string()), ("remaining", remaining.to_string()), ("keep", keep.clone())],
),
html: render(
brand,
accent,
logo,
"About to expire",
&format!("Your {word} {name} has been inactive and will expire in {remaining}."),
&crate::render(lang, "About to expire", &[]),
&crate::render(lang, "Your {word} {name} has been inactive and will expire in {remaining}.", &base()),
remaining,
keep,
&keep,
),
}
}

View file

@ -545,19 +545,27 @@ pub fn plural_category(lang: &str, n: u64) -> Plural {
}
}
/// Render the `one` or `other` English template for `n` under `lang`'s plural
/// rule, then substitute `{name}` args. The [`plural!`] macro wraps this for
/// service call sites; the engine and email layer (which have no `ctx`) call it
/// directly with an explicit language.
pub fn render_plural(lang: &str, n: u64, one: &str, other: &str, args: &[(&str, String)]) -> String {
let key = match plural_category(lang, n) {
Plural::One => one,
Plural::Other => other,
};
render(lang, key, args)
}
/// Localize a count-dependent reply. Selects the `one` or `other` English
/// template by the target language's plural rule, then renders it like [`t!`].
/// Both forms are message ids, so both need a catalog entry in each language.
/// `plural!(ctx, n, one = "…{n} entry…", other = "…{n} entries…", n = n, …)`.
#[macro_export]
macro_rules! plural {
($ctx:expr, $n:expr, one = $one:literal, other = $other:literal $(, $name:ident = $val:expr)* $(,)?) => {{
let __key = match $crate::plural_category($ctx.lang(), ($n) as u64) {
$crate::Plural::One => $one,
$crate::Plural::Other => $other,
};
$crate::render($ctx.lang(), __key, &[$((stringify!($name), format!("{}", $val))),*])
}};
($ctx:expr, $n:expr, one = $one:literal, other = $other:literal $(, $name:ident = $val:expr)* $(,)?) => {
$crate::render_plural($ctx.lang(), ($n) as u64, $one, $other, &[$((stringify!($name), format!("{}", $val))),*])
};
}
/// The intent sink a service writes to. A service never mutates the network or
@ -2594,7 +2602,9 @@ pub fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx, need: Option<
None => from.privs.any(),
};
if !ok {
ctx.notice(me, from.uid, format!("Access denied — {what} is for services operators."));
// Translate the action fragment, then the sentence around it.
let what = render(ctx.lang(), what, &[]);
ctx.notice(me, from.uid, render(ctx.lang(), "Access denied — {what} is for services operators.", &[("what", what)]));
}
ok
}