Current section
Files
Jump to
Current section
Files
src/timeago.gleam
//// A library for formatting timestamps as human-readable relative time strings.
////
//// This library provides a simple way to convert timestamps into relative time
//// expressions like "5 minutes ago" or "in 2 hours". It supports multiple
//// locales and allows customization of the reference time.
////
//// ## Basic Usage
////
//// ```gleam
//// import gleam/time/duration
//// import gleam/time/timestamp
//// import timeago
////
//// // Format a timestamp from 5 minutes ago
//// let past = timestamp.add(timestamp.system_time(), duration.minutes(-5))
//// timeago.new() |> timeago.format(past)
//// // -> "5 minutes ago"
//// ```
import gleam/int
import gleam/string
import gleam/time/duration.{
type Unit, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond,
Second, Week, Year,
}
import gleam/time/timestamp.{type Timestamp}
/// Configuration for formatting relative time strings.
///
/// Encapsulates the reference time and locale settings used when formatting
/// timestamps. Use the builder pattern with `new()`, `with_now()`, and
/// `with_locale()` to configure instances.
pub opaque type TimeAgo {
TimeAgo(now: Timestamp, locale: Locale)
}
/// Creates a new TimeAgo formatter with default settings.
///
/// Uses the current system time as the reference point and the English (US)
/// locale for formatting.
///
/// ## Examples
///
/// ```gleam
/// import gleam/time/duration
/// import gleam/time/timestamp
/// import timeago
///
/// timeago.new()
/// |> timeago.format(timestamp.add(timestamp.system_time(), duration.minutes(-5)))
/// // -> "5 minutes ago"
/// ```
pub fn new() -> TimeAgo {
TimeAgo(timestamp.system_time(), en_us)
}
/// Sets a custom reference time for calculating relative differences.
///
/// By default, TimeAgo uses the current system time when created. This allows
/// you to specify a different reference point for testing, calculating from
/// specific moments, or maintaining consistency across operations.
///
/// ## Examples
///
/// ```gleam
/// import gleam/time/timestamp
/// import timeago
///
/// let assert Ok(reference) = timestamp.parse_rfc3339("2024-01-01T12:00:00Z")
/// let assert Ok(past) = timestamp.parse_rfc3339("2024-01-01T11:00:00Z")
///
/// timeago.new()
/// |> timeago.with_now(reference)
/// |> timeago.format(past)
/// // -> "1 hour ago"
/// ```
pub fn with_now(time_ago: TimeAgo, now: Timestamp) -> TimeAgo {
TimeAgo(..time_ago, now: now)
}
/// Sets a custom locale function for formatting output strings.
///
/// The locale function determines how relative time is expressed in different
/// languages.
///
/// ## Examples
///
/// ```gleam
/// import gleam/time/duration
/// import gleam/time/timestamp
/// import timeago
///
/// // Using the built-in French locale
/// timeago.new()
/// |> timeago.with_locale(timeago.fr)
/// |> timeago.format(timestamp.add(timestamp.system_time(), duration.hours(-2)))
/// // -> "il y a 2 heures"
/// ```
pub fn with_locale(time_ago: TimeAgo, locale: Locale) -> TimeAgo {
TimeAgo(..time_ago, locale: locale)
}
/// Formats a timestamp as a human-readable relative time string.
///
/// Calculates the difference between the given timestamp and the reference time
/// (set via `with_now()` or defaulting to the current time), then formats it
/// using the configured locale.
///
/// The output automatically adjusts for singular/plural forms and selects
/// appropriate time units based on the magnitude of the difference.
///
/// ## Examples
///
/// ```gleam
/// import gleam/time/duration
/// import gleam/time/timestamp
/// import timeago
///
/// let now = timestamp.system_time()
///
/// // Past times
/// timeago.new()
/// |> timeago.format(timestamp.add(now, duration.seconds(-5)))
/// // -> "5 seconds ago"
///
/// timeago.new()
/// |> timeago.format(timestamp.add(now, duration.minutes(-1)))
/// // -> "1 minute ago"
///
/// // Future times
/// timeago.new()
/// |> timeago.format(timestamp.add(now, duration.hours(2)))
/// // -> "in 2 hours"
///
/// timeago.new()
/// |> timeago.format(timestamp.add(now, duration.days(1)))
/// // -> "in 1 day"
///
/// timeago.new()
/// |> timeago.format(timestamp.add(now, duration.milliseconds(-500)))
/// // -> "just now"
/// ```
pub fn format(time_ago: TimeAgo, timestamp: Timestamp) -> String {
let #(amount, unit) =
duration.approximate(timestamp.difference(timestamp, time_ago.now))
let magnitude = int.absolute_value(amount)
let tense = case amount > 0 {
True -> Past
False -> Future
}
time_ago.locale(tense, unit, magnitude)
}
/// Represents the temporal direction of a time difference.
///
/// Used to determine whether a timestamp is in the past or future relative
/// to a reference time.
pub type Tense {
/// Indicates that the timestamp occurred before the reference time.
/// Results in formatting like "X ago".
Past
/// Indicates that the timestamp will occur after the reference time.
/// Results in formatting like "in X".
Future
}
/// A function that formats relative time strings for a specific language and locale.
///
/// Receives tense (past/future), unit (seconds, minutes, etc.), and amount,
/// then returns a formatted string like "5 minutes ago" or "in 2 hours".
pub type Locale =
fn(Tense, Unit, Int) -> String
fn replace_percent_d_with_int(s: String, i: Int) -> String {
string.replace(s, "%d", int.to_string(i))
}
/// Translations for American English.
///
pub fn en_us(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "just now"
Past, Second, 1 -> "1 second ago"
Past, Second, a -> "%d seconds ago" |> replace_percent_d_with_int(a)
Past, Minute, 1 -> "1 minute ago"
Past, Minute, a -> "%d minutes ago" |> replace_percent_d_with_int(a)
Past, Hour, 1 -> "1 hour ago"
Past, Hour, a -> "%d hours ago" |> replace_percent_d_with_int(a)
Past, Day, 1 -> "1 day ago"
Past, Day, a -> "%d days ago" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "1 week ago"
Past, Week, a -> "%d weeks ago" |> replace_percent_d_with_int(a)
Past, Month, 1 -> "1 month ago"
Past, Month, a -> "%d months ago" |> replace_percent_d_with_int(a)
Past, Year, 1 -> "1 year ago"
Past, Year, a -> "%d years ago" |> replace_percent_d_with_int(a)
Future, Second, 1 -> "in 1 second"
Future, Second, a -> "in %d seconds" |> replace_percent_d_with_int(a)
Future, Minute, 1 -> "in 1 minute"
Future, Minute, a -> "in %d minutes" |> replace_percent_d_with_int(a)
Future, Hour, 1 -> "in 1 hour"
Future, Hour, a -> "in %d hours" |> replace_percent_d_with_int(a)
Future, Day, 1 -> "in 1 day"
Future, Day, a -> "in %d days" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "in 1 week"
Future, Week, a -> "in %d weeks" |> replace_percent_d_with_int(a)
Future, Month, 1 -> "in 1 month"
Future, Month, a -> "in %d months" |> replace_percent_d_with_int(a)
Future, Year, 1 -> "in 1 year"
Future, Year, a -> "in %d years" |> replace_percent_d_with_int(a)
}
}
/// Translations for French.
///
pub fn fr(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "à l'instant"
Past, Second, 1 -> "il y a 1 seconde"
Past, Second, a -> "il y a %d secondes" |> replace_percent_d_with_int(a)
Past, Minute, 1 -> "il y a 1 minute"
Past, Minute, a -> "il y a %d minutes" |> replace_percent_d_with_int(a)
Past, Hour, 1 -> "il y a 1 heure"
Past, Hour, a -> "il y a %d heures" |> replace_percent_d_with_int(a)
Past, Day, 1 -> "il y a 1 jour"
Past, Day, a -> "il y a %d jours" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "il y a 1 semaine"
Past, Week, a -> "il y a %d semaines" |> replace_percent_d_with_int(a)
Past, Month, 1 -> "il y a 1 mois"
Past, Month, a -> "il y a %d mois" |> replace_percent_d_with_int(a)
Past, Year, 1 -> "il y a 1 an"
Past, Year, a -> "il y a %d ans" |> replace_percent_d_with_int(a)
Future, Second, 1 -> "dans 1 seconde"
Future, Second, a -> "dans %d secondes" |> replace_percent_d_with_int(a)
Future, Minute, 1 -> "dans 1 minute"
Future, Minute, a -> "dans %d minutes" |> replace_percent_d_with_int(a)
Future, Hour, 1 -> "dans 1 heure"
Future, Hour, a -> "dans %d heures" |> replace_percent_d_with_int(a)
Future, Day, 1 -> "dans 1 jour"
Future, Day, a -> "dans %d jours" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "dans 1 semaine"
Future, Week, a -> "dans %d semaines" |> replace_percent_d_with_int(a)
Future, Month, 1 -> "dans 1 mois"
Future, Month, a -> "dans %d mois" |> replace_percent_d_with_int(a)
Future, Year, 1 -> "dans 1 an"
Future, Year, a -> "dans %d ans" |> replace_percent_d_with_int(a)
}
}
/// Translations for Brazilian Portuguese.
///
pub fn pt_br(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "agora mesmo"
Past, Second, 1 -> "1 segundo atrás"
Past, Second, a -> "%d segundos atrás" |> replace_percent_d_with_int(a)
Past, Minute, 1 -> "1 minuto atrás"
Past, Minute, a -> "%d minutos atrás" |> replace_percent_d_with_int(a)
Past, Hour, 1 -> "1 hora atrás"
Past, Hour, a -> "%d horas atrás" |> replace_percent_d_with_int(a)
Past, Day, 1 -> "1 dia atrás"
Past, Day, a -> "%d dias atrás" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "1 semana atrás"
Past, Week, a -> "%d semanas atrás" |> replace_percent_d_with_int(a)
Past, Month, 1 -> "1 mês atrás"
Past, Month, a -> "%d meses atrás" |> replace_percent_d_with_int(a)
Past, Year, 1 -> "1 ano atrás"
Past, Year, a -> "%d anos atrás" |> replace_percent_d_with_int(a)
Future, Second, 1 -> "daqui a 1 segundo"
Future, Second, a -> "daqui a %d segundos" |> replace_percent_d_with_int(a)
Future, Minute, 1 -> "daqui a 1 minuto"
Future, Minute, a -> "daqui a %d minutos" |> replace_percent_d_with_int(a)
Future, Hour, 1 -> "daqui a 1 hora"
Future, Hour, a -> "daqui a %d horas" |> replace_percent_d_with_int(a)
Future, Day, 1 -> "daqui a 1 dia"
Future, Day, a -> "daqui a %d dias" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "daqui a 1 semana"
Future, Week, a -> "daqui a %d semanas" |> replace_percent_d_with_int(a)
Future, Month, 1 -> "daqui a 1 mês"
Future, Month, a -> "daqui a %d meses" |> replace_percent_d_with_int(a)
Future, Year, 1 -> "daqui a 1 ano"
Future, Year, a -> "daqui a %d anos" |> replace_percent_d_with_int(a)
}
}
/// Translations for German.
///
pub fn de_de(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "jetzt"
Past, Second, 1 -> "vor einer Sekunde"
Past, Second, a -> "vor %d Sekunden" |> replace_percent_d_with_int(a)
Past, Minute, 1 -> "vor einer Minute"
Past, Minute, a -> "vor %d Minuten" |> replace_percent_d_with_int(a)
Past, Hour, 1 -> "vor einer Stunde"
Past, Hour, a -> "vor %d Stunden" |> replace_percent_d_with_int(a)
Past, Day, 1 -> "vor einem Tag"
Past, Day, a -> "vor %d Tagen" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "vor einer Woche"
Past, Week, a -> "vor %d Wochen" |> replace_percent_d_with_int(a)
Past, Month, 1 -> "vor einem Monat"
Past, Month, a -> "vor %d Monaten" |> replace_percent_d_with_int(a)
Past, Year, 1 -> "vor einem Jahr"
Past, Year, a -> "vor %d Jahren" |> replace_percent_d_with_int(a)
Future, Second, 1 -> "in einer Sekunde"
Future, Second, a -> "in %d Sekunden" |> replace_percent_d_with_int(a)
Future, Minute, 1 -> "in einer Minute"
Future, Minute, a -> "in %d Minuten" |> replace_percent_d_with_int(a)
Future, Hour, 1 -> "in einer Stunde"
Future, Hour, a -> "in %d Stunden" |> replace_percent_d_with_int(a)
Future, Day, 1 -> "in einem Tag"
Future, Day, a -> "in %d Tagen" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "in einer Woche"
Future, Week, a -> "in %d Wochen" |> replace_percent_d_with_int(a)
Future, Month, 1 -> "in einem Monat"
Future, Month, a -> "in %d Monaten" |> replace_percent_d_with_int(a)
Future, Year, 1 -> "in einem Jahr"
Future, Year, a -> "in %d Jahren" |> replace_percent_d_with_int(a)
}
}
/// Translations for Italian.
///
pub fn it_it(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "proprio adesso"
Past, Second, 1 -> "1 secondo fa"
Past, Second, a -> "%d secondi fa" |> replace_percent_d_with_int(a)
Past, Minute, 1 -> "1 minuto fa"
Past, Minute, a -> "%d minuti fa" |> replace_percent_d_with_int(a)
Past, Hour, 1 -> "1 ora fa"
Past, Hour, a -> "%d ore fa" |> replace_percent_d_with_int(a)
Past, Day, 1 -> "1 giorno fa"
Past, Day, a -> "%d giorni fa" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "1 settimana fa"
Past, Week, a -> "%d settimane fa" |> replace_percent_d_with_int(a)
Past, Month, 1 -> "1 mese fa"
Past, Month, a -> "%d mesi fa" |> replace_percent_d_with_int(a)
Past, Year, 1 -> "1 anno fa"
Past, Year, a -> "%d anni fa" |> replace_percent_d_with_int(a)
Future, Second, 1 -> "fra 1 secondo"
Future, Second, a -> "fra %d secondi" |> replace_percent_d_with_int(a)
Future, Minute, 1 -> "fra 1 minuto"
Future, Minute, a -> "fra %d minuti" |> replace_percent_d_with_int(a)
Future, Hour, 1 -> "fra 1 ora"
Future, Hour, a -> "fra %d ore" |> replace_percent_d_with_int(a)
Future, Day, 1 -> "fra 1 giorno"
Future, Day, a -> "fra %d giorni" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "fra 1 settimana"
Future, Week, a -> "fra %d settimane" |> replace_percent_d_with_int(a)
Future, Month, 1 -> "fra 1 mese"
Future, Month, a -> "fra %d mesi" |> replace_percent_d_with_int(a)
Future, Year, 1 -> "fra 1 anno"
Future, Year, a -> "fra %d anni" |> replace_percent_d_with_int(a)
}
}
/// Translations for Spanish.
///
pub fn es_es(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "ahora mismo"
Past, Second, 1 -> "hace 1 segundo"
Past, Second, a -> "hace %d segundos" |> replace_percent_d_with_int(a)
Past, Minute, 1 -> "hace 1 minuto"
Past, Minute, a -> "hace %d minutos" |> replace_percent_d_with_int(a)
Past, Hour, 1 -> "hace 1 hora"
Past, Hour, a -> "hace %d horas" |> replace_percent_d_with_int(a)
Past, Day, 1 -> "hace 1 día"
Past, Day, a -> "hace %d días" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "hace 1 semana"
Past, Week, a -> "hace %d semanas" |> replace_percent_d_with_int(a)
Past, Month, 1 -> "hace 1 mes"
Past, Month, a -> "hace %d meses" |> replace_percent_d_with_int(a)
Past, Year, 1 -> "hace 1 año"
Past, Year, a -> "hace %d años" |> replace_percent_d_with_int(a)
Future, Second, 1 -> "en 1 segundo"
Future, Second, a -> "en %d segundos" |> replace_percent_d_with_int(a)
Future, Minute, 1 -> "en 1 minuto"
Future, Minute, a -> "en %d minutos" |> replace_percent_d_with_int(a)
Future, Hour, 1 -> "en 1 hora"
Future, Hour, a -> "en %d horas" |> replace_percent_d_with_int(a)
Future, Day, 1 -> "en 1 día"
Future, Day, a -> "en %d días" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "en 1 semana"
Future, Week, a -> "en %d semanas" |> replace_percent_d_with_int(a)
Future, Month, 1 -> "en 1 mes"
Future, Month, a -> "en %d meses" |> replace_percent_d_with_int(a)
Future, Year, 1 -> "en 1 año"
Future, Year, a -> "en %d años" |> replace_percent_d_with_int(a)
}
}
/// Translations for Polish.
///
pub fn pl_pl(tense: Tense, unit: Unit, amount: Int) -> String {
case tense, unit, amount {
_, Nanosecond, _ | _, Microsecond, _ | _, Millisecond, _ -> "przed chwilą"
Past, Second, 1 -> "1 sekundę temu"
Past, Second, a ->
case a % 100 {
12 | 13 | 14 -> "%d sekund temu"
_ ->
case a % 10 {
2 | 3 | 4 -> "%d sekundy temu"
_ -> "%d sekund temu"
}
}
|> replace_percent_d_with_int(a)
Past, Minute, 1 -> "1 minutę temu"
Past, Minute, a ->
case a % 100 {
12 | 13 | 14 -> "%d minut temu"
_ ->
case a % 10 {
2 | 3 | 4 -> "%d minuty temu"
_ -> "%d minut temu"
}
}
|> replace_percent_d_with_int(a)
Past, Hour, 1 -> "1 godzinę temu"
Past, Hour, a ->
case a % 100 {
12 | 13 | 14 -> "%d godzin temu"
_ ->
case a % 10 {
2 | 3 | 4 -> "%d godziny temu"
_ -> "%d godzin temu"
}
}
|> replace_percent_d_with_int(a)
Past, Day, 1 -> "1 dzień temu"
Past, Day, a -> "%d dni temu" |> replace_percent_d_with_int(a)
Past, Week, 1 -> "1 tydzień temu"
Past, Week, a ->
case a % 100 {
12 | 13 | 14 -> "%d tygodni temu"
_ ->
case a % 10 {
2 | 3 | 4 -> "%d tygodnie temu"
_ -> "%d tygodni temu"
}
}
|> replace_percent_d_with_int(a)
Past, Month, 1 -> "1 miesiąc temu"
Past, Month, a ->
case a % 100 {
12 | 13 | 14 -> "%d miesięcy temu"
_ ->
case a % 10 {
2 | 3 | 4 -> "%d miesiące temu"
_ -> "%d miesięcy temu"
}
}
|> replace_percent_d_with_int(a)
Past, Year, 1 -> "1 rok temu"
Past, Year, a ->
case a % 100 {
12 | 13 | 14 -> "%d lat temu"
_ ->
case a % 10 {
2 | 3 | 4 -> "%d lata temu"
_ -> "%d lat temu"
}
}
|> replace_percent_d_with_int(a)
Future, Second, 1 -> "za 1 sekundę"
Future, Second, a ->
case a % 100 {
12 | 13 | 14 -> "za %d sekund"
_ ->
case a % 10 {
2 | 3 | 4 -> "za %d sekundy"
_ -> "za %d sekund"
}
}
|> replace_percent_d_with_int(a)
Future, Minute, 1 -> "za 1 minutę"
Future, Minute, a ->
case a % 100 {
12 | 13 | 14 -> "za %d minut"
_ ->
case a % 10 {
2 | 3 | 4 -> "za %d minuty"
_ -> "za %d minut"
}
}
|> replace_percent_d_with_int(a)
Future, Hour, 1 -> "za 1 godzinę"
Future, Hour, a ->
case a % 100 {
12 | 13 | 14 -> "za %d godzin"
_ ->
case a % 10 {
2 | 3 | 4 -> "za %d godziny"
_ -> "za %d godzin"
}
}
|> replace_percent_d_with_int(a)
Future, Day, 1 -> "za 1 dzień"
Future, Day, a -> "za %d dni" |> replace_percent_d_with_int(a)
Future, Week, 1 -> "za tydzień"
Future, Week, a ->
case a % 100 {
12 | 13 | 14 -> "za %d tygodni"
_ ->
case a % 10 {
2 | 3 | 4 -> "za %d tygodnie"
_ -> "za %d tygodni"
}
}
|> replace_percent_d_with_int(a)
Future, Month, 1 -> "za miesiąc"
Future, Month, a ->
case a % 100 {
12 | 13 | 14 -> "za %d miesięcy"
_ ->
case a % 10 {
2 | 3 | 4 -> "za %d miesiące"
_ -> "za %d miesięcy"
}
}
|> replace_percent_d_with_int(a)
Future, Year, 1 -> "za rok"
Future, Year, a ->
case a % 100 {
12 | 13 | 14 -> "za %d lat"
_ ->
case a % 10 {
2 | 3 | 4 -> "za %d lata"
_ -> "za %d lat"
}
}
|> replace_percent_d_with_int(a)
}
}