commit 0e65491ef17776df3142b37929357d609e45874e
parent 3f5036bd325b89010556fd7373dc86271b57d572
Author: Terry Yiu <git@tyiu.xyz>
Date: Fri, 27 Jun 2025 00:16:48 -0400
Clean up time_ago_since, add tests, and internationalize strings
Changelog-Changed: Internationalized time ago strings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Diffstat:
3 files changed, 346 insertions(+), 29 deletions(-)
diff --git a/assets/translations/en-US/main.ftl b/assets/translations/en-US/main.ftl
@@ -145,6 +145,27 @@ Copy_Pubkey_9cc4 = Copy Pubkey
# Copy the text content of the note to clipboard
Copy_Text_f81c = Copy Text
+# Relative time in days
+count_d_b9be = {$count}d
+
+# Relative time in hours
+count_h_3ecb = {$count}h
+
+# Relative time in minutes
+count_m_b41e = {$count}m
+
+# Relative time in months
+count_mo_7aba = {$count}mo
+
+# Relative time in seconds
+count_s_aa26 = {$count}s
+
+# Relative time in weeks
+count_w_7468 = {$count}w
+
+# Relative time in years
+count_y_9408 = {$count}y
+
# Button to create a new account
Create_Account_6994 = Create Account
@@ -346,6 +367,9 @@ Notifications_d673 = Notifications
# Title for notifications column
Notifications_ef56 = Notifications
+# Relative time for very recent events (less than 3 seconds)
+now_2181 = now
+
# Button label to open email client
Open_Email_25e9 = Open Email
diff --git a/assets/translations/en-XA/main.ftl b/assets/translations/en-XA/main.ftl
@@ -145,6 +145,27 @@ Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
# Copy the text content of the note to clipboard
Copy_Text_f81c = {"["}Çópy Téxt{"]"}
+# Relative time in days
+count_d_b9be = {"["}{$count}d{"]"}
+
+# Relative time in hours
+count_h_3ecb = {"["}{$count}h{"]"}
+
+# Relative time in minutes
+count_m_b41e = {"["}{$count}m{"]"}
+
+# Relative time in months
+count_mo_7aba = {"["}{$count}mó{"]"}
+
+# Relative time in seconds
+count_s_aa26 = {"["}{$count}s{"]"}
+
+# Relative time in weeks
+count_w_7468 = {"["}{$count}w{"]"}
+
+# Relative time in years
+count_y_9408 = {"["}{$count}y{"]"}
+
# Button to create a new account
Create_Account_6994 = {"["}Çréàté Àççóúñt{"]"}
@@ -346,6 +367,9 @@ Notifications_d673 = {"["}Ñótífíçàtíóñs{"]"}
# Title for notifications column
Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
+# Relative time for very recent events (less than 3 seconds)
+now_2181 = {"["}ñów{"]"}
+
# Button label to open email client
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
diff --git a/crates/notedeck/src/time.rs b/crates/notedeck/src/time.rs
@@ -1,11 +1,24 @@
+use crate::tr;
use std::time::{SystemTime, UNIX_EPOCH};
-pub fn time_ago_since(timestamp: u64) -> String {
- let now = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("Time went backwards")
- .as_secs();
+// Time duration constants in seconds
+const ONE_MINUTE_IN_SECONDS: u64 = 60;
+const ONE_HOUR_IN_SECONDS: u64 = 3600;
+const ONE_DAY_IN_SECONDS: u64 = 86_400;
+const ONE_WEEK_IN_SECONDS: u64 = 604_800;
+const ONE_MONTH_IN_SECONDS: u64 = 2_592_000; // 30 days
+const ONE_YEAR_IN_SECONDS: u64 = 31_536_000; // 365 days
+
+// Range boundary constants for match patterns
+const MAX_SECONDS: u64 = ONE_MINUTE_IN_SECONDS - 1;
+const MAX_SECONDS_FOR_MINUTES: u64 = ONE_HOUR_IN_SECONDS - 1;
+const MAX_SECONDS_FOR_HOURS: u64 = ONE_DAY_IN_SECONDS - 1;
+const MAX_SECONDS_FOR_DAYS: u64 = ONE_WEEK_IN_SECONDS - 1;
+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 {
// Determine if the timestamp is in the future or the past
let duration = if now >= timestamp {
now.saturating_sub(timestamp)
@@ -13,43 +26,299 @@ pub fn time_ago_since(timestamp: u64) -> String {
timestamp.saturating_sub(now)
};
- let future = timestamp > now;
- let relstr = if future { "+" } else { "" };
+ let time_str = match duration {
+ 0..=2 => tr!(
+ "now",
+ "Relative time for very recent events (less than 3 seconds)"
+ ),
+ 3..=MAX_SECONDS => tr!("{count}s", "Relative time in seconds", count = duration),
+ ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
+ "{count}m",
+ "Relative time in minutes",
+ count = duration / ONE_MINUTE_IN_SECONDS
+ ),
+ ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
+ "{count}h",
+ "Relative time in hours",
+ count = duration / ONE_HOUR_IN_SECONDS
+ ),
+ ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
+ "{count}d",
+ "Relative time in days",
+ count = duration / ONE_DAY_IN_SECONDS
+ ),
+ ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!(
+ "{count}w",
+ "Relative time in weeks",
+ count = duration / ONE_WEEK_IN_SECONDS
+ ),
+ ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!(
+ "{count}mo",
+ "Relative time in months",
+ count = duration / ONE_MONTH_IN_SECONDS
+ ),
+ _ => tr!(
+ "{count}y",
+ "Relative time in years",
+ count = duration / ONE_YEAR_IN_SECONDS
+ ),
+ };
- let years = duration / 31_536_000; // seconds in a year
- if years >= 1 {
- return format!("{relstr}{years}yr");
+ if timestamp > now {
+ format!("+{}", time_str)
+ } else {
+ time_str
}
+}
+
+pub fn time_ago_since(timestamp: u64) -> String {
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards")
+ .as_secs();
+
+ time_ago_between(timestamp, now)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::time::{SystemTime, UNIX_EPOCH};
- let months = duration / 2_592_000; // seconds in a month (30.44 days)
- if months >= 1 {
- return format!("{relstr}{months}mth");
+ fn get_current_timestamp() -> u64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards")
+ .as_secs()
}
- let weeks = duration / 604_800; // seconds in a week
- if weeks >= 1 {
- return format!("{relstr}{weeks}wk");
+ #[test]
+ fn test_now_condition() {
+ let now = get_current_timestamp();
+
+ // Test 0 seconds ago
+ let result = time_ago_between(now, now);
+ assert_eq!(
+ result, "now",
+ "Expected 'now' for 0 seconds, got: {}",
+ result
+ );
+
+ // Test 1 second ago
+ let result = time_ago_between(now - 1, now);
+ assert_eq!(
+ result, "now",
+ "Expected 'now' for 1 second, got: {}",
+ result
+ );
+
+ // Test 2 seconds ago
+ let result = time_ago_between(now - 2, now);
+ assert_eq!(
+ result, "now",
+ "Expected 'now' for 2 seconds, got: {}",
+ result
+ );
}
- let days = duration / 86_400; // seconds in a day
- if days >= 1 {
- return format!("{relstr}{days}d");
+ #[test]
+ fn test_seconds_condition() {
+ let now = get_current_timestamp();
+
+ // Test 3 seconds ago
+ let result = time_ago_between(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);
+ assert_eq!(
+ result, "30s",
+ "Expected '30s' for 30 seconds, got: {}",
+ result
+ );
+
+ // Test 59 seconds ago (max for seconds)
+ let result = time_ago_between(now - 59, now);
+ assert_eq!(
+ result, "59s",
+ "Expected '59s' for 59 seconds, got: {}",
+ result
+ );
}
- let hours = duration / 3600; // seconds in an hour
- if hours >= 1 {
- return format!("{relstr}{hours}h");
+ #[test]
+ fn test_minutes_condition() {
+ let now = get_current_timestamp();
+
+ // Test 1 minute ago
+ let result = time_ago_between(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);
+ assert_eq!(
+ result, "30m",
+ "Expected '30m' for 30 minutes, got: {}",
+ result
+ );
+
+ // Test 59 minutes ago (max for minutes)
+ let result = time_ago_between(now - 59 * ONE_MINUTE_IN_SECONDS, now);
+ assert_eq!(
+ result, "59m",
+ "Expected '59m' for 59 minutes, got: {}",
+ result
+ );
}
- let minutes = duration / 60; // seconds in a minute
- if minutes >= 1 {
- return format!("{relstr}{minutes}m");
+ #[test]
+ fn test_hours_condition() {
+ let now = get_current_timestamp();
+
+ // Test 1 hour ago
+ let result = time_ago_between(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);
+ assert_eq!(
+ result, "12h",
+ "Expected '12h' for 12 hours, got: {}",
+ result
+ );
+
+ // Test 23 hours ago (max for hours)
+ let result = time_ago_between(now - 23 * ONE_HOUR_IN_SECONDS, now);
+ assert_eq!(
+ result, "23h",
+ "Expected '23h' for 23 hours, got: {}",
+ result
+ );
+ }
+
+ #[test]
+ fn test_days_condition() {
+ let now = get_current_timestamp();
+
+ // Test 1 day ago
+ let result = time_ago_between(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);
+ 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);
+ assert_eq!(result, "6d", "Expected '6d' for 6 days, got: {}", result);
}
- let seconds = duration;
- if seconds >= 3 {
- return format!("{relstr}{seconds}s");
+ #[test]
+ fn test_weeks_condition() {
+ let now = get_current_timestamp();
+
+ // Test 1 week ago
+ let result = time_ago_between(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);
+ assert_eq!(result, "4w", "Expected '4w' for 4 weeks, got: {}", result);
}
- "now".to_string()
+ #[test]
+ fn test_months_condition() {
+ let now = get_current_timestamp();
+
+ // Test 1 month ago
+ let result = time_ago_between(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);
+ assert_eq!(
+ result, "11mo",
+ "Expected '11mo' for 11 months, got: {}",
+ result
+ );
+ }
+
+ #[test]
+ fn test_years_condition() {
+ let now = get_current_timestamp();
+
+ // Test 1 year ago
+ let result = time_ago_between(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);
+ 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);
+ assert_eq!(
+ result, "10y",
+ "Expected '10y' for 10 years, got: {}",
+ result
+ );
+ }
+
+ #[test]
+ fn test_future_timestamps() {
+ let now = get_current_timestamp();
+
+ // Test 1 minute in the future
+ let result = time_ago_between(now + ONE_MINUTE_IN_SECONDS, now);
+ assert_eq!(
+ result, "+1m",
+ "Expected '+1m' for 1 minute in future, got: {}",
+ result
+ );
+
+ // Test 1 hour in the future
+ let result = time_ago_between(now + ONE_HOUR_IN_SECONDS, now);
+ assert_eq!(
+ result, "+1h",
+ "Expected '+1h' for 1 hour in future, got: {}",
+ result
+ );
+
+ // Test 1 day in the future
+ let result = time_ago_between(now + ONE_DAY_IN_SECONDS, now);
+ assert_eq!(
+ result, "+1d",
+ "Expected '+1d' for 1 day in future, got: {}",
+ result
+ );
+ }
+
+ #[test]
+ fn test_boundary_conditions() {
+ let now = get_current_timestamp();
+
+ // Test boundary between seconds and minutes
+ let result = time_ago_between(now - 60, now);
+ assert_eq!(
+ result, "1m",
+ "Expected '1m' for exactly 60 seconds, got: {}",
+ result
+ );
+
+ // Test boundary between minutes and hours
+ let result = time_ago_between(now - 3600, now);
+ assert_eq!(
+ result, "1h",
+ "Expected '1h' for exactly 3600 seconds, got: {}",
+ result
+ );
+
+ // Test boundary between hours and days
+ let result = time_ago_between(now - 86400, now);
+ assert_eq!(
+ result, "1d",
+ "Expected '1d' for exactly 86400 seconds, got: {}",
+ result
+ );
+ }
}