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.
This commit is contained in:
npeter83 2026-06-15 00:47:04 +02:00
parent b38ae92d8d
commit 9c61bd898d
45 changed files with 1522 additions and 355 deletions

View file

@ -1,4 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { api, type AdminQuotaRow } from "../lib/api";
import { quotaActionLabel } from "../lib/format";
@ -6,6 +7,7 @@ import { quotaActionLabel } from "../lib/format";
const RANGES = [7, 30, 90] as const;
export default function Stats() {
const { t } = useTranslation();
const [days, setDays] = useState<number>(30);
const q = useQuery({
queryKey: ["admin-quota", days],
@ -18,7 +20,7 @@ export default function Stats() {
return (
<div className="p-4 max-w-4xl mx-auto">
<div className="flex items-center justify-between gap-3 mb-4">
<h2 className="text-lg font-semibold">API quota usage</h2>
<h2 className="text-lg font-semibold">{t("stats.title")}</h2>
<div className="flex items-center gap-1">
{RANGES.map((d) => (
<button
@ -30,30 +32,34 @@ export default function Stats() {
: "bg-card border-border text-muted hover:border-accent"
}`}
>
{d}d
{t("stats.rangeDays", { count: d })}
</button>
))}
</div>
</div>
<p className="text-xs text-muted mb-4 leading-relaxed">
YouTube Data API units attributed by who triggered the spend. <b className="text-fg/80">System</b> is
shared background work (scheduled backfill, enrichment, resync) that isn't tied to one person.
{t("stats.introBefore")}
<b className="text-fg/80">{t("stats.introSystem")}</b>
{t("stats.introAfter")}
</p>
{q.isLoading ? (
<div className="text-muted py-8">Loading</div>
<div className="text-muted py-8">{t("stats.loading")}</div>
) : !data ? (
<div className="text-muted py-8">No data.</div>
<div className="text-muted py-8">{t("stats.noData")}</div>
) : (
<>
{/* Daily totals (instance-wide, Pacific days) */}
<div className="glass-card rounded-xl p-3 mb-4">
<div className="text-xs text-muted mb-2">
Daily total ({data.range_days}d · {grandTotal.toLocaleString()} units)
{t("stats.dailyTotal", {
count: data.range_days,
units: grandTotal.toLocaleString(),
})}
</div>
{data.daily.length === 0 ? (
<div className="text-muted text-sm">No usage in this range.</div>
<div className="text-muted text-sm">{t("stats.noUsageInRange")}</div>
) : (
<div className="flex items-end gap-0.5 h-24">
{data.daily.map((d) => (
@ -61,7 +67,10 @@ export default function Stats() {
key={d.day}
className="flex-1 bg-accent/70 hover:bg-accent rounded-t transition"
style={{ height: `${Math.max(2, (d.total / maxDaily) * 100)}%` }}
title={`${d.day}: ${d.total.toLocaleString()} units`}
title={t("stats.dailyTooltip", {
day: d.day,
units: d.total.toLocaleString(),
})}
/>
))}
</div>
@ -71,7 +80,7 @@ export default function Stats() {
{/* Per-user breakdown */}
<div className="flex flex-col gap-1.5">
{data.rows.length === 0 ? (
<div className="text-muted py-4">No usage in this range.</div>
<div className="text-muted py-4">{t("stats.noUsageInRange")}</div>
) : (
data.rows.map((r) => (
<UserRow key={r.user_id ?? "system"} row={r} max={data.rows[0]?.total || 1} />
@ -85,6 +94,7 @@ export default function Stats() {
}
function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const actions = Object.entries(row.by_action).sort((a, b) => b[1] - a[1]);
const isSystem = row.user_id === null;
@ -95,7 +105,7 @@ function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) {
className="w-full flex items-center gap-3 text-left"
>
<span className={`text-sm font-medium truncate flex-1 ${isSystem ? "text-muted" : ""}`}>
{isSystem ? "System (background)" : row.email}
{isSystem ? t("stats.system") : row.email}
</span>
<span className="text-sm font-semibold tabular-nums shrink-0">
{row.total.toLocaleString()}