1
0
forked from GRIN/grim

goblin: polish FR stale label + fix raw-key leaks on this branch

FR wallets.tx_stale was 'En attente trop longtemps' (noun + adverb, not
grammatical); now 'En attente trop longue', adjective agreeing with attente.
Other five locales for tx_stale / receipt.stale_note read natural, left as-is.

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 standalone.
This commit is contained in:
2ro
2026-07-06 21:13:44 -04:00
parent 062cf2f2ab
commit d24a09074a
4 changed files with 106 additions and 3 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ wallets:
tx_finalizing: Finalisation
tx_posting: Publication
tx_confirmed: Confirmé
tx_stale: En attente trop longtemps
tx_stale: En attente trop longue
txs: Transactions
tx: Transaction
messages: Messages
+1 -1
View File
@@ -3503,7 +3503,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),
+1 -1
View File
@@ -572,7 +572,7 @@ impl WalletTransactionsContent {
)
}
}
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled")),
_ => format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")),
}
};
+103
View File
@@ -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")
);
}