Skip to content
Clone
main
98 lines 4.1 KB
import { getLocale, type Locale } from './locale'
/**
* Locale-aware display formatters. Every date/number the UI shows goes through
* here so that switching the interface language also switches the conventions
* (Y/M/D vs D/M/Y, 24h clock, relative-time wording, KiB grouping).
*
* The `null`/`undefined`/invalid paths keep returning the em dash used
* throughout the tables, so callers never need their own placeholder branch.
*/
export const DASH = '\u2014'
/** Rust's `time` crate emits `… +00:00:00`; `Date` rejects the seconds offset. */
export function parseDateValue(value: string): Date {
return new Date(value.replace(/([+-]\d{2}):(\d{2}):(\d{2})$/, '$1:$2'))
}
export function formatBytes(value: number | null | undefined): string {
if (value === null || value === undefined || !Number.isFinite(value)) return DASH
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = value
let unit = 0
while (size >= 1024 && unit < units.length - 1) {
size /= 1024
unit += 1
}
const formatted =
unit === 0
? new Intl.NumberFormat(getLocale()).format(size)
: new Intl.NumberFormat(getLocale(), { maximumFractionDigits: 1 }).format(size)
return `${formatted} ${units[unit]}`
}
export function shortSha(sha: string | null | undefined, len = 7): string {
return sha ? sha.slice(0, len) : DASH
}
export function formatDateTime(value: string | number | null | undefined): string {
if (value === null || value === undefined) return DASH
const date = typeof value === 'number' ? new Date(value * 1000) : parseDateValue(value)
if (Number.isNaN(date.getTime())) return typeof value === 'number' ? DASH : value
return new Intl.DateTimeFormat(getLocale(), {
dateStyle: 'medium',
timeStyle: 'short',
}).format(date)
}
/** Wall-clock time of day for chat and activity rows (locale 12h/24h). */
export function formatTime(value: number | null | undefined): string {
if (value === null || value === undefined) return DASH
const date = new Date(value * 1000)
if (Number.isNaN(date.getTime())) return DASH
return new Intl.DateTimeFormat(getLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)
}
/** Compact relative stamp for commit and comment rows. */
export function formatRelative(value: number | string | null | undefined): string {
const epoch =
typeof value === 'number' ? value : value ? Math.floor(parseDateValue(value).getTime() / 1000) : Number.NaN
if (!Number.isFinite(epoch)) return DASH
const delta = Math.floor(Date.now() / 1000) - epoch
// Future-dated commits (clock skew, rebased author time) read as "just now".
if (delta < 60) return relativeUnit(0, 'second')
const minutes = Math.floor(delta / 60)
if (minutes < 60) return relativeUnit(-minutes, 'minute')
const hours = Math.floor(minutes / 60)
if (hours < 24) return relativeUnit(-hours, 'hour')
const days = Math.floor(hours / 24)
if (days < 31) return relativeUnit(-days, 'day')
const months = Math.floor(days / 30)
if (months < 12) return relativeUnit(-months, 'month')
return relativeUnit(-Math.floor(days / 365), 'year')
}
function relativeUnit(amount: number, unit: Intl.RelativeTimeFormatUnit): string {
return new Intl.RelativeTimeFormat(getLocale(), { numeric: 'auto' }).format(amount, unit)
}
/** Long calendar-day label for grouped lists (commit history day headers). */
export function formatDay(value: string | number | null | undefined): string {
if (value === null || value === undefined) return DASH
const date = typeof value === 'number' ? new Date(value * 1000) : parseDateValue(value)
if (Number.isNaN(date.getTime())) return typeof value === 'number' ? DASH : value
return new Intl.DateTimeFormat(getLocale(), { dateStyle: 'long' }).format(date)
}
/** Medium date (no time) for settings rows and metadata blocks. */
export function formatDate(value: string | null | undefined, locale?: Locale): string {
if (!value) return DASH
const date = parseDateValue(value)
if (Number.isNaN(date.getTime())) return value
return new Intl.DateTimeFormat(locale ?? getLocale(), {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date)
}