More localisation

Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
William Oldham 2023-11-27 19:19:03 +00:00
parent d20fc4bf82
commit 75933e7080
21 changed files with 291 additions and 132 deletions

View File

@ -1,4 +1,60 @@
{ {
"auth": {
"deviceNameLabel": "Device name",
"deviceNamePlaceholder": "Muad'Dib's Nintendo Switch",
"register": {
"information": {
"title": "Account information",
"color1": "First color",
"color2": "Second color",
"icon": "User icon",
"header": "Enter a name for your device and choose a user icon and colours"
}
},
"login": {
"title": "Login to your account",
"description": "Oh, you're asking for the key to my top-secret lair, also known as The Fortress of Wordsmithery, accessed only by reciting the sacred incantation of the 12-word passphrase!",
"validationError": "Invalid or incomplete passphrase",
"submit": "Login",
"passphraseLabel": "12-Word Passphrase",
"passphrasePlaceholder": "Passphrase"
},
"generate": {
"title": "Your passphrase",
"description": "If you lose this, you're a silly goose and will be posted on the wall of shame™"
},
"trust": {
"title": "Do you trust this host?",
"host": "Do you trust <0>{{hostname}}</0>?",
"failed": {
"title": "Failed to reach backend",
"text": "Did you configure it correctly?"
},
"yes": "Trust",
"no": "Go back"
},
"verify": {
"title": "Enter your passphrase",
"description": "If you've already lost it, how will you ever be able to take care of a child?",
"invalidData": "Data is not valid",
"noMatch": "Passphrase doesn't match",
"recaptchaFailed": "ReCaptcha validation failed",
"passphraseLabel": "Your passphrase",
"register": "Register"
}
},
"errors": {
"details": "Error details",
"reloadPage": "Reload the page",
"badge": "It broke",
"title": "That's an error boss"
},
"notFound": {
"badge": "Not found",
"title": "Page could not be found",
"message": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
"goHome": "Go home"
},
"global": { "global": {
"name": "movie-web" "name": "movie-web"
}, },
@ -9,9 +65,57 @@
}, },
"episodeDisplay": "S{{season}} E{{episode}}" "episodeDisplay": "S{{season}} E{{episode}}"
}, },
"player": {
"scraping": {
"notFound": {
"badge": "Not found",
"title": "Goo goo gaa gaa",
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
"homeButton": "Go home"
}
},
"playbackError": {
"badge": "Not found",
"title": "Goo goo gaa gaa",
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
"homeButton": "Go home"
},
"metadata": {
"notFound": {
"badge": "Not found",
"title": "This media doesnt exist",
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
"homeButton": "Go home"
},
"failed": {
"badge": "Failed",
"title": "Failed to load meta data",
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
"homeButton": "Go home"
}
}
},
"home": { "home": {
"mediaList": { "mediaList": {
"stopEditing": "Stop editing" "stopEditing": "Stop editing"
},
"titles": {
"morning": ["Morning title"],
"day": ["Day title"],
"night": ["Night title"]
},
"search": {
"sectionTitle": "Search results",
"allResults": "That's all we have!",
"noResults": "We couldn't find anything!",
"failed": "Failed to find media, try again!",
"placeholder": "What do you want to watch?"
},
"continueWatching": {
"sectionTitle": "Continue Watching"
},
"bookmarks": {
"sectionTitle": "Bookmarks"
} }
}, },
"overlays": { "overlays": {
@ -21,16 +125,17 @@
"loadingUser": "Loading your profile", "loadingUser": "Loading your profile",
"loadingApp": "Loading application", "loadingApp": "Loading application",
"loadingUserError": { "loadingUserError": {
"text": "", "text": "Failed to load your profile",
"textWithReset": "", "textWithReset": "Failed to load your profile from your custom server, want to reset back to default?",
"reset": "Reset custom server" "reset": "Reset custom server"
}, },
"migration": { "migration": {
"failed": "Failed to migrate your data." "failed": "Failed to migrate your data.",
"inProgress": "Please hold, we are migrating your data. This shouldn't take long."
}, },
"dmca": { "dmca": {
"title": "", "title": "DMCA",
"text": "" "text": "In an effort to address the copyright concerns associated with the website known as \"movie-web,\" the DMCA, or Digital Millennium Copyright Act, has been initiated to safeguard the intellectual property rights of content creators by reporting infringements on this platform, thereby adhering to legal protocols for takedown requests, which, like, you know, it's all about, like, maintaining the integrity of intellectual property, and, um, making sure, like, creators get their fair share, but then, it's, like, this intricate dance of digital legalities, where you have to, uh, like, navigate this labyrinth of code and bytes and, uh, send, you know, these, like, electronic documents that, um, point out the, uh, alleged infringement, and it's, like, this whole, like, teeter-totter of legality, where you're, like, balancing, um, the rights of the, you know, creators and the, um, operation of this, like, online, uh, entity, and, like, the DMCA, it's, like, this, um, powerful tool, but, uh, it's also, like, this, um, complex puzzle, where, you know, you're, like, seeking justice in the digital wilderness, and, uh, striving for harmony amidst the chaos of the internet, and, um, yeah, that's, like, the whole, like, DMCA-ing thing with movie-web, you know?"
} }
}, },
"navigation": { "navigation": {
@ -46,7 +151,9 @@
} }
}, },
"actions": { "actions": {
"copy": "Copy" "copy": "Copy",
"copied": "Copied",
"next": "Next"
}, },
"settings": { "settings": {
"unsaved": "You have unsaved changes", "unsaved": "You have unsaved changes",
@ -96,6 +203,23 @@
"failed": "Failed to load sessions", "failed": "Failed to load sessions",
"deviceNameLabel": "Device name", "deviceNameLabel": "Device name",
"removeDevice": "Remove" "removeDevice": "Remove"
},
"accountDetails": {
"editProfile": "Edit",
"deviceNameLabel": "Device name",
"deviceNamePlaceholder": "Fremen tablet",
"logoutButton": "Log out"
},
"actions": {
"title": "Actions",
"delete": {
"title": "Delete account",
"text": "This action is irreversible. All data will be deleted and nothing can be recovered.",
"button": "Delete account",
"confirmTitle": "Are you sure?",
"confirmDescription": "Are you sure you want to delete your account? All your data will be lost!",
"confirmButton": "Delete account"
}
} }
}, },
"locale": { "locale": {
@ -104,7 +228,11 @@
"languageDescription": "Language applied to the entire application." "languageDescription": "Language applied to the entire application."
}, },
"captions": { "captions": {
"title": "Captions" "title": "Captions",
"previewQuote": "I must not fear. Fear is the mind-killer.",
"backgroundLabel": "Background opacity",
"textSizeLabel": "Text size",
"colorLabel": "Color"
}, },
"connections": { "connections": {
"title": "Connections", "title": "Connections",

View File

@ -1,4 +1,4 @@
import { NotFoundPart } from "@/pages/parts/errors/ErrorWrapperPart"; import { NotFoundPart } from "@/pages/parts/errors/NotFoundPart";
export function NotFoundPage() { export function NotFoundPage() {
return <NotFoundPart />; return <NotFoundPart />;

View File

@ -1,4 +1,5 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { ColorPicker } from "@/components/form/ColorPicker"; import { ColorPicker } from "@/components/form/ColorPicker";
@ -30,6 +31,7 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
const [colorA, setColorA] = useState("#2E65CF"); const [colorA, setColorA] = useState("#2E65CF");
const [colorB, setColorB] = useState("#2E65CF"); const [colorB, setColorB] = useState("#2E65CF");
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER); const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
const { t } = useTranslation();
// TODO validate device and account before next step // TODO validate device and account before next step
const nextStep = useCallback(() => { const nextStep = useCallback(() => {
@ -47,24 +49,36 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
<LargeCard> <LargeCard>
<LargeCardText <LargeCardText
icon={<Icon icon={Icons.USER} />} icon={<Icon icon={Icons.USER} />}
title="Account information" title={t("auth.register.information.title") ?? undefined}
> >
Set up your account.... OR ELSE! {t("auth.register.information.header")}
</LargeCardText> </LargeCardText>
<div className="space-y-6"> <div className="space-y-6">
<AuthInputBox <AuthInputBox
label="Device name" label={t("auth.deviceNameLabel") ?? undefined}
value={device} value={device}
onChange={setDevice} onChange={setDevice}
placeholder="Muad'Dib's Nintendo Switch" placeholder={t("auth.deviceNamePlaceholder") ?? undefined}
/>
<ColorPicker
label={t("auth.register.information.color1")}
value={colorA}
onInput={setColorA}
/>
<ColorPicker
label={t("auth.register.information.color2")}
value={colorB}
onInput={setColorB}
/>
<IconPicker
label={t("auth.register.information.icon")}
value={userIcon}
onInput={setUserIcon}
/> />
<ColorPicker label="First color" value={colorA} onInput={setColorA} />
<ColorPicker label="Second color" value={colorB} onInput={setColorB} />
<IconPicker label="User icon" value={userIcon} onInput={setUserIcon} />
</div> </div>
<LargeCardButtons> <LargeCardButtons>
<Button theme="purple" onClick={() => nextStep()}> <Button theme="purple" onClick={() => nextStep()}>
Next {t("actions.next")}
</Button> </Button>
</LargeCardButtons> </LargeCardButtons>
</LargeCard> </LargeCard>

View File

@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { verifyValidMnemonic } from "@/backend/accounts/crypto"; import { verifyValidMnemonic } from "@/backend/accounts/crypto";
@ -24,12 +25,13 @@ export function LoginFormPart(props: LoginFormPartProps) {
const { login, restore, importData } = useAuth(); const { login, restore, importData } = useAuth();
const progressItems = useProgressStore((store) => store.items); const progressItems = useProgressStore((store) => store.items);
const bookmarkItems = useBookmarkStore((store) => store.bookmarks); const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
const { t } = useTranslation();
const [result, execute] = useAsyncFn( const [result, execute] = useAsyncFn(
async (inputMnemonic: string, inputdevice: string) => { async (inputMnemonic: string, inputdevice: string) => {
// TODO verify valid device input // TODO verify valid device input
if (!verifyValidMnemonic(inputMnemonic)) if (!verifyValidMnemonic(inputMnemonic))
throw new Error("Invalid or incomplete passphrase"); throw new Error(t("auth.login.validationError") ?? undefined);
const account = await login({ const account = await login({
mnemonic: inputMnemonic, mnemonic: inputMnemonic,
@ -49,25 +51,23 @@ export function LoginFormPart(props: LoginFormPartProps) {
return ( return (
<LargeCard top={<BrandPill backgroundClass="bg-[#161527]" />}> <LargeCard top={<BrandPill backgroundClass="bg-[#161527]" />}>
<LargeCardText title="Login to your account"> <LargeCardText title={t("auth.login.title")}>
Oh, you&apos;re asking for the key to my top-secret lair, also known as {t("auth.login.description")}
The Fortress of Wordsmithery, accessed only by reciting the sacred
incantation of the 12-word passphrase!
</LargeCardText> </LargeCardText>
<div className="space-y-4"> <div className="space-y-4">
<AuthInputBox <AuthInputBox
label="12-Word Passphrase" label={t("auth.login.passphraseLabel") ?? undefined}
value={mnemonic} value={mnemonic}
autoComplete="username" autoComplete="username"
name="username" name="username"
onChange={setMnemonic} onChange={setMnemonic}
placeholder="Passphrase" placeholder={t("auth.login.passphrasePlaceholder") ?? undefined}
/> />
<AuthInputBox <AuthInputBox
label="Device name" label={t("auth.deviceNameLabel") ?? undefined}
value={device} value={device}
onChange={setDevice} onChange={setDevice}
placeholder="Device" placeholder={t("auth.deviceNamePlaceholder") ?? undefined}
/> />
{result.error && !result.loading ? ( {result.error && !result.loading ? (
<p className="text-authentication-errorText"> <p className="text-authentication-errorText">
@ -82,7 +82,7 @@ export function LoginFormPart(props: LoginFormPartProps) {
loading={result.loading} loading={result.loading}
onClick={() => execute(mnemonic, device)} onClick={() => execute(mnemonic, device)}
> >
LET ME IN! {t("auth.login.submit")}
</Button> </Button>
</LargeCardButtons> </LargeCardButtons>
</LargeCard> </LargeCard>

View File

@ -1,4 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { genMnemonic } from "@/backend/accounts/crypto"; import { genMnemonic } from "@/backend/accounts/crypto";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
@ -16,18 +17,21 @@ interface PassphraseGeneratePartProps {
export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) { export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
const mnemonic = useMemo(() => genMnemonic(), []); const mnemonic = useMemo(() => genMnemonic(), []);
const { t } = useTranslation();
return ( return (
<LargeCard> <LargeCard>
<LargeCardText title="Your passphrase" icon={<Icon icon={Icons.USER} />}> <LargeCardText
If you lose this, you&apos;re a silly goose and will be posted on the title={t("auth.generate.title")}
wall of shame icon={<Icon icon={Icons.USER} />}
>
{t("auth.generate.description")}
</LargeCardText> </LargeCardText>
<PassphraseDisplay mnemonic={mnemonic} /> <PassphraseDisplay mnemonic={mnemonic} />
<LargeCardButtons> <LargeCardButtons>
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}> <Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>
NEXT! {t("actions.next")}
</Button> </Button>
</LargeCardButtons> </LargeCardButtons>
</LargeCard> </LargeCard>

View File

@ -1,4 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
@ -21,18 +22,18 @@ interface TrustBackendPartProps {
export function TrustBackendPart(props: TrustBackendPartProps) { export function TrustBackendPart(props: TrustBackendPartProps) {
const history = useHistory(); const history = useHistory();
const backendUrl = useBackendUrl(); const backendUrl = useBackendUrl();
const backendHostname = useMemo( const hostname = useMemo(() => new URL(backendUrl).hostname, [backendUrl]);
() => new URL(backendUrl).hostname,
[backendUrl]
);
const result = useAsync(() => { const result = useAsync(() => {
return getBackendMeta(conf().BACKEND_URL); return getBackendMeta(conf().BACKEND_URL);
}, [backendUrl]); }, [backendUrl]);
const { t } = useTranslation();
let cardContent = ( let cardContent = (
<> <>
<h3 className="text-white font-bold text-lg">Failed to reach backend</h3> <h3 className="text-white font-bold text-lg">
<p>Did you configure it correctly?</p> {t("auth.trust.failed.title")}
</h3>
<p>{t("auth.trust.failed.text")}</p>
</> </>
); );
if (result.loading) cardContent = <Loading />; if (result.loading) cardContent = <Loading />;
@ -47,10 +48,12 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
return ( return (
<LargeCard> <LargeCard>
<LargeCardText <LargeCardText
title="Do you trust this host?" title={t("auth.trust.title")}
icon={<Icon icon={Icons.CIRCLE_EXCLAMATION} />} icon={<Icon icon={Icons.CIRCLE_EXCLAMATION} />}
> >
Do you trust <span className="text-white">{backendHostname}</span>? <Trans i18nKey="auth.trust.host">
<span className="text-white">{{ hostname }}</span>
</Trans>
</LargeCardText> </LargeCardText>
<div className="border border-authentication-border rounded-xl px-4 py-8 flex flex-col items-center space-y-2 my-8"> <div className="border border-authentication-border rounded-xl px-4 py-8 flex flex-col items-center space-y-2 my-8">
@ -61,10 +64,10 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
theme="purple" theme="purple"
onClick={() => result.value && props.onNext?.(result.value)} onClick={() => result.value && props.onNext?.(result.value)}
> >
I pledge my life to the United States {t("auth.trust.yes")}
</Button> </Button>
<Button theme="secondary" onClick={() => history.push("/")}> <Button theme="secondary" onClick={() => history.push("/")}>
I WILL NEVER SUCCUMB! {t("auth.trust.no")}
</Button> </Button>
</LargeCardButtons> </LargeCardButtons>
</LargeCard> </LargeCard>

View File

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3"; import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { updateSettings } from "@/backend/accounts/settings"; import { updateSettings } from "@/backend/accounts/settings";
@ -40,24 +41,26 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
const applicationTheme = useThemeStore((store) => store.theme); const applicationTheme = useThemeStore((store) => store.theme);
const backendUrl = useBackendUrl(); const backendUrl = useBackendUrl();
const { t } = useTranslation();
const { executeRecaptcha } = useGoogleReCaptcha(); const { executeRecaptcha } = useGoogleReCaptcha();
const [result, execute] = useAsyncFn( const [result, execute] = useAsyncFn(
async (inputMnemonic: string) => { async (inputMnemonic: string) => {
if (!props.mnemonic || !props.userData) if (!props.mnemonic || !props.userData)
throw new Error("Data is not valid"); throw new Error(t("auth.verify.invalidData") ?? undefined);
let recaptchaToken: string | undefined; let recaptchaToken: string | undefined;
if (props.hasCaptcha) { if (props.hasCaptcha) {
recaptchaToken = executeRecaptcha recaptchaToken = executeRecaptcha
? await executeRecaptcha() ? await executeRecaptcha()
: undefined; : undefined;
if (!recaptchaToken) throw new Error("ReCaptcha validation failed"); if (!recaptchaToken)
throw new Error(t("auth.verify.recaptchaFailed") ?? undefined);
} }
if (inputMnemonic !== props.mnemonic) if (inputMnemonic !== props.mnemonic)
throw new Error("Passphrase doesn't match"); throw new Error(t("auth.verify.noMatch") ?? undefined);
const account = await register({ const account = await register({
mnemonic: inputMnemonic, mnemonic: inputMnemonic,
@ -85,13 +88,12 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
<form> <form>
<LargeCardText <LargeCardText
icon={<Icon icon={Icons.CIRCLE_CHECK} />} icon={<Icon icon={Icons.CIRCLE_CHECK} />}
title="Enter your passphrase" title={t("auth.verify.title")}
> >
If you&apos;ve already lost it, how will you ever be able to take care {t("auth.verify.description")}
of a child?
</LargeCardText> </LargeCardText>
<AuthInputBox <AuthInputBox
label="Your passphrase" label={t("auth.verify.passphraseLabel") ?? undefined}
autoComplete="username" autoComplete="username"
name="username" name="username"
value={mnemonic} value={mnemonic}
@ -108,7 +110,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
loading={result.loading} loading={result.loading}
onClick={() => execute(mnemonic)} onClick={() => execute(mnemonic)}
> >
Register {t("auth.verify.register")}
</Button> </Button>
</LargeCardButtons> </LargeCardButtons>
</form> </form>

View File

@ -1,4 +1,5 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
@ -10,6 +11,7 @@ export function ErrorCard(props: { error: DisplayError | string }) {
const hasCopiedUnsetDebounce = useRef<ReturnType<typeof setTimeout> | null>( const hasCopiedUnsetDebounce = useRef<ReturnType<typeof setTimeout> | null>(
null null
); );
const { t } = useTranslation();
const errorMessage = const errorMessage =
typeof props.error === "string" ? props.error : props.error.message; typeof props.error === "string" ? props.error : props.error.message;
@ -32,7 +34,7 @@ export function ErrorCard(props: { error: DisplayError | string }) {
// I didn't put a <Transition> here because it'd fade out, then jump height weirdly // I didn't put a <Transition> here because it'd fade out, then jump height weirdly
<div className="w-full bg-errors-card p-6 rounded-lg"> <div className="w-full bg-errors-card p-6 rounded-lg">
<div className="flex justify-between items-center pb-2 border-b border-errors-border"> <div className="flex justify-between items-center pb-2 border-b border-errors-border">
<span className="text-white font-medium">Error details</span> <span className="text-white font-medium">{t("errors.details")}</span>
<div className="flex justify-center items-center gap-3"> <div className="flex justify-center items-center gap-3">
<Button <Button
theme="secondary" theme="secondary"
@ -42,12 +44,12 @@ export function ErrorCard(props: { error: DisplayError | string }) {
{hasCopied ? ( {hasCopied ? (
<> <>
<Icon icon={Icons.CHECKMARK} className="text-xs mr-3" /> <Icon icon={Icons.CHECKMARK} className="text-xs mr-3" />
Copied {t("actions.copied")}
</> </>
) : ( ) : (
<> <>
<Icon icon={Icons.COPY} className="text-2xl mr-3" /> <Icon icon={Icons.COPY} className="text-2xl mr-3" />
Copy {t("actions.copy")}
</> </>
)} )}
</Button> </Button>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { ButtonPlain } from "@/components/buttons/Button"; import { ButtonPlain } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill"; import { IconPill } from "@/components/layout/IconPill";
@ -10,20 +12,22 @@ export function ErrorPart(props: { error: any; errorInfo: any }) {
error: props.error, error: props.error,
errorInfo: props.errorInfo, errorInfo: props.errorInfo,
}); });
const { t } = useTranslation();
return ( return (
<div className="relative flex flex-1 flex-col"> <div className="relative flex flex-1 flex-col">
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center"> <div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.EYE_SLASH}>It broke</IconPill> <IconPill icon={Icons.EYE_SLASH}>{t("errors.badge")}</IconPill>
<Title>Failed to load meta data</Title> <Title>{t("errors.title")}</Title>
<Paragraph>{data}</Paragraph> <Paragraph>{data}</Paragraph>
<ButtonPlain <ButtonPlain
theme="purple" theme="purple"
className="mt-6 md:px-12 p-2.5" className="mt-6 md:px-12 p-2.5"
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
> >
Reload the page {t("errors.reloadPage")}
</ButtonPlain> </ButtonPlain>
</ErrorContainer> </ErrorContainer>
</ErrorLayout> </ErrorLayout>

View File

@ -15,29 +15,22 @@ export function NotFoundPart() {
return ( return (
<div className="relative flex flex-1 flex-col"> <div className="relative flex flex-1 flex-col">
<Helmet> <Helmet>
<title>{t("notFound.genericTitle")}</title> <title>{t("notFound.badge")}</title>
</Helmet> </Helmet>
<Navigation /> <Navigation />
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center"> <div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.EYE_SLASH}> <IconPill icon={Icons.EYE_SLASH}>{t("notFound.badge")}</IconPill>
{t("notFound.genericTitle")} <Title>{t("notFound.title")}</Title>
</IconPill> <Paragraph>{t("notFound.message")}</Paragraph>
<Title>Failed to load meta data</Title>
<Paragraph>
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
bestest, but alas, no wucky videos to be spotted anywhere (´ω`)
Please don&apos;t be angwy, wittle movie-web ish twying so hard.
Can you find it in your heart to forgive? UwU 💖
</Paragraph>
<Button <Button
href="/" href="/"
theme="purple" theme="purple"
padding="md:px-12 p-2.5" padding="md:px-12 p-2.5"
className="mt-6" className="mt-6"
> >
Go home {t("notFound.goHome")}
</Button> </Button>
</ErrorContainer> </ErrorContainer>
</ErrorLayout> </ErrorLayout>

View File

@ -46,7 +46,7 @@ export function BookmarksPart() {
return ( return (
<div> <div>
<SectionHeading <SectionHeading
title={t("search.bookmarks") || "Bookmarks"} title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
icon={Icons.BOOKMARK} icon={Icons.BOOKMARK}
> >
<EditButton editing={editing} onEdit={setEditing} /> <EditButton editing={editing} onEdit={setEditing} />

View File

@ -31,7 +31,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
if (hour < 12) time = "morning"; if (hour < 12) time = "morning";
else if (hour < 19) time = "day"; else if (hour < 19) time = "day";
const title = t(`search.title.${time}`); const title = t(`home.titles.${time}`);
return ( return (
<ThinContainer> <ThinContainer>
@ -51,9 +51,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
onChange={setSearch} onChange={setSearch}
value={search} value={search}
onUnFocus={setSearchUnFocus} onUnFocus={setSearchUnFocus}
placeholder={ placeholder={t("home.search.placeholder")}
t("search.placeholder") || "What do you want to watch?"
}
/> />
</Sticky> </Sticky>
</div> </div>

View File

@ -44,7 +44,7 @@ export function WatchingPart() {
return ( return (
<div> <div>
<SectionHeading <SectionHeading
title={t("search.continueWatching") || "Continue Watching"} title={t("home.continueWatching.sectionTitle")}
icon={Icons.CLOCK} icon={Icons.CLOCK}
> >
<EditButton editing={editing} onEdit={setEditing} /> <EditButton editing={editing} onEdit={setEditing} />

View File

@ -1,8 +1,11 @@
import { useTranslation } from "react-i18next";
import { BrandPill } from "@/components/layout/BrandPill"; import { BrandPill } from "@/components/layout/BrandPill";
import { Loading } from "@/components/layout/Loading"; import { Loading } from "@/components/layout/Loading";
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout"; import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
export function MigrationPart() { export function MigrationPart() {
const { t } = useTranslation();
return ( return (
<div className="flex flex-col justify-center items-center h-screen text-center font-medium"> <div className="flex flex-col justify-center items-center h-screen text-center font-medium">
{/* Overlaid elements */} {/* Overlaid elements */}
@ -14,8 +17,7 @@ export function MigrationPart() {
{/* Content */} {/* Content */}
<Loading /> <Loading />
<p className="max-w-[19rem] mt-3 mb-12 text-type-secondary"> <p className="max-w-[19rem] mt-3 mb-12 text-type-secondary">
Please hold, we are migrating your data. This shouldn&apos;t take long. {t("screens.migration.inProgress")}
Also, fuck you.
</p> </p>
</div> </div>
); );

View File

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import type { AsyncReturnType } from "type-fest"; import type { AsyncReturnType } from "type-fest";
@ -18,6 +19,7 @@ export interface MetaPartProps {
} }
export function MetaPart(props: MetaPartProps) { export function MetaPart(props: MetaPartProps) {
const { t } = useTranslation();
const params = useParams<{ const params = useParams<{
media: string; media: string;
episode?: string; episode?: string;
@ -70,21 +72,18 @@ export function MetaPart(props: MetaPartProps) {
return ( return (
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.WAND}>Failed to load</IconPill> <IconPill icon={Icons.WAND}>
<Title>Failed to load meta data</Title> {t("player.metadata.failed.badge")}
<Paragraph> </IconPill>
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost <Title>{t("player.metadata.failed.title")}</Title>
bestest, but alas, no wucky videos to be spotted anywhere (´ω`) <Paragraph>{t("player.metadata.failed.text")}</Paragraph>
Please don&apos;t be angwy, wittle movie-web ish twying so hard. Can
you find it in your heart to forgive? UwU 💖
</Paragraph>
<Button <Button
href="/" href="/"
theme="purple" theme="purple"
padding="md:px-12 p-2.5" padding="md:px-12 p-2.5"
className="mt-6" className="mt-6"
> >
Go home {t("player.metadata.failed.homeButton")}
</Button> </Button>
</ErrorContainer> </ErrorContainer>
</ErrorLayout> </ErrorLayout>
@ -95,21 +94,18 @@ export function MetaPart(props: MetaPartProps) {
return ( return (
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.WAND}>Not found</IconPill> <IconPill icon={Icons.WAND}>
<Title>This media doesnt exist</Title> {t("player.metadata.notFound.badge")}
<Paragraph> </IconPill>
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost <Title>{t("player.metadata.notFound.title")}</Title>
bestest, but alas, no wucky videos to be spotted anywhere (´ω`) <Paragraph>{t("player.metadata.notFound.text")}</Paragraph>
Please don&apos;t be angwy, wittle movie-web ish twying so hard. Can
you find it in your heart to forgive? UwU 💖
</Paragraph>
<Button <Button
href="/" href="/"
theme="purple" theme="purple"
padding="md:px-12 p-2.5" padding="md:px-12 p-2.5"
className="mt-6" className="mt-6"
> >
Go home {t("player.metadata.notFound.homeButton")}
</Button> </Button>
</ErrorContainer> </ErrorContainer>
</ErrorLayout> </ErrorLayout>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill"; import { IconPill } from "@/components/layout/IconPill";
@ -9,26 +11,22 @@ import { usePlayerStore } from "@/stores/player/store";
import { ErrorCard } from "../errors/ErrorCard"; import { ErrorCard } from "../errors/ErrorCard";
export function PlaybackErrorPart() { export function PlaybackErrorPart() {
const { t } = useTranslation();
const playbackError = usePlayerStore((s) => s.interface.error); const playbackError = usePlayerStore((s) => s.interface.error);
return ( return (
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.WAND}>Not found</IconPill> <IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill>
<Title>Goo goo gaa gaa</Title> <Title>{t("player.playbackError.title")}</Title>
<Paragraph> <Paragraph>{t("player.playbackError.text")}</Paragraph>
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
bestest, but alas, no wucky videos to be spotted anywhere (´ω`)
Please don&apos;t be angwy, wittle movie-web ish twying so hard. Can
you find it in your heart to forgive? UwU 💖
</Paragraph>
<Button <Button
href="/" href="/"
theme="purple" theme="purple"
padding="md:px-12 p-2.5" padding="md:px-12 p-2.5"
className="mt-6" className="mt-6"
> >
Go home {t("player.playbackError.homeButton")}
</Button> </Button>
</ErrorContainer> </ErrorContainer>
<ErrorContainer maxWidth="max-w-[45rem]"> <ErrorContainer maxWidth="max-w-[45rem]">

View File

@ -1,4 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
@ -18,6 +19,7 @@ export interface ScrapeErrorPartProps {
} }
export function ScrapeErrorPart(props: ScrapeErrorPartProps) { export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
const { t } = useTranslation();
const error = useMemo(() => { const error = useMemo(() => {
const data = props.data; const data = props.data;
const amountError = Object.values(data.sources).filter( const amountError = Object.values(data.sources).filter(
@ -36,21 +38,18 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
return ( return (
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.WAND}>Not found</IconPill> <IconPill icon={Icons.WAND}>
<Title>Goo goo gaa gaa</Title> {t("player.scraping.notFound.badge")}
<Paragraph> </IconPill>
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost <Title>{t("player.scraping.notFound.title")}</Title>
bestest, but alas, no wucky videos to be spotted anywhere (´ω`) <Paragraph>{t("player.scraping.notFound.text")}</Paragraph>
Please don&apos;t be angwy, wittle movie-web ish twying so hard. Can
you find it in your heart to forgive? UwU 💖
</Paragraph>
<Button <Button
href="/" href="/"
theme="purple" theme="purple"
padding="md:px-12 p-2.5" padding="md:px-12 p-2.5"
className="mt-6" className="mt-6"
> >
Go home {t("player.scraping.notFound.homeButton")}
</Button> </Button>
</ErrorContainer> </ErrorContainer>
<ErrorContainer maxWidth="max-w-[45rem]"> <ErrorContainer maxWidth="max-w-[45rem]">

View File

@ -30,9 +30,9 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
{!props.failed ? ( {!props.failed ? (
<div> <div>
{(props.results ?? 0) > 0 ? ( {(props.results ?? 0) > 0 ? (
<p>{t("search.allResults")}</p> <p>{t("home.search.allResults")}</p>
) : ( ) : (
<p>{t("search.noResults")}</p> <p>{t("home.search.noResults")}</p>
)} )}
</div> </div>
) : null} ) : null}
@ -40,7 +40,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
{/* Error result */} {/* Error result */}
{props.failed ? ( {props.failed ? (
<div> <div>
<p>{t("search.allFailed")}</p> <p>{t("home.search.failed")}</p>
</div> </div>
) : null} ) : null}
</div> </div>
@ -72,7 +72,7 @@ export function SearchListPart({ searchQuery }: { searchQuery: string }) {
{results.length > 0 ? ( {results.length > 0 ? (
<div> <div>
<SectionHeading <SectionHeading
title={t("search.headingTitle") || "Search results"} title={t("home.search.sectionTitle")}
icon={Icons.SEARCH} icon={Icons.SEARCH}
/> />
<MediaGrid> <MediaGrid>

View File

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { deleteUser } from "@/backend/accounts/user"; import { deleteUser } from "@/backend/accounts/user";
@ -10,6 +11,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
export function AccountActionsPart() { export function AccountActionsPart() {
const { t } = useTranslation();
const url = useBackendUrl(); const url = useBackendUrl();
const account = useAuthStore((s) => s.account); const account = useAuthStore((s) => s.account);
const { logout } = useAuthData(); const { logout } = useAuthData();
@ -26,16 +28,15 @@ export function AccountActionsPart() {
return ( return (
<div> <div>
<Heading2 border>Actions</Heading2> <Heading2 border>{t("settings.account.actions.title")}</Heading2>
<SolidSettingsCard <SolidSettingsCard
paddingClass="px-6 py-12" paddingClass="px-6 py-12"
className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12" className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12"
> >
<div> <div>
<Heading3>Delete account</Heading3> <Heading3>{t("settings.account.actions.delete.title")}</Heading3>
<p className="text-type-text"> <p className="text-type-text">
This action is irreversible. All data will be deleted and nothing {t("settings.account.actions.delete.text")}
can be recovered.
</p> </p>
</div> </div>
<div className="flex justify-start lg:justify-end items-center"> <div className="flex justify-start lg:justify-end items-center">
@ -44,23 +45,24 @@ export function AccountActionsPart() {
loading={deleteResult.loading} loading={deleteResult.loading}
onClick={deleteModal.show} onClick={deleteModal.show}
> >
Delete account {t("settings.account.actions.delete.button")}
</Button> </Button>
</div> </div>
</SolidSettingsCard> </SolidSettingsCard>
<Modal id={deleteModal.id}> <Modal id={deleteModal.id}>
<ModalCard> <ModalCard>
<Heading2 className="!mt-0">Are you sure?</Heading2> <Heading2 className="!mt-0">
{t("settings.account.actions.delete.confirmTitle")}
</Heading2>
<Paragraph> <Paragraph>
Are you sure you want to delete your account? All your data will be {t("settings.account.actions.delete.confirmDescription")}
lost!
</Paragraph> </Paragraph>
<Button <Button
theme="danger" theme="danger"
loading={deleteResult.loading} loading={deleteResult.loading}
onClick={deleteExec} onClick={deleteExec}
> >
Delete account {t("settings.account.actions.delete.confirmButton")}
</Button> </Button>
</ModalCard> </ModalCard>
</Modal> </Modal>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Avatar } from "@/components/Avatar"; import { Avatar } from "@/components/Avatar";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
@ -18,6 +20,7 @@ export function AccountEditPart(props: {
userIcon: UserIcons; userIcon: UserIcons;
setUserIcon: (s: UserIcons) => void; setUserIcon: (s: UserIcons) => void;
}) { }) {
const { t } = useTranslation();
const { logout } = useAuth(); const { logout } = useAuth();
const profileEditModal = useModal("profile-edit"); const profileEditModal = useModal("profile-edit");
@ -50,7 +53,7 @@ export function AccountEditPart(props: {
onClick={profileEditModal.show} onClick={profileEditModal.show}
> >
<Icon icon={Icons.EDIT} /> <Icon icon={Icons.EDIT} />
Edit {t("settings.account.accountDetails.editProfile")}
</button> </button>
} }
/> />
@ -58,14 +61,20 @@ export function AccountEditPart(props: {
<div> <div>
<div className="space-y-8 max-w-xs"> <div className="space-y-8 max-w-xs">
<AuthInputBox <AuthInputBox
label="Device name" label={
placeholder="Fremen tablet" t("settings.account.accountDetails.deviceNameLabel") ??
undefined
}
placeholder={
t("settings.account.accountDetails.deviceNamePlaceholder") ??
undefined
}
value={props.deviceName} value={props.deviceName}
onChange={(value) => props.setDeviceName(value)} onChange={(value) => props.setDeviceName(value)}
/> />
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button theme="danger" onClick={logout}> <Button theme="danger" onClick={logout}>
Log out {t("settings.account.accountDetails.logoutButton")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { import {
@ -19,6 +20,7 @@ export function CaptionPreview(props: {
styling: SubtitleStyling; styling: SubtitleStyling;
onToggle: () => void; onToggle: () => void;
}) { }) {
const { t } = useTranslation();
return ( return (
<div <div
className={classNames({ className={classNames({
@ -50,7 +52,7 @@ export function CaptionPreview(props: {
} }
> >
<CaptionCue <CaptionCue
text="I must not fear. Fear is the mind-killer." text={t("settings.captions.previewQuote") ?? undefined}
styling={props.styling} styling={props.styling}
overrideCasing={false} overrideCasing={false}
/> />
@ -66,15 +68,16 @@ export function CaptionsPart(props: {
styling: SubtitleStyling; styling: SubtitleStyling;
setStyling: (s: SubtitleStyling) => void; setStyling: (s: SubtitleStyling) => void;
}) { }) {
const { t } = useTranslation();
const [fullscreenPreview, setFullscreenPreview] = useState(false); const [fullscreenPreview, setFullscreenPreview] = useState(false);
return ( return (
<div> <div>
<Heading1 border>Captions</Heading1> <Heading1 border>{t("settings.captions.title")}</Heading1>
<div className="grid md:grid-cols-[1fr,356px] gap-8"> <div className="grid md:grid-cols-[1fr,356px] gap-8">
<div className="space-y-6"> <div className="space-y-6">
<CaptionSetting <CaptionSetting
label="Background opacity" label={t("settings.captions.backgroundLabel")}
max={100} max={100}
min={0} min={0}
onChange={(v) => onChange={(v) =>
@ -84,7 +87,7 @@ export function CaptionsPart(props: {
textTransformer={(s) => `${s}%`} textTransformer={(s) => `${s}%`}
/> />
<CaptionSetting <CaptionSetting
label="Text size" label={t("settings.captions.textSizeLabel")}
max={200} max={200}
min={1} min={1}
textTransformer={(s) => `${s}%`} textTransformer={(s) => `${s}%`}
@ -94,7 +97,9 @@ export function CaptionsPart(props: {
value={props.styling.size * 100} value={props.styling.size * 100}
/> />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Menu.FieldTitle>Color</Menu.FieldTitle> <Menu.FieldTitle>
{t("settings.captions.colorLabel")}
</Menu.FieldTitle>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
{colors.map((v) => ( {colors.map((v) => (
<ColorOption <ColorOption