feat: extract all user-facing strings into a Fluent catalogue

This commit is contained in:
randogoth 2026-10-08 22:10:29 +03:00
parent 4c5665a6ff
commit 57e75af3ac
8 changed files with 1024 additions and 326 deletions

View file

@ -15,6 +15,7 @@ use fumi_core::crypto::{fingerprint, unb32, KEY_LEN};
use fumi_core::store::Store;
use gtk::prelude::*;
use crate::i18n;
use crate::ops::{self, Draft, Folder, OpenedMsg, Row, Snapshot};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -514,7 +515,7 @@ impl Carrier {
at: fumi_core::store::now(),
});
ops::save_drafts(&self.drafts_path(), &self.drafts);
self.note("draft saved".into());
self.note(i18n::t("draft-saved-note"));
}
/// Tapping a draft in the folder puts it back in the compose it came
@ -555,7 +556,7 @@ impl Carrier {
}
let (app, _) = Carrier::boot(self.dir.clone());
*self = app;
self.note("logged out. Create a new identity or restore a seed.".into());
self.note(i18n::t("logged-out-note"));
}
/// Cancelling out of a not-yet-registered identity removes the key
@ -853,9 +854,7 @@ impl Carrier {
self.master = Some(master);
self.onboard.seed_shown = ops::seed_text(&master);
self.onboard.mode = OnboardMode::Created;
self.note(
"Identity created. Write the seed down; it is the only secret.".into(),
);
self.note(i18n::t("identity-created-note"));
}
}
Message::OnboardRestore => {
@ -877,7 +876,7 @@ impl Carrier {
} else {
self.master = Some(master);
self.page = Page::Signup;
self.note("Seed restored. Bind a username or recall an address.".into());
self.note(i18n::t("seed-restored-note"));
}
}
Err(e) => self.fail(e),
@ -904,14 +903,14 @@ impl Carrier {
return Vec::new();
}
let Some(addr) = self.signup_host_address() else {
self.fail("enter the server host first".into());
self.fail(i18n::t("signup-enter-host-error"));
return Vec::new();
};
return vec![PendingOp::LearnKey { addr }];
}
Message::SignupRegister => {
let Some(addr) = self.signup_address() else {
self.fail("enter user and host".into());
self.fail(i18n::t("signup-enter-user-host-error"));
return Vec::new();
};
let invite = if self.signup.invite.is_empty() {
@ -926,7 +925,7 @@ impl Carrier {
}
Message::SignupRecall => {
let Some(addr) = self.signup_address() else {
self.fail("enter user and host".into());
self.fail(i18n::t("signup-enter-user-host-error"));
return Vec::new();
};
if !self.start("restoring") {
@ -993,7 +992,7 @@ impl Carrier {
let to = if let Some(uri) = &msg.reply_to {
uri.clone()
} else if msg.from.starts_with('<') {
self.fail("this sender left no address to reply to".into());
self.fail(i18n::t("reply-no-address-error"));
return Vec::new();
} else {
msg.from.clone()
@ -1001,7 +1000,7 @@ impl Carrier {
let subject = if msg.subject.is_empty() || msg.subject.starts_with("Re:") {
msg.subject.clone()
} else {
format!("Re: {}", msg.subject)
i18n::t1("reply-subject-prefix", "subject", msg.subject.clone())
};
// The sender's own fields are the one thing a reply has to
// decide about: carry copies, or start clean. Only the
@ -1057,7 +1056,7 @@ impl Carrier {
return Vec::new();
};
if compose.to.trim().is_empty() {
self.fail("enter a recipient".into());
self.fail(i18n::t("compose-enter-recipient-error"));
return Vec::new();
}
let mut headers = Vec::new();
@ -1066,7 +1065,10 @@ impl Carrier {
Ok(Some(pair)) => headers.push(pair),
Ok(None) => {}
Err(reason) => {
self.fail(format!("header {}: {reason}", index + 1));
let mut args = fluent_bundle::FluentArgs::new();
args.set("index", (index + 1) as i64);
args.set("reason", reason);
self.fail(i18n::t_args("compose-header-row-error", Some(&args)));
return Vec::new();
}
}
@ -1109,11 +1111,10 @@ impl Carrier {
}
if self.strict_pinning {
if let Some(addr) = self.unpinned_first_contact(&to) {
self.fail(format!(
"{} is not a contact yet and {} is not pinned — pin their server's key to send.",
addr.short(),
trust_label(&addr.host, addr.port),
));
let mut args = fluent_bundle::FluentArgs::new();
args.set("address", addr.short());
args.set("host", trust_label(&addr.host, addr.port));
self.fail(i18n::t_args("first-contact-blocked-error", Some(&args)));
self.pin_prompt = Some(PinPrompt {
addr,
offered: None,
@ -1139,7 +1140,7 @@ impl Carrier {
return Vec::new();
};
let Ok(addr) = Address::parse(&to) else {
self.fail("that address does not parse".into());
self.fail(i18n::t("address-invalid-error"));
return Vec::new();
};
let fetched = self
@ -1203,17 +1204,17 @@ impl Carrier {
return Vec::new();
}
let Some(uri) = msg.reply_to.clone() else {
self.fail("this sender left no address to accept".into());
self.fail(i18n::t("contacts-accept-no-address-error"));
return Vec::new();
};
let Ok(addr) = Address::parse(&uri) else {
self.fail("the sender's reply address does not parse".into());
self.fail(i18n::t("contacts-reply-address-invalid-error"));
return Vec::new();
};
let Ok(identity) =
<[u8; KEY_LEN]>::try_from(unb32(&msg.sender).unwrap_or_default())
else {
self.fail("the sender's key does not parse".into());
self.fail(i18n::t("contacts-sender-key-invalid-error"));
return Vec::new();
};
return vec![PendingOp::Accept {
@ -1226,11 +1227,11 @@ impl Carrier {
return Vec::new();
};
let Some(uri) = msg.reply_to.clone() else {
self.fail("this sender left no address to block".into());
self.fail(i18n::t("contacts-block-no-address-error"));
return Vec::new();
};
let Ok(addr) = Address::parse(&uri) else {
self.fail("the sender's reply address does not parse".into());
self.fail(i18n::t("contacts-reply-address-invalid-error"));
return Vec::new();
};
return vec![PendingOp::Block {
@ -1414,11 +1415,7 @@ impl Carrier {
Some(prompt) => prompt.offered = Some(key),
None => self.pin_offer = Some((addr, key)),
}
self.note(
"The server presented this key. Pin it only if the fingerprint \
matches what the operator published."
.into(),
);
self.note(i18n::t("server-key-offered-note"));
None
}
OpResult::PinOffered(Ok(None)) => {
@ -1428,7 +1425,7 @@ impl Carrier {
if self.pin_prompt.take().is_none() {
self.signup.key_trusted = true;
}
self.note("This server's key is already pinned.".into());
self.note(i18n::t("server-key-already-pinned-note"));
None
}
OpResult::PinOffered(Err(e)) => {
@ -1438,7 +1435,7 @@ impl Carrier {
}
OpResult::Pinned(Ok(())) => {
self.done();
self.note("server key pinned".into());
self.note(i18n::t("server-key-pinned-note"));
// The contact detail shows the pin state, so that pane
// needs the fresh row; signup's own pin changes nothing
// that is on screen.