← Back
import { translations } from '../i18n/translations';

// Active language for pure (non-hook) formatters: stored choice → browser → en.
// Re-reads on every call so a language toggle (which re-renders the tree) updates.
function _lang(): 'en' | 'ru' {
  try { const v = localStorage.getItem('sz_lang'); if (v === 'en' || v === 'ru') return v; } catch { /* ignore */ }
  const nav = (typeof navigator !== 'undefined' ? navigator.language : 'en').toLowerCase();
  return nav.startsWith('ru') ? 'ru' : 'en';
}
function _t(key: string, n?: number): string {
  const tpl = translations[_lang()][key] ?? translations.en[key] ?? key;
  return n != null ? tpl.replace('{n}', String(n)) : tpl;
}

/**
 * Format large numbers: $1.2M, $350K, $42
 */
export function formatVolume(val: number): string {
  if (!Number.isFinite(val)) return '$0';
  if (val >= 1_000_000) return `$${(val / 1_000_000).toFixed(1)}M`;
  if (val >= 1_000) return `$${(val / 1_000).toFixed(0)}K`;
  return `$${val.toFixed(0)}`;
}

/**
 * Format an exact USD amount with 2 decimals + thousands separator: 1234.5 → $1,234.50, -3.58 → -$3.58
 * Use for portfolio/P&L sums where cents matter (vs formatVolume's K/M rounding for market volumes).
 */
export function formatUsd(val: number): string {
  if (!Number.isFinite(val)) return '$0.00';
  const sign = val < 0 ? '-' : '';
  return `${sign}$${Math.abs(val).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}

/**
 * Format price as cents: 0.65 → 65¢
 */
export function formatPrice(val: number): string {
  if (!Number.isFinite(val) || val <= 0) return '<1¢';
  if (val >= 1) return '99¢+';
  if (val < 0.01) return '<1¢';      // sub-cent longshots round to 0¢ otherwise
  if (val > 0.99) return '>99¢';
  return `${Math.round(val * 100)}¢`;
}

/**
 * Format spread: 0.05 → 5.0%
 */
export function formatSpread(val: number): string {
  return `${(val * 100).toFixed(1)}%`;
}

/**
 * Relative date: "in 3d", "in 12h", "ended"
 */
export function formatEndDate(dateStr: string): string {
  if (!dateStr) return '—';
  const end = new Date(dateStr);
  const now = new Date();
  const diffMs = end.getTime() - now.getTime();

  if (diffMs < 0) return 'Ended';
  const diffH = diffMs / (1000 * 60 * 60);
  if (diffH < 1) return `${Math.round(diffH * 60)}m`;
  if (diffH < 24) return `${Math.round(diffH)}h`;
  const diffD = diffH / 24;
  if (diffD < 30) return `${Math.round(diffD)}d`;
  return `${Math.round(diffD / 30)}mo`;
}

/**
 * Time remaining until a market resolves, in Russian ("через 3 д", "12 ч",
 * "45 мин", "завершён"). Tz-less timestamps are treated as UTC.
 */
export function timeUntil(iso: string | null | undefined): string {
  if (!iso) return '';
  const hasTz = /[zZ]$|[+-]\d\d:?\d\d$/.test(iso);
  const norm = hasTz ? iso : iso.replace(' ', 'T') + 'Z';
  const ms = new Date(norm).getTime();
  if (!Number.isFinite(ms)) return '';
  const diff = ms - Date.now();
  if (diff <= 0) return _t('time.ended');
  const mins = Math.floor(diff / 60_000);
  if (mins < 60) return _t('time.min', mins);
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return _t('time.hr', hrs);
  const days = Math.floor(hrs / 24);
  if (days < 30) return _t('time.day', days);
  return _t('time.mo', Math.floor(days / 30));
}

/**
 * Truncate text with ellipsis
 */
export function truncate(str: string, max: number): string {
  if (str.length <= max) return str;
  return str.slice(0, max) + '…';
}

/**
 * Display title: group_item_title + event_title for multi-outcome, else question
 */
export function getMarketTitle(m: { group_item_title?: string; event_title?: string; question: string }): string {
  return m.group_item_title
    ? `${m.group_item_title} — ${m.event_title}`
    : m.question;
}

/**
 * Build Polymarket URL from event_slug or market slug
 */
export function getPolymarketUrl(m: { event_slug?: string; slug?: string }): string {
  if (m.event_slug) return `https://polymarket.com/event/${m.event_slug}`;
  if (m.slug) return `https://polymarket.com/market/${m.slug}`;
  return 'https://polymarket.com';
}

/**
 * Relative time in Russian ("только что", "5 мин", "2 ч", "3 д").
 * Tz-less timestamps (SQLite "YYYY-MM-DD HH:MM:SS") are treated as UTC so
 * non-UTC browsers don't get negative diffs.
 */
export function timeAgo(iso: string | null | undefined): string {
  if (!iso) return '';
  const hasTz = /[zZ]$|[+-]\d\d:?\d\d$/.test(iso);
  const norm = hasTz ? iso : iso.replace(' ', 'T') + 'Z';
  const diff = Date.now() - new Date(norm).getTime();
  if (diff < 60_000) return _t('time.justNow');
  const mins = Math.floor(diff / 60_000);
  if (mins < 60) return _t('time.min', mins);
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return _t('time.hr', hrs);
  const days = Math.floor(hrs / 24);
  return _t('time.day', days);
}

📜 Git History

6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...