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
b38ae92d8d
commit
9c61bd898d
45 changed files with 1522 additions and 355 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react";
|
||||
import { api, type Me } from "../lib/api";
|
||||
|
|
@ -20,20 +21,20 @@ function ConsentHeadsUp() {
|
|||
<div className="flex gap-2.5 rounded-xl border border-amber-400/30 bg-amber-400/5 p-3 text-left">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-amber-400" />
|
||||
<p className="text-xs leading-relaxed text-fg/80">
|
||||
Google will show a <span className="font-medium">"Google hasn't verified this app"</span>{" "}
|
||||
screen — that's expected, because Siftlode hasn't gone through Google's full review.
|
||||
It's safe to continue: click <span className="font-medium">Advanced</span> →{" "}
|
||||
<span className="font-medium">Go to Siftlode</span>. You can revoke access anytime from
|
||||
your{" "}
|
||||
<a
|
||||
href="https://myaccount.google.com/permissions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
Google Account
|
||||
</a>
|
||||
.
|
||||
<Trans
|
||||
i18nKey="onboarding.consent"
|
||||
components={[
|
||||
<span className="font-medium" />,
|
||||
<span className="font-medium" />,
|
||||
<span className="font-medium" />,
|
||||
<a
|
||||
href="https://myaccount.google.com/permissions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -55,6 +56,7 @@ function Dots({ index, total }: { index: number; total: number }) {
|
|||
}
|
||||
|
||||
export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const importTriggered = useRef(false);
|
||||
|
||||
|
|
@ -126,7 +128,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
<button
|
||||
onClick={close}
|
||||
className="absolute top-3 right-3 p-2 rounded-lg text-muted hover:text-fg hover:bg-card/60 transition"
|
||||
aria-label="Close"
|
||||
aria-label={t("onboarding.close")}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
|
@ -136,11 +138,12 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
<div className="mx-auto mb-4 grid place-items-center w-12 h-12 rounded-2xl bg-accent/15 text-accent">
|
||||
<Youtube className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold">Connect your YouTube subscriptions</h2>
|
||||
<h2 className="text-lg font-semibold">{t("onboarding.read.title")}</h2>
|
||||
<p className="text-sm text-muted leading-relaxed mt-2 mb-4">
|
||||
You're signed in. To build your feed, Siftlode needs{" "}
|
||||
<span className="text-fg/90">read-only</span> access to the channels you're
|
||||
subscribed to on YouTube. It never posts, deletes, or changes anything with this.
|
||||
<Trans
|
||||
i18nKey="onboarding.read.body"
|
||||
components={[<span className="text-fg/90" />]}
|
||||
/>
|
||||
</p>
|
||||
<ConsentHeadsUp />
|
||||
<div className="mt-5 flex flex-col gap-2">
|
||||
|
|
@ -148,13 +151,13 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
onClick={() => beginGrant("read")}
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
|
||||
>
|
||||
<Youtube className="w-4 h-4" /> Connect (read-only)
|
||||
<Youtube className="w-4 h-4" /> {t("onboarding.read.connect")}
|
||||
</button>
|
||||
<button
|
||||
onClick={close}
|
||||
className="px-5 py-2 rounded-xl text-sm text-muted hover:text-fg transition"
|
||||
>
|
||||
Skip for now
|
||||
{t("onboarding.read.skip")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -166,10 +169,9 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
<div className="mx-auto mb-4 grid place-items-center w-12 h-12 rounded-2xl bg-accent/15 text-accent">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold">Building your feed…</h2>
|
||||
<h2 className="text-lg font-semibold">{t("onboarding.importing.title")}</h2>
|
||||
<p className="text-sm text-muted leading-relaxed mt-2 mb-2">
|
||||
Importing your YouTube subscriptions. Channels already in Siftlode show up
|
||||
instantly; any new ones fill in automatically in the background.
|
||||
{t("onboarding.importing.body")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -179,24 +181,24 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
<div className="mx-auto mb-4 grid place-items-center w-12 h-12 rounded-2xl bg-accent/15 text-accent">
|
||||
<Check className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold">You're connected</h2>
|
||||
<h2 className="text-lg font-semibold">{t("onboarding.write.title")}</h2>
|
||||
<p className="text-sm text-muted leading-relaxed mt-2 mb-4">
|
||||
{importSubs.isError ? (
|
||||
<>Your YouTube account is connected, but importing subscriptions failed — you
|
||||
can retry from Settings → Sync. </>
|
||||
<>{t("onboarding.write.importFailed")}</>
|
||||
) : (
|
||||
<>
|
||||
Your feed is ready
|
||||
{myStatus.data ? ` (${myStatus.data.channels_total} channels)` : ""}. Optionally,
|
||||
let Siftlode{" "}
|
||||
{t("onboarding.write.feedReady", {
|
||||
channels: myStatus.data
|
||||
? t("onboarding.write.feedReadyChannels", { count: myStatus.data.channels_total })
|
||||
: "",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{!importSubs.isError && (
|
||||
<>
|
||||
<span className="text-fg/90">unsubscribe</span> from channels on YouTube for you
|
||||
— this needs an extra write permission. Skip it to stay read-only; you can
|
||||
always enable it later in Settings.
|
||||
</>
|
||||
<Trans
|
||||
i18nKey="onboarding.write.rationale"
|
||||
components={[<span className="text-fg/90" />]}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
{importSubs.isError && (
|
||||
|
|
@ -204,7 +206,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
onClick={() => importSubs.mutate()}
|
||||
className="mb-3 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-sm bg-card border border-border hover:border-accent transition"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" /> Retry import
|
||||
<RefreshCw className="w-4 h-4" /> {t("onboarding.write.retryImport")}
|
||||
</button>
|
||||
)}
|
||||
<ConsentHeadsUp />
|
||||
|
|
@ -213,7 +215,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
onClick={() => beginGrant("write")}
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" /> Enable unsubscribe
|
||||
<ShieldCheck className="w-4 h-4" /> {t("onboarding.write.enableUnsubscribe")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -222,7 +224,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
}}
|
||||
className="px-5 py-2 rounded-xl text-sm text-muted hover:text-fg transition"
|
||||
>
|
||||
No thanks — keep it read-only
|
||||
{t("onboarding.write.keepReadOnly")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -233,10 +235,9 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
<div className="mx-auto mb-4 grid place-items-center w-12 h-12 rounded-2xl bg-accent/15 text-accent">
|
||||
<Check className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold">All set</h2>
|
||||
<h2 className="text-lg font-semibold">{t("onboarding.done.title")}</h2>
|
||||
<p className="text-sm text-muted leading-relaxed mt-2 mb-5">
|
||||
Read and unsubscribe access are both granted. You can review or revoke either one
|
||||
anytime in Settings → Account, or from your Google Account.
|
||||
{t("onboarding.done.body")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -245,7 +246,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
|||
}}
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition w-full"
|
||||
>
|
||||
Done
|
||||
{t("onboarding.done.done")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue