i18n: make localization context non-global

- Simplify Localization{Context,Manager} to just Localization
- Fixed a bunch of lifetime issueo
- Removed all Arcs and Locks
- Removed globals
  * widgets now need access to &mut Localization for i18n

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-06-29 11:05:31 -07:00
parent d1e222f732
commit 3d4db820b4
47 changed files with 1414 additions and 1166 deletions

View File

@@ -1,5 +1,5 @@
use crate::account::FALLBACK_PUBKEY;
use crate::i18n::{LocalizationContext, LocalizationManager};
use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, ZoomHandler};
use crate::wallet::GlobalWallet;
use crate::zaps::Zaps;
@@ -18,7 +18,6 @@ use std::cell::RefCell;
use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use tracing::{error, info};
pub enum AppAction {
@@ -50,7 +49,7 @@ pub struct Notedeck {
zaps: Zaps,
frame_history: FrameHistory,
job_pool: JobPool,
i18n: LocalizationContext,
i18n: Localization,
}
/// Our chrome, which is basically nothing
@@ -231,19 +230,10 @@ impl Notedeck {
let job_pool = JobPool::default();
// Initialize localization
let i18n_resource_dir = Path::new("assets/translations");
let localization_manager = Arc::new(
LocalizationManager::new(i18n_resource_dir).unwrap_or_else(|e| {
error!("Failed to initialize localization manager: {}", e);
// Create a fallback manager with a temporary directory
LocalizationManager::new(&std::env::temp_dir().join("notedeck_i18n_fallback"))
.expect("Failed to create fallback localization manager")
}),
);
let i18n = LocalizationContext::new(localization_manager);
let i18n = Localization::new();
// Initialize global i18n context
crate::i18n::init_global_i18n(i18n.clone());
//crate::i18n::init_global_i18n(i18n.clone());
Self {
ndb,
@@ -289,7 +279,7 @@ impl Notedeck {
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool,
i18n: &self.i18n,
i18n: &mut self.i18n,
}
}

View File

@@ -1,5 +1,5 @@
use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::LocalizationContext,
account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
UnknownIds,
};
@@ -25,5 +25,5 @@ pub struct AppContext<'a> {
pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory,
pub job_pool: &'a mut JobPool,
pub i18n: &'a LocalizationContext,
pub i18n: &'a mut Localization,
}

View File

@@ -0,0 +1,24 @@
use super::IntlKeyBuf;
use unic_langid::LanguageIdentifier;
/// App related errors
#[derive(thiserror::Error, Debug)]
pub enum IntlError {
#[error("message not found: {0}")]
NotFound(IntlKeyBuf),
#[error("message has no value: {0}")]
NoValue(IntlKeyBuf),
#[error("Locale({0}) parse error: {1}")]
LocaleParse(LanguageIdentifier, String),
#[error("locale not available: {0}")]
LocaleNotAvailable(LanguageIdentifier),
#[error("FTL for '{0}' is not available")]
NoFtl(LanguageIdentifier),
#[error("Bundle for '{0}' is not available")]
NoBundle(LanguageIdentifier),
}

View File

