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:
parent
941fb7d756
commit
ea317c0009
45 changed files with 1522 additions and 355 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowLeft, Check, CheckCheck, X } from "lucide-react";
|
||||
|
|
@ -65,8 +67,10 @@ function renderDescription(
|
|||
currentId: string;
|
||||
onSeek: (seconds: number) => void;
|
||||
onLoadVideo: (id: string, start: number | null) => void;
|
||||
t: TFunction;
|
||||
}
|
||||
): ReactNode[] {
|
||||
const { t } = opts;
|
||||
const out: ReactNode[] = [];
|
||||
let key = 0;
|
||||
const linkCls = "text-accent hover:underline break-all";
|
||||
|
|
@ -86,7 +90,7 @@ function renderDescription(
|
|||
sameVideo ? opts.onSeek(yt.start ?? 0) : opts.onLoadVideo(yt.id, yt.start)
|
||||
}
|
||||
className={linkCls}
|
||||
title={sameVideo ? "Jump to this time" : "Play in the in-app player"}
|
||||
title={sameVideo ? t("player.jumpToTime") : t("player.playInApp")}
|
||||
>
|
||||
{href}
|
||||
</button>
|
||||
|
|
@ -189,6 +193,7 @@ export default function PlayerModal({
|
|||
onClose: () => void;
|
||||
onState: (id: string, status: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
// Resume point for the video we opened with.
|
||||
const resumeAt = startAt != null ? startAt : video.position_seconds || 0;
|
||||
|
|
@ -395,17 +400,17 @@ export default function PlayerModal({
|
|||
onMouseEnter={openDesc}
|
||||
onMouseLeave={scheduleCloseDesc}
|
||||
>
|
||||
{navigated ? liveData?.title ?? "Loading…" : video.title}
|
||||
{navigated ? liveData?.title ?? t("player.loading") : video.title}
|
||||
</span>
|
||||
</h2>
|
||||
{navigated && (
|
||||
<button
|
||||
onClick={() => loadVideo(video.id, null)}
|
||||
title="Back to the original video"
|
||||
title={t("player.backToOriginal")}
|
||||
className="shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg text-muted hover:text-fg hover:bg-surface transition"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back
|
||||
{t("player.back")}
|
||||
</button>
|
||||
)}
|
||||
{showDesc &&
|
||||
|
|
@ -422,30 +427,31 @@ export default function PlayerModal({
|
|||
onMouseLeave={scheduleCloseDesc}
|
||||
>
|
||||
<div className="text-xs uppercase tracking-wide text-muted mb-2">
|
||||
Description
|
||||
{t("player.description")}
|
||||
</div>
|
||||
{detail.isLoading ? (
|
||||
<div className="text-sm text-muted">Loading…</div>
|
||||
<div className="text-sm text-muted">{t("player.loading")}</div>
|
||||
) : detail.data?.description ? (
|
||||
<div className="text-sm whitespace-pre-wrap break-words max-h-64 overflow-y-auto leading-relaxed">
|
||||
{renderDescription(detail.data.description, {
|
||||
currentId: currentVideoId,
|
||||
onSeek: seekTo,
|
||||
onLoadVideo: loadVideo,
|
||||
t,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted">No description.</div>
|
||||
<div className="text-sm text-muted">{t("player.noDescription")}</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
title="Close (Esc)"
|
||||
title={t("player.closeEsc")}
|
||||
className="shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg text-muted hover:text-fg hover:bg-surface transition"
|
||||
>
|
||||
Close
|
||||
{t("player.close")}
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -469,7 +475,7 @@ export default function PlayerModal({
|
|||
rel="noreferrer"
|
||||
className="font-medium hover:text-accent shrink-0"
|
||||
>
|
||||
{liveData?.author ?? detail.data.channel_title ?? "Channel"}
|
||||
{liveData?.author ?? detail.data.channel_title ?? t("player.channel")}
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-medium shrink-0">{liveData?.author ?? ""}</span>
|
||||
|
|
@ -486,14 +492,16 @@ export default function PlayerModal({
|
|||
)}
|
||||
{!navigated ? (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted min-w-0">
|
||||
{video.view_count != null && <span>· {formatViews(video.view_count)} views</span>}
|
||||
{video.view_count != null && (
|
||||
<span>· {t("player.views", { count: video.view_count, formattedCount: formatViews(video.view_count) })}</span>
|
||||
)}
|
||||
<span>· {relativeTime(video.published_at)}</span>
|
||||
{video.duration_seconds != null && (
|
||||
<span>· {formatDuration(video.duration_seconds)}</span>
|
||||
)}
|
||||
{video.live_status === "was_live" && (
|
||||
<span className="bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded">
|
||||
stream
|
||||
{t("player.stream")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -501,7 +509,7 @@ export default function PlayerModal({
|
|||
// Linked video's stats come from the (already-fetched) video detail.
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted min-w-0">
|
||||
{detail.data?.view_count != null && (
|
||||
<span>· {formatViews(detail.data.view_count)} views</span>
|
||||
<span>· {t("player.views", { count: detail.data.view_count, formattedCount: formatViews(detail.data.view_count) })}</span>
|
||||
)}
|
||||
{detail.data?.published_at && <span>· {relativeTime(detail.data.published_at)}</span>}
|
||||
{detail.data?.duration_seconds != null && (
|
||||
|
|
@ -513,7 +521,7 @@ export default function PlayerModal({
|
|||
{!navigated && (
|
||||
<button
|
||||
onClick={() => setWatched(!watched)}
|
||||
title={watched ? "Watched — click to unmark" : "Mark watched"}
|
||||
title={watched ? t("player.watchedUnmark") : t("player.markWatched")}
|
||||
className={
|
||||
"ml-auto shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg transition " +
|
||||
(watched
|
||||
|
|
@ -522,7 +530,7 @@ export default function PlayerModal({
|
|||
}
|
||||
>
|
||||
{watched ? <CheckCheck className="w-4 h-4" /> : <Check className="w-4 h-4" />}
|
||||
{watched ? "Watched" : "Mark watched"}
|
||||
{watched ? t("player.watched") : t("player.markWatched")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue