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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -572,7 +572,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