borsa / nextjs-app /src /lib /formatters.ts
veteroner's picture
feat: complete US market bilingual support across all pages, components, and API routes
20f3b43
/**
* Centralized formatting utilities for consistent display across the app.
* Solves F-M3 (inconsistent dates) and F-M4 (inconsistent numbers).
*/
// ─── Number Formatting ────────────────────────────
const trLocale = 'tr-TR'
const usLocale = 'en-US'
function loc(market?: 'bist' | 'us') { return market === 'us' ? usLocale : trLocale }
/**
* Format a price value with 2 decimal places.
* e.g. 1234.56 β†’ "1.234,56" (BIST) or "1,234.56" (US)
*/
export function formatPrice(value: number | null | undefined, currency = 'β‚Ί', market?: 'bist' | 'us'): string {
if (value == null || !Number.isFinite(value)) return 'β€”'
const formatted = value.toLocaleString(loc(market), {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
return currency ? `${currency}${formatted}` : formatted
}
/**
* Format a price without currency symbol for charts/tables.
*/
export function formatNumber(value: number | null | undefined, decimals = 2, market?: 'bist' | 'us'): string {
if (value == null || !Number.isFinite(value)) return 'β€”'
return value.toLocaleString(loc(market), {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})
}
/**
* Format a percentage value with sign.
* e.g. 5.678 β†’ "+5,68%", -3.2 β†’ "-3,20%"
*/
export function formatPercent(value: number | null | undefined, decimals = 2, market?: 'bist' | 'us'): string {
if (value == null || !Number.isFinite(value)) return 'β€”'
const sign = value > 0 ? '+' : ''
return `${sign}${value.toLocaleString(loc(market), {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})}%`
}
/**
* Format large numbers with abbreviations.
* e.g. 1500000 β†’ "1,5M", 2300 β†’ "2,3K"
*/
export function formatCompact(value: number | null | undefined, market?: 'bist' | 'us'): string {
if (value == null || !Number.isFinite(value)) return 'β€”'
const abs = Math.abs(value)
const sign = value < 0 ? '-' : ''
const l = loc(market)
if (abs >= 1_000_000_000) {
return `${sign}${(abs / 1_000_000_000).toLocaleString(l, { maximumFractionDigits: 1 })}B`
}
if (abs >= 1_000_000) {
return `${sign}${(abs / 1_000_000).toLocaleString(l, { maximumFractionDigits: 1 })}M`
}
if (abs >= 1_000) {
return `${sign}${(abs / 1_000).toLocaleString(l, { maximumFractionDigits: 1 })}K`
}
return value.toLocaleString(l, { maximumFractionDigits: 0 })
}
/**
* Format volume (shares traded).
*/
export function formatVolume(value: number | null | undefined): string {
return formatCompact(value)
}
// ─── Date Formatting ──────────────────────────────
/**
* Format a date string or Date object.
* e.g. "2025-01-15" β†’ "15 Oca 2025" (BIST) or "Jan 15, 2025" (US)
*/
export function formatDate(date: string | Date | null | undefined, market?: 'bist' | 'us'): string {
if (!date) return 'β€”'
try {
const d = typeof date === 'string' ? new Date(date) : date
if (isNaN(d.getTime())) return 'β€”'
return d.toLocaleDateString(loc(market), {
day: 'numeric',
month: 'short',
year: 'numeric',
})
} catch {
return 'β€”'
}
}
/**
* Format a date with time.
*/
export function formatDateTime(date: string | Date | null | undefined, market?: 'bist' | 'us'): string {
if (!date) return 'β€”'
try {
const d = typeof date === 'string' ? new Date(date) : date
if (isNaN(d.getTime())) return 'β€”'
return d.toLocaleDateString(loc(market), {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return 'β€”'
}
}
/**
* Relative time formatting.
*/
export function formatRelativeTime(date: string | Date | null | undefined, market?: 'bist' | 'us'): string {
if (!date) return 'β€”'
try {
const d = typeof date === 'string' ? new Date(date) : date
if (isNaN(d.getTime())) return 'β€”'
const now = Date.now()
const diffMs = now - d.getTime()
const diffSec = Math.floor(diffMs / 1000)
const diffMin = Math.floor(diffSec / 60)
const diffHours = Math.floor(diffMin / 60)
const diffDays = Math.floor(diffHours / 24)
const isUS = market === 'us'
if (diffSec < 60) return isUS ? 'Just now' : 'Az ΓΆnce'
if (diffMin < 60) return isUS ? `${diffMin}m ago` : `${diffMin} dakika ΓΆnce`
if (diffHours < 24) return isUS ? `${diffHours}h ago` : `${diffHours} saat ΓΆnce`
if (diffDays < 7) return isUS ? `${diffDays}d ago` : `${diffDays} gΓΌn ΓΆnce`
if (diffDays < 30) return isUS ? `${Math.floor(diffDays / 7)}w ago` : `${Math.floor(diffDays / 7)} hafta ΓΆnce`
return formatDate(d, market)
} catch {
return 'β€”'
}
}