import { loadUiPrefs } from "./i18n/prefs"; import { translate } from "./i18n/messages"; import type { AppLocale } from "./i18n/types"; /** Unix nanoseconds (UTC), matching product contract. */ export function nowUnixNano(): number { return Date.now() * 1_000_000; } function currentLocale(): AppLocale { return loadUiPrefs().locale; } function t(key: string, params?: Record): string { return translate(currentLocale(), key, params); } /** 後端可能回 ns/ms/sec;統一成 unix nanoseconds(顯示用,毫秒精度即可) */ export function normalizeUnixNano(raw: number | string | null | undefined): number | null { if (raw === null || raw === undefined || raw === "") return null; let n: number; if (typeof raw === "string") { const trimmed = raw.trim(); if (!trimmed) return null; // 大整數字串:用 BigInt 收到毫秒再還原 ns,避免 Number 精準度問題 try { const bi = BigInt(trimmed); if (bi <= 0n) return null; // sec ~1e9, ms ~1e12, ns ~1e18 if (bi < 1_000_000_000_000n) return Number(bi) * 1_000_000_000; // sec if (bi < 1_000_000_000_000_000n) return Number(bi) * 1_000_000; // ms return Number(bi / 1_000_000n) * 1_000_000; // ns → ms 精度 } catch { n = Number(trimmed); } } else { n = raw; } if (!Number.isFinite(n) || n <= 0) return null; if (n < 1e12) return Math.round(n * 1e9); // seconds if (n < 1e15) return Math.round(n * 1e6); // milliseconds return n; // nanoseconds } export function formatLocalDateTime(nano: number | null | undefined): string { const n = normalizeUnixNano(nano ?? null); if (!n) return "—"; const ms = Math.floor(n / 1_000_000); const locale = currentLocale() === "en" ? "en-US" : "zh-TW"; return new Date(ms).toLocaleString(locale, { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } export type SessionExpiryKind = "unknown" | "ok" | "soon" | "expired"; /** soon = 48h 內到期 */ export function sessionExpiryKind( expiresAt: number | null | undefined, now = nowUnixNano(), ): SessionExpiryKind { const exp = normalizeUnixNano(expiresAt ?? null); if (exp == null || exp <= 0) return "unknown"; if (exp <= now) return "expired"; const soonNs = 48 * 60 * 60 * 1_000_000_000; if (exp - now <= soonNs) return "soon"; return "ok"; } export function formatRelativeFromNow( nano: number | null | undefined, now = nowUnixNano(), ): string { const n = normalizeUnixNano(nano ?? null); if (n == null || n <= 0) return "—"; const diffMs = Math.floor((n - now) / 1_000_000); const abs = Math.abs(diffMs); const min = Math.round(abs / 60_000); const hour = Math.round(abs / 3_600_000); const day = Math.round(abs / 86_400_000); let span: string; if (min < 60) span = t("time.min", { n: Math.max(1, min) }); else if (hour < 48) span = t("time.hour", { n: hour }); else span = t("time.day", { n: day }); if (diffMs < 0) return t("time.expired", { span }); return t("time.remaining", { span }); } /** 過去時間:通知列表用(剛剛/N 分前) */ export function formatTimeAgo(nano: number | null | undefined, now = nowUnixNano()): string { // 這裡是唯一漏掉正規化的時間格式化函式:後端若回秒或毫秒,未換算的數值會遠小於 now, // 算出來就變成「56 年前」。 const n = normalizeUnixNano(nano ?? null); if (n == null || n <= 0) return "—"; const diffMs = Math.floor((now - n) / 1_000_000); if (diffMs < 45_000) return t("time.justNow"); const min = Math.floor(diffMs / 60_000); if (min < 60) return t("time.minAgo", { n: min }); const hour = Math.floor(diffMs / 3_600_000); if (hour < 48) return t("time.hourAgo", { n: hour }); const day = Math.floor(diffMs / 86_400_000); if (day < 14) return t("time.dayAgo", { n: day }); return formatLocalDateTime(nano); } export function formatSessionExpiry(expiresAt: number | null | undefined): { kind: SessionExpiryKind; absolute: string; relative: string; label: string; } { const exp = normalizeUnixNano(expiresAt ?? null); const kind = sessionExpiryKind(exp); const absolute = formatLocalDateTime(exp); const relative = formatRelativeFromNow(exp); const label = kind === "unknown" ? t("time.sessionUnknown") : kind === "expired" ? t("time.sessionExpired", { absolute }) : kind === "soon" ? t("time.sessionSoon", { relative, absolute }) : t("time.sessionOk", { relative, absolute }); return { kind, absolute, relative, label }; } export function fromDatetimeLocalValue(value: string): number { if (!value) return nowUnixNano(); const ms = new Date(value).getTime(); if (Number.isNaN(ms)) return nowUnixNano(); return ms * 1_000_000; } export function toDatetimeLocalValue(nano: number | null | undefined): string { if (!nano) return ""; const d = new Date(Math.floor(nano / 1_000_000)); const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; }