From 941fb7d756be2082279e5847088862ec4a686745 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Mon, 15 Jun 2026 00:30:34 +0200 Subject: [PATCH] feat(i18n): translate login and app chrome (HU/EN/DE) Login screen (with a language picker), header, account menu, sync status, About and Release Notes dialogs, and the version banner are now fully translated in Hungarian, English and German. --- frontend/src/components/About.tsx | 18 +++++----- frontend/src/components/Header.tsx | 29 +++++++++------ frontend/src/components/Login.tsx | 44 ++++++++++++----------- frontend/src/components/ReleaseNotes.tsx | 8 +++-- frontend/src/components/SyncStatus.tsx | 23 +++++++----- frontend/src/components/VersionBanner.tsx | 10 +++--- frontend/src/i18n/locales/de/about.json | 12 +++++++ frontend/src/i18n/locales/de/common.json | 12 +++++++ frontend/src/i18n/locales/de/header.json | 32 +++++++++++++++++ frontend/src/i18n/locales/de/login.json | 12 +++++++ frontend/src/i18n/locales/en/about.json | 12 +++++++ frontend/src/i18n/locales/en/common.json | 12 +++++++ frontend/src/i18n/locales/en/header.json | 32 +++++++++++++++++ frontend/src/i18n/locales/en/login.json | 12 +++++++ frontend/src/i18n/locales/hu/about.json | 12 +++++++ frontend/src/i18n/locales/hu/common.json | 12 +++++++ frontend/src/i18n/locales/hu/header.json | 32 +++++++++++++++++ frontend/src/i18n/locales/hu/login.json | 12 +++++++ 18 files changed, 279 insertions(+), 57 deletions(-) create mode 100644 frontend/src/i18n/locales/de/about.json create mode 100644 frontend/src/i18n/locales/de/common.json create mode 100644 frontend/src/i18n/locales/de/header.json create mode 100644 frontend/src/i18n/locales/de/login.json create mode 100644 frontend/src/i18n/locales/en/about.json create mode 100644 frontend/src/i18n/locales/en/common.json create mode 100644 frontend/src/i18n/locales/en/header.json create mode 100644 frontend/src/i18n/locales/en/login.json create mode 100644 frontend/src/i18n/locales/hu/about.json create mode 100644 frontend/src/i18n/locales/hu/common.json create mode 100644 frontend/src/i18n/locales/hu/header.json create mode 100644 frontend/src/i18n/locales/hu/login.json diff --git a/frontend/src/components/About.tsx b/frontend/src/components/About.tsx index 2e0a379..6310ae8 100644 --- a/frontend/src/components/About.tsx +++ b/frontend/src/components/About.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Info, Sparkles } from "lucide-react"; import { api } from "../lib/api"; import { FRONTEND_VERSION, FRONTEND_SHA, FRONTEND_BUILD_DATE } from "../lib/version"; @@ -19,23 +20,24 @@ export default function About({ onClose: () => void; onOpenReleaseNotes: () => void; }) { + const { t } = useTranslation(); const { data } = useQuery({ queryKey: ["version"], queryFn: api.version, staleTime: 5 * 60_000 }); const rows: [string, string][] = [ - ["Frontend", FRONTEND_VERSION + (FRONTEND_SHA !== "unknown" ? ` · ${FRONTEND_SHA}` : "")], + [t("about.frontend"), FRONTEND_VERSION + (FRONTEND_SHA !== "unknown" ? ` · ${FRONTEND_SHA}` : "")], [ - "Backend", + t("about.backend"), data ? data.app_version + (data.git_sha !== "unknown" ? ` · ${data.git_sha}` : "") : "…", ], - ["Database", data?.db_revision ?? "…"], - ["Build date", fmtDate(data?.build_date ?? FRONTEND_BUILD_DATE)], + [t("about.database"), data?.db_revision ?? "…"], + [t("about.buildDate"), fmtDate(data?.build_date ?? FRONTEND_BUILD_DATE)], ]; return ( - About + {t("about.title")} } onClose={onClose} @@ -46,9 +48,7 @@ export default function About({
v{FRONTEND_VERSION}
-

- Self-hosted, multi-user reader for your own YouTube subscriptions. -

+

{t("about.tagline")}

{rows.map(([k, v], i) => ( @@ -66,7 +66,7 @@ export default function About({ onClick={onOpenReleaseNotes} className="mt-4 w-full inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition" > - What's new + {t("about.whatsNew")} ); diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index cdcb060..7474091 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,9 +1,12 @@ import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { BarChart3, Home, Info, LogOut, Search, Settings, Shield, Tv } from "lucide-react"; import type { FeedFilters, Me } from "../lib/api"; import type { Page } from "../lib/urlState"; +import { type LangCode } from "../i18n"; import SyncStatus from "./SyncStatus"; import NotificationCenter from "./NotificationCenter"; +import LanguageSwitcher from "./LanguageSwitcher"; import AvatarImg from "./Avatar"; export default function Header({ @@ -14,6 +17,7 @@ export default function Header({ setPage, onOpenSettings, onOpenAbout, + onChangeLanguage, onGoToFullHistory, }: { me: Me; @@ -23,8 +27,11 @@ export default function Header({ setPage: (p: Page) => void; onOpenSettings: () => void; onOpenAbout: () => void; + onChangeLanguage: (code: LangCode) => void; onGoToFullHistory: () => void; }) { + const { t, i18n } = useTranslation(); + async function logout() { await fetch("/auth/logout", { method: "POST", credentials: "include" }); location.reload(); @@ -35,7 +42,7 @@ export default function Header({ @@ -48,17 +55,18 @@ export default function Header({ setFilters({ ...filters, q: e.target.value })} - placeholder="Search your subscriptions…" + placeholder={t("header.searchPlaceholder")} className="w-full bg-card border border-border rounded-full pl-9 pr-4 py-2 text-sm outline-none focus:border-accent" />
) : (
- {page === "stats" ? "Usage & stats" : "Channel manager"} + {page === "stats" ? t("header.usageStats") : t("header.channelManager")}
)}
+
void; onOpenAbout: () => void; }) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const closeTimer = useRef | null>(null); @@ -139,7 +148,7 @@ function AccountMenu({ {me.role === "admin" && (
- Admin + {t("header.account.admin")}
)} @@ -147,32 +156,32 @@ function AccountMenu({ {page !== "feed" && ( )} {page !== "channels" && ( )} {me.role === "admin" && page !== "stats" && ( )}
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index 98604d7..71bee93 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -1,9 +1,14 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { api } from "../lib/api"; +import { setLanguage, type LangCode } from "../i18n"; +import LanguageSwitcher from "./LanguageSwitcher"; type Phase = "idle" | "sending" | "requested" | "approved" | "error"; export default function Login() { + const { t, i18n } = useTranslation(); + // A denied Google login bounces back here with ?access=requested (we recorded it) or // ?access=denied (no usable email). Surface that so the user isn't left on a raw error. const params = new URLSearchParams(window.location.search); @@ -21,7 +26,7 @@ export default function Login() { const r = await api.requestAccess(email.trim()); setPhase(r.status === "approved" ? "approved" : "requested"); } catch { - setError("Couldn't submit — check the address and try again."); + setError(t("login.submitError")); setPhase("error"); } } @@ -30,40 +35,33 @@ export default function Login() { return (
+
+ +
Siftlode
-

- A self-hosted, filterable feed of your YouTube subscriptions — sort, tag, and - tune out the noise. Sign in with Google to get started; YouTube access is an - optional, separate step you control. -

+

{t("login.tagline")}

- Sign in with Google + {t("login.signIn")}
{phase === "approved" ? ( -

- You're already approved — just sign in with Google above. -

+

{t("login.approved")}

) : done ? ( -

- Thanks — your request is in. We'll email you when it's approved. -

+

{t("login.requested")}

) : ( <>
- No access yet? + {t("login.noAccessYet")}
{bounced === "denied" && ( -

- That Google account isn't approved. Request access below. -

+

{t("login.denied")}

)}
setEmail(e.target.value)} - placeholder="you@gmail.com" + placeholder={t("login.emailPlaceholder")} className="flex-1 min-w-0 bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent" />
{error &&

{error}

} @@ -88,8 +86,12 @@ export default function Login() {
); diff --git a/frontend/src/components/ReleaseNotes.tsx b/frontend/src/components/ReleaseNotes.tsx index 2e50063..0934dda 100644 --- a/frontend/src/components/ReleaseNotes.tsx +++ b/frontend/src/components/ReleaseNotes.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { Bug, Sparkles } from "lucide-react"; import { RELEASE_NOTES } from "../lib/releaseNotes"; import Modal from "./Modal"; @@ -18,8 +19,9 @@ export default function ReleaseNotes({ onClose: () => void; highlight?: string; }) { + const { t } = useTranslation(); return ( - +
{RELEASE_NOTES.map((r) => (
- New + {t("about.new")}
    {r.features.map((f, i) => ( @@ -56,7 +58,7 @@ export default function ReleaseNotes({ {r.fixes?.length ? (
    - Fixes + {t("about.fixes")}
      {r.fixes.map((f, i) => ( diff --git a/frontend/src/components/SyncStatus.tsx b/frontend/src/components/SyncStatus.tsx index 5e463b6..447238e 100644 --- a/frontend/src/components/SyncStatus.tsx +++ b/frontend/src/components/SyncStatus.tsx @@ -1,4 +1,5 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Database, History, Loader2, Pause, Play } from "lucide-react"; import { api } from "../lib/api"; import { formatViews } from "../lib/format"; @@ -14,6 +15,7 @@ export default function SyncStatus({ isAdmin: boolean; onGoToFullHistory: () => void; }) { + const { t } = useTranslation(); const qc = useQueryClient(); const { data } = useQuery({ queryKey: ["my-status"], @@ -34,37 +36,40 @@ export default function SyncStatus({ return (
      - + - {formatViews(data.my_videos)} yours + {formatViews(data.my_videos)}{" "} + {t("header.sync.yours")} / - {formatViews(data.total_videos)} total + + {formatViews(data.total_videos)} {t("header.sync.total")} + · {data.paused ? ( - paused + {t("header.sync.paused")} ) : syncing > 0 ? ( - {syncing} syncing + {t("header.sync.syncing", { count: syncing })} ) : ( - all synced + {t("header.sync.allSynced")} )} {notFull > 0 && ( <> · - + @@ -75,7 +80,7 @@ export default function SyncStatus({