goblin: move Trusted Sites into the nostr settings group + fix raw-key leaks
Trusted Sites is nostr-identity signing, and name authority now lives on the Username page, so the nostr cluster is its logical home. Pure placement move: the row now renders inside the identity card just below Nostr Relays; label, live session-count value, and open handler are unchanged. Also carries the two raw-key leak fixes (goblin.receipt.copied -> goblin.receive.copied, wallets.canceled -> wallets.tx_canceled) and the every_t_call_site_key_exists_in_en guard, so this branch is clean on its own. Translations for the new settings/username/advprivacy keys were reviewed across all six locales and left as-is (consistent with house register).
This commit is contained in:
+19
-15
@@ -3458,6 +3458,10 @@ impl GoblinWalletView {
|
||||
// Username has its own home (claim/release + name authority); the
|
||||
// row shows the current name (or "Not set") and opens that page.
|
||||
let mut open_username = false;
|
||||
// Trusted Sites (the active Authorize Sessions) lives with the
|
||||
// nostr rows — it is nostr-identity signing, not a wallet setting
|
||||
// — but its open handler runs further down, so the flag is here.
|
||||
let mut open_trusted = false;
|
||||
w::card(ui, |ui| {
|
||||
if !npub.is_empty() {
|
||||
let username = wallet
|
||||
@@ -3510,6 +3514,20 @@ impl GoblinWalletView {
|
||||
) {
|
||||
open_relays = true;
|
||||
}
|
||||
// Trusted Sites: the active Authorize Sessions, with a
|
||||
// one-tap end; the row value is the live session count.
|
||||
// Nostr-identity signing, so it sits with the keys/relays.
|
||||
let session_count = wallet
|
||||
.nostr_service()
|
||||
.map(|s| s.session_summaries().len())
|
||||
.unwrap_or(0);
|
||||
if settings_row_nav(
|
||||
ui,
|
||||
&t!("goblin.settings.trusted_sites"),
|
||||
&session_count.to_string(),
|
||||
) {
|
||||
open_trusted = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if open_username {
|
||||
@@ -3536,7 +3554,7 @@ impl GoblinWalletView {
|
||||
RichText::new(format!(
|
||||
"{} {}",
|
||||
crate::gui::icons::CHECK,
|
||||
t!("goblin.receipt.copied")
|
||||
t!("goblin.receive.copied")
|
||||
))
|
||||
.font(FontId::new(13.0, fonts::medium()))
|
||||
.color(t.pos),
|
||||
@@ -3570,7 +3588,6 @@ impl GoblinWalletView {
|
||||
ui.add_space(16.0);
|
||||
let mut open_node = false;
|
||||
let mut open_slatepack = false;
|
||||
let mut open_trusted = false;
|
||||
settings_group(ui, &t!("goblin.settings.wallet"), |ui| {
|
||||
if settings_row_nav(ui, &t!("goblin.settings.node"), &node_summary(wallet)) {
|
||||
open_node = true;
|
||||
@@ -3600,19 +3617,6 @@ impl GoblinWalletView {
|
||||
) {
|
||||
open_slatepack = true;
|
||||
}
|
||||
// Trusted Sites: the active Authorize Sessions, with a one-tap
|
||||
// end. Shows the live count as the row value.
|
||||
let session_count = wallet
|
||||
.nostr_service()
|
||||
.map(|s| s.session_summaries().len())
|
||||
.unwrap_or(0);
|
||||
if settings_row_nav(
|
||||
ui,
|
||||
&t!("goblin.settings.trusted_sites"),
|
||||
&session_count.to_string(),
|
||||
) {
|
||||
open_trusted = true;
|
||||
}
|
||||
});
|
||||
if open_slatepack {
|
||||
self.slatepack = SlatepackManual::default();
|
||||
|
||||
@@ -569,7 +569,7 @@ impl WalletTransactionsContent {
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")),
|
||||
_ => format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -123,3 +123,106 @@ fn every_locale_has_all_goblin_keys() {
|
||||
problems.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// Every `t!("literal.key")` in the source must resolve to a key present in
|
||||
/// en.yml. Guards against raw-key leaks (a call site typing a key that was
|
||||
/// never added, or renamed out from under it) that render the dotted key
|
||||
/// verbatim in the UI. The cross-locale check above only compares locale files
|
||||
/// to each other; it cannot see the call sites.
|
||||
#[test]
|
||||
fn every_t_call_site_key_exists_in_en() {
|
||||
// Full en.yml, all namespaces (t! is used for goblin.*, wallets.*, network.*, …).
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("locales")
|
||||
.join("en.yml");
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
|
||||
let doc: serde_yaml::Value = serde_yaml::from_str(&text).expect("invalid YAML in en.yml");
|
||||
let mut en = BTreeMap::new();
|
||||
flatten(&doc, "", &mut en);
|
||||
let en_keys: BTreeSet<&String> = en.keys().collect();
|
||||
|
||||
// A localization key: dotted, lowercase-ish namespace segments. This filters
|
||||
// out format strings, URIs, and other literals that happen to sit in a t! arg
|
||||
// position but are not keys (there are none today, but keep the guard honest).
|
||||
fn is_key_like(s: &str) -> bool {
|
||||
s.contains('.')
|
||||
&& s.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_')
|
||||
&& s.split('.').all(|seg| {
|
||||
!seg.is_empty()
|
||||
&& seg
|
||||
.bytes()
|
||||
.next()
|
||||
.map_or(false, |b| b.is_ascii_alphabetic())
|
||||
})
|
||||
}
|
||||
|
||||
// Walk src/ collecting t!("…") literal keys with their location.
|
||||
fn walk(dir: &Path, out: &mut Vec<(String, String, usize)>) {
|
||||
let entries = match std::fs::read_dir(dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_dir() {
|
||||
walk(&p, out);
|
||||
} else if p.extension().and_then(|e| e.to_str()) == Some("rs") {
|
||||
let Ok(src) = std::fs::read_to_string(&p) else {
|
||||
continue;
|
||||
};
|
||||
for (lineno, line) in src.lines().enumerate() {
|
||||
collect_line(line, &p.display().to_string(), lineno + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find `t!("key"` occurrences on a line, honoring a word boundary so we do
|
||||
// not match `format!("…")`, `print!("…")`, etc.
|
||||
fn collect_line(line: &str, file: &str, lineno: usize, out: &mut Vec<(String, String, usize)>) {
|
||||
let bytes = line.as_bytes();
|
||||
let mut i = 0;
|
||||
while let Some(rel) = line[i..].find("t!(") {
|
||||
let start = i + rel;
|
||||
// word boundary before the `t`
|
||||
let boundary = start == 0
|
||||
|| !matches!(bytes[start - 1], b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_');
|
||||
let after = &line[start + 3..];
|
||||
let after_trim = after.trim_start();
|
||||
if boundary {
|
||||
if let Some(rest) = after_trim.strip_prefix('"') {
|
||||
if let Some(end) = rest.find('"') {
|
||||
out.push((rest[..end].to_string(), file.to_string(), lineno));
|
||||
}
|
||||
}
|
||||
}
|
||||
i = start + 3;
|
||||
}
|
||||
}
|
||||
|
||||
let mut sites = Vec::new();
|
||||
walk(
|
||||
&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
|
||||
&mut sites,
|
||||
);
|
||||
|
||||
let mut leaks = Vec::new();
|
||||
for (key, file, lineno) in &sites {
|
||||
if is_key_like(key) && !en_keys.contains(key) {
|
||||
leaks.push(format!(
|
||||
"{file}:{lineno}: t!(\"{key}\") — key not in en.yml"
|
||||
));
|
||||
}
|
||||
}
|
||||
leaks.sort();
|
||||
leaks.dedup();
|
||||
|
||||
assert!(
|
||||
leaks.is_empty(),
|
||||
"raw-key leak: {} t! call site(s) reference a key absent from en.yml:\n{}",
|
||||
leaks.len(),
|
||||
leaks.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user