import type { GamePricePoint, GameProfile, ItadReview } from "@/lib/game/game-profile-types"; import type { Dictionary } from "@/lib/i18n/get-dictionary"; import type { Locale } from "@/lib/i18n/locales"; export type ModalDict = Dictionary["discord"]["gameModal"]; export const STEAM_SCORE_KEYS: Partial> = { "Overwhelmingly Positive": "overwhelminglyPositive", "Very Positive": "veryPositive", Positive: "positive", "Mostly Positive": "mostlyPositive", Mixed: "mixed", "Mostly Negative": "mostlyNegative", Negative: "negative", "Very Negative": "veryNegative", "Overwhelmingly Negative": "overwhelminglyNegative", }; export const LOCALE_TAGS: Record = { tr: "tr-TR", en: "en-US", de: "de-DE", }; export const COUNTRIES: Record = { tr: "TR", en: "US", de: "DE", }; export function localizeScore(label: string, dict: ModalDict): string { const key = STEAM_SCORE_KEYS[label]; return key ? dict.reviewScores[key] : label; } export function formatRatingPercent(value: number, localeTag: string): string { try { return new Intl.NumberFormat(localeTag, { style: "percent", maximumFractionDigits: 2, }).format(value); } catch { return `%${(value * 100).toFixed(2)}`; } } export function formatPrice(point: GamePricePoint, localeTag: string): string { try { return new Intl.NumberFormat(localeTag, { style: "currency", currency: point.currency, }).format(point.price); } catch { return `${point.price} ${point.currency}`; } } export function formatReleaseDate(value: string, localeTag: string): string { if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { const parsed = new Date(`${value}T00:00:00`); if (!Number.isNaN(parsed.getTime())) { return parsed.toLocaleDateString(localeTag, { day: "numeric", month: "long", year: "numeric", }); } } return value; } export function formatCount( value: number | null | undefined, localeTag: string, unit: string, ): string { if (value == null) { return "—"; } return `${value.toLocaleString(localeTag)} ${unit}`; } export function discountPercent(point: GamePricePoint): number | null { if (point.regular > point.price && point.regular > 0) { return Math.round((1 - point.price / point.regular) * 100); } return null; } export function steamdbRating(positive: number, total: number): number | null { if ( !Number.isFinite(positive) || !Number.isFinite(total) || total <= 0 || positive < 0 || positive > total ) { return null; } const score = positive / total; return score - (score - 0.5) * 2 ** -Math.log10(total + 1); } export function toIsoDate(unixSeconds: number | undefined): string | null { if (!unixSeconds) { return null; } return new Date(unixSeconds * 1000).toISOString().slice(0, 10); } export interface GameView { gameUrl: string; name: string; bestPrice: GamePricePoint | null; historyLow: GamePricePoint | null; isFree: boolean; } export function selectGameView( profile: GameProfile | null, fallbackName: string, fallbackGameUrl: string, ): GameView { const bestPrice = profile?.itad?.prices ?? null; const historyLow = profile?.itad?.historyLow ?? null; return { gameUrl: profile?.gameUrl ?? fallbackGameUrl, name: profile?.name || fallbackName, bestPrice, historyLow, isFree: !!bestPrice && bestPrice.price === 0, }; } export interface ProfileSummary { dbRating: number | null; itadReviews: ItadReview[]; hasDetails: boolean; hasReviews: boolean; metascore: ItadReview | null; opencritic: ItadReview | null; } export function describeProfile(profile: GameProfile | null): ProfileSummary { const dbRating = profile?.steamReviews?.positive != null && profile?.steamReviews?.total != null ? steamdbRating(profile.steamReviews.positive, profile.steamReviews.total) : null; const itadReviews = profile?.itad?.reviews ?? []; const hasDetails = !!( profile && (profile.developers.length > 0 || profile.publishers.length > 0 || profile.releaseDate || profile.platforms.length > 0 || profile.links.length > 0) ); const metascore = itadReviews.find((r) => r.source === "Metascore") ?? null; const opencritic = itadReviews.find((r) => r.source === "OpenCritic") ?? null; const hasReviews = !!(profile?.steamReviews || metascore || opencritic); return { dbRating, itadReviews, hasDetails, hasReviews, metascore, opencritic }; } const DEFAULT_ELAPSED_UNITS = { hour: "h", minute: "m", lessThanMinute: "<1m" }; const ELAPSED_UNITS: Partial< Record > = { tr: { hour: "s", minute: "dk", lessThanMinute: "<1dk" }, de: { hour: " Std.", minute: " Min.", lessThanMinute: "<1 Min." }, en: DEFAULT_ELAPSED_UNITS, }; export function formatElapsed(start: number, lang: string): string { const seconds = Math.max(0, Math.floor((Date.now() - start) / 1000)); const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const units = ELAPSED_UNITS[lang] ?? ELAPSED_UNITS.en ?? DEFAULT_ELAPSED_UNITS; if (hours > 0) { return `${hours}${units.hour} ${minutes}${units.minute}`; } if (minutes > 0) { return `${minutes}${units.minute}`; } return units.lessThanMinute; } export function normalizeGameTitle(value: string): string { return value .toLowerCase() .replace(/[®™©]/g, "") .replace(/[^a-z0-9]+/g, " ") .trim(); } export interface GameLocale { lang: Locale; country: string; localeTag: string; steamLang: string; } const STEAM_LANG: Record = { tr: "turkish", en: "english", de: "german", }; export function resolveGameLocale(lang: string): GameLocale { const resolved = lang === "tr" || lang === "en" || lang === "de" ? lang : "en"; return { lang: resolved, country: COUNTRIES[resolved], localeTag: LOCALE_TAGS[resolved], steamLang: STEAM_LANG[resolved], }; }