//! Small shared helpers: clock, randomness, hashing, language maps. use rand::RngCore; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; /// Languages of the interface and of the editorial translations (Yvv, 2026-10-21). pub const LANGS: [&str; 3] = ["fr", "en", "es"]; pub fn now() -> String { stamp(OffsetDateTime::now_utc()) } pub fn stamp(at: OffsetDateTime) -> String { at.replace_nanosecond(0).unwrap_or(at).format(&Rfc3339).unwrap_or_default() } pub fn in_days(days: i64) -> String { stamp(OffsetDateTime::now_utc() + time::Duration::days(days)) } pub fn in_seconds(secs: i64) -> String { stamp(OffsetDateTime::now_utc() + time::Duration::seconds(secs)) } /// 32 random bytes, hex — session tokens, invitation tokens, OAuth state. pub fn token() -> String { let mut bytes = [0u8; 32]; rand::rng().fill_bytes(&mut bytes); hex::encode(bytes) } /// 16 random bytes, hex — row identifiers. pub fn id() -> String { let mut bytes = [0u8; 16]; rand::rng().fill_bytes(&mut bytes); hex::encode(bytes) } pub fn sha256_hex(data: &str) -> String { hex::encode(Sha256::digest(data.as_bytes())) } /// The hash a translation is pinned to: it goes stale when the source changes. pub fn source_hash(title: &str, body: &str) -> String { sha256_hex(&format!("{title}\n\u{1f}\n{body}")) } pub type LangMap = BTreeMap; pub fn lang_json(map: &LangMap) -> String { serde_json::to_string(map).unwrap_or_else(|_| "{}".into()) } pub fn parse_lang(raw: &str) -> LangMap { serde_json::from_str(raw).unwrap_or_default() } pub fn is_lang(lang: &str) -> bool { LANGS.contains(&lang) }