@@ -0,0 +1,47 @@
use std::fmt;
/// An owned key used to lookup i18n translations. Mostly used for errors
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct IntlKeyBuf(String);
/// A key used to lookup i18n translations
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub struct IntlKey<'a>(&'a str);
impl fmt::Display for IntlKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", self.0)
}
}
impl fmt::Display for IntlKeyBuf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", &self.0)
}
}
impl IntlKeyBuf {
pub fn new(string: impl Into<String>) -> Self {
IntlKeyBuf(string.into())
}
pub fn borrow<'a>(&'a self) -> IntlKey<'a> {
IntlKey::new(&self.0)
}
}
impl<'a> IntlKey<'a> {
pub fn new(string: &'a str) -> IntlKey<'a> {
IntlKey(string)
}
pub fn to_owned(&self) -> IntlKeyBuf {
IntlKeyBuf::new(self.0)
}
pub fn as_str(&self) -> &'a str {
self.0
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,104 +4,22 @@
//! It handles loading translation files, managing locales, and providing
//! localized strings throughout the application.
mod error;
mod key;
pub mod manager;
pub use error::IntlError;
pub use key::{IntlKey, IntlKeyBuf};
pub use manager::CacheStats;
pub use manager::LocalizationContext;
pub use manager::LocalizationManager;
pub use manager::Localization;
pub use manager::StringCacheResult;
/// Re-export commonly used types for convenience
pub use fluent::FluentArgs;
pub use fluent::FluentValue;
pub use unic_langid::LanguageIdentifier;
use md5;
use once_cell::sync::OnceCell;
use regex::Regex;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use tracing::info;
/// Global localization manager for easy access from anywhere
static GLOBAL_I18N: OnceCell<Arc<LocalizationManager>> = OnceCell::new();
/// Cache for normalized FTL keys to avoid repeated normalization
static NORMALIZED_KEY_CACHE: OnceCell<Mutex<HashMap<String, String>>> = OnceCell::new();
/// Initialize the global localization context
pub fn init_global_i18n(context: LocalizationContext) {
info!("Initializing global i18n context");
let _ = GLOBAL_I18N.set(context.manager().clone());
// Initialize the normalized key cache
let _ = NORMALIZED_KEY_CACHE.set(Mutex::new(HashMap::new()));
info!("Global i18n context initialized successfully");
}
/// Get the global localization manager
pub fn get_global_i18n() -> Option<Arc<LocalizationManager>> {
GLOBAL_I18N.get().cloned()
}
fn simple_hash(s: &str) -> String {
let digest = md5::compute(s.as_bytes());
// Take the first 2 bytes and convert to 4 hex characters
format!("{:02x}{:02x}", digest[0], digest[1])
}
pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
// Try to get from cache first
let cache_key = if let Some(comment) = comment {
format!("{key}:{comment}")
} else {
key.to_string()
};
if let Some(cache) = NORMALIZED_KEY_CACHE.get() {
if let Ok(cache) = cache.lock() {
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
}
}
// Replace each invalid character with exactly one underscore
// This matches the behavior of the Python extraction script
let re = Regex::new(r"[^a-zA-Z0-9_-]").unwrap();
let mut result = re.replace_all(key, "_").to_string();
// Remove leading/trailing underscores
result = result.trim_matches('_').to_string();
// Ensure the key starts with a letter (Fluent requirement)
if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
result = format!("k_{result}");
}
// If we have a comment, append a hash of it to reduce collisions
if let Some(comment) = comment {
let hash_str = format!("_{}", simple_hash(comment));
result.push_str(&hash_str);
}
// Cache the result
if let Some(cache) = NORMALIZED_KEY_CACHE.get() {
if let Ok(mut cache) = cache.lock() {
cache.insert(cache_key, result.clone());
}
}
tracing::debug!(
"normalize_ftl_key: original='{}', comment='{:?}', final='{}'",
key,
comment,
result
);
result
}
/// Macro for getting localized strings with format-like syntax
///
/// Syntax: tr!("message", comment)
@@ -114,53 +32,36 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
/// All placeholders must be named and start with a letter (a-zA-Z).
#[macro_export]
macro_rules! tr {
// Simple case: just message and comment
($message:expr, $comment:expr) => {
($i18n:expr, $message:expr, $comment:expr) => {
{
let norm_key = $crate::i18n::normalize_ftl_key($message, Some($comment));
if let Some(i18n) = $crate::i18n::get_global_i18n() {
let result = i18n.get_string(&norm_key);
match result {
Ok(ref s) if s != $message => s.clone(),
_ => {
tracing::warn!("FALLBACK: Using key '{}' as string (not found in FTL)", $message);
$message.to_string()
}
let key = $i18n.normalized_ftl_key($message, $comment);
match $i18n.get_string(key.borrow()) {
Ok(r) => r,
Err(_err) => {
$message.to_string()
}
} else {
tracing::warn!("FALLBACK: Global i18n not initialized, using key '{}' as string", $message);
$message.to_string()
}
}
};
// Case with named parameters: message, comment, param=value, ...
($message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
($i18n:expr, $message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
{
let norm_key = $crate::i18n::normalize_ftl_key($message, Some($comment));
if let Some(i18n) = $crate::i18n::get_global_i18n() {
let mut args = $crate::i18n::FluentArgs::new();
$(
args.set(stringify!($param), $value);
)*
match i18n.get_string_with_args(&norm_key, Some(&args)) {
Ok(s) => s,
Err(_) => {
// Fallback: replace placeholders with values
let mut result = $message.to_string();
$(
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
)*
result
}
let key = $i18n.normalized_ftl_key($message, $comment);
let mut args = $crate::i18n::FluentArgs::new();
$(
args.set(stringify!($param), $value);
)*
match $i18n.get_cached_string(key.borrow(), Some(&args)) {
Ok(r) => r,
Err(_) => {
// Fallback: replace placeholders with values
let mut result = $message.to_string();
$(
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
)*
result
}
} else {
// Fallback: replace placeholders with values
let mut result = $message.to_string();
$(
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
)*
result
}
}
};
@@ -177,42 +78,27 @@ macro_rules! tr {
#[macro_export]
macro_rules! tr_plural {
// With named parameters
($one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
let norm_key = $crate::i18n::normalize_ftl_key($other, Some($comment));
if let Some(i18n) = $crate::i18n::get_global_i18n() {
let mut args = $crate::i18n::FluentArgs::new();
args.set("count", $count);
$(args.set(stringify!($param), $value);)*
match i18n.get_string_with_args(&norm_key, Some(&args)) {
Ok(s) => s,
Err(_) => {
// Fallback: use simple pluralization
if $count == 1 {
let mut result = $one.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
} else {
let mut result = $other.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
}
($i18n:expr, $one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
let norm_key = $i18n.normalized_ftl_key($other, $comment);
let mut args = $crate::i18n::FluentArgs::new();
args.set("count", $count);
$(args.set(stringify!($param), $value);)*
match $i18n.get_cached_string(norm_key.borrow(), Some(&args)) {
Ok(s) => s,
Err(_) => {
// Fallback: use simple pluralization
if $count == 1 {
let mut result = $one.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
} else {
let mut result = $other.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
}
}
} else {
// Fallback: use simple pluralization
if $count == 1 {
let mut result = $one.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
} else {
let mut result = $other.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
}
}
}};
// Without named parameters

View File

@@ -45,11 +45,7 @@ pub use context::AppContext;
pub use error::{show_one_error_message, Error, FilterError, ZapError};
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use i18n::manager::Localizable;
pub use i18n::{
CacheStats, FluentArgs, FluentValue, LanguageIdentifier, LocalizationContext,
LocalizationManager,
};
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use imgcache::{
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
@@ -89,5 +85,3 @@ pub use enostr;
pub use nostrdb;
pub use zaps::Zaps;
pub use crate::i18n::{get_global_i18n, init_global_i18n};

View File

@@ -6,6 +6,7 @@ pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
use crate::Accounts;
use crate::JobPool;
use crate::Localization;
use crate::UnknownIds;
use crate::{notecache::NoteCache, zaps::Zaps, Images};
use enostr::{NoteId, RelayPool};
@@ -19,6 +20,7 @@ use std::fmt;
pub struct NoteContext<'d> {
pub ndb: &'d Ndb,
pub accounts: &'d Accounts,
pub i18n: &'d mut Localization,
pub img_cache: &'d mut Images,
pub note_cache: &'d mut NoteCache,
pub zaps: &'d mut Zaps,

View File

@@ -1,7 +1,5 @@
use crate::{time_ago_since, TimeCached};
use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Default)]
pub struct NoteCache {
@@ -32,7 +30,7 @@ impl NoteCache {
#[derive(Clone)]
pub struct CachedNote {
reltime: TimeCached<String>,
//reltime: TimeCached<String>,
pub client: Option<String>,
pub reply: NoteReplyBuf,
}
@@ -41,22 +39,25 @@ impl CachedNote {
pub fn new(note: &Note) -> Self {
use crate::note::event_tag;
/*
let created_at = note.created_at();
let reltime = TimeCached::new(
Duration::from_secs(1),
Box::new(move || time_ago_since(created_at)),
Box::new(move || time_ago_since(i18n, created_at)),
);
*/
let reply = NoteReply::new(note.tags()).to_owned();
let client = event_tag(note, "client");
CachedNote {
client: client.map(|c| c.to_string()),
reltime,
// reltime,
reply,
}
}
/*
pub fn reltime_str_mut(&mut self) -> &str {
self.reltime.get_mut()
}
@@ -64,4 +65,5 @@ impl CachedNote {
pub fn reltime_str(&self) -> Option<&str> {
self.reltime.get().map(|x| x.as_str())
}
*/
}

View File

@@ -1,4 +1,4 @@
use crate::tr;
use crate::{tr, Localization};
use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds
@@ -18,7 +18,7 @@ const MAX_SECONDS_FOR_WEEKS: u64 = ONE_MONTH_IN_SECONDS - 1;
const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1;
/// Calculate relative time between two timestamps
fn time_ago_between(timestamp: u64, now: u64) -> String {
fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String {
// Determine if the timestamp is in the future or the past
let duration = if now >= timestamp {
now.saturating_sub(timestamp)
@@ -28,36 +28,48 @@ fn time_ago_between(timestamp: u64, now: u64) -> String {
let time_str = match duration {
0..=2 => tr!(
i18n,
"now",
"Relative time for very recent events (less than 3 seconds)"
),
3..=MAX_SECONDS => tr!("{count}s", "Relative time in seconds", count = duration),
3..=MAX_SECONDS => tr!(
i18n,
"{count}s",
"Relative time in seconds",
count = duration
),
ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
i18n,
"{count}m",
"Relative time in minutes",
count = duration / ONE_MINUTE_IN_SECONDS
),
ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
i18n,
"{count}h",
"Relative time in hours",
count = duration / ONE_HOUR_IN_SECONDS
),
ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
i18n,
"{count}d",
"Relative time in days",
count = duration / ONE_DAY_IN_SECONDS
),
ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!(
i18n,
"{count}w",
"Relative time in weeks",
count = duration / ONE_WEEK_IN_SECONDS
),
ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!(
i18n,
"{count}mo",
"Relative time in months",
count = duration / ONE_MONTH_IN_SECONDS
),
_ => tr!(
i18n,
"{count}y",
"Relative time in years",
count = duration / ONE_YEAR_IN_SECONDS
@@ -65,19 +77,19 @@ fn time_ago_between(timestamp: u64, now: u64) -> String {
};
if timestamp > now {
format!("+{}", time_str)
format!("+{time_str}")
} else {
time_str
}
}
pub fn time_ago_since(timestamp: u64) -> String {
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
time_ago_between(timestamp, now)
time_ago_between(i18n, timestamp, now)
}
#[cfg(test)]
@@ -95,9 +107,10 @@ mod tests {
#[test]
fn test_now_condition() {
let now = get_current_timestamp();
let mut intl = Localization::default();
// Test 0 seconds ago
let result = time_ago_between(now, now);
let result = time_ago_between(&mut intl, now, now);
assert_eq!(
result, "now",
"Expected 'now' for 0 seconds, got: {}",
@@ -105,7 +118,7 @@ mod tests {
);
// Test 1 second ago
let result = time_ago_between(now - 1, now);
let result = time_ago_between(&mut intl, now - 1, now);
assert_eq!(
result, "now",
"Expected 'now' for 1 second, got: {}",
@@ -113,7 +126,7 @@ mod tests {
);
// Test 2 seconds ago
let result = time_ago_between(now - 2, now);
let result = time_ago_between(&mut intl, now - 2, now);
assert_eq!(
result, "now",
"Expected 'now' for 2 seconds, got: {}",
@@ -124,13 +137,14 @@ mod tests {
#[test]
fn test_seconds_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 3 seconds ago
let result = time_ago_between(now - 3, now);
let result = time_ago_between(&mut i18n, now - 3, now);
assert_eq!(result, "3s", "Expected '3s' for 3 seconds, got: {}", result);
// Test 30 seconds ago
let result = time_ago_between(now - 30, now);
let result = time_ago_between(&mut i18n, now - 30, now);
assert_eq!(
result, "30s",
"Expected '30s' for 30 seconds, got: {}",
@@ -138,7 +152,7 @@ mod tests {
);
// Test 59 seconds ago (max for seconds)
let result = time_ago_between(now - 59, now);
let result = time_ago_between(&mut i18n, now - 59, now);
assert_eq!(
result, "59s",
"Expected '59s' for 59 seconds, got: {}",
@@ -149,13 +163,14 @@ mod tests {
#[test]
fn test_minutes_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 minute ago
let result = time_ago_between(now - ONE_MINUTE_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - ONE_MINUTE_IN_SECONDS, now);
assert_eq!(result, "1m", "Expected '1m' for 1 minute, got: {}", result);
// Test 30 minutes ago
let result = time_ago_between(now - 30 * ONE_MINUTE_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 30 * ONE_MINUTE_IN_SECONDS, now);
assert_eq!(
result, "30m",
"Expected '30m' for 30 minutes, got: {}",
@@ -163,7 +178,7 @@ mod tests {
);
// Test 59 minutes ago (max for minutes)
let result = time_ago_between(now - 59 * ONE_MINUTE_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 59 * ONE_MINUTE_IN_SECONDS, now);
assert_eq!(
result, "59m",
"Expected '59m' for 59 minutes, got: {}",
@@ -174,13 +189,14 @@ mod tests {
#[test]
fn test_hours_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 hour ago
let result = time_ago_between(now - ONE_HOUR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - ONE_HOUR_IN_SECONDS, now);
assert_eq!(result, "1h", "Expected '1h' for 1 hour, got: {}", result);
// Test 12 hours ago
let result = time_ago_between(now - 12 * ONE_HOUR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 12 * ONE_HOUR_IN_SECONDS, now);
assert_eq!(
result, "12h",
"Expected '12h' for 12 hours, got: {}",
@@ -188,7 +204,7 @@ mod tests {
);
// Test 23 hours ago (max for hours)
let result = time_ago_between(now - 23 * ONE_HOUR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 23 * ONE_HOUR_IN_SECONDS, now);
assert_eq!(
result, "23h",
"Expected '23h' for 23 hours, got: {}",
@@ -199,43 +215,46 @@ mod tests {
#[test]
fn test_days_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 day ago
let result = time_ago_between(now - ONE_DAY_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "1d", "Expected '1d' for 1 day, got: {}", result);
// Test 3 days ago
let result = time_ago_between(now - 3 * ONE_DAY_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 3 * ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "3d", "Expected '3d' for 3 days, got: {}", result);
// Test 6 days ago (max for days, before weeks)
let result = time_ago_between(now - 6 * ONE_DAY_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 6 * ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "6d", "Expected '6d' for 6 days, got: {}", result);
}
#[test]
fn test_weeks_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 week ago
let result = time_ago_between(now - ONE_WEEK_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - ONE_WEEK_IN_SECONDS, now);
assert_eq!(result, "1w", "Expected '1w' for 1 week, got: {}", result);
// Test 4 weeks ago
let result = time_ago_between(now - 4 * ONE_WEEK_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 4 * ONE_WEEK_IN_SECONDS, now);
assert_eq!(result, "4w", "Expected '4w' for 4 weeks, got: {}", result);
}
#[test]
fn test_months_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 month ago
let result = time_ago_between(now - ONE_MONTH_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - ONE_MONTH_IN_SECONDS, now);
assert_eq!(result, "1mo", "Expected '1mo' for 1 month, got: {}", result);
// Test 11 months ago (max for months, before years)
let result = time_ago_between(now - 11 * ONE_MONTH_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 11 * ONE_MONTH_IN_SECONDS, now);
assert_eq!(
result, "11mo",
"Expected '11mo' for 11 months, got: {}",
@@ -246,17 +265,18 @@ mod tests {
#[test]
fn test_years_condition() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 year ago
let result = time_ago_between(now - ONE_YEAR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - ONE_YEAR_IN_SECONDS, now);
assert_eq!(result, "1y", "Expected '1y' for 1 year, got: {}", result);
// Test 5 years ago
let result = time_ago_between(now - 5 * ONE_YEAR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 5 * ONE_YEAR_IN_SECONDS, now);
assert_eq!(result, "5y", "Expected '5y' for 5 years, got: {}", result);
// Test 10 years ago (reduced from 100 to avoid overflow)
let result = time_ago_between(now - 10 * ONE_YEAR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now - 10 * ONE_YEAR_IN_SECONDS, now);
assert_eq!(
result, "10y",
"Expected '10y' for 10 years, got: {}",
@@ -267,9 +287,10 @@ mod tests {
#[test]
fn test_future_timestamps() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 minute in the future
let result = time_ago_between(now + ONE_MINUTE_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now + ONE_MINUTE_IN_SECONDS, now);
assert_eq!(
result, "+1m",
"Expected '+1m' for 1 minute in future, got: {}",
@@ -277,7 +298,7 @@ mod tests {
);
// Test 1 hour in the future
let result = time_ago_between(now + ONE_HOUR_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now + ONE_HOUR_IN_SECONDS, now);
assert_eq!(
result, "+1h",
"Expected '+1h' for 1 hour in future, got: {}",
@@ -285,7 +306,7 @@ mod tests {
);
// Test 1 day in the future
let result = time_ago_between(now + ONE_DAY_IN_SECONDS, now);
let result = time_ago_between(&mut i18n, now + ONE_DAY_IN_SECONDS, now);
assert_eq!(
result, "+1d",
"Expected '+1d' for 1 day in future, got: {}",
@@ -296,9 +317,10 @@ mod tests {
#[test]
fn test_boundary_conditions() {
let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test boundary between seconds and minutes
let result = time_ago_between(now - 60, now);
let result = time_ago_between(&mut i18n, now - 60, now);
assert_eq!(
result, "1m",
"Expected '1m' for exactly 60 seconds, got: {}",
@@ -306,7 +328,7 @@ mod tests {
);
// Test boundary between minutes and hours
let result = time_ago_between(now - 3600, now);
let result = time_ago_between(&mut i18n, now - 3600, now);
assert_eq!(
result, "1h",
"Expected '1h' for exactly 3600 seconds, got: {}",
@@ -314,7 +336,7 @@ mod tests {
);
// Test boundary between hours and days
let result = time_ago_between(now - 86400, now);
let result = time_ago_between(&mut i18n, now - 86400, now);
assert_eq!(
result, "1d",
"Expected '1d' for exactly 86400 seconds, got: {}",