feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
import i18n from "../i18n";
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
export function relativeTime(iso: string | null): string {
|
|
|
|
|
if (!iso) return "";
|
|
|
|
|
const then = new Date(iso).getTime();
|
|
|
|
|
const secs = Math.max(0, (Date.now() - then) / 1000);
|
|
|
|
|
const units: [number, string][] = [
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
[60, "time.secondsAgo"],
|
|
|
|
|
[3600, "time.minutesAgo"],
|
|
|
|
|
[86400, "time.hoursAgo"],
|
|
|
|
|
[604800, "time.daysAgo"],
|
|
|
|
|
[2592000, "time.weeksAgo"],
|
|
|
|
|
[31536000, "time.monthsAgo"],
|
2026-06-11 02:19:47 +02:00
|
|
|
];
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
if (secs < 60) return i18n.t("time.justNow");
|
2026-06-11 02:19:47 +02:00
|
|
|
for (let i = 0; i < units.length - 1; i++) {
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
const [, key] = units[i];
|
2026-06-11 02:19:47 +02:00
|
|
|
const next = units[i + 1][0];
|
|
|
|
|
if (secs < next) {
|
|
|
|
|
const v = Math.floor(secs / units[i][0]);
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
return i18n.t(key, { count: v });
|
2026-06-11 02:19:47 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const years = Math.floor(secs / 31536000);
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
if (years >= 1) return i18n.t("time.yearsAgo", { count: years });
|
2026-06-11 02:19:47 +02:00
|
|
|
const months = Math.floor(secs / 2592000);
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
return i18n.t("time.monthsAgo", { count: months });
|
2026-06-11 02:19:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatDuration(sec: number | null): string {
|
|
|
|
|
if (sec == null) return "";
|
|
|
|
|
const h = Math.floor(sec / 3600);
|
|
|
|
|
const m = Math.floor((sec % 3600) / 60);
|
|
|
|
|
const s = Math.floor(sec % 60);
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 02:47:55 +02:00
|
|
|
const QUOTA_ACTION_LABELS: Record<string, string> = {
|
|
|
|
|
sync_subscriptions: "Sync subscriptions",
|
|
|
|
|
backfill_recent: "Recent backfill",
|
|
|
|
|
backfill_deep: "Full-history backfill",
|
|
|
|
|
enrich: "Enrichment",
|
|
|
|
|
unsubscribe: "Unsubscribe",
|
|
|
|
|
subscription_resync: "Auto subscription resync",
|
|
|
|
|
api: "Other",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function quotaActionLabel(action: string): string {
|
|
|
|
|
return QUOTA_ACTION_LABELS[action] ?? action;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 23:07:09 +02:00
|
|
|
/** Coarse, human ETA for a remaining duration in seconds ("~3 hours", "~2 days"). */
|
|
|
|
|
export function formatEta(seconds: number): string {
|
|
|
|
|
if (seconds <= 0) return "done";
|
|
|
|
|
const hours = seconds / 3600;
|
|
|
|
|
if (hours < 1) return "< 1 hour";
|
|
|
|
|
if (hours < 24) {
|
|
|
|
|
const h = Math.round(hours);
|
|
|
|
|
return `~${h} hour${h === 1 ? "" : "s"}`;
|
|
|
|
|
}
|
|
|
|
|
const d = Math.round(hours / 24);
|
|
|
|
|
return `~${d} day${d === 1 ? "" : "s"}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
export function formatViews(n: number | null): string {
|
|
|
|
|
if (n == null) return "";
|
|
|
|
|
if (n >= 1e9) return `${(n / 1e9).toFixed(1).replace(/\.0$/, "")}B`;
|
|
|
|
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
|
|
|
|
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
|
|
|
|
|
return String(n);
|
|
|
|
|
}
|