storebound/apps/desktop/src/lib/format.ts

55 lines
2.7 KiB
TypeScript

// Locale-aware formatting for the UI (numbers, percentages, dates, "3 min ago"). Pure apart from Intl.
import { lang, t } from '../i18n';
const loc = (): string => (lang() === 'tr' ? 'tr-TR' : 'en-US');
/** 0.0109 -> "1.09%" (Turkish: "%1,09"). */
export function pct(x: number | null | undefined, digits = 2): string {
if (x === null || x === undefined || Number.isNaN(x)) return '–';
return new Intl.NumberFormat(loc(), { style: 'percent', minimumFractionDigits: digits, maximumFractionDigits: digits }).format(x);
}
/** Rollout share: 0.2 -> "20%", 0.05 -> "5%". */
export function share(x: number): string {
return new Intl.NumberFormat(loc(), { style: 'percent', maximumFractionDigits: x < 0.1 ? 1 : 0 }).format(x);
}
export const num = (n: number, digits = 0): string => new Intl.NumberFormat(loc(), { maximumFractionDigits: digits, minimumFractionDigits: digits }).format(n);
/** 4.99 USD -> "$4.99" (Turkish: "$4,99"); an unknown currency code keeps the code. */
export function money(n: number, currency: string): string {
try { return new Intl.NumberFormat(loc(), { style: 'currency', currency, maximumFractionDigits: n % 1 ? 2 : 0, minimumFractionDigits: n % 1 ? 2 : 0 }).format(n); }
catch { return `${num(n, n % 1 ? 2 : 0)} ${currency}`; }
}
export function bytes(n: number | null | undefined): string {
if (!n) return '–';
const mb = n / 1e6;
return mb >= 1 ? `${num(mb, mb < 10 ? 1 : 0)} MB` : `${num(n / 1e3, 0)} KB`;
}
export function date(iso: string | null | undefined): string {
if (!iso) return '–';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '–';
return d.toLocaleDateString(loc(), { day: 'numeric', month: 'short', year: d.getFullYear() === new Date().getFullYear() ? undefined : 'numeric' });
}
export function dateTime(iso: string | null | undefined): string {
if (!iso) return '–';
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? '–' : d.toLocaleString(loc(), { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
}
/** "just now", "4 min ago", "3 h ago", "2 d ago" (and the Turkish forms). */
export function ago(iso: string | null | undefined, now: number = Date.now()): string {
if (!iso) return t('time.never');
const s = Math.round((now - new Date(iso).getTime()) / 1000);
if (!Number.isFinite(s)) return t('time.never');
if (s < 45) return t('time.justNow');
const rtf = new Intl.RelativeTimeFormat(loc(), { numeric: 'auto', style: 'short' });
if (s < 3600) return rtf.format(-Math.round(s / 60), 'minute');
if (s < 86_400) return rtf.format(-Math.round(s / 3600), 'hour');
if (s < 86_400 * 30) return rtf.format(-Math.round(s / 86_400), 'day');
return date(iso);
}