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": {
"name": "movie-web"
},
@ -9,9 +65,57 @@
},
"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": {
"mediaList": {
"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": {
@ -21,16 +125,17 @@
"loadingUser": "Loading your profile",
"loadingApp": "Loading application",
"loadingUserError": {
"text": "",
"textWithReset": "",
"text": "Failed to load your profile",
"textWithReset": "Failed to load your profile from your custom server, want to reset back to default?",
"reset": "Reset custom server"
},
"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": {
"title": "",
"text": ""
"title": "DMCA",
"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": {
@ -46,7 +151,9 @@
}
},
"actions": {
"copy": "Copy"
"copy": "Copy",
"copied": "Copied",
"next": "Next"
},
"settings": {
"unsaved": "You have unsaved changes",
@ -96,6 +203,23 @@
"failed": "Failed to load sessions",
"deviceNameLabel": "Device name",
"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": {
@ -104,7 +228,11 @@
"languageDescription": "Language applied to the entire application."
},
"captions": {
"title": "Captions"
"title": "Captions",
"previewQuote": "I must not fear. Fear is the mind-killer.",
"backgroundLabel": "Background opacity",
"textSizeLabel": "Text size",
"colorLabel": "Color"
},
"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() {
return <NotFoundPart />;

View File

@ -1,4 +1,5 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { ColorPicker } from "@/components/form/ColorPicker";
@ -30,6 +31,7 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
const [colorA, setColorA] = useState("#2E65CF");
const [colorB, setColorB] = useState("#2E65CF");
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
const { t } = useTranslation();
// TODO validate device and account before next step
const nextStep = useCallback(() => {
@ -47,24 +49,36 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
<LargeCard>
<LargeCardText
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>
<div className="space-y-6">
<AuthInputBox
label="Device name"
label={t("auth.deviceNameLabel") ?? undefined}
value={device}
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>
<LargeCardButtons>
<Button theme="purple" onClick={() => nextStep()}>
Next
{t("actions.next")}
</Button>
</LargeCardButtons>
</LargeCard>

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { useMemo } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useAsync } from "react-use";
@ -21,18 +22,18 @@ interface TrustBackendPartProps {
export function TrustBackendPart(props: TrustBackendPartProps) {
const history = useHistory();
const backendUrl = useBackendUrl();
const backendHostname = useMemo(
() => new URL(backendUrl).hostname,
[backendUrl]
);
const hostname = useMemo(() => new URL(backendUrl).hostname, [backendUrl]);
const result = useAsync(() => {
return getBackendMeta(conf().BACKEND_URL);
}, [backendUrl]);
const { t } = useTranslation();
let cardContent = (
<>
<h3 className="text-white font-bold text-lg">Failed to reach backend</h3>
<p>Did you configure it correctly?</p>
<h3 className="text-white font-bold text-lg">
{t("auth.trust.failed.title")}
</h3>
<p>{t("auth.trust.failed.text")}</p>
</>
);
if (result.loading) cardContent = <Loading />;
@ -47,10 +48,12 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
return (
<LargeCard>
<LargeCardText
title="Do you trust this host?"
title={t("auth.trust.title")}
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>
<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"
onClick={() => result.value && props.onNext?.(result.value)}
>
I pledge my life to the United States
{t("auth.trust.yes")}
</Button>
<Button theme="secondary" onClick={() => history.push("/")}>
I WILL NEVER SUCCUMB!
{t("auth.trust.no")}
</Button>
</LargeCardButtons>
</LargeCard>

View File

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

View File

@ -1,4 +1,5 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
@ -10,6 +11,7 @@ export function ErrorCard(props: { error: DisplayError | string }) {
const hasCopiedUnsetDebounce = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const { t } = useTranslation();
const errorMessage =
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
<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">
<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">
<Button
theme="secondary"
@ -42,12 +44,12 @@ export function ErrorCard(props: { error: DisplayError | string }) {
{hasCopied ? (
<>
<Icon icon={Icons.CHECKMARK} className="text-xs mr-3" />
Copied
{t("actions.copied")}
</>
) : (
<>
<Icon icon={Icons.COPY} className="text-2xl mr-3" />
Copy
{t("actions.copy")}
</>
)}
</Button>

View File

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

View File

@ -15,29 +15,22 @@ export function NotFoundPart() {
return (
<div className="relative flex flex-1 flex-col">
<Helmet>
<title>{t("notFound.genericTitle")}</title>
<title>{t("notFound.badge")}</title>
</Helmet>
<Navigation />
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.EYE_SLASH}>
{t("notFound.genericTitle")}
</IconPill>
<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>
<IconPill icon={Icons.EYE_SLASH}>{t("notFound.badge")}</IconPill>
<Title>{t("notFound.title")}</Title>
<Paragraph>{t("notFound.message")}</Paragraph>
<Button
href="/"
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
Go home
{t("notFound.goHome")}
</Button>
</ErrorContainer>
</ErrorLayout>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
import { useTranslation } from "react-i18next";
import { BrandPill } from "@/components/layout/BrandPill";
import { Loading } from "@/components/layout/Loading";
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
export function MigrationPart() {
const { t } = useTranslation();
return (
<div className="flex flex-col justify-center items-center h-screen text-center font-medium">
{/* Overlaid elements */}
@ -14,8 +17,7 @@ export function MigrationPart() {
{/* Content */}
<Loading />
<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.
Also, fuck you.
{t("screens.migration.inProgress")}
</p>
</div>
);

View File

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useHistory, useParams } from "react-router-dom";
import { useAsync } from "react-use";
import type { AsyncReturnType } from "type-fest";
@ -18,6 +19,7 @@ export interface MetaPartProps {
}
export function MetaPart(props: MetaPartProps) {
const { t } = useTranslation();
const params = useParams<{
media: string;
episode?: string;
@ -70,21 +72,18 @@ export function MetaPart(props: MetaPartProps) {
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.WAND}>Failed to load</IconPill>
<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>
<IconPill icon={Icons.WAND}>
{t("player.metadata.failed.badge")}
</IconPill>
<Title>{t("player.metadata.failed.title")}</Title>
<Paragraph>{t("player.metadata.failed.text")}</Paragraph>
<Button
href="/"
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
Go home
{t("player.metadata.failed.homeButton")}
</Button>
</ErrorContainer>
</ErrorLayout>
@ -95,21 +94,18 @@ export function MetaPart(props: MetaPartProps) {
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.WAND}>Not found</IconPill>
<Title>This media doesnt exist</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>
<IconPill icon={Icons.WAND}>
{t("player.metadata.notFound.badge")}
</IconPill>
<Title>{t("player.metadata.notFound.title")}</Title>
<Paragraph>{t("player.metadata.notFound.text")}</Paragraph>
<Button
href="/"
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
Go home
{t("player.metadata.notFound.homeButton")}
</Button>
</ErrorContainer>
</ErrorLayout>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill";
@ -9,26 +11,22 @@ import { usePlayerStore } from "@/stores/player/store";
import { ErrorCard } from "../errors/ErrorCard";
export function PlaybackErrorPart() {
const { t } = useTranslation();
const playbackError = usePlayerStore((s) => s.interface.error);
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.WAND}>Not found</IconPill>
<Title>Goo goo gaa gaa</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>
<IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill>
<Title>{t("player.playbackError.title")}</Title>
<Paragraph>{t("player.playbackError.text")}</Paragraph>
<Button
href="/"
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
Go home
{t("player.playbackError.homeButton")}
</Button>
</ErrorContainer>
<ErrorContainer maxWidth="max-w-[45rem]">

View File

@ -1,4 +1,5 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon";
@ -18,6 +19,7 @@ export interface ScrapeErrorPartProps {
}
export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
const { t } = useTranslation();
const error = useMemo(() => {
const data = props.data;
const amountError = Object.values(data.sources).filter(
@ -36,21 +38,18 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.WAND}>Not found</IconPill>
<Title>Goo goo gaa gaa</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>
<IconPill icon={Icons.WAND}>
{t("player.scraping.notFound.badge")}
</IconPill>
<Title>{t("player.scraping.notFound.title")}</Title>
<Paragraph>{t("player.scraping.notFound.text")}</Paragraph>
<Button
href="/"
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
Go home
{t("player.scraping.notFound.homeButton")}
</Button>
</ErrorContainer>
<ErrorContainer maxWidth="max-w-[45rem]">

View File

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

View File

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

View File

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

View File

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