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) }