` popover inside a , and the
// wrapper's clipping was hiding the bottom half of the menu.
- s.push_str(
+ let _ = write!(
+ s,
r##"
- | Username |
- Display name |
- Email |
- Status |
- Admin |
- TOTP |
- Last seen |
- Actions |
+ {c_username} |
+ {c_display_name} |
+ {c_email} |
+ {c_status} |
+ {c_admin} |
+ {c_totp} |
+ {c_last_seen} |
+ {c_actions} |
"##,
+ c_username = t(lang, "users.col_username"),
+ c_display_name = t(lang, "users.col_display_name"),
+ c_email = t(lang, "users.col_email"),
+ c_status = t(lang, "users.col_status"),
+ c_admin = t(lang, "users.col_admin"),
+ c_totp = t(lang, "users.col_totp"),
+ c_last_seen = t(lang, "users.col_last_seen"),
+ c_actions = t(lang, "common.actions"),
);
for u in &users {
render_user_row(
&mut s,
+ lang,
u,
*totp.get(&u.id).unwrap_or(&false),
last_seen.get(&u.id).map(String::as_str),
@@ -420,50 +426,75 @@ async fn render_table(state: &Arc) -> Result {
Ok(s)
}
-fn render_user_row(s: &mut String, u: &UserRow, has_totp: bool, last_seen: Option<&str>) {
+fn render_user_row(
+ s: &mut String,
+ lang: Lang,
+ u: &UserRow,
+ has_totp: bool,
+ last_seen: Option<&str>,
+) {
let status_badge = match u.status {
- 1 => r#"active"#,
- 0 => r#"disabled"#,
- -1 => r#"unverified"#,
- _ => "",
+ 1 => format!(
+ r#"{}"#,
+ t(lang, "users.status_active")
+ ),
+ 0 => format!(
+ r#"{}"#,
+ t(lang, "users.status_disabled")
+ ),
+ -1 => format!(
+ r#"{}"#,
+ t(lang, "users.status_unverified")
+ ),
+ _ => String::new(),
};
let admin_badge = if u.is_admin {
- r#"admin"#
+ format!(
+ r#"{}"#,
+ t(lang, "users.admin_label"),
+ )
} else {
- ""
+ String::new()
};
// The TOTP column doubles as an "auth path" indicator: OIDC-linked
// users get an "OIDC" badge (their MFA lives at the IdP — local
// TOTP is moot), and OIDC takes precedence over local TOTP if both
// somehow exist.
let totp_badge = if u.is_oidc_linked() {
- r#"OIDC"#
+ r#"OIDC"#.to_string()
} else if has_totp {
- r#"enrolled"#
+ format!(
+ r#"{}"#,
+ t(lang, "users.totp_enrolled")
+ )
} else {
- ""
+ String::new()
};
let oidc_linked = u.is_oidc_linked();
// OIDC-linked users sign in via the IdP — adding a local password
// would let them bypass the IdP (and any MFA enforced there). Show
// a note instead of the password-reset form for these accounts.
let password_form = if oidc_linked {
- r##"
- Linked to OIDC — password sign-in is disabled.
- "##
- .to_string()
+ format!(
+ r##"
+ {msg}
+ "##,
+ msg = t(lang, "users.oidc_disabled"),
+ )
} else {
format!(
r##""##,
id = u.id,
+ ph = t(lang, "users.new_password"),
+ set = t(lang, "users.password_set"),
)
};
let (last_seen_rel, last_seen_abs) = match last_seen {
Some(ts) => (relative_ts(ts), html_escape(ts)),
- None => ("never".to_string(), String::new()),
+ None => (t(lang, "common.never").to_string(), String::new()),
};
// TOTP enrollment is self-service (the user does it on their
// profile page so they can scan the QR + verify a code before
@@ -473,11 +504,12 @@ fn render_user_row(s: &mut String, u: &UserRow, has_totp: bool, last_seen: Optio
format!(
r##""##,
id = u.id,
- username = html_escape(&u.username),
+ confirm = html_escape_attr(&tf1(lang, "users.confirm_disable_totp", &u.username)),
+ label = t(lang, "users.disable_totp"),
)
} else {
String::new()
@@ -497,9 +529,9 @@ fn render_user_row(s: &mut String, u: &UserRow, has_totp: bool, last_seen: Optio
···
{password_form}
@@ -528,15 +560,32 @@ fn render_user_row(s: &mut String, u: &UserRow, has_totp: bool, last_seen: Optio
status = status_badge,
admin = admin_badge,
totp = totp_badge,
- admin_label = if u.is_admin { "Revoke admin" } else { "Grant admin" },
- status_label = if u.status == 1 { "Disable user" } else { "Enable user" },
+ ph_dn = t(lang, "users.display_name"),
+ ph_email = t(lang, "users.col_email"),
+ save_profile = t(lang, "users.save_profile"),
+ admin_label = if u.is_admin {
+ t(lang, "users.revoke_admin")
+ } else {
+ t(lang, "users.grant_admin")
+ },
+ status_label = if u.status == 1 {
+ t(lang, "users.disable_user")
+ } else {
+ t(lang, "users.enable_user")
+ },
totp_button = totp_button,
last_seen_rel = last_seen_rel,
last_seen_abs = last_seen_abs,
password_form = password_form,
+ confirm_delete = html_escape_attr(&tf1(lang, "users.confirm_delete", &u.username)),
+ delete_user = t(lang, "users.delete_user"),
);
}
+fn html_escape_attr(s: &str) -> String {
+ html_escape(s)
+}
+
/// Format a SQLite `current_timestamp` string ("YYYY-MM-DD HH:MM:SS",
/// always UTC) as a relative time-ago label. Renders short forms — "5m
/// ago", "3h ago", "2d ago" — for the at-a-glance column; the absolute
|